diff --git a/components/heap/heap_task_info.c b/components/heap/heap_task_info.c index e992040dccb..f3a69996ad8 100644 --- a/components/heap/heap_task_info.c +++ b/components/heap/heap_task_info.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -14,14 +14,18 @@ #include "esp_heap_task_info.h" #include "esp_heap_task_info_internal.h" #include "heap_memory_layout.h" -#include "esp_log.h" +#include "esp_rom_sys.h" #ifdef CONFIG_HEAP_TASK_TRACKING -const static char *TAG = "heap_task_tracking"; - -static SemaphoreHandle_t s_task_tracking_mutex = NULL; -static StaticSemaphore_t s_task_tracking_mutex_buf; +/* malloc/free can run from ISR or while a spinlock is already held, so this + * lock is a portmux critical section, matching multi_heap. + * + * Do not use std i/o here (use esp_rom_printf()). Do not print statistics or + * free tracking nodes while the lock is held: both can take unbounded time, + * and multi_heap_free() takes the lock of the heap the node was allocated from. + */ +static portMUX_TYPE s_task_tracking_mux = portMUX_INITIALIZER_UNLOCKED; typedef struct alloc_stats { @@ -90,7 +94,7 @@ static HEAP_IRAM_ATTR void create_new_alloc_stats_entry(heap_stats_t *heap_stats alloc_stats = multi_heap_malloc(heap_used_for_alloc->heap, sizeof(alloc_stats_t)); if (!alloc_stats) { - ESP_LOGE(TAG, "Could not allocate memory to add new task statistics"); + esp_rom_printf("heap_task_tracking: could not allocate memory to add new task statistics\n"); return; } } @@ -119,7 +123,7 @@ static HEAP_IRAM_ATTR void create_new_heap_stats_entry(task_info_t *task_stats, // to memset the memory since all field will be set later in the function. heap_stats_t *heap_stats = multi_heap_malloc(heap_used_for_alloc->heap, sizeof(heap_stats_t)); if (!heap_stats) { - ESP_LOGE(TAG, "Could not allocate memory to add new task statistics"); + esp_rom_printf("heap_task_tracking: could not allocate memory to add new task statistics\n"); return; } @@ -167,7 +171,7 @@ static HEAP_IRAM_ATTR void create_new_task_stats_entry(heap_t *used_heap, TaskHa // create the task_stats entry. No need to memset since all fields are set later task_info = multi_heap_malloc(heap_used_for_alloc->heap, sizeof(task_info_t)); if (!task_info) { - ESP_LOGE(TAG, "Could not allocate memory to add new task statistics"); + esp_rom_printf("heap_task_tracking: could not allocate memory to add new task statistics\n"); return; } @@ -219,9 +223,12 @@ static HEAP_IRAM_ATTR void create_new_task_stats_entry(heap_t *used_heap, TaskHa #if !CONFIG_HEAP_TRACK_DELETED_TASKS /** - * @brief Delete an entry from the list of task statistics + * @brief Free the heap, alloc, and task statistics nodes of a detached task. * - * @param task_info The task statistics to delete from the list of task statistics + * The caller must already have removed the task from the tracking list and + * must not hold the task tracking lock: multi_heap_free() takes the heap lock. + * + * @param task_info Detached task statistics to free */ static HEAP_IRAM_ATTR void delete_task_info_entry(task_info_t *task_info) { @@ -259,28 +266,19 @@ static HEAP_IRAM_ATTR void delete_task_info_entry(task_info_t *task_info) } } } - if (STAILQ_EMPTY(&task_info->heaps_stats)) { - // remove task_info from task_stats (and free the memory) - SLIST_REMOVE(&task_stats, task_info, task_stats, next_task_info); - containing_heap = find_containing_heap(task_info); - if (containing_heap != NULL) { - multi_heap_free(containing_heap->heap, task_info); - } + containing_heap = find_containing_heap(task_info); + if (containing_heap != NULL) { + multi_heap_free(containing_heap->heap, task_info); } } #endif // !CONFIG_HEAP_TRACK_DELETED_TASKS HEAP_IRAM_ATTR void heap_caps_update_per_task_info_alloc(heap_t *heap, void *ptr, size_t size, uint32_t caps) { - if (s_task_tracking_mutex == NULL) { - s_task_tracking_mutex = xSemaphoreCreateMutexStatic(&s_task_tracking_mutex_buf); - assert(s_task_tracking_mutex); - } - TaskHandle_t task_handle = xTaskGetCurrentTaskHandle(); task_info_t *task_info = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); /* find the task in the list and update the overall stats */ SLIST_FOREACH(task_info, &task_stats, next_task_info) { if (task_info->task_stat.handle == task_handle && task_info->task_stat.is_alive) { @@ -302,7 +300,7 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_alloc(heap_t *heap, void *ptr /* add the alloc info to the list */ create_new_alloc_stats_entry(heap_stats, NULL, task_handle, ptr, size); - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); return; } } @@ -322,7 +320,7 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_alloc(heap_t *heap, void *ptr // Add the info to the list (either new task stats or new heap stat if task_info not NULL) create_new_task_stats_entry(heap, task_handle, task_info, ptr, size, caps); - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); } HEAP_IRAM_ATTR void heap_caps_update_per_task_info_realloc(heap_t *heap, void *old_ptr, void *new_ptr, @@ -334,7 +332,7 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_realloc(heap_t *heap, void *o task_info_t *task_info = NULL; alloc_stats_t *alloc_stat = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); SLIST_FOREACH(task_info, &task_stats, next_task_info) { if (task_info->task_stat.handle == old_task) { heap_stats_t *heap_stats = NULL; @@ -393,7 +391,7 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_realloc(heap_t *heap, void *o create_new_task_stats_entry(heap, task_handle, task_info, new_ptr, new_size, caps); } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); } HEAP_IRAM_ATTR void heap_caps_update_per_task_info_free(heap_t *heap, void *ptr) @@ -405,11 +403,12 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_free(heap_t *heap, void *ptr) } task_info_t *task_info = NULL; + alloc_stats_t *alloc_stat_to_free = NULL; #if !CONFIG_HEAP_TRACK_DELETED_TASKS task_info_t *task_info_to_delete = NULL; #endif // !CONFIG_HEAP_TRACK_DELETED_TASKS - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); /* find the matching task */ SLIST_FOREACH(task_info, &task_stats, next_task_info) { /* check all tasks (alive and deleted) since the free can come from any tasks, @@ -434,18 +433,12 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_free(heap_t *heap, void *ptr) heap_stats->heap_stat.alloc_count--; heap_stats->heap_stat.current_usage -= alloc_stat->alloc_stat.size; task_info->task_stat.overall_current_usage -= alloc_stat->alloc_stat.size; + alloc_stat_to_free = alloc_stat; } break; } } - - /* free the memory used to store alloc_stat */ - heap_t *containing_heap = find_containing_heap(alloc_stat); - // task_stats must be allocated somewhere - if (containing_heap != NULL) { - multi_heap_free(containing_heap->heap, alloc_stat); - } } // when a task is deleted, esp_caps_free is called to delete the TCB of the task from vTaskDelete. @@ -463,13 +456,23 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_free(heap_t *heap, void *ptr) } #if !CONFIG_HEAP_TRACK_DELETED_TASKS - // remove the entry related to the task that was just deleted. + /* Drop the task from the list under the lock. Its nodes are freed after + * the lock is released, so multi_heap_free() is not nested in this CS. */ if (task_info_to_delete != NULL) { - delete_task_info_entry(task_info_to_delete); + SLIST_REMOVE(&task_stats, task_info_to_delete, task_stats, next_task_info); } #endif // !CONFIG_HEAP_TRACK_DELETED_TASKS - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); + + /* free the memory used to store alloc_stat */ + heap_t *containing_heap = find_containing_heap(alloc_stat_to_free); + if (containing_heap != NULL) { + multi_heap_free(containing_heap->heap, alloc_stat_to_free); + } +#if !CONFIG_HEAP_TRACK_DELETED_TASKS + delete_task_info_entry(task_info_to_delete); +#endif // !CONFIG_HEAP_TRACK_DELETED_TASKS } esp_err_t heap_caps_get_all_task_stat(heap_all_tasks_stat_t *tasks_stat) @@ -486,7 +489,7 @@ esp_err_t heap_caps_get_all_task_stat(heap_all_tasks_stat_t *tasks_stat) size_t alloc_index = 0; task_info_t *task_info = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); SLIST_FOREACH(task_info, &task_stats, next_task_info) { // If there is no more task stat entries available in tasks_stat->stat_arr // break the loop and return the function. @@ -543,7 +546,7 @@ esp_err_t heap_caps_get_all_task_stat(heap_all_tasks_stat_t *tasks_stat) } } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); tasks_stat->task_count = task_index; tasks_stat->heap_count = heap_index; @@ -566,7 +569,7 @@ esp_err_t heap_caps_get_single_task_stat(heap_single_task_stat_t *task_stat, Tas task_info_t *task_info = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); SLIST_FOREACH(task_info, &task_stats, next_task_info) { if(task_info->task_stat.handle == task_handle) { // copy the task_stat of the task itself @@ -574,7 +577,7 @@ esp_err_t heap_caps_get_single_task_stat(heap_single_task_stat_t *task_stat, Tas break; } } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); if (task_info == NULL) { return ESP_FAIL; @@ -589,7 +592,7 @@ esp_err_t heap_caps_get_single_task_stat(heap_single_task_stat_t *task_stat, Tas size_t heap_index = 0; size_t alloc_index = 0; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); heap_stats_t *heap_info = STAILQ_FIRST(&task_info->heaps_stats); while(heap_index < task_info->task_stat.heap_count || heap_info != NULL) { // check that there is enough heap_stat entry left to add another one to the user defined @@ -623,7 +626,7 @@ esp_err_t heap_caps_get_single_task_stat(heap_single_task_stat_t *task_stat, Tas heap_index++; heap_info = STAILQ_NEXT(heap_info, next_heap_stat); } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); task_stat->heap_count = heap_index; task_stat->alloc_count = alloc_index; @@ -631,7 +634,15 @@ esp_err_t heap_caps_get_single_task_stat(heap_single_task_stat_t *task_stat, Tas return ESP_OK; } -static void heap_caps_print_task_info(FILE *stream, task_info_t *task_info, bool is_last_task_info) +/** + * @brief Print the statistics of a task from a snapshot of the internal statistics. + * + * @param stream The stream to print to, if NULL then stdout is used + * @param task_stat The task statistics to print + * @param heap_count The number of valid entries in task_stat->heap_stat + * @param is_last_task_info Whether this is the last task to be printed + */ +static void heap_caps_print_task_info(FILE *stream, const task_stat_t *task_stat, size_t heap_count, bool is_last_task_info) { if (stream == NULL) { stream = stdout; @@ -640,39 +651,44 @@ static void heap_caps_print_task_info(FILE *stream, task_info_t *task_info, bool const char *task_info_visual = is_last_task_info ? " " : "│"; const char *task_info_visual_start = is_last_task_info ? "└" : "├"; fprintf(stream, "%s %s: %s, CURRENT MEMORY USAGE %d, PEAK MEMORY USAGE %d, TOTAL HEAP USED %d:\n", task_info_visual_start, - task_info->task_stat.is_alive ? "ALIVE" : "DELETED", - task_info->task_stat.name, - task_info->task_stat.overall_current_usage, - task_info->task_stat.overall_peak_usage, - task_info->task_stat.heap_count); + task_stat->is_alive ? "ALIVE" : "DELETED", + task_stat->name, + task_stat->overall_current_usage, + task_stat->overall_peak_usage, + task_stat->heap_count); - heap_stats_t *heap_info = NULL; - STAILQ_FOREACH(heap_info, &task_info->heaps_stats, next_heap_stat) { - const char *next_heap_visual = !STAILQ_NEXT(heap_info, next_heap_stat) ? " " : "│"; - const char *next_heap_visual_start = !STAILQ_NEXT(heap_info, next_heap_stat) ? "└" : "├"; + for (size_t heap_index = 0; heap_index < heap_count; heap_index++) { + const heap_stat_t *heap_stat = &task_stat->heap_stat[heap_index]; + const bool is_last_heap_stat = (heap_index + 1 == heap_count); + const char *next_heap_visual = is_last_heap_stat ? " " : "│"; + const char *next_heap_visual_start = is_last_heap_stat ? "└" : "├"; fprintf(stream, "%s %s HEAP: %s, CAPS: 0x%08lx, SIZE: %d, USAGE: CURRENT %d (%d%%), PEAK %d (%d%%), ALLOC COUNT: %d\n", task_info_visual, next_heap_visual_start, - heap_info->heap_stat.name, - heap_info->heap_stat.caps, - heap_info->heap_stat.size, - heap_info->heap_stat.current_usage, - (heap_info->heap_stat.current_usage * 100) / heap_info->heap_stat.size, - heap_info->heap_stat.peak_usage, - (heap_info->heap_stat.peak_usage * 100) / heap_info->heap_stat.size, - heap_info->heap_stat.alloc_count); + heap_stat->name, + heap_stat->caps, + heap_stat->size, + heap_stat->current_usage, + (heap_stat->current_usage * 100) / heap_stat->size, + heap_stat->peak_usage, + (heap_stat->peak_usage * 100) / heap_stat->size, + heap_stat->alloc_count); - alloc_stats_t *alloc_stats = NULL; - STAILQ_FOREACH(alloc_stats, &heap_info->allocs_stats, next_alloc_stat) { + // the alloc statistics are not available when the snapshot could not hold them all + if (heap_stat->alloc_stat == NULL) { + continue; + } + + for (size_t alloc_index = 0; alloc_index < heap_stat->alloc_count; alloc_index++) { fprintf(stream, "%s %s ├ ALLOC %p, SIZE %" PRIu32 "\n", task_info_visual, next_heap_visual, - alloc_stats->alloc_stat.address, - alloc_stats->alloc_stat.size); + heap_stat->alloc_stat[alloc_index].address, + heap_stat->alloc_stat[alloc_index].size); } } } -static void heap_caps_print_task_overview(FILE *stream, task_info_t *task_info, bool is_first_task_info, bool is_last_task_info) +static void heap_caps_print_task_overview(FILE *stream, const task_stat_t *task_stat, bool is_first_task_info, bool is_last_task_info) { if (stream == NULL) { stream = stdout; @@ -684,86 +700,124 @@ static void heap_caps_print_task_overview(FILE *stream, task_info_t *task_info, fprintf(stream, "├────────────────────┼─────────┼──────────────────────┼───────────────────┼─────────────────┤\n"); } - task_stat_t task_stat = task_info->task_stat; fprintf(stream, "│ %18s │ %7s │ %20d │ %17d │ %15d │\n", - task_stat.name, - task_stat.is_alive ? "ALIVE " : "DELETED", - task_stat.overall_current_usage, - task_stat.overall_peak_usage, - task_stat.heap_count); + task_stat->name, + task_stat->is_alive ? "ALIVE " : "DELETED", + task_stat->overall_current_usage, + task_stat->overall_peak_usage, + task_stat->heap_count); if (is_last_task_info) { fprintf(stream, "└────────────────────┴─────────┴──────────────────────┴───────────────────┴─────────────────┘\n"); } } -void heap_caps_print_single_task_stat(FILE *stream, TaskHandle_t task_handle) -{ - if (task_handle == NULL) { - task_handle = xTaskGetCurrentTaskHandle(); - } +static esp_err_t alloc_stat_arrays_for_single_task(heap_single_task_stat_t *task_stat, TaskHandle_t task_handle, bool alive_only); +/** + * @brief Allocate an array holding the task level statistics of every tracked task. + * + * Contrary to heap_caps_alloc_all_task_stat_arrays(), the arrays holding the heap and alloc + * statistics are not allocated since the overview only prints task level information. + * + * @param tasks_stat Structure filled by this function + * @return ESP_OK if the array was allocated successfully, ESP_FAIL otherwise + */ +static esp_err_t alloc_task_stat_array_for_overview(heap_all_tasks_stat_t *tasks_stat) +{ + memset(tasks_stat, 0x00, sizeof(heap_all_tasks_stat_t)); + + size_t task_count = 0; task_info_t *task_info = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); SLIST_FOREACH(task_info, &task_stats, next_task_info) { - if (task_info->task_stat.handle == task_handle) { - heap_caps_print_task_info(stream, task_info, true); - - xSemaphoreGive(s_task_tracking_mutex); - return; - } + task_count++; } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); + + if (task_count == 0) { + return ESP_OK; + } + + heap_t *heap_used_for_alloc = find_biggest_heap(); + tasks_stat->stat_arr = multi_heap_malloc(heap_used_for_alloc->heap, task_count * sizeof(task_stat_t)); + if (tasks_stat->stat_arr == NULL) { + return ESP_FAIL; + } + tasks_stat->task_count = task_count; + + return ESP_OK; +} + +void heap_caps_print_single_task_stat(FILE *stream, TaskHandle_t task_handle) +{ + heap_single_task_stat_t task_stat = {}; + + /* Print from a copy: fprintf must not run with the tracking lock held. + * Deleted tasks are printed too, so statistics of a task that is no longer + * running can still be inspected. */ + if (alloc_stat_arrays_for_single_task(&task_stat, task_handle, false) == ESP_OK && + heap_caps_get_single_task_stat(&task_stat, task_handle) == ESP_OK) { + heap_caps_print_task_info(stream, &task_stat.stat, task_stat.heap_count, true); + } + + heap_caps_free_single_task_stat_arrays(&task_stat); } void heap_caps_print_all_task_stat(FILE *stream) { - task_info_t *task_info = NULL; + heap_all_tasks_stat_t tasks_stat = {}; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); - SLIST_FOREACH(task_info, &task_stats, next_task_info) { - const bool last_task_info = (SLIST_NEXT(task_info, next_task_info) == NULL); - heap_caps_print_task_info(stream, task_info, last_task_info); + if (heap_caps_alloc_all_task_stat_arrays(&tasks_stat) == ESP_OK && + heap_caps_get_all_task_stat(&tasks_stat) == ESP_OK) { + for (size_t task_index = 0; task_index < tasks_stat.task_count; task_index++) { + const task_stat_t *task_stat = &tasks_stat.stat_arr[task_index]; + // heap_stat is NULL when the snapshot could not hold the heap statistics of this task + const size_t heap_count = (task_stat->heap_stat != NULL) ? task_stat->heap_count : 0; + heap_caps_print_task_info(stream, task_stat, heap_count, task_index + 1 == tasks_stat.task_count); + } } - xSemaphoreGive(s_task_tracking_mutex); + + heap_caps_free_all_task_stat_arrays(&tasks_stat); } void heap_caps_print_single_task_stat_overview(FILE *stream, TaskHandle_t task_handle) { - if (task_handle == NULL) { - task_handle = xTaskGetCurrentTaskHandle(); + /* The overview only uses task level statistics, so no heap or alloc statistics + * array is needed to take the snapshot. */ + heap_single_task_stat_t task_stat = {}; + + if (heap_caps_get_single_task_stat(&task_stat, task_handle) == ESP_OK) { + heap_caps_print_task_overview(stream, &task_stat.stat, true, true); } - - task_info_t *task_info = NULL; - - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); - SLIST_FOREACH(task_info, &task_stats, next_task_info) { - if (task_info->task_stat.handle == task_handle) { - heap_caps_print_task_overview(stream, task_info, true, true); - - xSemaphoreGive(s_task_tracking_mutex); - return; - } - } - xSemaphoreGive(s_task_tracking_mutex); } void heap_caps_print_all_task_stat_overview(FILE *stream) { - task_info_t *task_info = NULL; - bool is_first_task_info = true; + heap_all_tasks_stat_t tasks_stat = {}; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); - SLIST_FOREACH(task_info, &task_stats, next_task_info) { - const bool last_task_info = (SLIST_NEXT(task_info, next_task_info) == NULL); - heap_caps_print_task_overview(stream, task_info, is_first_task_info, last_task_info); - is_first_task_info = false; + if (alloc_task_stat_array_for_overview(&tasks_stat) == ESP_OK && + heap_caps_get_all_task_stat(&tasks_stat) == ESP_OK) { + for (size_t task_index = 0; task_index < tasks_stat.task_count; task_index++) { + heap_caps_print_task_overview(stream, &tasks_stat.stat_arr[task_index], task_index == 0, + task_index + 1 == tasks_stat.task_count); + } } - xSemaphoreGive(s_task_tracking_mutex); + + heap_caps_free_all_task_stat_arrays(&tasks_stat); } -esp_err_t heap_caps_alloc_single_task_stat_arrays(heap_single_task_stat_t *task_stat, TaskHandle_t task_handle) +/** + * @brief Allocate the arrays used to store the heap and alloc statistics of a given task. + * + * @param task_stat Structure containing information filled by this function + * @param task_handle The task from which to get the information. If NULL, the calling task is used + * @param alive_only When true, only a task that is still running is matched. Task handles are + * recycled by FreeRTOS, so a running task can share its handle with a deleted one + * @return ESP_OK if the memory necessary to gather the statistics was allocated successfully + */ +static esp_err_t alloc_stat_arrays_for_single_task(heap_single_task_stat_t *task_stat, TaskHandle_t task_handle, bool alive_only) { if (task_handle == NULL) { task_handle = xTaskGetCurrentTaskHandle(); @@ -776,9 +830,9 @@ esp_err_t heap_caps_alloc_single_task_stat_arrays(heap_single_task_stat_t *task_ task_info_t *task_info = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); SLIST_FOREACH(task_info, &task_stats, next_task_info) { - if(task_info->task_stat.handle == task_handle && task_info->task_stat.is_alive) { + if(task_info->task_stat.handle == task_handle && (task_info->task_stat.is_alive || !alive_only)) { task_stat->heap_count = task_info->task_stat.heap_count; heap_stats_t *heap_info = NULL; STAILQ_FOREACH(heap_info, &task_info->heaps_stats, next_heap_stat) { @@ -787,7 +841,7 @@ esp_err_t heap_caps_alloc_single_task_stat_arrays(heap_single_task_stat_t *task_ break; } } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); // allocate the memory used to store the statistics of allocs, heaps if (task_stat->heap_count != 0) { @@ -808,6 +862,11 @@ esp_err_t heap_caps_alloc_single_task_stat_arrays(heap_single_task_stat_t *task_ return ESP_OK; } +esp_err_t heap_caps_alloc_single_task_stat_arrays(heap_single_task_stat_t *task_stat, TaskHandle_t task_handle) +{ + return alloc_stat_arrays_for_single_task(task_stat, task_handle, true); +} + void heap_caps_free_single_task_stat_arrays(heap_single_task_stat_t *task_stat) { if (task_stat->heap_stat_start != NULL) { @@ -837,7 +896,7 @@ esp_err_t heap_caps_alloc_all_task_stat_arrays(heap_all_tasks_stat_t *tasks_stat task_info_t *task_info = NULL; - xSemaphoreTake(s_task_tracking_mutex, portMAX_DELAY); + portENTER_CRITICAL_SAFE(&s_task_tracking_mux); SLIST_FOREACH(task_info, &task_stats, next_task_info) { tasks_stat->task_count += 1; @@ -847,7 +906,7 @@ esp_err_t heap_caps_alloc_all_task_stat_arrays(heap_all_tasks_stat_t *tasks_stat tasks_stat->alloc_count += heap_info->heap_stat.alloc_count; } } - xSemaphoreGive(s_task_tracking_mutex); + portEXIT_CRITICAL_SAFE(&s_task_tracking_mux); // allocate the memory used to store the statistics of allocs, heaps and tasks if (tasks_stat->task_count != 0) { diff --git a/components/heap/test_apps/heap_tests/main/CMakeLists.txt b/components/heap/test_apps/heap_tests/main/CMakeLists.txt index 69af24fd943..9e3f86f877d 100644 --- a/components/heap/test_apps/heap_tests/main/CMakeLists.txt +++ b/components/heap/test_apps/heap_tests/main/CMakeLists.txt @@ -14,5 +14,5 @@ set(src_test "test_heap_main.c" idf_component_register(SRCS ${src_test} INCLUDE_DIRS "." - REQUIRES unity esp_psram spi_flash esp_mm + REQUIRES unity esp_psram spi_flash esp_mm esp_timer WHOLE_ARCHIVE) diff --git a/components/heap/test_apps/heap_tests/main/test_task_tracking.c b/components/heap/test_apps/heap_tests/main/test_task_tracking.c index 7dad2a28e39..cf037aaef73 100644 --- a/components/heap/test_apps/heap_tests/main/test_task_tracking.c +++ b/components/heap/test_apps/heap_tests/main/test_task_tracking.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -10,8 +10,10 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "esp_attr.h" #include "esp_heap_caps.h" #include "esp_heap_task_info.h" +#include "esp_timer.h" extern void set_leak_threshold(int threshold); @@ -382,4 +384,136 @@ TEST_CASE("heap task tracking realloc with reused TaskHandle does not underflow vTaskDelete(dummy_task_handle); } +#define STRESS_ALLOC_BYTES 128 +#define STRESS_DURATION_MS 3000 + +static int64_t s_stress_end_time; + +static void task_tracking_stress_task(void *args) +{ + while (esp_timer_get_time() < s_stress_end_time) { + void *ptr = heap_caps_malloc(STRESS_ALLOC_BYTES, MALLOC_CAP_INTERNAL); + if (ptr != NULL) { + heap_caps_free(ptr); + } + } + + // let the test know that this task is done before deleting itself + xTaskNotifyGive((TaskHandle_t)args); + vTaskDelete(NULL); +} + +/** + * @brief Start one allocation stress task on every core, each running for STRESS_DURATION_MS. + * + * The tasks are created with the priority of the calling task so that they share the CPU with + * it instead of starving it. + */ +static void start_stress_tasks(void) +{ + s_stress_end_time = esp_timer_get_time() + (STRESS_DURATION_MS * 1000); + for (int core = 0; core < CONFIG_FREERTOS_NUMBER_OF_CORES; core++) { + xTaskCreatePinnedToCore(&task_tracking_stress_task, "tt_stress", 3072, + (void *)xTaskGetCurrentTaskHandle(), + uxTaskPriorityGet(NULL), NULL, core); + } +} + +/** + * @brief Wait for all the allocation stress tasks to be done. + */ +static void wait_for_stress_tasks(void) +{ + for (int core = 0; core < CONFIG_FREERTOS_NUMBER_OF_CORES; core++) { + // the notification count is not cleared: each stress task notifies once + ulTaskNotifyTake(pdFALSE, portMAX_DELAY); + } +} + +#if CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD + +#define ISR_ALLOC_BYTES 64 +#define ISR_TIMER_PERIOD_US 500 + +static volatile uint32_t s_isr_alloc_count; +static volatile bool s_isr_alloc_failed; + +static void IRAM_ATTR task_tracking_isr_alloc_cb(void *args) +{ + void *ptr = heap_caps_malloc(ISR_ALLOC_BYTES, MALLOC_CAP_INTERNAL); + if (ptr == NULL) { + s_isr_alloc_failed = true; + return; + } + + heap_caps_free(ptr); + s_isr_alloc_count++; +} + +/* malloc() and free() can run from an ISR or a critical section, and task tracking + * is updated on those paths. A blocking lock around the statistics deadlocks here + * (interrupt watchdog timeout). Allocate and free from an ISR while every core does + * the same from a task. + */ +TEST_CASE("heap task tracking is safe when malloc/free run from an ISR", "[heap]") +{ + const esp_timer_create_args_t timer_args = { + .callback = &task_tracking_isr_alloc_cb, + .dispatch_method = ESP_TIMER_ISR, + .name = "tt_isr", + }; + esp_timer_handle_t timer = NULL; + + // the task tracking statistics of the stress tasks are kept after their deletion + set_leak_threshold(-5000); + + s_isr_alloc_count = 0; + s_isr_alloc_failed = false; + + start_stress_tasks(); + + TEST_ESP_OK(esp_timer_create(&timer_args, &timer)); + TEST_ESP_OK(esp_timer_start_periodic(timer, ISR_TIMER_PERIOD_US)); + + wait_for_stress_tasks(); + + TEST_ESP_OK(esp_timer_stop(timer)); + TEST_ESP_OK(esp_timer_delete(timer)); + + TEST_ASSERT_FALSE_MESSAGE(s_isr_alloc_failed, "allocation from ISR failed"); + TEST_ASSERT_GREATER_THAN_UINT32(0, s_isr_alloc_count); + TEST_ASSERT_TRUE(heap_caps_check_integrity_all(true)); +} + +#endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD + +/* Reading and printing statistics must not hold the tracking lock for the whole + * operation. Call the getters and printers while every core allocates and frees, + * so a lock that is held too long would stall the allocator. + */ +TEST_CASE("heap task tracking get and print stats while allocating from all cores", "[heap][qemu-ignore]") +{ + // the task tracking statistics of the stress tasks are kept after their deletion + set_leak_threshold(-5000); + + start_stress_tasks(); + + for (size_t i = 0; i < 5; i++) { + heap_all_tasks_stat_t tasks_stat = {}; + + TEST_ESP_OK(heap_caps_alloc_all_task_stat_arrays(&tasks_stat)); + TEST_ESP_OK(heap_caps_get_all_task_stat(&tasks_stat)); + heap_caps_free_all_task_stat_arrays(&tasks_stat); + + heap_caps_print_all_task_stat_overview(stdout); + heap_caps_print_single_task_stat(stdout, xTaskGetCurrentTaskHandle()); + + vTaskDelay(pdMS_TO_TICKS(100)); + } + + wait_for_stress_tasks(); + + TEST_ASSERT_TRUE(heap_caps_check_integrity_all(true)); +} + #endif // CONFIG_HEAP_TASK_TRACKING diff --git a/components/heap/test_apps/heap_tests/sdkconfig.ci.no_poisoning b/components/heap/test_apps/heap_tests/sdkconfig.ci.no_poisoning index 2280067160e..90278257dc0 100644 --- a/components/heap/test_apps/heap_tests/sdkconfig.ci.no_poisoning +++ b/components/heap/test_apps/heap_tests/sdkconfig.ci.no_poisoning @@ -4,4 +4,5 @@ CONFIG_HEAP_POISONING_COMPREHENSIVE=n CONFIG_HEAP_TASK_TRACKING=y # to make sure the config doesn't induce unexpected behavior CONFIG_HEAP_TRACK_DELETED_TASKS=y # to make sure the config doesn't induce unexpected behavior +CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=y # to allow allocating from an ISR in the task tracking tests CONFIG_COMPILER_WARN_WRITE_STRINGS=y