Line data Source code
1 : /*
2 : * SPDX-License-Identifier: MPL-2.0
3 : *
4 : * This Source Code Form is subject to the terms of the Mozilla Public
5 : * License, v. 2.0. If a copy of the MPL was not distributed with this
6 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 : *
8 : * Copyright 2024 MonetDB Foundation;
9 : * Copyright August 2008 - 2023 MonetDB B.V.;
10 : * Copyright 1997 - July 2008 CWI.
11 : */
12 :
13 : #include "monetdb_config.h"
14 : #include "gdk.h"
15 : #include "gdk_private.h"
16 :
17 : #define ORDERIDX_VERSION ((oid) 3)
18 :
19 : #ifdef PERSISTENTIDX
20 : static void
21 6 : BATidxsync(void *arg)
22 : {
23 6 : BAT *b = arg;
24 6 : Heap *hp;
25 6 : int fd;
26 6 : lng t0 = GDKusec();
27 6 : const char *failed = " failed";
28 :
29 :
30 6 : MT_lock_set(&b->batIdxLock);
31 6 : if ((hp = b->torderidx) != NULL) {
32 6 : if (HEAPsave(hp, hp->filename, NULL, true, hp->free, NULL) == GDK_SUCCEED) {
33 6 : if (hp->storage == STORE_MEM) {
34 6 : if ((fd = GDKfdlocate(hp->farmid, hp->filename, "rb+", NULL)) >= 0) {
35 6 : ((oid *) hp->base)[0] |= (oid) 1 << 24;
36 6 : if (write(fd, hp->base, SIZEOF_OID) >= 0) {
37 6 : failed = ""; /* not failed */
38 6 : if (!(ATOMIC_GET(&GDKdebug) & NOSYNCMASK)) {
39 : #if defined(NATIVE_WIN32)
40 : _commit(fd);
41 : #elif defined(HAVE_FDATASYNC)
42 0 : fdatasync(fd);
43 : #elif defined(HAVE_FSYNC)
44 : fsync(fd);
45 : #endif
46 : }
47 6 : hp->dirty = false;
48 : } else {
49 0 : perror("write hash");
50 : }
51 6 : close(fd);
52 : }
53 : } else {
54 0 : ((oid *) hp->base)[0] |= (oid) 1 << 24;
55 0 : if (!(ATOMIC_GET(&GDKdebug) & NOSYNCMASK) &&
56 0 : MT_msync(hp->base, SIZEOF_OID) < 0) {
57 0 : ((oid *) hp->base)[0] &= ~((oid) 1 << 24);
58 : } else {
59 0 : hp->dirty = false;
60 0 : failed = ""; /* not failed */
61 : }
62 : }
63 6 : TRC_DEBUG(ACCELERATOR, "BATidxsync(%s): orderidx persisted"
64 : " (" LLFMT " usec)%s\n",
65 : BATgetId(b), GDKusec() - t0, failed);
66 : }
67 : }
68 6 : MT_lock_unset(&b->batIdxLock);
69 6 : BBPunfix(b->batCacheid);
70 6 : }
71 : #endif
72 :
73 : /* return TRUE if we have a orderidx on the tail, even if we need to read
74 : * one from disk */
75 : bool
76 1848610 : BATcheckorderidx(BAT *b)
77 : {
78 1848610 : bool ret;
79 1848610 : lng t = GDKusec();
80 :
81 1848597 : if (b == NULL)
82 : return false;
83 1848597 : MT_lock_set(&b->batIdxLock);
84 1848545 : if (b->torderidx == (Heap *) 1) {
85 0 : Heap *hp;
86 0 : const char *nme = BBP_physical(b->batCacheid);
87 0 : int fd;
88 :
89 0 : assert(!GDKinmemory(b->theap->farmid));
90 0 : b->torderidx = NULL;
91 0 : if ((hp = GDKzalloc(sizeof(*hp))) != NULL &&
92 0 : (hp->farmid = BBPselectfarm(b->batRole, b->ttype, orderidxheap)) >= 0) {
93 0 : strconcat_len(hp->filename,
94 : sizeof(hp->filename),
95 : nme, ".torderidx", NULL);
96 0 : hp->storage = hp->newstorage = STORE_INVALID;
97 :
98 : /* check whether a persisted orderidx can be found */
99 0 : if ((fd = GDKfdlocate(hp->farmid, nme, "rb+", "torderidx")) >= 0) {
100 0 : struct stat st;
101 0 : oid hdata[ORDERIDXOFF];
102 :
103 0 : if (read(fd, hdata, sizeof(hdata)) == sizeof(hdata) &&
104 0 : hdata[0] == (
105 : #ifdef PERSISTENTIDX
106 : ((oid) 1 << 24) |
107 : #endif
108 0 : ORDERIDX_VERSION) &&
109 0 : hdata[1] == (oid) BATcount(b) &&
110 0 : (hdata[2] == 0 || hdata[2] == 1) &&
111 0 : fstat(fd, &st) == 0 &&
112 0 : st.st_size >= (off_t) (hp->size = hp->free = (ORDERIDXOFF + hdata[1]) * SIZEOF_OID) &&
113 0 : HEAPload(hp, nme, "torderidx", false) == GDK_SUCCEED) {
114 0 : close(fd);
115 0 : ATOMIC_INIT(&hp->refs, 1);
116 0 : b->torderidx = hp;
117 0 : hp->hasfile = true;
118 0 : TRC_DEBUG(ACCELERATOR, "BATcheckorderidx(" ALGOBATFMT "): reusing persisted orderidx\n", ALGOBATPAR(b));
119 0 : MT_lock_unset(&b->batIdxLock);
120 0 : return true;
121 : }
122 0 : close(fd);
123 : /* unlink unusable file */
124 0 : GDKunlink(hp->farmid, BATDIR, nme, "torderidx");
125 0 : hp->hasfile = false;
126 : }
127 : }
128 0 : GDKfree(hp);
129 0 : GDKclrerr(); /* we're not currently interested in errors */
130 : }
131 1848545 : MT_lock_unset(&b->batIdxLock);
132 :
133 1848609 : ret = b->torderidx != NULL;
134 1848609 : if (ret)
135 139 : TRC_DEBUG(ACCELERATOR, "BATcheckorderidx(" ALGOBATFMT "): already has orderidx, waited " LLFMT " usec\n", ALGOBATPAR(b), GDKusec() - t);
136 : return ret;
137 : }
138 :
139 : /* create the heap for an order index; returns NULL on failure */
140 : Heap *
141 4343 : createOIDXheap(BAT *b, bool stable)
142 : {
143 4343 : Heap *m;
144 4343 : oid *restrict mv;
145 :
146 4343 : if ((m = GDKmalloc(sizeof(Heap))) == NULL)
147 : return NULL;
148 8686 : *m = (Heap) {
149 4343 : .farmid = BBPselectfarm(b->batRole, b->ttype, orderidxheap),
150 4343 : .parentid = b->batCacheid,
151 : .dirty = true,
152 : .refs = ATOMIC_VAR_INIT(1),
153 : };
154 4343 : strconcat_len(m->filename, sizeof(m->filename),
155 4343 : BBP_physical(b->batCacheid), ".torderidx", NULL);
156 8686 : if (m->farmid < 0 ||
157 4343 : HEAPalloc(m, BATcount(b) + ORDERIDXOFF, SIZEOF_OID) != GDK_SUCCEED) {
158 0 : GDKfree(m);
159 0 : return NULL;
160 : }
161 4343 : m->free = (BATcount(b) + ORDERIDXOFF) * SIZEOF_OID;
162 :
163 4343 : mv = (oid *) m->base;
164 4343 : *mv++ = ORDERIDX_VERSION;
165 4343 : *mv++ = (oid) BATcount(b);
166 4343 : *mv++ = (oid) stable;
167 4343 : return m;
168 : }
169 :
170 : /* maybe persist the order index heap */
171 : void
172 4343 : persistOIDX(BAT *b)
173 : {
174 : #ifdef PERSISTENTIDX
175 4343 : if ((BBP_status(b->batCacheid) & BBPEXISTING) &&
176 8 : b->batInserted == b->batCount &&
177 12 : !b->theap->dirty &&
178 12 : !GDKinmemory(b->theap->farmid)) {
179 6 : MT_Id tid;
180 6 : BBPfix(b->batCacheid);
181 6 : char name[MT_NAME_LEN];
182 6 : snprintf(name, sizeof(name), "oidxsync%d", b->batCacheid);
183 6 : if (MT_create_thread(&tid, BATidxsync, b,
184 : MT_THR_DETACHED, name) < 0)
185 0 : BBPunfix(b->batCacheid);
186 : } else
187 4337 : TRC_DEBUG(ACCELERATOR, "persistOIDX(" ALGOBATFMT "): NOT persisting order index\n", ALGOBATPAR(b));
188 : #else
189 : (void) b;
190 : #endif
191 4343 : }
192 :
193 : gdk_return
194 73 : BATorderidx(BAT *b, bool stable)
195 : {
196 73 : if (b->ttype == TYPE_void) {
197 0 : GDKerror("No order index on void type bats\n");
198 0 : return GDK_FAIL;
199 : }
200 73 : if (BATcheckorderidx(b))
201 : return GDK_SUCCEED;
202 73 : if (!BATtdense(b)) {
203 73 : BAT *on;
204 73 : MT_thread_setalgorithm("create order index");
205 73 : TRC_DEBUG(ACCELERATOR, "BATorderidx(" ALGOBATFMT ",%d) create index\n", ALGOBATPAR(b), stable);
206 73 : if (BATsort(NULL, &on, NULL, b, NULL, NULL, false, false, stable) != GDK_SUCCEED)
207 0 : return GDK_FAIL;
208 73 : assert(BATcount(b) == BATcount(on));
209 73 : if (BATtdense(on)) {
210 : /* if the order bat is dense, the input was
211 : * sorted and we don't need an order index */
212 0 : MT_lock_set(&b->theaplock);
213 0 : assert(!b->tnosorted);
214 0 : if (!b->tsorted) {
215 0 : b->tsorted = true;
216 0 : b->tnosorted = 0;
217 : }
218 0 : MT_lock_unset(&b->theaplock);
219 : } else {
220 : /* BATsort quite possibly already created the
221 : * order index, but just to be sure... */
222 73 : MT_lock_set(&b->batIdxLock);
223 73 : if (b->torderidx == NULL) {
224 0 : Heap *m;
225 0 : if ((m = createOIDXheap(b, stable)) == NULL) {
226 0 : MT_lock_unset(&b->batIdxLock);
227 0 : return GDK_FAIL;
228 : }
229 0 : memcpy((oid *) m->base + ORDERIDXOFF, Tloc(on, 0), BATcount(on) * sizeof(oid));
230 0 : b->torderidx = m;
231 0 : persistOIDX(b);
232 : }
233 73 : MT_lock_unset(&b->batIdxLock);
234 : }
235 73 : BBPunfix(on->batCacheid);
236 : }
237 : return GDK_SUCCEED;
238 : }
239 :
240 : #define BINARY_MERGE(TYPE) \
241 : do { \
242 : TYPE *v = (TYPE *) bi.base; \
243 : if (p0 < q0 && p1 < q1) { \
244 : if (v[*p0 - b->hseqbase] <= v[*p1 - b->hseqbase]) { \
245 : *mv++ = *p0++; \
246 : } else { \
247 : *mv++ = *p1++; \
248 : } \
249 : } else if (p0 < q0) { \
250 : assert(p1 == q1); \
251 : *mv++ = *p0++; \
252 : } else if (p1 < q1) { \
253 : assert(p0 == q0); \
254 : *mv++ = *p1++; \
255 : } else { \
256 : assert(p0 == q0 && p1 == q1); \
257 : break; \
258 : } \
259 : while (p0 < q0 && p1 < q1) { \
260 : if (v[*p0 - b->hseqbase] <= v[*p1 - b->hseqbase]) { \
261 : *mv++ = *p0++; \
262 : } else { \
263 : *mv++ = *p1++; \
264 : } \
265 : } \
266 : while (p0 < q0) { \
267 : *mv++ = *p0++; \
268 : } \
269 : while (p1 < q1) { \
270 : *mv++ = *p1++; \
271 : } \
272 : } while(0)
273 :
274 : #define swap(X,Y,TMP) (TMP)=(X);(X)=(Y);(Y)=(TMP)
275 :
276 : #define left_child(X) (2*(X)+1)
277 : #define right_child(X) (2*(X)+2)
278 :
279 : #define HEAPIFY(X) \
280 : do { \
281 : int cur, min = X, chld; \
282 : do { \
283 : cur = min; \
284 : if ((chld = left_child(cur)) < n_ar && \
285 : (minhp[chld] < minhp[min] || \
286 : (minhp[chld] == minhp[min] && \
287 : *p[cur] < *p[min]))) { \
288 : min = chld; \
289 : } \
290 : if ((chld = right_child(cur)) < n_ar && \
291 : (minhp[chld] < minhp[min] || \
292 : (minhp[chld] == minhp[min] && \
293 : *p[cur] < *p[min]))) { \
294 : min = chld; \
295 : } \
296 : if (min != cur) { \
297 : swap(minhp[cur], minhp[min], t); \
298 : swap(p[cur], p[min], t_oid); \
299 : swap(q[cur], q[min], t_oid); \
300 : } \
301 : } while (cur != min); \
302 : } while (0)
303 :
304 : #define NWAY_MERGE(TYPE) \
305 : do { \
306 : TYPE *minhp, t; \
307 : TYPE *v = (TYPE *) bi.base; \
308 : if ((minhp = GDKmalloc(sizeof(TYPE)*n_ar)) == NULL) { \
309 : goto bailout; \
310 : } \
311 : /* init min heap */ \
312 : for (i = 0; i < n_ar; i++) { \
313 : minhp[i] = v[*p[i] - b->hseqbase]; \
314 : } \
315 : for (i = n_ar/2; i >=0 ; i--) { \
316 : HEAPIFY(i); \
317 : } \
318 : /* merge */ \
319 : *mv++ = *(p[0])++; \
320 : if (p[0] < q[0]) { \
321 : minhp[0] = v[*p[0] - b->hseqbase]; \
322 : HEAPIFY(0); \
323 : } else { \
324 : swap(minhp[0], minhp[n_ar-1], t); \
325 : swap(p[0], p[n_ar-1], t_oid); \
326 : swap(q[0], q[n_ar-1], t_oid); \
327 : n_ar--; \
328 : HEAPIFY(0); \
329 : } \
330 : while (n_ar > 1) { \
331 : *mv++ = *(p[0])++; \
332 : if (p[0] < q[0]) { \
333 : minhp[0] = v[*p[0] - b->hseqbase]; \
334 : HEAPIFY(0); \
335 : } else { \
336 : swap(minhp[0], minhp[n_ar-1], t); \
337 : swap(p[0], p[n_ar-1], t_oid); \
338 : swap(q[0], q[n_ar-1], t_oid); \
339 : n_ar--; \
340 : HEAPIFY(0); \
341 : } \
342 : } \
343 : while (p[0] < q[0]) { \
344 : *mv++ = *(p[0])++; \
345 : } \
346 : GDKfree(minhp); \
347 : } while (0)
348 :
349 : gdk_return
350 0 : GDKmergeidx(BAT *b, BAT**a, int n_ar)
351 : {
352 0 : Heap *m;
353 0 : int i;
354 0 : oid *restrict mv;
355 0 : const char *nme = BBP_physical(b->batCacheid);
356 :
357 0 : if (BATcheckorderidx(b))
358 : return GDK_SUCCEED;
359 0 : switch (ATOMbasetype(b->ttype)) {
360 : case TYPE_bte:
361 : case TYPE_sht:
362 : case TYPE_int:
363 : case TYPE_lng:
364 : #ifdef HAVE_HGE
365 : case TYPE_hge:
366 : #endif
367 : case TYPE_flt:
368 : case TYPE_dbl:
369 0 : break;
370 0 : default:
371 0 : GDKerror("type %s not supported.\n", ATOMname(b->ttype));
372 0 : return GDK_FAIL;
373 : }
374 0 : TRC_DEBUG(ACCELERATOR, "GDKmergeidx(" ALGOBATFMT ") create index\n", ALGOBATPAR(b));
375 0 : BATiter bi = bat_iterator(b);
376 0 : MT_lock_set(&b->batIdxLock);
377 0 : if (b->torderidx) {
378 0 : MT_lock_unset(&b->batIdxLock);
379 0 : bat_iterator_end(&bi);
380 0 : return GDK_SUCCEED;
381 : }
382 0 : if ((m = GDKmalloc(sizeof(Heap))) == NULL) {
383 0 : MT_lock_unset(&b->batIdxLock);
384 0 : bat_iterator_end(&bi);
385 0 : return GDK_FAIL;
386 : }
387 0 : *m = (Heap) {
388 0 : .farmid = BBPselectfarm(b->batRole, bi.type, orderidxheap),
389 0 : .parentid = b->batCacheid,
390 : .dirty = true,
391 : .refs = ATOMIC_VAR_INIT(1),
392 : };
393 0 : strconcat_len(m->filename, sizeof(m->filename),
394 : nme, ".torderidx", NULL);
395 0 : if (m->farmid < 0 ||
396 0 : HEAPalloc(m, BATcount(b) + ORDERIDXOFF, SIZEOF_OID) != GDK_SUCCEED) {
397 0 : GDKfree(m);
398 0 : MT_lock_unset(&b->batIdxLock);
399 0 : bat_iterator_end(&bi);
400 0 : return GDK_FAIL;
401 : }
402 0 : m->free = (BATcount(b) + ORDERIDXOFF) * SIZEOF_OID;
403 :
404 0 : mv = (oid *) m->base;
405 0 : *mv++ = ORDERIDX_VERSION;
406 0 : *mv++ = (oid) BATcount(b);
407 : /* all participating indexes must be stable for the combined
408 : * index to be stable */
409 0 : *mv = 1;
410 0 : for (i = 0; i < n_ar; i++) {
411 0 : if ((*mv &= ((const oid *) a[i]->torderidx->base)[2]) == 0)
412 : break;
413 : }
414 0 : mv++;
415 :
416 0 : if (n_ar == 1) {
417 : /* One oid order bat, nothing to merge */
418 0 : assert(BATcount(a[0]) == BATcount(b));
419 0 : assert((VIEWtparent(a[0]) == b->batCacheid ||
420 : VIEWtparent(a[0]) == VIEWtparent(b)) &&
421 : a[0]->torderidx);
422 0 : memcpy(mv, (const oid *) a[0]->torderidx->base + ORDERIDXOFF,
423 : BATcount(a[0]) * SIZEOF_OID);
424 0 : } else if (n_ar == 2) {
425 : /* sort merge with 1 comparison per BUN */
426 0 : const oid *restrict p0, *restrict p1, *q0, *q1;
427 0 : assert(BATcount(a[0]) + BATcount(a[1]) == BATcount(b));
428 0 : assert((VIEWtparent(a[0]) == b->batCacheid ||
429 : VIEWtparent(a[0]) == VIEWtparent(b)) &&
430 : a[0]->torderidx);
431 0 : assert((VIEWtparent(a[1]) == b->batCacheid ||
432 : VIEWtparent(a[1]) == VIEWtparent(b)) &&
433 : a[1]->torderidx);
434 0 : p0 = (const oid *) a[0]->torderidx->base + ORDERIDXOFF;
435 0 : p1 = (const oid *) a[1]->torderidx->base + ORDERIDXOFF;
436 0 : q0 = p0 + BATcount(a[0]);
437 0 : q1 = p1 + BATcount(a[1]);
438 :
439 0 : switch (ATOMbasetype(bi.type)) {
440 0 : case TYPE_bte: BINARY_MERGE(bte); break;
441 0 : case TYPE_sht: BINARY_MERGE(sht); break;
442 0 : case TYPE_int: BINARY_MERGE(int); break;
443 0 : case TYPE_lng: BINARY_MERGE(lng); break;
444 : #ifdef HAVE_HGE
445 0 : case TYPE_hge: BINARY_MERGE(hge); break;
446 : #endif
447 0 : case TYPE_flt: BINARY_MERGE(flt); break;
448 0 : case TYPE_dbl: BINARY_MERGE(dbl); break;
449 : default:
450 : /* TODO: support strings, date, timestamps etc. */
451 0 : assert(0);
452 : HEAPfree(m, true);
453 : GDKfree(m);
454 : MT_lock_unset(&b->batIdxLock);
455 : bat_iterator_end(&bi);
456 : return GDK_FAIL;
457 : }
458 :
459 : } else {
460 : /* use min-heap */
461 0 : oid **p, **q, *t_oid;
462 :
463 0 : p = GDKmalloc(n_ar*sizeof(oid *));
464 0 : q = GDKmalloc(n_ar*sizeof(oid *));
465 0 : if (p == NULL || q == NULL) {
466 0 : bailout:
467 0 : GDKfree(p);
468 0 : GDKfree(q);
469 0 : HEAPfree(m, true);
470 0 : GDKfree(m);
471 0 : MT_lock_unset(&b->batIdxLock);
472 0 : bat_iterator_end(&bi);
473 0 : return GDK_FAIL;
474 : }
475 0 : for (i = 0; i < n_ar; i++) {
476 0 : assert((VIEWtparent(a[i]) == b->batCacheid ||
477 : VIEWtparent(a[i]) == VIEWtparent(b)) &&
478 : a[i]->torderidx);
479 0 : p[i] = (oid *) a[i]->torderidx->base + ORDERIDXOFF;
480 0 : q[i] = p[i] + BATcount(a[i]);
481 : }
482 :
483 0 : switch (ATOMbasetype(bi.type)) {
484 0 : case TYPE_bte: NWAY_MERGE(bte); break;
485 0 : case TYPE_sht: NWAY_MERGE(sht); break;
486 0 : case TYPE_int: NWAY_MERGE(int); break;
487 0 : case TYPE_lng: NWAY_MERGE(lng); break;
488 : #ifdef HAVE_HGE
489 0 : case TYPE_hge: NWAY_MERGE(hge); break;
490 : #endif
491 0 : case TYPE_flt: NWAY_MERGE(flt); break;
492 0 : case TYPE_dbl: NWAY_MERGE(dbl); break;
493 : case TYPE_void:
494 : case TYPE_str:
495 : case TYPE_ptr:
496 : default:
497 : /* TODO: support strings, date, timestamps etc. */
498 0 : assert(0);
499 : goto bailout;
500 : }
501 0 : GDKfree(p);
502 0 : GDKfree(q);
503 : }
504 :
505 0 : b->torderidx = m;
506 : #ifdef PERSISTENTIDX
507 0 : if ((BBP_status(b->batCacheid) & BBPEXISTING) &&
508 0 : b->batInserted == b->batCount) {
509 0 : MT_Id tid;
510 0 : BBPfix(b->batCacheid);
511 0 : char name[MT_NAME_LEN];
512 0 : snprintf(name, sizeof(name), "oidxsync%d", b->batCacheid);
513 0 : if (MT_create_thread(&tid, BATidxsync, b,
514 : MT_THR_DETACHED, name) < 0)
515 0 : BBPunfix(b->batCacheid);
516 : } else
517 0 : TRC_DEBUG(ACCELERATOR, "GDKmergeidx(%s): NOT persisting index\n", BATgetId(b));
518 : #endif
519 :
520 0 : MT_lock_unset(&b->batIdxLock);
521 0 : bat_iterator_end(&bi);
522 0 : return GDK_SUCCEED;
523 : }
524 :
525 : void
526 355272 : OIDXfree(BAT *b)
527 : {
528 355272 : if (b) {
529 355272 : Heap *hp;
530 :
531 355272 : MT_lock_set(&b->batIdxLock);
532 355272 : if ((hp = b->torderidx) != NULL && hp != (Heap *) 1) {
533 3 : if (GDKinmemory(b->theap->farmid)) {
534 0 : b->torderidx = NULL;
535 0 : HEAPdecref(hp, true);
536 : } else {
537 3 : b->torderidx = (Heap *) 1;
538 3 : HEAPdecref(hp, false);
539 : }
540 : }
541 355272 : MT_lock_unset(&b->batIdxLock);
542 : }
543 355272 : }
544 :
545 : void
546 76099136 : OIDXdestroy(BAT *b)
547 : {
548 76099136 : if (b) {
549 76099136 : Heap *hp;
550 :
551 76099136 : MT_lock_set(&b->batIdxLock);
552 76065415 : hp = b->torderidx;
553 76065415 : b->torderidx = NULL;
554 76065415 : MT_lock_unset(&b->batIdxLock);
555 76094994 : if (hp == (Heap *) 1) {
556 0 : GDKunlink(BBPselectfarm(b->batRole, b->ttype, orderidxheap),
557 : BATDIR,
558 0 : BBP_physical(b->batCacheid),
559 : "torderidx");
560 76094994 : } else if (hp != NULL) {
561 4340 : HEAPdecref(hp, true);
562 : }
563 : }
564 76094994 : }
|