1 /*
2  * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
3  *
4  * SPDX-License-Identifier: Apache-2.0
5  */
6 #include <string.h>
7 #include <sdkconfig.h>
8 #include <inttypes.h>
9 
10 #define HEAP_TRACE_SRCFILE /* don't warn on inclusion here */
11 #include "esp_heap_trace.h"
12 #undef HEAP_TRACE_SRCFILE
13 #include "esp_heap_caps.h"
14 #include "esp_attr.h"
15 #include "freertos/FreeRTOS.h"
16 #include "freertos/task.h"
17 #include "esp_memory_utils.h"
18 #include "sys/queue.h"
19 
20 #define STACK_DEPTH CONFIG_HEAP_TRACING_STACK_DEPTH
21 
22 #if CONFIG_HEAP_TRACING_STANDALONE
23 
24 static portMUX_TYPE trace_mux = portMUX_INITIALIZER_UNLOCKED;
25 static bool tracing;
26 static heap_trace_mode_t mode;
27 
28 /* Define struct: linked list of records */
29 TAILQ_HEAD(heap_trace_record_list_struct_t, heap_trace_record_t);
30 typedef struct heap_trace_record_list_struct_t heap_trace_record_list_t;
31 
32 /* Linked List of Records */
33 typedef struct {
34 
35     /* Buffer used for records. */
36     heap_trace_record_t *buffer;
37 
38     /* Linked list of recorded allocations */
39     heap_trace_record_list_t list;
40 
41     /* Linked list of available record objects */
42     heap_trace_record_list_t unused;
43 
44     /* capacity of 'buffer' */
45     size_t capacity;
46 
47     /* Count of entries in 'list' */
48     size_t count;
49 
50     /* During execution, we remember the maximum
51        value of 'count'. This can help you
52        choose the right size for your buffer capacity.*/
53     size_t high_water_mark;
54 
55     /* Has the buffer overflowed and lost trace entries? */
56     bool has_overflowed;
57 } records_t;
58 
59 // Forward Defines
60 static void heap_trace_dump_base(bool internal_ram, bool psram);
61 static void record_deep_copy(heap_trace_record_t *r_dest, const heap_trace_record_t *r_src);
62 static void list_setup(void);
63 static void list_remove(heap_trace_record_t *r_remove);
64 static heap_trace_record_t* list_add(const heap_trace_record_t *r_append);
65 static heap_trace_record_t* list_pop_unused(void);
66 static heap_trace_record_t* list_find_address_reverse(void *p);
67 
68 /* The actual records. */
69 static records_t records;
70 
71 /* Actual number of allocations logged */
72 static size_t total_allocations;
73 
74 /* Actual number of frees logged */
75 static size_t total_frees;
76 
77 /* Used to speed up heap_trace_get */
78 static heap_trace_record_t* r_get;
79 static size_t r_get_idx;
80 
81 #if CONFIG_HEAP_TRACE_HASH_MAP
82 
83 /* Define struct: linked list of records used in hash map */
84 TAILQ_HEAD(heap_trace_hash_list_struct_t, heap_trace_record_t);
85 typedef struct heap_trace_hash_list_struct_t heap_trace_hash_list_t;
86 
87 static heap_trace_hash_list_t hash_map[(size_t)CONFIG_HEAP_TRACE_HASH_MAP_SIZE]; // Buffer used for hashmap entries
88 static size_t total_hashmap_hits;
89 static size_t total_hashmap_miss;
90 
hash_idx(void * p)91 static HEAP_IRAM_ATTR size_t hash_idx(void* p)
92 {
93     static const uint32_t fnv_prime = 16777619UL; // expression 2^24 + 2^8 + 0x93 (32 bits size)
94     // since all the addresses are 4 bytes aligned, computing address * fnv_prime always gives
95     // a modulo 4 number. The bit shift goal is to distribute more evenly the hashes in the
96     // given range [0 , CONFIG_HEAP_TRACE_HASH_MAP_SIZE - 1].
97     return ((((uint32_t)p >> 3) +
98              ((uint32_t)p >> 5) +
99              ((uint32_t)p >> 7)) * fnv_prime) % (uint32_t)CONFIG_HEAP_TRACE_HASH_MAP_SIZE;
100 }
101 
map_add(heap_trace_record_t * r_add)102 static HEAP_IRAM_ATTR void map_add(heap_trace_record_t *r_add)
103 {
104     size_t idx = hash_idx(r_add->address);
105     TAILQ_INSERT_TAIL(&hash_map[idx], r_add, tailq_hashmap);
106 }
107 
map_remove(heap_trace_record_t * r_remove)108 static HEAP_IRAM_ATTR void map_remove(heap_trace_record_t *r_remove)
109 {
110     size_t idx = hash_idx(r_remove->address);
111     TAILQ_REMOVE(&hash_map[idx], r_remove, tailq_hashmap);
112 }
113 
map_find(void * p)114 static HEAP_IRAM_ATTR heap_trace_record_t* map_find(void *p)
115 {
116     size_t idx = hash_idx(p);
117     heap_trace_record_t *r_cur = NULL;
118     TAILQ_FOREACH(r_cur, &hash_map[idx], tailq_hashmap) {
119         if (r_cur->address == p) {
120             total_hashmap_hits++;
121             return r_cur;
122         }
123     }
124     total_hashmap_miss++;
125     return NULL;
126 }
127 #endif // CONFIG_HEAP_TRACE_HASH_MAP
128 
heap_trace_init_standalone(heap_trace_record_t * record_buffer,size_t num_records)129 esp_err_t heap_trace_init_standalone(heap_trace_record_t *record_buffer, size_t num_records)
130 {
131     if (tracing) {
132         return ESP_ERR_INVALID_STATE;
133     }
134 
135     if (record_buffer == NULL || num_records == 0) {
136         return ESP_ERR_INVALID_ARG;
137     }
138 
139     records.buffer = record_buffer;
140     records.capacity = num_records;
141 
142     return ESP_OK;
143 }
144 
set_tracing(bool enable)145 static esp_err_t set_tracing(bool enable)
146 {
147     if (tracing == enable) {
148         return ESP_ERR_INVALID_STATE;
149     }
150     tracing = enable;
151     return ESP_OK;
152 }
153 
heap_trace_start(heap_trace_mode_t mode_param)154 esp_err_t heap_trace_start(heap_trace_mode_t mode_param)
155 {
156     if (records.buffer == NULL || records.capacity == 0) {
157         return ESP_ERR_INVALID_STATE;
158     }
159 
160     portENTER_CRITICAL(&trace_mux);
161 
162     set_tracing(false);
163     mode = mode_param;
164 
165     // clear buffers
166     memset(records.buffer, 0, sizeof(heap_trace_record_t) * records.capacity);
167 
168 #if CONFIG_HEAP_TRACE_HASH_MAP
169     for (size_t i = 0; i < (size_t)CONFIG_HEAP_TRACE_HASH_MAP_SIZE; i++) {
170         TAILQ_INIT(&hash_map[i]);
171     }
172 
173     total_hashmap_hits = 0;
174     total_hashmap_miss = 0;
175 #endif // CONFIG_HEAP_TRACE_HASH_MAP
176 
177     records.count = 0;
178     records.has_overflowed = false;
179     list_setup();
180 
181     total_allocations = 0;
182     total_frees = 0;
183 
184     const esp_err_t ret_val = set_tracing(true);
185 
186     portEXIT_CRITICAL(&trace_mux);
187     return ret_val;
188 }
189 
heap_trace_stop(void)190 esp_err_t heap_trace_stop(void)
191 {
192     portENTER_CRITICAL(&trace_mux);
193     const esp_err_t ret_val = set_tracing(false);
194     portEXIT_CRITICAL(&trace_mux);
195     return ret_val;
196 }
197 
heap_trace_resume(void)198 esp_err_t heap_trace_resume(void)
199 {
200     portENTER_CRITICAL(&trace_mux);
201     const esp_err_t ret_val = set_tracing(true);
202     portEXIT_CRITICAL(&trace_mux);
203     return ret_val;
204 }
205 
heap_trace_get_count(void)206 size_t heap_trace_get_count(void)
207 {
208     return records.count;
209 }
210 
heap_trace_get(size_t index,heap_trace_record_t * r_out)211 esp_err_t heap_trace_get(size_t index, heap_trace_record_t *r_out)
212 {
213     if (r_out == NULL) {
214         return ESP_ERR_INVALID_STATE;
215     }
216 
217     esp_err_t result = ESP_OK;
218 
219     portENTER_CRITICAL(&trace_mux);
220 
221     if (index >= records.count) {
222 
223         result = ESP_ERR_INVALID_ARG; /* out of range for 'count' */
224 
225     } else {
226 
227         // Perf: speed up sequential access
228         if (r_get && r_get_idx == index - 1) {
229 
230             r_get = TAILQ_NEXT(r_get, tailq_list);
231             r_get_idx = index;
232 
233         } else {
234 
235             // Iterate through through the linked list
236 
237             r_get = TAILQ_FIRST(&records.list);
238 
239             for (int i = 0; i < index; i++) {
240 
241                 if (r_get == NULL) {
242                     break;
243                 }
244 
245                 r_get = TAILQ_NEXT(r_get, tailq_list);
246                 r_get_idx = i + 1;
247             }
248         }
249 
250         // We already checked that index < records.count,
251         // This could be indicative of memory corruption.
252         assert(r_get != NULL);
253         memcpy(r_out, r_get, sizeof(heap_trace_record_t));
254     }
255 
256     portEXIT_CRITICAL(&trace_mux);
257     return result;
258 }
259 
heap_trace_summary(heap_trace_summary_t * summary)260 esp_err_t heap_trace_summary(heap_trace_summary_t *summary)
261 {
262     if (summary == NULL) {
263         return ESP_ERR_INVALID_ARG;
264     }
265 
266     portENTER_CRITICAL(&trace_mux);
267     summary->mode = mode;
268     summary->total_allocations = total_allocations;
269     summary->total_frees = total_frees;
270     summary->count = records.count;
271     summary->capacity = records.capacity;
272     summary->high_water_mark = records.high_water_mark;
273     summary->has_overflowed = records.has_overflowed;
274 #if CONFIG_HEAP_TRACE_HASH_MAP
275     summary->total_hashmap_hits = total_hashmap_hits;
276     summary->total_hashmap_miss = total_hashmap_miss;
277 #endif // CONFIG_HEAP_TRACE_HASH_MAP
278     portEXIT_CRITICAL(&trace_mux);
279 
280     return ESP_OK;
281 }
282 
heap_trace_dump(void)283 void heap_trace_dump(void) {
284     heap_trace_dump_caps(MALLOC_CAP_INTERNAL | MALLOC_CAP_SPIRAM);
285 }
286 
heap_trace_dump_caps(const uint32_t caps)287 void heap_trace_dump_caps(const uint32_t caps) {
288     heap_trace_dump_base(caps & MALLOC_CAP_INTERNAL, caps & MALLOC_CAP_SPIRAM);
289 }
290 
heap_trace_dump_base(bool internal_ram,bool psram)291 static void heap_trace_dump_base(bool internal_ram, bool psram)
292 {
293     portENTER_CRITICAL(&trace_mux);
294 
295     size_t delta_size = 0;
296     size_t delta_allocs = 0;
297     size_t start_count = records.count;
298 
299     esp_rom_printf("====== Heap Trace: %"PRIu32" records (%"PRIu32" capacity) ======\n",
300         records.count, records.capacity);
301 
302     // Iterate through through the linked list
303 
304     heap_trace_record_t *r_cur = TAILQ_FIRST(&records.list);
305 
306     for (int i = 0; i < records.count; i++) {
307 
308         // check corruption
309         if (r_cur == NULL) {
310             esp_rom_printf("\nError: heap trace linked list is corrupt. expected more records.\n");
311             break;
312         }
313 
314         bool should_print = r_cur->address != NULL &&
315             ((psram && internal_ram) ||
316              (internal_ram && esp_ptr_internal(r_cur->address)) ||
317              (psram && esp_ptr_external_ram(r_cur->address)));
318 
319         if (should_print) {
320 
321             const char* label = "";
322             if (esp_ptr_internal(r_cur->address)) {
323                 label = ", Internal";
324             }
325             if (esp_ptr_external_ram(r_cur->address)) {
326                 label = ",    PSRAM";
327             }
328 
329             esp_rom_printf("%6d bytes (@ %p%s) allocated CPU %d ccount 0x%08x caller ",
330                    r_cur->size, r_cur->address, label, r_cur->ccount & 1, r_cur->ccount & ~3);
331 
332             for (int j = 0; j < STACK_DEPTH && r_cur->alloced_by[j] != 0; j++) {
333                 esp_rom_printf("%p%s", r_cur->alloced_by[j],
334                        (j < STACK_DEPTH - 1) ? ":" : "");
335             }
336 
337             if (mode != HEAP_TRACE_ALL || STACK_DEPTH == 0 || r_cur->freed_by[0] == NULL) {
338                 delta_size += r_cur->size;
339                 delta_allocs++;
340                 esp_rom_printf("\n");
341             } else {
342                 esp_rom_printf("\nfreed by ");
343                 for (int j = 0; j < STACK_DEPTH; j++) {
344                     esp_rom_printf("%p%s", r_cur->freed_by[j],
345                            (j < STACK_DEPTH - 1) ? ":" : "\n");
346                 }
347             }
348         }
349 
350         r_cur = TAILQ_NEXT(r_cur, tailq_list);
351     }
352 
353     esp_rom_printf("====== Heap Trace Summary ======\n");
354 
355     if (mode == HEAP_TRACE_ALL) {
356         esp_rom_printf("Mode: Heap Trace All\n");
357         esp_rom_printf("%"PRIu32" bytes alive in trace (%"PRIu32"/%"PRIu32" allocations)\n",
358                delta_size, delta_allocs, heap_trace_get_count());
359     } else {
360         esp_rom_printf("Mode: Heap Trace Leaks\n");
361         esp_rom_printf("%"PRIu32" bytes 'leaked' in trace (%"PRIu32" allocations)\n", delta_size, delta_allocs);
362     }
363 
364     esp_rom_printf("records: %"PRIu32" (%"PRIu32" capacity, %"PRIu32" high water mark)\n",
365         records.count, records.capacity, records.high_water_mark);
366 
367 #if CONFIG_HEAP_TRACE_HASH_MAP
368     esp_rom_printf("hashmap: %"PRIu32" capacity (%"PRIu32" hits, %"PRIu32" misses)\n",
369         (size_t)CONFIG_HEAP_TRACE_HASH_MAP_SIZE, total_hashmap_hits, total_hashmap_miss);
370 #endif // CONFIG_HEAP_TRACE_HASH_MAP
371 
372     esp_rom_printf("total allocations: %"PRIu32"\n", total_allocations);
373     esp_rom_printf("total frees: %"PRIu32"\n", total_frees);
374 
375     if (start_count != records.count) { // only a problem if trace isn't stopped before dumping
376         esp_rom_printf("(NB: New entries were traced while dumping, so trace dump may have duplicate entries.)\n");
377     }
378     if (records.has_overflowed) {
379         esp_rom_printf("(NB: Internal Buffer has overflowed, so trace data is incomplete.)\n");
380     }
381     esp_rom_printf("================================\n");
382 
383     portEXIT_CRITICAL(&trace_mux);
384 }
385 
386 /* Add a new allocation to the heap trace records */
record_allocation(const heap_trace_record_t * r_allocation)387 static HEAP_IRAM_ATTR void record_allocation(const heap_trace_record_t *r_allocation)
388 {
389     if (!tracing || r_allocation->address == NULL) {
390         return;
391     }
392 
393     portENTER_CRITICAL(&trace_mux);
394 
395     if (tracing) {
396         // If buffer is full, pop off the oldest
397         // record to make more space
398         if (records.count == records.capacity) {
399 
400             records.has_overflowed = true;
401 
402             heap_trace_record_t *r_first = TAILQ_FIRST(&records.list);
403 
404             list_remove(r_first);
405         }
406         // push onto end of list
407         list_add(r_allocation);
408         total_allocations++;
409     }
410 
411     portEXIT_CRITICAL(&trace_mux);
412 }
413 
414 /* record a free event in the heap trace log
415 
416    For HEAP_TRACE_ALL, this means filling in the freed_by pointer.
417    For HEAP_TRACE_LEAKS, this means removing the record from the log.
418 
419    callers is an array of  STACK_DEPTH function pointer from the call stack
420    leading to the call of record_free.
421 */
record_free(void * p,void ** callers)422 static HEAP_IRAM_ATTR void record_free(void *p, void **callers)
423 {
424        if (!tracing || p == NULL) {
425         return;
426     }
427 
428     portENTER_CRITICAL(&trace_mux);
429     // return directly if records.count == 0. In case of hashmap being used
430     // this prevents the hashmap to return an item that is no longer in the
431     // records list.
432     if (records.count == 0) {
433         portEXIT_CRITICAL(&trace_mux);
434         return;
435     }
436 
437     if (tracing) {
438 
439         total_frees++;
440 
441         heap_trace_record_t *r_found = list_find_address_reverse(p);
442         if (r_found) {
443             if (mode == HEAP_TRACE_ALL) {
444 
445                 // add 'freed_by' info to the record
446                 memcpy(r_found->freed_by, callers, sizeof(void *) * STACK_DEPTH);
447 
448             } else { // HEAP_TRACE_LEAKS
449                 // Leak trace mode, once an allocation is freed
450                 // we remove it from the list & hashmap
451                 list_remove(r_found);
452             }
453         }
454     }
455 
456     portEXIT_CRITICAL(&trace_mux);
457 }
458 
459 // connect all records into a linked list of 'unused' records
list_setup(void)460 static void list_setup(void)
461 {
462     TAILQ_INIT(&records.list);
463     TAILQ_INIT(&records.unused);
464 
465     for (int i = 0; i < records.capacity; i++) {
466 
467         heap_trace_record_t *r_cur = &records.buffer[i];
468 
469         TAILQ_INSERT_TAIL(&records.unused, r_cur, tailq_list);
470     }
471 }
472 
473 /* 1. removes record r_remove from records.list,
474    2. places it into records.unused */
list_remove(heap_trace_record_t * r_remove)475 static HEAP_IRAM_ATTR void list_remove(heap_trace_record_t* r_remove)
476 {
477     assert(records.count > 0);
478 
479 #if CONFIG_HEAP_TRACE_HASH_MAP
480     map_remove(r_remove);
481 #endif
482 
483     // remove from records.list
484     TAILQ_REMOVE(&records.list, r_remove, tailq_list);
485 
486     // set as unused
487     r_remove->address = 0;
488     r_remove->size = 0;
489 
490     // add to records.unused
491     TAILQ_INSERT_HEAD(&records.unused, r_remove, tailq_list);
492 
493     // decrement
494     records.count--;
495 }
496 
497 
498 // pop record from unused list
list_pop_unused(void)499 static HEAP_IRAM_ATTR heap_trace_record_t* list_pop_unused(void)
500 {
501     // no records left?
502     if (records.count >= records.capacity) {
503         return NULL;
504     }
505 
506     // get from records.unused
507     heap_trace_record_t *r_unused = TAILQ_FIRST(&records.unused);
508     assert(r_unused->address == NULL);
509     assert(r_unused->size == 0);
510 
511     // remove from records.unused
512     TAILQ_REMOVE(&records.unused, r_unused, tailq_list);
513 
514     return r_unused;
515 }
516 
517 // deep copy a record.
518 // Note: only copies the *allocation data*, not the next & prev ptrs
record_deep_copy(heap_trace_record_t * r_dest,const heap_trace_record_t * r_src)519 static HEAP_IRAM_ATTR void record_deep_copy(heap_trace_record_t *r_dest, const heap_trace_record_t *r_src)
520 {
521     r_dest->ccount  = r_src->ccount;
522     r_dest->address = r_src->address;
523     r_dest->size    = r_src->size;
524     memcpy(r_dest->freed_by,   r_src->freed_by,   sizeof(void *) * STACK_DEPTH);
525     memcpy(r_dest->alloced_by, r_src->alloced_by, sizeof(void *) * STACK_DEPTH);
526 }
527 
528 // Append a record to records.list
529 // Note: This deep copies r_append
list_add(const heap_trace_record_t * r_append)530 static HEAP_IRAM_ATTR heap_trace_record_t* list_add(const heap_trace_record_t *r_append)
531 {
532     if (records.count < records.capacity) {
533 
534         // get unused record
535         heap_trace_record_t *r_dest = list_pop_unused();
536 
537         // we checked that there is capacity, so this
538         // should never be null.
539         assert(r_dest != NULL);
540 
541         // copy allocation data
542         record_deep_copy(r_dest, r_append);
543 
544         // append to records.list
545         TAILQ_INSERT_TAIL(&records.list, r_dest, tailq_list);
546 
547         // increment
548         records.count++;
549 
550         // high water mark
551         if (records.count > records.high_water_mark) {
552             records.high_water_mark = records.count;
553         }
554 
555 #if CONFIG_HEAP_TRACE_HASH_MAP
556         map_add(r_dest);
557 #endif
558 
559         return r_dest;
560 
561     } else {
562         records.has_overflowed = true;
563         return NULL;
564     }
565 }
566 
567 // search records.list backwards for the allocation record matching this address
list_find_address_reverse(void * p)568 static HEAP_IRAM_ATTR heap_trace_record_t* list_find_address_reverse(void* p)
569 {
570     heap_trace_record_t *r_found = NULL;
571 
572 #if CONFIG_HEAP_TRACE_HASH_MAP
573         // check the hashmap
574         r_found = map_find(p);
575 
576         if (r_found != NULL) {
577             return r_found;
578         }
579 #endif
580 
581     // Perf: We search backwards because new allocations are appended
582     // to the end of the list and most allocations are short lived.
583     heap_trace_record_t *r_cur = NULL;
584     TAILQ_FOREACH(r_cur, &records.list, tailq_list) {
585         if (r_cur->address == p) {
586             r_found = r_cur;
587             break;
588         }
589     }
590 
591     return r_found;
592 }
593 
594 #include "heap_trace.inc"
595 
596 #endif // CONFIG_HEAP_TRACING_STANDALONE
597