Mercurial > emacs
comparison gc/AmigaOS.c @ 51488:5de98dce4bd1
*** empty log message ***
author | Dave Love <fx@gnu.org> |
---|---|
date | Thu, 05 Jun 2003 17:49:22 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
51487:01d68b199093 | 51488:5de98dce4bd1 |
---|---|
1 | |
2 | |
3 /****************************************************************** | |
4 | |
5 AmigaOS-spesific routines for GC. | |
6 This file is normally included from os_dep.c | |
7 | |
8 ******************************************************************/ | |
9 | |
10 | |
11 #if !defined(GC_AMIGA_DEF) && !defined(GC_AMIGA_SB) && !defined(GC_AMIGA_DS) && !defined(GC_AMIGA_AM) | |
12 # include "gc_priv.h" | |
13 # include <stdio.h> | |
14 # include <signal.h> | |
15 # define GC_AMIGA_DEF | |
16 # define GC_AMIGA_SB | |
17 # define GC_AMIGA_DS | |
18 # define GC_AMIGA_AM | |
19 #endif | |
20 | |
21 | |
22 #ifdef GC_AMIGA_DEF | |
23 | |
24 # ifndef __GNUC__ | |
25 # include <exec/exec.h> | |
26 # endif | |
27 # include <proto/exec.h> | |
28 # include <proto/dos.h> | |
29 # include <dos/dosextens.h> | |
30 # include <workbench/startup.h> | |
31 | |
32 #endif | |
33 | |
34 | |
35 | |
36 | |
37 #ifdef GC_AMIGA_SB | |
38 | |
39 /****************************************************************** | |
40 Find the base of the stack. | |
41 ******************************************************************/ | |
42 | |
43 ptr_t GC_get_stack_base() | |
44 { | |
45 struct Process *proc = (struct Process*)SysBase->ThisTask; | |
46 | |
47 /* Reference: Amiga Guru Book Pages: 42,567,574 */ | |
48 if (proc->pr_Task.tc_Node.ln_Type==NT_PROCESS | |
49 && proc->pr_CLI != NULL) { | |
50 /* first ULONG is StackSize */ | |
51 /*longPtr = proc->pr_ReturnAddr; | |
52 size = longPtr[0];*/ | |
53 | |
54 return (char *)proc->pr_ReturnAddr + sizeof(ULONG); | |
55 } else { | |
56 return (char *)proc->pr_Task.tc_SPUpper; | |
57 } | |
58 } | |
59 | |
60 #if 0 /* old version */ | |
61 ptr_t GC_get_stack_base() | |
62 { | |
63 extern struct WBStartup *_WBenchMsg; | |
64 extern long __base; | |
65 extern long __stack; | |
66 struct Task *task; | |
67 struct Process *proc; | |
68 struct CommandLineInterface *cli; | |
69 long size; | |
70 | |
71 if ((task = FindTask(0)) == 0) { | |
72 GC_err_puts("Cannot find own task structure\n"); | |
73 ABORT("task missing"); | |
74 } | |
75 proc = (struct Process *)task; | |
76 cli = BADDR(proc->pr_CLI); | |
77 | |
78 if (_WBenchMsg != 0 || cli == 0) { | |
79 size = (char *)task->tc_SPUpper - (char *)task->tc_SPLower; | |
80 } else { | |
81 size = cli->cli_DefaultStack * 4; | |
82 } | |
83 return (ptr_t)(__base + GC_max(size, __stack)); | |
84 } | |
85 #endif | |
86 | |
87 | |
88 #endif | |
89 | |
90 | |
91 #ifdef GC_AMIGA_DS | |
92 /****************************************************************** | |
93 Register data segments. | |
94 ******************************************************************/ | |
95 | |
96 void GC_register_data_segments() | |
97 { | |
98 struct Process *proc; | |
99 struct CommandLineInterface *cli; | |
100 BPTR myseglist; | |
101 ULONG *data; | |
102 | |
103 int num; | |
104 | |
105 | |
106 # ifdef __GNUC__ | |
107 ULONG dataSegSize; | |
108 GC_bool found_segment = FALSE; | |
109 extern char __data_size[]; | |
110 | |
111 dataSegSize=__data_size+8; | |
112 /* Can`t find the Location of __data_size, because | |
113 it`s possible that is it, inside the segment. */ | |
114 | |
115 # endif | |
116 | |
117 proc= (struct Process*)SysBase->ThisTask; | |
118 | |
119 /* Reference: Amiga Guru Book Pages: 538ff,565,573 | |
120 and XOper.asm */ | |
121 if (proc->pr_Task.tc_Node.ln_Type==NT_PROCESS) { | |
122 if (proc->pr_CLI == NULL) { | |
123 myseglist = proc->pr_SegList; | |
124 } else { | |
125 /* ProcLoaded 'Loaded as a command: '*/ | |
126 cli = BADDR(proc->pr_CLI); | |
127 myseglist = cli->cli_Module; | |
128 } | |
129 } else { | |
130 ABORT("Not a Process."); | |
131 } | |
132 | |
133 if (myseglist == NULL) { | |
134 ABORT("Arrrgh.. can't find segments, aborting"); | |
135 } | |
136 | |
137 /* xoper hunks Shell Process */ | |
138 | |
139 num=0; | |
140 for (data = (ULONG *)BADDR(myseglist); data != NULL; | |
141 data = (ULONG *)BADDR(data[0])) { | |
142 if (((ULONG) GC_register_data_segments < (ULONG) &data[1]) || | |
143 ((ULONG) GC_register_data_segments > (ULONG) &data[1] + data[-1])) { | |
144 # ifdef __GNUC__ | |
145 if (dataSegSize == data[-1]) { | |
146 found_segment = TRUE; | |
147 } | |
148 # endif | |
149 GC_add_roots_inner((char *)&data[1], | |
150 ((char *)&data[1]) + data[-1], FALSE); | |
151 } | |
152 ++num; | |
153 } /* for */ | |
154 # ifdef __GNUC__ | |
155 if (!found_segment) { | |
156 ABORT("Can`t find correct Segments.\nSolution: Use an newer version of ixemul.library"); | |
157 } | |
158 # endif | |
159 } | |
160 | |
161 #if 0 /* old version */ | |
162 void GC_register_data_segments() | |
163 { | |
164 extern struct WBStartup *_WBenchMsg; | |
165 struct Process *proc; | |
166 struct CommandLineInterface *cli; | |
167 BPTR myseglist; | |
168 ULONG *data; | |
169 | |
170 if ( _WBenchMsg != 0 ) { | |
171 if ((myseglist = _WBenchMsg->sm_Segment) == 0) { | |
172 GC_err_puts("No seglist from workbench\n"); | |
173 return; | |
174 } | |
175 } else { | |
176 if ((proc = (struct Process *)FindTask(0)) == 0) { | |
177 GC_err_puts("Cannot find process structure\n"); | |
178 return; | |
179 } | |
180 if ((cli = BADDR(proc->pr_CLI)) == 0) { | |
181 GC_err_puts("No CLI\n"); | |
182 return; | |
183 } | |
184 if ((myseglist = cli->cli_Module) == 0) { | |
185 GC_err_puts("No seglist from CLI\n"); | |
186 return; | |
187 } | |
188 } | |
189 | |
190 for (data = (ULONG *)BADDR(myseglist); data != 0; | |
191 data = (ULONG *)BADDR(data[0])) { | |
192 # ifdef AMIGA_SKIP_SEG | |
193 if (((ULONG) GC_register_data_segments < (ULONG) &data[1]) || | |
194 ((ULONG) GC_register_data_segments > (ULONG) &data[1] + data[-1])) { | |
195 # else | |
196 { | |
197 # endif /* AMIGA_SKIP_SEG */ | |
198 GC_add_roots_inner((char *)&data[1], | |
199 ((char *)&data[1]) + data[-1], FALSE); | |
200 } | |
201 } | |
202 } | |
203 #endif /* old version */ | |
204 | |
205 | |
206 #endif | |
207 | |
208 | |
209 | |
210 #ifdef GC_AMIGA_AM | |
211 | |
212 #ifndef GC_AMIGA_FASTALLOC | |
213 | |
214 void *GC_amiga_allocwrapper(size_t size,void *(*AllocFunction)(size_t size2)){ | |
215 return (*AllocFunction)(size); | |
216 } | |
217 | |
218 void *(*GC_amiga_allocwrapper_do)(size_t size,void *(*AllocFunction)(size_t size2)) | |
219 =GC_amiga_allocwrapper; | |
220 | |
221 #else | |
222 | |
223 | |
224 | |
225 | |
226 void *GC_amiga_allocwrapper_firsttime(size_t size,void *(*AllocFunction)(size_t size2)); | |
227 | |
228 void *(*GC_amiga_allocwrapper_do)(size_t size,void *(*AllocFunction)(size_t size2)) | |
229 =GC_amiga_allocwrapper_firsttime; | |
230 | |
231 | |
232 /****************************************************************** | |
233 Amiga-spesific routines to obtain memory, and force GC to give | |
234 back fast-mem whenever possible. | |
235 These hacks makes gc-programs go many times faster when | |
236 the amiga is low on memory, and are therefore strictly necesarry. | |
237 | |
238 -Kjetil S. Matheussen, 2000. | |
239 ******************************************************************/ | |
240 | |
241 | |
242 | |
243 /* List-header for all allocated memory. */ | |
244 | |
245 struct GC_Amiga_AllocedMemoryHeader{ | |
246 ULONG size; | |
247 struct GC_Amiga_AllocedMemoryHeader *next; | |
248 }; | |
249 struct GC_Amiga_AllocedMemoryHeader *GC_AMIGAMEM=(struct GC_Amiga_AllocedMemoryHeader *)(int)~(NULL); | |
250 | |
251 | |
252 | |
253 /* Type of memory. Once in the execution of a program, this might change to MEMF_ANY|MEMF_CLEAR */ | |
254 | |
255 ULONG GC_AMIGA_MEMF = MEMF_FAST | MEMF_CLEAR; | |
256 | |
257 | |
258 /* Prevents GC_amiga_get_mem from allocating memory if this one is TRUE. */ | |
259 #ifndef GC_AMIGA_ONLYFAST | |
260 BOOL GC_amiga_dontalloc=FALSE; | |
261 #endif | |
262 | |
263 #ifdef GC_AMIGA_PRINTSTATS | |
264 int succ=0,succ2=0; | |
265 int nsucc=0,nsucc2=0; | |
266 int nullretries=0; | |
267 int numcollects=0; | |
268 int chipa=0; | |
269 int allochip=0; | |
270 int allocfast=0; | |
271 int cur0=0; | |
272 int cur1=0; | |
273 int cur10=0; | |
274 int cur50=0; | |
275 int cur150=0; | |
276 int cur151=0; | |
277 int ncur0=0; | |
278 int ncur1=0; | |
279 int ncur10=0; | |
280 int ncur50=0; | |
281 int ncur150=0; | |
282 int ncur151=0; | |
283 #endif | |
284 | |
285 /* Free everything at program-end. */ | |
286 | |
287 void GC_amiga_free_all_mem(void){ | |
288 struct GC_Amiga_AllocedMemoryHeader *gc_am=(struct GC_Amiga_AllocedMemoryHeader *)(~(int)(GC_AMIGAMEM)); | |
289 struct GC_Amiga_AllocedMemoryHeader *temp; | |
290 | |
291 #ifdef GC_AMIGA_PRINTSTATS | |
292 printf("\n\n" | |
293 "%d bytes of chip-mem, and %d bytes of fast-mem where allocated from the OS.\n", | |
294 allochip,allocfast | |
295 ); | |
296 printf( | |
297 "%d bytes of chip-mem were returned from the GC_AMIGA_FASTALLOC supported allocating functions.\n", | |
298 chipa | |
299 ); | |
300 printf("\n"); | |
301 printf("GC_gcollect was called %d times to avoid returning NULL or start allocating with the MEMF_ANY flag.\n",numcollects); | |
302 printf("%d of them was a success. (the others had to use allocation from the OS.)\n",nullretries); | |
303 printf("\n"); | |
304 printf("Succeded forcing %d gc-allocations (%d bytes) of chip-mem to be fast-mem.\n",succ,succ2); | |
305 printf("Failed forcing %d gc-allocations (%d bytes) of chip-mem to be fast-mem.\n",nsucc,nsucc2); | |
306 printf("\n"); | |
307 printf( | |
308 "Number of retries before succeding a chip->fast force:\n" | |
309 "0: %d, 1: %d, 2-9: %d, 10-49: %d, 50-149: %d, >150: %d\n", | |
310 cur0,cur1,cur10,cur50,cur150,cur151 | |
311 ); | |
312 printf( | |
313 "Number of retries before giving up a chip->fast force:\n" | |
314 "0: %d, 1: %d, 2-9: %d, 10-49: %d, 50-149: %d, >150: %d\n", | |
315 ncur0,ncur1,ncur10,ncur50,ncur150,ncur151 | |
316 ); | |
317 #endif | |
318 | |
319 while(gc_am!=NULL){ | |
320 temp=gc_am->next; | |
321 FreeMem(gc_am,gc_am->size); | |
322 gc_am=(struct GC_Amiga_AllocedMemoryHeader *)(~(int)(temp)); | |
323 } | |
324 } | |
325 | |
326 #ifndef GC_AMIGA_ONLYFAST | |
327 | |
328 /* All memory with address lower than this one is chip-mem. */ | |
329 | |
330 char *chipmax; | |
331 | |
332 | |
333 /* | |
334 * Allways set to the last size of memory tried to be allocated. | |
335 * Needed to ensure allocation when the size is bigger than 100000. | |
336 * | |
337 */ | |
338 size_t latestsize; | |
339 | |
340 #endif | |
341 | |
342 | |
343 /* | |
344 * The actual function that is called with the GET_MEM macro. | |
345 * | |
346 */ | |
347 | |
348 void *GC_amiga_get_mem(size_t size){ | |
349 struct GC_Amiga_AllocedMemoryHeader *gc_am; | |
350 | |
351 #ifndef GC_AMIGA_ONLYFAST | |
352 if(GC_amiga_dontalloc==TRUE){ | |
353 // printf("rejected, size: %d, latestsize: %d\n",size,latestsize); | |
354 return NULL; | |
355 } | |
356 | |
357 // We really don't want to use chip-mem, but if we must, then as little as possible. | |
358 if(GC_AMIGA_MEMF==(MEMF_ANY|MEMF_CLEAR) && size>100000 && latestsize<50000) return NULL; | |
359 #endif | |
360 | |
361 gc_am=AllocMem((ULONG)(size + sizeof(struct GC_Amiga_AllocedMemoryHeader)),GC_AMIGA_MEMF); | |
362 if(gc_am==NULL) return NULL; | |
363 | |
364 gc_am->next=GC_AMIGAMEM; | |
365 gc_am->size=size + sizeof(struct GC_Amiga_AllocedMemoryHeader); | |
366 GC_AMIGAMEM=(struct GC_Amiga_AllocedMemoryHeader *)(~(int)(gc_am)); | |
367 | |
368 // printf("Allocated %d (%d) bytes at address: %x. Latest: %d\n",size,tot,gc_am,latestsize); | |
369 | |
370 #ifdef GC_AMIGA_PRINTSTATS | |
371 if((char *)gc_am<chipmax){ | |
372 allochip+=size; | |
373 }else{ | |
374 allocfast+=size; | |
375 } | |
376 #endif | |
377 | |
378 return gc_am+1; | |
379 | |
380 } | |
381 | |
382 | |
383 | |
384 | |
385 #ifndef GC_AMIGA_ONLYFAST | |
386 | |
387 /* Tries very hard to force GC to find fast-mem to return. Done recursively | |
388 * to hold the rejected memory-pointers reachable from the collector in an | |
389 * easy way. | |
390 * | |
391 */ | |
392 #ifdef GC_AMIGA_RETRY | |
393 void *GC_amiga_rec_alloc(size_t size,void *(*AllocFunction)(size_t size2),const int rec){ | |
394 void *ret; | |
395 | |
396 ret=(*AllocFunction)(size); | |
397 | |
398 #ifdef GC_AMIGA_PRINTSTATS | |
399 if((char *)ret>chipmax || ret==NULL){ | |
400 if(ret==NULL){ | |
401 nsucc++; | |
402 nsucc2+=size; | |
403 if(rec==0) ncur0++; | |
404 if(rec==1) ncur1++; | |
405 if(rec>1 && rec<10) ncur10++; | |
406 if(rec>=10 && rec<50) ncur50++; | |
407 if(rec>=50 && rec<150) ncur150++; | |
408 if(rec>=150) ncur151++; | |
409 }else{ | |
410 succ++; | |
411 succ2+=size; | |
412 if(rec==0) cur0++; | |
413 if(rec==1) cur1++; | |
414 if(rec>1 && rec<10) cur10++; | |
415 if(rec>=10 && rec<50) cur50++; | |
416 if(rec>=50 && rec<150) cur150++; | |
417 if(rec>=150) cur151++; | |
418 } | |
419 } | |
420 #endif | |
421 | |
422 if (((char *)ret)<=chipmax && ret!=NULL && (rec<(size>500000?9:size/5000))){ | |
423 ret=GC_amiga_rec_alloc(size,AllocFunction,rec+1); | |
424 // GC_free(ret2); | |
425 } | |
426 | |
427 return ret; | |
428 } | |
429 #endif | |
430 | |
431 | |
432 /* The allocating-functions defined inside the amiga-blocks in gc.h is called | |
433 * via these functions. | |
434 */ | |
435 | |
436 | |
437 void *GC_amiga_allocwrapper_any(size_t size,void *(*AllocFunction)(size_t size2)){ | |
438 void *ret,*ret2; | |
439 | |
440 GC_amiga_dontalloc=TRUE; // Pretty tough thing to do, but its indeed necesarry. | |
441 latestsize=size; | |
442 | |
443 ret=(*AllocFunction)(size); | |
444 | |
445 if(((char *)ret) <= chipmax){ | |
446 if(ret==NULL){ | |
447 //Give GC access to allocate memory. | |
448 #ifdef GC_AMIGA_GC | |
449 if(!GC_dont_gc){ | |
450 GC_gcollect(); | |
451 #ifdef GC_AMIGA_PRINTSTATS | |
452 numcollects++; | |
453 #endif | |
454 ret=(*AllocFunction)(size); | |
455 } | |
456 #endif | |
457 if(ret==NULL){ | |
458 GC_amiga_dontalloc=FALSE; | |
459 ret=(*AllocFunction)(size); | |
460 if(ret==NULL){ | |
461 WARN("Out of Memory! Returning NIL!\n", 0); | |
462 } | |
463 } | |
464 #ifdef GC_AMIGA_PRINTSTATS | |
465 else{ | |
466 nullretries++; | |
467 } | |
468 if(ret!=NULL && (char *)ret<=chipmax) chipa+=size; | |
469 #endif | |
470 } | |
471 #ifdef GC_AMIGA_RETRY | |
472 else{ | |
473 /* We got chip-mem. Better try again and again and again etc., we might get fast-mem sooner or later... */ | |
474 /* Using gctest to check the effectiviness of doing this, does seldom give a very good result. */ | |
475 /* However, real programs doesn't normally rapidly allocate and deallocate. */ | |
476 // printf("trying to force... %d bytes... ",size); | |
477 if( | |
478 AllocFunction!=GC_malloc_uncollectable | |
479 #ifdef ATOMIC_UNCOLLECTABLE | |
480 && AllocFunction!=GC_malloc_atomic_uncollectable | |
481 #endif | |
482 ){ | |
483 ret2=GC_amiga_rec_alloc(size,AllocFunction,0); | |
484 }else{ | |
485 ret2=(*AllocFunction)(size); | |
486 #ifdef GC_AMIGA_PRINTSTATS | |
487 if((char *)ret2<chipmax || ret2==NULL){ | |
488 nsucc++; | |
489 nsucc2+=size; | |
490 ncur0++; | |
491 }else{ | |
492 succ++; | |
493 succ2+=size; | |
494 cur0++; | |
495 } | |
496 #endif | |
497 } | |
498 if(((char *)ret2)>chipmax){ | |
499 // printf("Succeeded.\n"); | |
500 GC_free(ret); | |
501 ret=ret2; | |
502 }else{ | |
503 GC_free(ret2); | |
504 // printf("But did not succeed.\n"); | |
505 } | |
506 } | |
507 #endif | |
508 } | |
509 | |
510 GC_amiga_dontalloc=FALSE; | |
511 | |
512 return ret; | |
513 } | |
514 | |
515 | |
516 | |
517 void (*GC_amiga_toany)(void)=NULL; | |
518 | |
519 void GC_amiga_set_toany(void (*func)(void)){ | |
520 GC_amiga_toany=func; | |
521 } | |
522 | |
523 #endif // !GC_AMIGA_ONLYFAST | |
524 | |
525 | |
526 void *GC_amiga_allocwrapper_fast(size_t size,void *(*AllocFunction)(size_t size2)){ | |
527 void *ret; | |
528 | |
529 ret=(*AllocFunction)(size); | |
530 | |
531 if(ret==NULL){ | |
532 // Enable chip-mem allocation. | |
533 // printf("ret==NULL\n"); | |
534 #ifdef GC_AMIGA_GC | |
535 if(!GC_dont_gc){ | |
536 GC_gcollect(); | |
537 #ifdef GC_AMIGA_PRINTSTATS | |
538 numcollects++; | |
539 #endif | |
540 ret=(*AllocFunction)(size); | |
541 } | |
542 #endif | |
543 if(ret==NULL){ | |
544 #ifndef GC_AMIGA_ONLYFAST | |
545 GC_AMIGA_MEMF=MEMF_ANY | MEMF_CLEAR; | |
546 if(GC_amiga_toany!=NULL) (*GC_amiga_toany)(); | |
547 GC_amiga_allocwrapper_do=GC_amiga_allocwrapper_any; | |
548 return GC_amiga_allocwrapper_any(size,AllocFunction); | |
549 #endif | |
550 } | |
551 #ifdef GC_AMIGA_PRINTSTATS | |
552 else{ | |
553 nullretries++; | |
554 } | |
555 #endif | |
556 } | |
557 | |
558 return ret; | |
559 } | |
560 | |
561 void *GC_amiga_allocwrapper_firsttime(size_t size,void *(*AllocFunction)(size_t size2)){ | |
562 atexit(&GC_amiga_free_all_mem); | |
563 chipmax=(char *)SysBase->MaxLocMem; // For people still having SysBase in chip-mem, this might speed up a bit. | |
564 GC_amiga_allocwrapper_do=GC_amiga_allocwrapper_fast; | |
565 return GC_amiga_allocwrapper_fast(size,AllocFunction); | |
566 } | |
567 | |
568 | |
569 #endif //GC_AMIGA_FASTALLOC | |
570 | |
571 | |
572 | |
573 /* | |
574 * The wrapped realloc function. | |
575 * | |
576 */ | |
577 void *GC_amiga_realloc(void *old_object,size_t new_size_in_bytes){ | |
578 #ifndef GC_AMIGA_FASTALLOC | |
579 return GC_realloc(old_object,new_size_in_bytes); | |
580 #else | |
581 void *ret; | |
582 latestsize=new_size_in_bytes; | |
583 ret=GC_realloc(old_object,new_size_in_bytes); | |
584 if(ret==NULL && GC_AMIGA_MEMF==(MEMF_FAST | MEMF_CLEAR)){ | |
585 /* Out of fast-mem. */ | |
586 #ifdef GC_AMIGA_GC | |
587 if(!GC_dont_gc){ | |
588 GC_gcollect(); | |
589 #ifdef GC_AMIGA_PRINTSTATS | |
590 numcollects++; | |
591 #endif | |
592 ret=GC_realloc(old_object,new_size_in_bytes); | |
593 } | |
594 #endif | |
595 if(ret==NULL){ | |
596 #ifndef GC_AMIGA_ONLYFAST | |
597 GC_AMIGA_MEMF=MEMF_ANY | MEMF_CLEAR; | |
598 if(GC_amiga_toany!=NULL) (*GC_amiga_toany)(); | |
599 GC_amiga_allocwrapper_do=GC_amiga_allocwrapper_any; | |
600 ret=GC_realloc(old_object,new_size_in_bytes); | |
601 #endif | |
602 } | |
603 #ifdef GC_AMIGA_PRINTSTATS | |
604 else{ | |
605 nullretries++; | |
606 } | |
607 #endif | |
608 } | |
609 if(ret==NULL){ | |
610 WARN("Out of Memory! Returning NIL!\n", 0); | |
611 } | |
612 #ifdef GC_AMIGA_PRINTSTATS | |
613 if(((char *)ret)<chipmax && ret!=NULL){ | |
614 chipa+=new_size_in_bytes; | |
615 } | |
616 #endif | |
617 return ret; | |
618 #endif | |
619 } | |
620 | |
621 #endif //GC_AMIGA_AM | |
622 | |
623 |