Line | Count | Source (jump to first uncovered line) |
1 | | /* falloc.c - The file space management routines for dbm. */ |
2 | | |
3 | | /* This file is part of GDBM, the GNU data base manager. |
4 | | Copyright (C) 1990-2023 Free Software Foundation, Inc. |
5 | | |
6 | | GDBM is free software; you can redistribute it and/or modify |
7 | | it under the terms of the GNU General Public License as published by |
8 | | the Free Software Foundation; either version 3, or (at your option) |
9 | | any later version. |
10 | | |
11 | | GDBM is distributed in the hope that it will be useful, |
12 | | but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | | GNU General Public License for more details. |
15 | | |
16 | | You should have received a copy of the GNU General Public License |
17 | | along with GDBM. If not, see <http://www.gnu.org/licenses/>. */ |
18 | | |
19 | | /* Include system configuration before all else. */ |
20 | | #include "autoconf.h" |
21 | | |
22 | | #include "gdbmdefs.h" |
23 | | |
24 | | |
25 | | /* The forward definitions for this file. See the functions for |
26 | | the definition of the function. */ |
27 | | |
28 | | static avail_elem get_elem (int, avail_elem [], int *); |
29 | | static avail_elem get_block (int, GDBM_FILE); |
30 | | static int push_avail_block (GDBM_FILE); |
31 | | static int pop_avail_block (GDBM_FILE); |
32 | | static int adjust_bucket_avail (GDBM_FILE); |
33 | | |
34 | | int |
35 | | _gdbm_avail_block_read (GDBM_FILE dbf, avail_block *avblk, size_t size) |
36 | 1.50k | { |
37 | 1.50k | int rc; |
38 | | |
39 | 1.50k | rc = _gdbm_full_read (dbf, avblk, size); |
40 | 1.50k | if (rc) |
41 | 1.13k | { |
42 | 1.13k | GDBM_DEBUG (GDBM_DEBUG_ERR|GDBM_DEBUG_OPEN, |
43 | 1.13k | "%s: error reading av_table: %s", |
44 | 1.13k | dbf->name, gdbm_db_strerror (dbf)); |
45 | 1.13k | } |
46 | 365 | else |
47 | 365 | rc = gdbm_avail_block_validate (dbf, avblk, size); |
48 | 1.50k | return rc; |
49 | 1.50k | } |
50 | | |
51 | | /* Allocate space in the file DBF for a block NUM_BYTES in length. Return |
52 | | the file address of the start of the block. |
53 | | |
54 | | Each hash bucket has a fixed size avail table. We first check this |
55 | | avail table to satisfy the request for space. In most cases we can |
56 | | and this causes changes to be only in the current hash bucket. |
57 | | Allocation is done on a first fit basis from the entries. If a |
58 | | request can not be satisfied from the current hash bucket, then it is |
59 | | satisfied from the file header avail block. If nothing is there that |
60 | | has enough space, another block at the end of the file is allocated |
61 | | and the unused portion is returned to the avail block. This routine |
62 | | "guarantees" that an allocation does not cross a block boundary unless |
63 | | the size is larger than a single block. The avail structure is |
64 | | changed by this routine if a change is needed. If an error occurs, |
65 | | the value of 0 will be returned. */ |
66 | | |
67 | | off_t |
68 | | _gdbm_alloc (GDBM_FILE dbf, int num_bytes) |
69 | 246k | { |
70 | 246k | off_t file_adr; /* The address of the block. */ |
71 | 246k | avail_elem av_el; /* For temporary use. */ |
72 | | |
73 | | /* The current bucket is the first place to look for space. */ |
74 | 246k | av_el = get_elem (num_bytes, dbf->bucket->bucket_avail, |
75 | 246k | &dbf->bucket->av_count); |
76 | | |
77 | | /* If we did not find some space, we have more work to do. */ |
78 | 246k | if (av_el.av_size == 0) |
79 | 118k | { |
80 | | /* If the header avail table is less than half full, and there's |
81 | | something on the stack. */ |
82 | 118k | if ((dbf->avail->count <= (dbf->avail->size >> 1)) |
83 | 118k | && (dbf->avail->next_block != 0)) |
84 | 25 | if (pop_avail_block (dbf)) |
85 | 0 | return 0; |
86 | | |
87 | | /* check the header avail table next */ |
88 | 118k | av_el = get_elem (num_bytes, dbf->avail->av_table, &dbf->avail->count); |
89 | 118k | if (av_el.av_size == 0) |
90 | | /* Get another full block from end of file. */ |
91 | 115k | av_el = get_block (num_bytes, dbf); |
92 | | |
93 | 118k | dbf->header_changed = TRUE; |
94 | 118k | } |
95 | | |
96 | | /* We now have the place from which we will allocate the new space. */ |
97 | 246k | file_adr = av_el.av_adr; |
98 | | |
99 | | /* Put the unused space back in the avail block. */ |
100 | 246k | av_el.av_adr += num_bytes; |
101 | 246k | av_el.av_size -= num_bytes; |
102 | 246k | if (_gdbm_free (dbf, av_el.av_adr, av_el.av_size)) |
103 | 0 | return 0; |
104 | | |
105 | | /* Return the address. */ |
106 | 246k | return file_adr; |
107 | | |
108 | 246k | } |
109 | | |
110 | | /* Free space of size NUM_BYTES in the file DBF at file address FILE_ADR. Make |
111 | | it available for reuse through _gdbm_alloc. This routine changes the |
112 | | avail structure. */ |
113 | | |
114 | | int |
115 | | _gdbm_free (GDBM_FILE dbf, off_t file_adr, int num_bytes) |
116 | 249k | { |
117 | 249k | avail_elem temp; |
118 | | |
119 | | /* Is it too small to worry about? */ |
120 | 249k | if (num_bytes <= IGNORE_SIZE) |
121 | 49.9k | return 0; |
122 | | |
123 | | /* Initialize the avail element. */ |
124 | 199k | temp.av_size = num_bytes; |
125 | 199k | temp.av_adr = file_adr; |
126 | | |
127 | | /* Is the freed space large or small? */ |
128 | 199k | if ((num_bytes >= dbf->header->block_size) || dbf->central_free) |
129 | 1.05k | { |
130 | 1.05k | if (dbf->avail->count == dbf->avail->size) |
131 | 2 | { |
132 | 2 | if (push_avail_block (dbf)) |
133 | 0 | return -1; |
134 | 2 | } |
135 | 1.05k | _gdbm_put_av_elem (temp, dbf->avail->av_table, |
136 | 1.05k | &dbf->avail->count, dbf->coalesce_blocks); |
137 | 1.05k | dbf->header_changed = TRUE; |
138 | 1.05k | } |
139 | 198k | else |
140 | 198k | { |
141 | | /* Try to put into the current bucket. */ |
142 | 198k | if (dbf->bucket->av_count < BUCKET_AVAIL) |
143 | 197k | _gdbm_put_av_elem (temp, dbf->bucket->bucket_avail, |
144 | 197k | &dbf->bucket->av_count, dbf->coalesce_blocks); |
145 | 358 | else |
146 | 358 | { |
147 | 358 | if (dbf->avail->count == dbf->avail->size) |
148 | 234 | { |
149 | 234 | if (push_avail_block (dbf)) |
150 | 0 | return -1; |
151 | 234 | } |
152 | 358 | _gdbm_put_av_elem (temp, dbf->avail->av_table, |
153 | 358 | &dbf->avail->count, dbf->coalesce_blocks); |
154 | 358 | dbf->header_changed = TRUE; |
155 | 358 | } |
156 | 198k | } |
157 | | |
158 | 199k | if (dbf->header_changed && adjust_bucket_avail (dbf)) |
159 | 0 | return -1; |
160 | | |
161 | | /* All work is done. */ |
162 | 199k | return 0; |
163 | 199k | } |
164 | | |
165 | | |
166 | | |
167 | | /* The following are all utility routines needed by the previous two. */ |
168 | | |
169 | | |
170 | | /* Gets the avail block at the top of the stack and loads it into the |
171 | | active avail block. It does a "free" for itself! This can be (and is) |
172 | | now called even when the avail block is not empty, so we must be |
173 | | smart about things. */ |
174 | | |
175 | | static int |
176 | | pop_avail_block (GDBM_FILE dbf) |
177 | 25 | { |
178 | 25 | off_t file_pos; /* For use with the lseek system call. */ |
179 | 25 | avail_elem new_el; |
180 | 25 | avail_block *new_blk; |
181 | 25 | int index; |
182 | | |
183 | 25 | if (dbf->avail->count == dbf->avail->size) |
184 | 0 | { |
185 | | /* We're kind of stuck here, so we re-split the header in order to |
186 | | avoid crashing. Sigh. */ |
187 | 0 | if (push_avail_block (dbf)) |
188 | 0 | return -1; |
189 | 0 | } |
190 | | |
191 | | /* Set up variables. */ |
192 | 25 | new_el.av_adr = dbf->avail->next_block; |
193 | 25 | new_el.av_size = ( ( (dbf->avail->size * sizeof (avail_elem)) >> 1) |
194 | 25 | + sizeof (avail_block)); |
195 | | |
196 | | /* Allocate space for the block. */ |
197 | 25 | new_blk = malloc (new_el.av_size); |
198 | 25 | if (new_blk == NULL) |
199 | 0 | { |
200 | 0 | GDBM_SET_ERRNO (dbf, GDBM_MALLOC_ERROR, TRUE); |
201 | 0 | _gdbm_fatal (dbf, _("malloc failed")); |
202 | 0 | return -1; |
203 | 0 | } |
204 | | |
205 | | /* Read the block. */ |
206 | 25 | file_pos = gdbm_file_seek (dbf, new_el.av_adr, SEEK_SET); |
207 | 25 | if (file_pos != new_el.av_adr) |
208 | 0 | { |
209 | 0 | GDBM_SET_ERRNO (dbf, GDBM_FILE_SEEK_ERROR, TRUE); |
210 | 0 | free (new_blk); |
211 | 0 | _gdbm_fatal (dbf, _("lseek error")); |
212 | 0 | return -1; |
213 | 0 | } |
214 | | |
215 | 25 | if (_gdbm_avail_block_read (dbf, new_blk, new_el.av_size)) |
216 | 0 | { |
217 | 0 | free (new_blk); |
218 | 0 | _gdbm_fatal (dbf, gdbm_db_strerror (dbf)); |
219 | 0 | return -1; |
220 | 0 | } |
221 | | |
222 | | /* Add the elements from the new block to the header. */ |
223 | 25 | index = 0; |
224 | 50 | while (index < new_blk->count) |
225 | 25 | { |
226 | 2.03k | while (index < new_blk->count |
227 | 2.03k | && dbf->avail->count < dbf->avail->size) |
228 | 2.00k | { |
229 | | /* With luck, this will merge a lot of blocks! */ |
230 | 2.00k | _gdbm_put_av_elem (new_blk->av_table[index], |
231 | 2.00k | dbf->avail->av_table, |
232 | 2.00k | &dbf->avail->count, TRUE); |
233 | 2.00k | index++; |
234 | 2.00k | } |
235 | 25 | if (dbf->avail->count == dbf->avail->size) |
236 | 0 | { |
237 | | /* We're kind of stuck here, so we re-split the header in order to |
238 | | avoid crashing. Sigh. */ |
239 | 0 | if (push_avail_block (dbf)) |
240 | 0 | { |
241 | 0 | free (new_blk); |
242 | 0 | return -1; |
243 | 0 | } |
244 | 0 | } |
245 | 25 | } |
246 | | |
247 | | /* Fix next_block, as well. */ |
248 | 25 | dbf->avail->next_block = new_blk->next_block; |
249 | | |
250 | | /* We changed the header. */ |
251 | | //FIXME: or avail block, when it is separate |
252 | 25 | dbf->header_changed = TRUE; |
253 | | |
254 | | /* Free the previous avail block. It is possible that the header table |
255 | | is now FULL, which will cause us to overflow it! */ |
256 | 25 | if (dbf->avail->count == dbf->avail->size) |
257 | 0 | { |
258 | | /* We're kind of stuck here, so we re-split the header in order to |
259 | | avoid crashing. Sigh. */ |
260 | 0 | if (push_avail_block (dbf)) |
261 | 0 | { |
262 | 0 | free (new_blk); |
263 | 0 | return -1; |
264 | 0 | } |
265 | 0 | } |
266 | | |
267 | 25 | _gdbm_put_av_elem (new_el, dbf->avail->av_table, &dbf->avail->count, TRUE); |
268 | 25 | free (new_blk); |
269 | | |
270 | 25 | return 0; |
271 | 25 | } |
272 | | |
273 | | |
274 | | /* Splits the header avail block and pushes half onto the avail stack. */ |
275 | | |
276 | | static int |
277 | | push_avail_block (GDBM_FILE dbf) |
278 | 236 | { |
279 | 236 | int av_size; |
280 | 236 | off_t av_adr; |
281 | 236 | int index; |
282 | 236 | off_t file_pos; |
283 | 236 | avail_block *temp; |
284 | 236 | avail_elem new_loc; |
285 | 236 | int rc; |
286 | | |
287 | | /* Caclulate the size of the split block. */ |
288 | 236 | av_size = ( (dbf->avail->size * sizeof (avail_elem)) >> 1) |
289 | 236 | + sizeof (avail_block); |
290 | | |
291 | | /* Get address in file for new av_size bytes. */ |
292 | 236 | new_loc = get_elem (av_size, dbf->avail->av_table, &dbf->avail->count); |
293 | 236 | if (new_loc.av_size == 0) |
294 | 0 | new_loc = get_block (av_size, dbf); |
295 | 236 | av_adr = new_loc.av_adr; |
296 | | |
297 | | /* Split the header block. */ |
298 | 236 | temp = calloc (1, av_size); |
299 | 236 | if (temp == NULL) |
300 | 0 | { |
301 | 0 | GDBM_SET_ERRNO (dbf, GDBM_MALLOC_ERROR, TRUE); |
302 | 0 | _gdbm_fatal (dbf, _("malloc error")); |
303 | 0 | return -1; |
304 | 0 | } |
305 | | |
306 | | /* Set the size to be correct AFTER the pop_avail_block. */ |
307 | 236 | temp->size = dbf->avail->size; |
308 | 236 | temp->count = 0; |
309 | 236 | temp->next_block = dbf->avail->next_block; |
310 | 236 | dbf->avail->next_block = av_adr; |
311 | 41.0k | for (index = 1; index < dbf->avail->count; index++) |
312 | 40.8k | if ( (index & 0x1) == 1) /* Index is odd. */ |
313 | 20.4k | temp->av_table[temp->count++] = dbf->avail->av_table[index]; |
314 | 20.4k | else |
315 | 20.4k | dbf->avail->av_table[index>>1] = dbf->avail->av_table[index]; |
316 | | |
317 | | /* Update the header avail count. */ |
318 | 236 | dbf->avail->count -= temp->count; |
319 | | |
320 | 236 | rc = 0; |
321 | 236 | do |
322 | 236 | { |
323 | | /* Free the unneeded space. */ |
324 | 236 | new_loc.av_adr += av_size; |
325 | 236 | new_loc.av_size -= av_size; |
326 | 236 | if (_gdbm_free (dbf, new_loc.av_adr, new_loc.av_size)) |
327 | 0 | { |
328 | 0 | rc = -1; |
329 | 0 | break; |
330 | 0 | } |
331 | | |
332 | | /* Update the disk. */ |
333 | 236 | file_pos = gdbm_file_seek (dbf, av_adr, SEEK_SET); |
334 | 236 | if (file_pos != av_adr) |
335 | 0 | { |
336 | 0 | GDBM_SET_ERRNO (dbf, GDBM_FILE_SEEK_ERROR, TRUE); |
337 | 0 | _gdbm_fatal (dbf, _("lseek error")); |
338 | 0 | rc = -1; |
339 | 0 | break; |
340 | 0 | } |
341 | | |
342 | 236 | rc = _gdbm_full_write (dbf, temp, av_size); |
343 | 236 | if (rc) |
344 | 0 | { |
345 | 0 | GDBM_DEBUG (GDBM_DEBUG_STORE|GDBM_DEBUG_ERR, |
346 | 0 | "%s: error writing avail data: %s", |
347 | 0 | dbf->name, gdbm_db_strerror (dbf)); |
348 | 0 | _gdbm_fatal (dbf, gdbm_db_strerror (dbf)); |
349 | 0 | rc = -1; |
350 | 0 | } |
351 | 236 | } |
352 | 236 | while (0); |
353 | | |
354 | 0 | free (temp); |
355 | | |
356 | 236 | return rc; |
357 | 236 | } |
358 | | |
359 | | /* AV_TABLE contains COUNT entries sorted by AV_SIZE in ascending order. |
360 | | Avail_lookup returns index of an item whose AV_SIZE member is greater |
361 | | than or equal to SIZE. */ |
362 | | static int |
363 | | avail_lookup (int size, avail_elem *av_table, int count) |
364 | 696k | { |
365 | 696k | int start = 0; |
366 | | |
367 | 2.70M | while (count > 0) |
368 | 2.04M | { |
369 | 2.04M | int pivot = start + (count >> 1); |
370 | 2.04M | if (size == av_table[pivot].av_size) |
371 | 33.7k | return pivot; |
372 | 2.00M | if (size > av_table[pivot].av_size) |
373 | 1.07M | { |
374 | 1.07M | start = pivot + 1; |
375 | 1.07M | count--; |
376 | 1.07M | } |
377 | 2.00M | count >>= 1; |
378 | 2.00M | } |
379 | 662k | return start; |
380 | 696k | } |
381 | | |
382 | | /* In array AV_TABLE of *AV_COUNT items, move all items from |
383 | | position SRC to DST and update *AV_COUNT accordingly. |
384 | | */ |
385 | | static inline void |
386 | | avail_move (avail_elem *av_table, int *av_count, int src, int dst) |
387 | 462k | { |
388 | 462k | memmove (av_table + dst, av_table + src, |
389 | 462k | (*av_count - src) * sizeof av_table[0]); |
390 | 462k | *av_count += dst - src; |
391 | 462k | } |
392 | | |
393 | | /* Get_elem returns an element in the AV_TABLE block which is |
394 | | larger than SIZE. AV_COUNT is the number of elements in the |
395 | | AV_TABLE. If an item is found, it extracts it from the AV_TABLE |
396 | | and moves the other elements up to fill the space. If no block is |
397 | | found larger than SIZE, get_elem returns a size of zero. This |
398 | | routine does no I/O. */ |
399 | | |
400 | | static avail_elem |
401 | | get_elem (int size, avail_elem av_table[], int *av_count) |
402 | 427k | { |
403 | 427k | int index; /* For searching through the avail block. */ |
404 | 427k | avail_elem val; /* The default return value. */ |
405 | | |
406 | | /* Initialize default return value. */ |
407 | 427k | val.av_adr = 0; |
408 | 427k | val.av_size = 0; |
409 | | |
410 | | /* Search for element. List is sorted by size. */ |
411 | 427k | index = avail_lookup (size, av_table, *av_count); |
412 | | |
413 | | /* Did we find one of the right size? */ |
414 | 427k | if (index >= *av_count) |
415 | 234k | return val; |
416 | | |
417 | | /* Ok, save that element and move all others up one. */ |
418 | 193k | val = av_table[index]; |
419 | 193k | avail_move (av_table, av_count, index + 1, index); |
420 | 193k | return val; |
421 | 427k | } |
422 | | |
423 | | /* This routine inserts a single NEW_EL into the AV_TABLE block. |
424 | | This routine does no I/O. */ |
425 | | |
426 | | void |
427 | | _gdbm_put_av_elem (avail_elem new_el, avail_elem av_table[], int *av_count, |
428 | | int can_merge) |
429 | 268k | { |
430 | 268k | int index; |
431 | | |
432 | | /* Is it too small to deal with? */ |
433 | 268k | if (new_el.av_size <= IGNORE_SIZE) |
434 | 0 | return; |
435 | | |
436 | 268k | if (can_merge == TRUE) |
437 | 2.15k | { |
438 | | /* Search for blocks to coalesce with this one. */ |
439 | 2.15k | int i; |
440 | | |
441 | 361k | for (i = 0; i < *av_count; i++) |
442 | 359k | { |
443 | 359k | if ((av_table[i].av_adr + av_table[i].av_size) == new_el.av_adr) |
444 | 6 | { |
445 | | /* Right adjacent */ |
446 | 6 | new_el.av_size += av_table[i].av_size; |
447 | 6 | new_el.av_adr = av_table[i].av_adr; |
448 | 6 | avail_move (av_table, av_count, i + 1, i); |
449 | 6 | --i; |
450 | 6 | } |
451 | | |
452 | 359k | if ((new_el.av_adr + new_el.av_size) == av_table[i].av_adr) |
453 | 17 | { |
454 | | /* Left adjacent */ |
455 | 17 | new_el.av_size += av_table[i].av_size; |
456 | 17 | avail_move (av_table, av_count, i + 1, i); |
457 | 17 | --i; |
458 | 17 | } |
459 | 359k | } |
460 | 2.15k | } |
461 | | |
462 | | /* Search for place to put element. List is sorted by size. */ |
463 | 268k | index = avail_lookup (new_el.av_size, av_table, *av_count); |
464 | | /* Move all others up one. */ |
465 | 268k | avail_move (av_table, av_count, index, index + 1); |
466 | | /* Add the new element. */ |
467 | 268k | av_table[index] = new_el; |
468 | 268k | } |
469 | | |
470 | | /* Get_block "allocates" new file space and the end of the file. This is |
471 | | done in integral block sizes. (This helps ensure that data smaller than |
472 | | one block size is in a single block.) Enough blocks are allocated to |
473 | | make sure the number of bytes allocated in the blocks is larger than SIZE. |
474 | | DBF contains the file header that needs updating. This routine does |
475 | | no I/O. */ |
476 | | |
477 | | static avail_elem |
478 | | get_block (int size, GDBM_FILE dbf) |
479 | 115k | { |
480 | 115k | avail_elem val; |
481 | | |
482 | | /* Need at least one block. */ |
483 | 115k | val.av_adr = dbf->header->next_block; |
484 | 115k | val.av_size = dbf->header->block_size; |
485 | | |
486 | | /* Get enough blocks to fit the need. */ |
487 | 3.34M | while (val.av_size < size) |
488 | 3.22M | val.av_size += dbf->header->block_size; |
489 | | |
490 | | /* Update the header and return. */ |
491 | 115k | dbf->header->next_block += val.av_size; |
492 | | |
493 | | /* We changed the header. */ |
494 | 115k | dbf->header_changed = TRUE; |
495 | | |
496 | 115k | return val; |
497 | | |
498 | 115k | } |
499 | | |
500 | | |
501 | | /* When the header already needs writing, we can make sure the current |
502 | | bucket has its avail block as close to 1/3 full as possible. */ |
503 | | static int |
504 | | adjust_bucket_avail (GDBM_FILE dbf) |
505 | 77.7k | { |
506 | 77.7k | int third = BUCKET_AVAIL / 3; |
507 | 77.7k | avail_elem av_el; |
508 | | |
509 | | /* Can we add more entries to the bucket? */ |
510 | 77.7k | if (dbf->bucket->av_count < third) |
511 | 1.20k | { |
512 | 1.20k | if (dbf->avail->count > 0) |
513 | 1.02k | { |
514 | 1.02k | dbf->avail->count -= 1; |
515 | 1.02k | av_el = dbf->avail->av_table[dbf->avail->count]; |
516 | 1.02k | _gdbm_put_av_elem (av_el, dbf->bucket->bucket_avail, |
517 | 1.02k | &dbf->bucket->av_count, dbf->coalesce_blocks); |
518 | 1.02k | _gdbm_current_bucket_changed (dbf); |
519 | 1.02k | } |
520 | 1.20k | return 0; |
521 | 1.20k | } |
522 | | |
523 | | /* Is there too much in the bucket? */ |
524 | 139k | while (dbf->bucket->av_count > BUCKET_AVAIL-third |
525 | 139k | && dbf->avail->count < dbf->avail->size) |
526 | 63.0k | { |
527 | 63.0k | av_el = get_elem (0, dbf->bucket->bucket_avail, &dbf->bucket->av_count); |
528 | 63.0k | if (av_el.av_size == 0) |
529 | 0 | { |
530 | 0 | GDBM_SET_ERRNO (dbf, GDBM_BAD_AVAIL, TRUE); |
531 | 0 | return -1; |
532 | 0 | } |
533 | 63.0k | _gdbm_put_av_elem (av_el, dbf->avail->av_table, |
534 | 63.0k | &dbf->avail->count, |
535 | 63.0k | dbf->coalesce_blocks); |
536 | 63.0k | _gdbm_current_bucket_changed (dbf); |
537 | 63.0k | } |
538 | 76.5k | return 0; |
539 | 76.5k | } |