]> git.xonotic.org Git - xonotic/gmqcc.git/blob - stat.c
Merge remote-tracking branch 'origin/master' into cooking
[xonotic/gmqcc.git] / stat.c
1 /*
2  * Copyright (C) 2012, 2013, 2014
3  *     Dale Weiler
4  *     Wolfgang Bumiller
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy of
7  * this software and associated documentation files (the "Software"), to deal in
8  * the Software without restriction, including without limitation the rights to
9  * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10  * of the Software, and to permit persons to whom the Software is furnished to do
11  * so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in all
14  * copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22  * SOFTWARE.
23  */
24
25 #include <string.h>
26 #include <stdlib.h>
27
28 #include "gmqcc.h"
29
30 /*
31  * For the valgrind integration of our allocator. This allows us to have
32  * more `accurate` valgrind output for our allocator, and also secures the
33  * possible underflows (where one could obtain access to the redzone that
34  * represents info about that allocation).
35  */
36 #ifndef NVALGRIND
37 #   include <valgrind/valgrind.h>
38 #   include <valgrind/memcheck.h>
39 #else
40 #   define VALGRIND_MALLOCLIKE_BLOCK(PTR, ALLOC_SIZE, REDZONE_SIZE, ZEROED)
41 #   define VALGRIND_FREELIKE_BLOCK(PTR, REDZONE_SIZE)
42 #   define VALGRIND_MAKE_MEM_DEFINED(PTR, REDZONE_SIZE)
43 #   define VALGRIND_MAKE_MEM_NOACCESS(PTR, REDZONE_SIZE)
44 #endif
45
46 /*
47  * GMQCC performs tons of allocations, constructions, and crazyness
48  * all around. When trying to optimizes systems, or just get fancy
49  * statistics out of the compiler, it's often printf mess. This file
50  * implements the statistics system of the compiler. I.E the allocator
51  * we use to track allocations, and other systems of interest.
52  */
53 #define ST_SIZE 1024
54
55 typedef struct stat_mem_block_s {
56     const char              *file;
57     size_t                   line;
58     size_t                   size;
59     const char              *expr;
60     struct stat_mem_block_s *next;
61     struct stat_mem_block_s *prev;
62 } stat_mem_block_t;
63
64 typedef struct {
65     size_t key;
66     size_t value;
67 } stat_size_entry_t, **stat_size_table_t;
68
69 static uint64_t          stat_mem_allocated         = 0;
70 static uint64_t          stat_mem_deallocated       = 0;
71 static uint64_t          stat_mem_allocated_total   = 0;
72 static uint64_t          stat_mem_deallocated_total = 0;
73 static uint64_t          stat_mem_high              = 0;
74 static uint64_t          stat_mem_peak              = 0;
75 static uint64_t          stat_mem_strdups           = 0;
76 static uint64_t          stat_used_strdups          = 0;
77 static uint64_t          stat_used_vectors          = 0;
78 static uint64_t          stat_used_hashtables       = 0;
79 static uint64_t          stat_type_vectors          = 0;
80 static uint64_t          stat_type_hashtables       = 0;
81 static stat_size_table_t stat_size_vectors          = NULL;
82 static stat_size_table_t stat_size_hashtables       = NULL;
83 static stat_mem_block_t *stat_mem_block_root        = NULL;
84
85 /*
86  * A tiny size_t key-value hashtbale for tracking vector and hashtable
87  * sizes. We can use it for other things too, if we need to. This is
88  * very TIGHT, and efficent in terms of space though.
89  */
90 static stat_size_table_t stat_size_new(void) {
91     return (stat_size_table_t)memset(
92         mem_a(sizeof(stat_size_entry_t*) * ST_SIZE),
93         0, ST_SIZE * sizeof(stat_size_entry_t*)
94     );
95 }
96
97 static void stat_size_del(stat_size_table_t table) {
98     size_t i = 0;
99     for (; i < ST_SIZE; i++) if(table[i]) mem_d(table[i]);
100     mem_d(table);
101 }
102
103 static stat_size_entry_t *stat_size_get(stat_size_table_t table, size_t key) {
104     size_t hash = (key % ST_SIZE);
105     while (table[hash] && table[hash]->key != key)
106         hash = (hash + 1) % ST_SIZE;
107     return table[hash];
108 }
109 static void stat_size_put(stat_size_table_t table, size_t key, size_t value) {
110     size_t hash = (key % ST_SIZE);
111     while (table[hash] && table[hash]->key != key)
112         hash = (hash + 1) % ST_SIZE;
113     table[hash]        = (stat_size_entry_t*)mem_a(sizeof(stat_size_entry_t));
114     table[hash]->key   = key;
115     table[hash]->value = value;
116 }
117
118 /*
119  * A basic header of information wrapper allocator. Simply stores
120  * information as a header, returns the memory + 1 past it, can be
121  * retrieved again with - 1. Where type is stat_mem_block_t*.
122  */
123 void *stat_mem_allocate(size_t size, size_t line, const char *file, const char *expr) {
124     stat_mem_block_t *info = (stat_mem_block_t*)malloc(sizeof(stat_mem_block_t) + size);
125     void             *data = (void*)(info + 1);
126
127     if(GMQCC_UNLIKELY(!info))
128         return NULL;
129
130     info->line = line;
131     info->size = size;
132     info->file = file;
133     info->expr = expr;
134     info->prev = NULL;
135     info->next = stat_mem_block_root;
136
137     /* likely since it only happens once */
138     if (GMQCC_LIKELY(stat_mem_block_root != NULL)) {
139         VALGRIND_MAKE_MEM_DEFINED(stat_mem_block_root, sizeof(stat_mem_block_t));
140         stat_mem_block_root->prev = info;
141         VALGRIND_MAKE_MEM_NOACCESS(stat_mem_block_root, sizeof(stat_mem_block_t));
142     }
143
144     stat_mem_block_root       = info;
145     stat_mem_allocated       += size;
146     stat_mem_high            += size;
147     stat_mem_allocated_total ++;
148
149     if (stat_mem_high > stat_mem_peak)
150         stat_mem_peak = stat_mem_high;
151
152     VALGRIND_MALLOCLIKE_BLOCK(data, size, sizeof(stat_mem_block_t), 0);
153     return data;
154 }
155
156 void stat_mem_deallocate(void *ptr) {
157     stat_mem_block_t *info = NULL;
158
159     if (GMQCC_UNLIKELY(!ptr))
160         return;
161
162     info = ((stat_mem_block_t*)ptr - 1);
163
164     /*
165      * we need access to the redzone that represents the info block
166      * so lets do that.
167      */
168     VALGRIND_MAKE_MEM_DEFINED(info, sizeof(stat_mem_block_t));
169
170     stat_mem_deallocated       += info->size;
171     stat_mem_high              -= info->size;
172     stat_mem_deallocated_total ++;
173
174     if (info->prev) {
175         /* just need access for a short period */
176         VALGRIND_MAKE_MEM_DEFINED(info->prev, sizeof(stat_mem_block_t));
177         info->prev->next = info->next;
178         /* don't need access anymore */
179         VALGRIND_MAKE_MEM_NOACCESS(info->prev, sizeof(stat_mem_block_t));
180     }
181     if (info->next) {
182         /* just need access for a short period */
183         VALGRIND_MAKE_MEM_DEFINED(info->next, sizeof(stat_mem_block_t));
184         info->next->prev = info->prev;
185         /* don't need access anymore */
186         VALGRIND_MAKE_MEM_NOACCESS(info->next, sizeof(stat_mem_block_t));
187     }
188
189     /* move ahead */
190     if (info == stat_mem_block_root)
191         stat_mem_block_root = info->next;
192
193     free(info);
194     VALGRIND_MAKE_MEM_NOACCESS(info, sizeof(stat_mem_block_t));
195     VALGRIND_FREELIKE_BLOCK(ptr, sizeof(stat_mem_block_t));
196 }
197
198 void *stat_mem_reallocate(void *ptr, size_t size, size_t line, const char *file, const char *expr) {
199     stat_mem_block_t *oldinfo = NULL;
200     stat_mem_block_t *newinfo;
201
202     if (GMQCC_UNLIKELY(!ptr))
203         return stat_mem_allocate(size, line, file, expr);
204
205     /* stay consistent with glibc */
206     if (GMQCC_UNLIKELY(!size)) {
207         stat_mem_deallocate(ptr);
208         return NULL;
209     }
210
211     oldinfo = ((stat_mem_block_t*)ptr - 1);
212     newinfo = ((stat_mem_block_t*)malloc(sizeof(stat_mem_block_t) + size));
213
214     if (GMQCC_UNLIKELY(!newinfo)) {
215         stat_mem_deallocate(ptr);
216         return NULL;
217     }
218
219     VALGRIND_MALLOCLIKE_BLOCK(newinfo + 1, size, sizeof(stat_mem_block_t), 0);
220
221     /* we need access to the old info redzone */
222     VALGRIND_MAKE_MEM_DEFINED(oldinfo, sizeof(stat_mem_block_t));
223
224     memcpy(newinfo+1, oldinfo+1, oldinfo->size);
225
226     if (oldinfo->prev) {
227         /* just need access for a short period */
228         VALGRIND_MAKE_MEM_DEFINED(oldinfo->prev, sizeof(stat_mem_block_t));
229         oldinfo->prev->next = oldinfo->next;
230         /* don't need access anymore */
231         VALGRIND_MAKE_MEM_NOACCESS(oldinfo->prev, sizeof(stat_mem_block_t));
232     }
233
234     if (oldinfo->next) {
235         /* just need access for a short period */
236         VALGRIND_MAKE_MEM_DEFINED(oldinfo->next, sizeof(stat_mem_block_t));
237         oldinfo->next->prev = oldinfo->prev;
238         /* don't need access anymore */
239         VALGRIND_MAKE_MEM_NOACCESS(oldinfo->next, sizeof(stat_mem_block_t));
240     }
241
242     /* move ahead */
243     if (oldinfo == stat_mem_block_root)
244         stat_mem_block_root = oldinfo->next;
245
246     /* we need access to the redzone for the newinfo block */
247     VALGRIND_MAKE_MEM_DEFINED(newinfo, sizeof(stat_mem_block_t));
248
249     newinfo->line = line;
250     newinfo->size = size;
251     newinfo->file = file;
252     newinfo->expr = expr;
253     newinfo->prev = NULL;
254     newinfo->next = stat_mem_block_root;
255
256     /*
257      * likely since the only time there is no root is when it's
258      * being initialized first.
259      */
260     if (GMQCC_LIKELY(stat_mem_block_root != NULL)) {
261         /* we need access to the root */
262         VALGRIND_MAKE_MEM_DEFINED(stat_mem_block_root, sizeof(stat_mem_block_t));
263         stat_mem_block_root->prev = newinfo;
264         /* kill access */
265         VALGRIND_MAKE_MEM_NOACCESS(stat_mem_block_root, sizeof(stat_mem_block_t));
266     }
267
268     stat_mem_block_root = newinfo;
269     stat_mem_allocated -= oldinfo->size;
270     stat_mem_high      -= oldinfo->size;
271     stat_mem_allocated += newinfo->size;
272     stat_mem_high      += newinfo->size;
273
274     /*
275      * we're finished with the redzones, lets kill the access
276      * to them.
277      */
278     VALGRIND_MAKE_MEM_NOACCESS(newinfo, sizeof(stat_mem_block_t));
279     VALGRIND_MAKE_MEM_NOACCESS(oldinfo, sizeof(stat_mem_block_t));
280
281     if (stat_mem_high > stat_mem_peak)
282         stat_mem_peak = stat_mem_high;
283
284     free(oldinfo);
285     VALGRIND_FREELIKE_BLOCK(ptr, sizeof(stat_mem_block_t));
286     return newinfo + 1;
287 }
288
289 /*
290  * strdup does it's own malloc, we need to track malloc. We don't want
291  * to overwrite malloc though, infact, we can't really hook it at all
292  * without library specific assumptions. So we re implement strdup.
293  */
294 char *stat_mem_strdup(const char *src, size_t line, const char *file, bool empty) {
295     size_t len = 0;
296     char  *ptr = NULL;
297
298     if (!src)
299         return NULL;
300
301     len = strlen(src);
302     if (((!empty) ? len : true) && (ptr = (char*)stat_mem_allocate(len + 1, line, file, "strdup"))) {
303         memcpy(ptr, src, len);
304         ptr[len] = '\0';
305     }
306
307     stat_used_strdups ++;
308     stat_mem_strdups  += len;
309     return ptr;
310 }
311
312 /*
313  * The reallocate function for resizing vectors.
314  */
315 void _util_vec_grow(void **a, size_t i, size_t s) {
316     vector_t          *d = vec_meta(*a);
317     size_t             m = 0;
318     stat_size_entry_t *e = NULL;
319     void              *p = NULL;
320
321     if (*a) {
322         m = 2 * d->allocated + i;
323         p = mem_r(d, s * m + sizeof(vector_t));
324     } else {
325         m = i + 1;
326         p = mem_a(s * m + sizeof(vector_t));
327         ((vector_t*)p)->used = 0;
328         stat_used_vectors++;
329     }
330
331     if (!stat_size_vectors)
332         stat_size_vectors = stat_size_new();
333
334     if ((e = stat_size_get(stat_size_vectors, s))) {
335         e->value ++;
336     } else {
337         stat_size_put(stat_size_vectors, s, 1); /* start off with 1 */
338         stat_type_vectors++;
339     }
340
341     *a = (vector_t*)p + 1;
342     vec_meta(*a)->allocated = m;
343 }
344
345 /*
346  * Hash table for generic data, based on dynamic memory allocations
347  * all around.  This is the internal interface, please look for
348  * EXPOSED INTERFACE comment below
349  */
350 typedef struct hash_node_t {
351     char               *key;   /* the key for this node in table */
352     void               *value; /* pointer to the data as void*   */
353     struct hash_node_t *next;  /* next node (linked list)        */
354 } hash_node_t;
355
356
357 size_t hash(const char *key);
358 size_t util_hthash(hash_table_t *ht, const char *key) {
359     return hash(key) % ht->size;
360 }
361
362 static hash_node_t *_util_htnewpair(const char *key, void *value) {
363     hash_node_t *node;
364     if (!(node = (hash_node_t*)mem_a(sizeof(hash_node_t))))
365         return NULL;
366
367     if (!(node->key = util_strdupe(key))) {
368         mem_d(node);
369         return NULL;
370     }
371
372     node->value = value;
373     node->next  = NULL;
374
375     return node;
376 }
377
378 /*
379  * EXPOSED INTERFACE for the hashtable implementation
380  * util_htnew(size)                             -- to make a new hashtable
381  * util_htset(table, key, value, sizeof(value)) -- to set something in the table
382  * util_htget(table, key)                       -- to get something from the table
383  * util_htdel(table)                            -- to delete the table
384  */
385 hash_table_t *util_htnew(size_t size) {
386     hash_table_t      *hashtable = NULL;
387     stat_size_entry_t *find      = NULL;
388
389     if (size < 1)
390         return NULL;
391
392     if (!stat_size_hashtables)
393         stat_size_hashtables = stat_size_new();
394
395     if (!(hashtable = (hash_table_t*)mem_a(sizeof(hash_table_t))))
396         return NULL;
397
398     if (!(hashtable->table = (hash_node_t**)mem_a(sizeof(hash_node_t*) * size))) {
399         mem_d(hashtable);
400         return NULL;
401     }
402
403     if ((find = stat_size_get(stat_size_hashtables, size)))
404         find->value++;
405     else {
406         stat_type_hashtables++;
407         stat_size_put(stat_size_hashtables, size, 1);
408     }
409
410     hashtable->size = size;
411     memset(hashtable->table, 0, sizeof(hash_node_t*) * size);
412
413     stat_used_hashtables++;
414     return hashtable;
415 }
416
417 void util_htseth(hash_table_t *ht, const char *key, size_t bin, void *value) {
418     hash_node_t *newnode = NULL;
419     hash_node_t *next    = NULL;
420     hash_node_t *last    = NULL;
421
422     next = ht->table[bin];
423
424     while (next && next->key && strcmp(key, next->key) > 0)
425         last = next, next = next->next;
426
427     /* already in table, do a replace */
428     if (next && next->key && strcmp(key, next->key) == 0) {
429         next->value = value;
430     } else {
431         /* not found, grow a pair man :P */
432         newnode = _util_htnewpair(key, value);
433         if (next == ht->table[bin]) {
434             newnode->next  = next;
435             ht->table[bin] = newnode;
436         } else if (!next) {
437             last->next = newnode;
438         } else {
439             newnode->next = next;
440             last->next = newnode;
441         }
442     }
443 }
444
445 void util_htset(hash_table_t *ht, const char *key, void *value) {
446     util_htseth(ht, key, util_hthash(ht, key), value);
447 }
448
449 void *util_htgeth(hash_table_t *ht, const char *key, size_t bin) {
450     hash_node_t *pair = ht->table[bin];
451
452     while (pair && pair->key && strcmp(key, pair->key) > 0)
453         pair = pair->next;
454
455     if (!pair || !pair->key || strcmp(key, pair->key) != 0)
456         return NULL;
457
458     return pair->value;
459 }
460
461 void *util_htget(hash_table_t *ht, const char *key) {
462     return util_htgeth(ht, key, util_hthash(ht, key));
463 }
464
465 void *code_util_str_htgeth(hash_table_t *ht, const char *key, size_t bin);
466 void *code_util_str_htgeth(hash_table_t *ht, const char *key, size_t bin) {
467     hash_node_t *pair;
468     size_t len, keylen;
469     int cmp;
470
471     keylen = strlen(key);
472
473     pair = ht->table[bin];
474     while (pair && pair->key) {
475         len = strlen(pair->key);
476         if (len < keylen) {
477             pair = pair->next;
478             continue;
479         }
480         if (keylen == len) {
481             cmp = strcmp(key, pair->key);
482             if (cmp == 0)
483                 return pair->value;
484             if (cmp < 0)
485                 return NULL;
486             pair = pair->next;
487             continue;
488         }
489         cmp = strcmp(key, pair->key + len - keylen);
490         if (cmp == 0) {
491             uintptr_t up = (uintptr_t)pair->value;
492             up += len - keylen;
493             return (void*)up;
494         }
495         pair = pair->next;
496     }
497     return NULL;
498 }
499
500 /*
501  * Free all allocated data in a hashtable, this is quite the amount
502  * of work.
503  */
504 void util_htrem(hash_table_t *ht, void (*callback)(void *data)) {
505     size_t i = 0;
506
507     for (; i < ht->size; ++i) {
508         hash_node_t *n = ht->table[i];
509         hash_node_t *p;
510
511         /* free in list */
512         while (n) {
513             if (n->key)
514                 mem_d(n->key);
515             if (callback)
516                 callback(n->value);
517             p = n;
518             n = p->next;
519             mem_d(p);
520         }
521
522     }
523     /* free table */
524     mem_d(ht->table);
525     mem_d(ht);
526 }
527
528 void util_htrmh(hash_table_t *ht, const char *key, size_t bin, void (*cb)(void*)) {
529     hash_node_t **pair = &ht->table[bin];
530     hash_node_t *tmp;
531
532     while (*pair && (*pair)->key && strcmp(key, (*pair)->key) > 0)
533         pair = &(*pair)->next;
534
535     tmp = *pair;
536     if (!tmp || !tmp->key || strcmp(key, tmp->key) != 0)
537         return;
538
539     if (cb)
540         (*cb)(tmp->value);
541
542     *pair = tmp->next;
543     mem_d(tmp->key);
544     mem_d(tmp);
545 }
546
547 void util_htrm(hash_table_t *ht, const char *key, void (*cb)(void*)) {
548     util_htrmh(ht, key, util_hthash(ht, key), cb);
549 }
550
551 void util_htdel(hash_table_t *ht) {
552     util_htrem(ht, NULL);
553 }
554
555 /*
556  * The following functions below implement printing / dumping of statistical
557  * information.
558  */
559 static void stat_dump_mem_contents(stat_mem_block_t *block, uint16_t cols) {
560     unsigned char *buffer = (unsigned char *)mem_a(cols);
561     unsigned char *memory = (unsigned char *)(block + 1);
562     size_t         i;
563
564     for (i = 0; i < block->size; i++) {
565         if (!(i % 16)) {
566             if (i != 0)
567                 con_out(" %s\n", buffer);
568             con_out(" 0x%08X: ", i);
569         }
570
571         con_out(" %02X", memory[i]);
572
573         buffer[i % cols] = ((memory[i] < 0x20) || (memory[i] > 0x7E))
574                             ? '.'
575                             : memory[i];
576
577         buffer[(i % cols) + 1] = '\0';
578     }
579
580     while ((i % cols) != 0) {
581         con_out("   ");
582         i++;
583     }
584
585     con_out(" %s\n", buffer);
586     mem_d(buffer);
587 }
588
589 static void stat_dump_mem_leaks(void) {
590     stat_mem_block_t *info;
591     /* we need access to the root for this */
592     VALGRIND_MAKE_MEM_DEFINED(stat_mem_block_root, sizeof(stat_mem_block_t));
593     for (info = stat_mem_block_root; info; info = info->next) {
594         /* we need access to the block */
595         VALGRIND_MAKE_MEM_DEFINED(info, sizeof(stat_mem_block_t));
596         con_out("lost: %u (bytes) at %s:%u from expression `%s`\n",
597             info->size,
598             info->file,
599             info->line,
600             info->expr
601         );
602
603         stat_dump_mem_contents(info, OPTS_OPTION_U16(OPTION_MEMDUMPCOLS));
604
605         /*
606          * we're finished with the access, the redzone should be marked
607          * inaccesible so that invalid read/writes that could 'step-into'
608          * those redzones will show up as invalid read/writes in valgrind.
609          */
610         VALGRIND_MAKE_MEM_NOACCESS(info, sizeof(stat_mem_block_t));
611     }
612     VALGRIND_MAKE_MEM_NOACCESS(stat_mem_block_root, sizeof(stat_mem_block_t));
613 }
614
615 static void stat_dump_mem_info(void) {
616     con_out("Memory Information:\n\
617     Total allocations:   %llu\n\
618     Total deallocations: %llu\n\
619     Total allocated:     %f (MB)\n\
620     Total deallocated:   %f (MB)\n\
621     Total peak memory:   %f (MB)\n\
622     Total leaked memory: %f (MB) in %llu allocations\n",
623         stat_mem_allocated_total,
624         stat_mem_deallocated_total,
625         (float)(stat_mem_allocated)                        / 1048576.0f,
626         (float)(stat_mem_deallocated)                      / 1048576.0f,
627         (float)(stat_mem_peak)                             / 1048576.0f,
628         (float)(stat_mem_allocated - stat_mem_deallocated) / 1048576.0f,
629         stat_mem_allocated_total - stat_mem_deallocated_total
630     );
631 }
632
633 static void stat_dump_stats_table(stat_size_table_t table, const char *string, uint64_t *size) {
634     size_t i,j;
635
636     if (!table)
637         return;
638
639     for (i = 0, j = 1; i < ST_SIZE; i++) {
640         stat_size_entry_t *entry;
641
642         if (!(entry = table[i]))
643             continue;
644
645         con_out(string, (unsigned)j, (unsigned)entry->key, (unsigned)entry->value);
646         j++;
647
648         if (size)
649             *size += entry->key * entry->value;
650     }
651 }
652
653 void stat_info() {
654     if (OPTS_OPTION_BOOL(OPTION_MEMCHK) ||
655         OPTS_OPTION_BOOL(OPTION_STATISTICS)) {
656         uint64_t mem = 0;
657
658         con_out("Memory Statistics:\n\
659     Total vectors allocated:       %llu\n\
660     Total string duplicates:       %llu\n\
661     Total string duplicate memory: %f (MB)\n\
662     Total hashtables allocated:    %llu\n\
663     Total unique vector sizes:     %llu\n",
664             stat_used_vectors,
665             stat_used_strdups,
666             (float)(stat_mem_strdups) / 1048576.0f,
667             stat_used_hashtables,
668             stat_type_vectors
669         );
670
671         stat_dump_stats_table (
672             stat_size_vectors,
673             "        %2u| # of %5u byte vectors: %u\n",
674             &mem
675         );
676
677         con_out (
678             "    Total unique hashtable sizes: %llu\n",
679             stat_type_hashtables
680         );
681
682         stat_dump_stats_table (
683             stat_size_hashtables,
684             "        %2u| # of %5u element hashtables: %u\n",
685             NULL
686         );
687
688         con_out (
689             "    Total vector memory:          %f (MB)\n\n",
690             (float)(mem) / 1048576.0f
691         );
692     }
693
694     if (stat_size_vectors)
695         stat_size_del(stat_size_vectors);
696     if (stat_size_hashtables)
697         stat_size_del(stat_size_hashtables);
698
699     if (OPTS_OPTION_BOOL(OPTION_DEBUG) ||
700         OPTS_OPTION_BOOL(OPTION_MEMCHK))
701         stat_dump_mem_info();
702
703     if (OPTS_OPTION_BOOL(OPTION_DEBUG))
704         stat_dump_mem_leaks();
705 }
706 #undef ST_SIZE