feat(heap): Add feature to get peak heap usage

This feature keeps track of the per task peak memory usage.

- Update the heap_task_tracking example to make use of the new feature
Cleanup the implementation:
- multi_heap_get_free_size() is never used, remove it.
- Minor update in heap_caps_update_per_task_info_xx() funcitons.
- Update settting on block owner in heap_caps.c to work with the
get peak usage feature.

- Update heap_caps_update_per_task_info_free() to detect when it
is called to delete the memory allocated for a task TCB. Mark
the corresponding task in the statistic list as deleted.

- Add a Kconfig option dependant on HEAP_TASK_TRACKING being enabled
that force the deletion of the statistics related to deleted task
when set to true.

- In task tracking feature, add a current and peak memory usage
to the heap_stat_t structure to keep track of the current and
peak memory usage of the given task across all heaps.

- Fix missing block owner when allocating memory for heaps_array
in heap_caps_init.

- Keep the original implementation of the task tracking
for backward compatibility reasons.
This commit is contained in:
Guillaume Souchere
2025-02-14 08:47:29 +01:00
parent 47df2ed524
commit daf8f9edb6
16 changed files with 1368 additions and 75 deletions

View File

@@ -18,7 +18,7 @@
#include <stdlib.h>
#include <sys/param.h>
#if !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT)
#if !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT) && !(CONFIG_HEAP_TASK_TRACKING)
TEST_CASE("Capabilities allocator test", "[heap]")
{
char *m1, *m2[10];
@@ -108,7 +108,7 @@ TEST_CASE("Capabilities allocator test", "[heap]")
free(m1);
printf("Done.\n");
}
#endif // !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT)
#endif // !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT) && !(CONFIG_HEAP_TASK_TRACKING)
#ifdef CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY
TEST_CASE("IRAM_8BIT capability test", "[heap]")
@@ -230,7 +230,7 @@ TEST_CASE("heap caps minimum free bytes fault cases", "[heap]")
/* Small function runs from IRAM to check that malloc/free/realloc
all work OK when cache is disabled...
*/
#if !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT) && !CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH
#if !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT) && !CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH && !CONFIG_HEAP_TASK_TRACKING
static IRAM_ATTR __attribute__((noinline)) bool iram_malloc_test(void)
{
spi_flash_guard_get()->start(); // Disables flash cache
@@ -252,7 +252,7 @@ TEST_CASE("heap_caps_xxx functions work with flash cache disabled", "[heap]")
{
TEST_ASSERT( iram_malloc_test() );
}
#endif // !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT) && !CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH
#endif // !(CONFIG_ESP_SYSTEM_MEMPROT_FEATURE || CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT) && !CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH && !CONFIG_HEAP_TASK_TRACKING
#ifdef CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS
TEST_CASE("When enabled, allocation operation failure generates an abort", "[heap][reset=abort,SW_CPU_RESET]")
@@ -272,6 +272,7 @@ void heap_caps_alloc_failed_hook(size_t requested_size, uint32_t caps, const cha
called_user_failed_hook = true;
}
TEST_CASE("user provided alloc failed hook must be called when allocation fails", "[heap]")
{
TEST_ASSERT(heap_caps_register_failed_alloc_callback(heap_caps_alloc_failed_hook) == ESP_OK);

View File

@@ -1,10 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "unity.h"
#include "stdio.h"
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -12,52 +13,34 @@
#include "esp_heap_task_info.h"
// This test only apply when task tracking is enabled
#if defined(CONFIG_HEAP_TASK_TRACKING)
#if defined(CONFIG_HEAP_TASK_TRACKING) && defined(CONFIG_HEAP_TRACK_DELETED_TASKS)
#define MAX_TASK_NUM 10 // Max number of per tasks info that it can store
#define MAX_BLOCK_NUM 10 // Max number of per block info that it can store
#define ALLOC_BYTES 36
static void check_heap_task_info(TaskHandle_t taskHdl)
static void check_heap_task_info(const char *task_name, const bool task_active)
{
size_t num_totals = 0;
heap_task_totals_t s_totals_arr[MAX_TASK_NUM];
heap_task_block_t s_block_arr[MAX_BLOCK_NUM];
heap_all_tasks_stat_t heap_tasks_stat;
heap_task_info_params_t heap_info = {0};
heap_info.caps[0] = MALLOC_CAP_32BIT; // Gets heap info with CAP_32BIT capabilities
heap_info.mask[0] = MALLOC_CAP_32BIT;
heap_info.tasks = NULL; // Passing NULL captures heap info for all tasks
heap_info.num_tasks = 0;
heap_info.totals = s_totals_arr; // Gets task wise allocation details
heap_info.num_totals = &num_totals;
heap_info.max_totals = MAX_TASK_NUM; // Maximum length of "s_totals_arr"
heap_info.blocks = s_block_arr; // Gets block wise allocation details. For each block, gets owner task, address and size
heap_info.max_blocks = MAX_BLOCK_NUM; // Maximum length of "s_block_arr"
heap_tasks_stat.task_count = 10;
heap_tasks_stat.heap_count = 20;
heap_tasks_stat.alloc_count = 60;
task_stat_t arr_task_stat[heap_tasks_stat.task_count];
heap_stat_t arr_heap_stat[heap_tasks_stat.heap_count];
heap_task_block_t arr_alloc_stat[heap_tasks_stat.alloc_count];
heap_tasks_stat.stat_arr = arr_task_stat;
heap_tasks_stat.heap_stat_start = arr_heap_stat;
heap_tasks_stat.alloc_stat_start = arr_alloc_stat;
heap_caps_get_per_task_info(&heap_info);
heap_caps_get_all_task_stat(&heap_tasks_stat);
bool task_found = false;
for (int i = 0 ; i < *heap_info.num_totals; i++) {
for (size_t task_index = 0; task_index < heap_tasks_stat.task_count; task_index++) {
// the prescheduler allocs and free are stored as a
// task with a handle set to 0, avoid calling pcTaskGetName
// in that case.
if (heap_info.totals[i].task != 0 && (uint32_t*)(heap_info.totals[i].task) == (uint32_t*)taskHdl) {
task_stat_t task_stat = heap_tasks_stat.stat_arr[task_index];
if (0 == strcmp(task_stat.name, task_name) && task_stat.is_alive == task_active) {
task_found = true;
// check the number of byte allocated according to the task tracking feature
// and make sure it matches the expected value. The size returned by the
// heap_caps_get_per_task_info includes the size of the block owner (4 bytes)
TEST_ASSERT(heap_info.totals[i].size[0] == ALLOC_BYTES + 4);
}
// test that if not 0, the task handle corresponds to an actual task.
// this test is to make sure no rubbish is stored as a task handle.
if (heap_info.totals[i].task != 0) {
// feeding the task name returned by pcTaskGetName() to xTaskGetHandle().
// xTaskGetHandle would return the task handler used as parameter in
// pcTaskGetName if the task handle is valid. Otherwise, it will return
// NULL or just crash if the pointer to the task name is complete nonsense.
TEST_ASSERT_EQUAL(heap_info.totals[i].task, xTaskGetHandle(pcTaskGetName(heap_info.totals[i].task)));
}
}
TEST_ASSERT_TRUE(task_found);
@@ -70,36 +53,196 @@ static void test_task(void *args)
abort();
}
// unlock main too check task tracking feature
// unlock main to check task tracking feature
xTaskNotifyGive((TaskHandle_t)args);
// wait for main to delete this task
// wait for main to give back the hand to the task to delete the pointer
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
heap_caps_free(ptr);
// unlock main to delete the task
xTaskNotifyGive((TaskHandle_t)args);
// wait for main to delete the task
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
}
static void test_task_a(void *args)
{
test_task(args);
}
static void test_task_b(void *args)
{
test_task(args);
}
/* This test will create a task, wait for the task to allocate / free memory
* so it is added to the task tracking info in the heap component and then
* call heap_caps_get_per_task_info() and make sure a task with the name test_task
* call heap_caps_get_all_task_stat() and make sure a task with the name test_task
* is in the list, and that the right ALLOC_BYTES are shown.
*
* Note: The memory allocated in the task is not freed for the sake of the test
* so it is normal that memory leak will be reported by the test environment. It
* shouldn't be more than the byte allocated by the task + associated metadata
*/
TEST_CASE("heap task tracking reports created task", "[heap]")
TEST_CASE("heap task tracking reports created / deleted task", "[heap]")
{
TaskHandle_t test_task_handle;
xTaskCreate(&test_task, "test_task", 3072, (void *)xTaskGetCurrentTaskHandle(), 5, &test_task_handle);
const char *task_name = "test_task_a";
xTaskCreate(&test_task_a, task_name, 3072, (void *)xTaskGetCurrentTaskHandle(), 5, &test_task_handle);
// wait for task to allocate memory and give the hand back to the test
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// check that the task is referenced in the list of task
// by the task tracking feature. Check the number of bytes
// the task has allocated and make sure it is matching the
// expected value.
check_heap_task_info(test_task_handle);
// by the task tracking feature. check that the task name is
// matching and the task is running.
check_heap_task_info(task_name, true);
// unlock main to check task tracking feature
xTaskNotifyGive(test_task_handle);
// wait for the task to free the memory
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// delete the task.
vTaskDelete(test_task_handle);
// check that the task is referenced in the list of task
// by the task tracking feature. check that the task name is
// matching and the task is marked as deleted.
check_heap_task_info(task_name, false);
}
/* The test case calls heap_caps_alloc_all_task_stat_arrays and heap_caps_get_all_task_stat
* after creating new tasks and allocating in new heaps to check that the number of tasks, heaps and
* allocation statistics provided by heap_caps_get_all_task_stat is updated accordingly.
*/
TEST_CASE("heap task tracking check alloc array and get all tasks info", "[heap]")
{
// call heap_caps_alloc_all_task_stat_arrays and save the number of tasks, heaps and allocs
// statistics available when the test starts
heap_all_tasks_stat_t tasks_stat;
esp_err_t ret_val = heap_caps_alloc_all_task_stat_arrays(&tasks_stat);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
ret_val = heap_caps_get_all_task_stat(&tasks_stat);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
const size_t nb_of_tasks_stat = tasks_stat.task_count;
const size_t nb_of_heaps_stat = tasks_stat.heap_count;
const size_t nb_of_allocs_stat = tasks_stat.alloc_count;
heap_caps_free_all_task_stat_arrays(&tasks_stat);
// Create a task that will allocate memory
TaskHandle_t test_task_handle;
const char *task_name = "test_task_b";
xTaskCreate(&test_task_b, task_name, 3072, (void *)xTaskGetCurrentTaskHandle(), 5, &test_task_handle);
// wait for the task to give the hand to the test and call heap_caps_alloc_all_task_stat_arrays.
// Compare the number of tasks, heaps and allocs statistics available to make sure they contain the stats
// related to the newly created task.
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
ret_val = heap_caps_alloc_all_task_stat_arrays(&tasks_stat);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
ret_val = heap_caps_get_all_task_stat(&tasks_stat);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
TEST_ASSERT(nb_of_tasks_stat < tasks_stat.task_count);
TEST_ASSERT(nb_of_heaps_stat < tasks_stat.heap_count);
TEST_ASSERT(nb_of_allocs_stat < tasks_stat.alloc_count);
// free the arrays of stat in tasks_stat and reset the counters
heap_caps_free_all_task_stat_arrays(&tasks_stat);
// unlock task to delete allocated memory
xTaskNotifyGive(test_task_handle);
// wait for the task to free the memory
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// delete the task.
vTaskDelete(test_task_handle);
}
static void task_self_check(void *args)
{
const size_t alloc_size = 100;
const uint32_t caps = MALLOC_CAP_32BIT | MALLOC_CAP_DMA;
// call heap_caps_alloc_single_task_stat_arrays on the current task. Since no alloc was made, the
// function should return ESP_OK but the heap_count and alloc_count should be 0, the pointer to the
// allocated arrays should be NULL.
heap_single_task_stat_t task_stat;
esp_err_t ret_val = heap_caps_alloc_single_task_stat_arrays(&task_stat, NULL);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
TEST_ASSERT_EQUAL(task_stat.heap_count, 0);
TEST_ASSERT_EQUAL(task_stat.alloc_count, 0);
TEST_ASSERT_NULL(task_stat.heap_stat_start);
TEST_ASSERT_NULL(task_stat.alloc_stat_start);
// allocate memory
void *ptr = heap_caps_malloc(alloc_size, caps);
// allocate arrays for the statistics of the task. This time, it should succeed as we just
// allocated memory. This information should be stored in the task info list.
ret_val = heap_caps_alloc_single_task_stat_arrays(&task_stat, NULL);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
// The number of heap info should be one and the number of alloc should be one too
TEST_ASSERT_EQUAL(1, task_stat.heap_count);
TEST_ASSERT_EQUAL(1, task_stat.alloc_count);
ret_val = heap_caps_get_single_task_stat(&task_stat, NULL);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
// the caps of the heap info should contain the caps used to allocate the memory
TEST_ASSERT((task_stat.stat.heap_stat[0].caps & caps) == caps);
// The size of the alloc found in the stat should be not null and the address
// of the alloc should match too
TEST_ASSERT(task_stat.stat.heap_stat[0].alloc_stat[0].size > 0);
TEST_ASSERT(task_stat.stat.heap_stat[0].alloc_stat[0].address == ptr);
// free the memory and get the updated statistics on the task
heap_caps_free(ptr);
heap_caps_free_single_task_stat_arrays(&task_stat);
ret_val = heap_caps_alloc_single_task_stat_arrays(&task_stat, NULL);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
// The number of heap info should be one and the number of alloc should be zero
// since the allocated memory was just freed
TEST_ASSERT_EQUAL(1, task_stat.heap_count);
TEST_ASSERT_EQUAL(0, task_stat.alloc_count);
ret_val = heap_caps_get_single_task_stat(&task_stat, NULL);
TEST_ASSERT_EQUAL(ret_val, ESP_OK);
TEST_ASSERT((task_stat.stat.heap_stat[0].caps & caps) == caps);
TEST_ASSERT(task_stat.stat.heap_stat[0].alloc_stat == NULL);
// unlock main to check task tracking feature
xTaskNotifyGive((TaskHandle_t)args);
// wait for main to give back the hand to the task to delete the pointer
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
}
/* The test case calls heap_caps_alloc_single_task_stat_arrays and heap_caps_get_single_task_stat
* after creating new task and allocating in new heaps to check that the number of heaps and
* allocation statistics provided by heap_caps_get_single_task_stat is updated accordingly.
*/
TEST_CASE("heap task tracking check alloc arrays and get info on specific task", "[heap]")
{
TaskHandle_t test_task_handle;
const char *task_name = "task_self_check";
xTaskCreate(&task_self_check, task_name, 3072, (void *)xTaskGetCurrentTaskHandle(), 5, &test_task_handle);
// wait for the task to free the memory
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// delete the task.
vTaskDelete(test_task_handle);

View File

@@ -3,3 +3,4 @@ CONFIG_HEAP_POISONING_LIGHT=n
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