Merge branch 'bugfix/heap_task_tracking_isr_safe' into 'master'

fix(heap): use critical section for task tracking locks

Closes IDFGH-17819 and IDFGH-16380

See merge request espressif/esp-idf!51674
This commit is contained in:
Marius Vikhammer
2026-08-24 17:22:59 +08:00
4 changed files with 318 additions and 124 deletions

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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