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