This commit is contained in:
2026-02-01 12:56:05 +01:00
parent f51adeecca
commit 0bdbcb1657
857 changed files with 0 additions and 97661 deletions

View File

@@ -0,0 +1,574 @@
/**
* @file perf_tests.cpp
* @brief Unity tests for CPU load, memory consumption, and stack usage.
*
* This file implements comprehensive performance monitoring for ESP32 systems.
* It tests three critical aspects of system health:
* 1. Memory Usage - Ensures sufficient free heap memory
* 2. Stack Usage - Monitors task stack consumption to prevent overflows
* 3. CPU Load - Analyzes processor utilization across both cores
*
* @author Mahmoud Elmohtady (improvements)
* @company Nabd solutions - ASF
* @copyright 2025
*/
// Standard C library includes for string operations and formatted output
#include <string.h> // For strcasecmp(), strstr() - string comparison functions
#include <stdio.h> // For printf-style functions (used by ESP_LOGI)
#include <inttypes.h> // For portable integer type definitions
// FreeRTOS includes - the real-time operating system running on ESP32
#include "freertos/FreeRTOS.h" // Core FreeRTOS definitions and types
#include "freertos/task.h" // Task management functions (create, delay, etc.)
#include "freertos/portmacro.h" // Platform-specific macros and definitions
// ESP-IDF (Espressif IoT Development Framework) includes
#include "esp_timer.h" // High-resolution timer functions
#include "esp_system.h" // System-level functions and utilities
#include "esp_heap_caps.h" // Memory management with capability-based allocation
#include "esp_log.h" // Logging system for debug output
// Conditional includes based on configuration
#if CONFIG_SPIRAM_SUPPORT
#include "esp_spiram.h" // External SPIRAM (Serial Peripheral RAM) support
#endif
#if CONFIG_ENABLE_PERF_TESTS
#include "unity.h" // Unity test framework for professional testing
#endif
// extern "C" tells the C++ compiler to use C-style function naming
// This is necessary because FreeRTOS and ESP-IDF are written in C
extern "C" {
// TAG is used by ESP_LOGI() for log message identification
// All log messages from this file will be prefixed with "perf_tests"
static const char *TAG = "perf_tests";
/* ========================================================================
* CONFIGURABLE THRESHOLDS - Adjust these values for your specific application
* ======================================================================== */
// Minimum free heap memory (in bytes) to consider the system "healthy"
// 1024 bytes = 1KB - very conservative threshold
// Increase this value for applications that need more memory headroom
#ifndef PERF_MIN_HEAP_BYTES
#define PERF_MIN_HEAP_BYTES (1024U)
#endif
// Minimum unused stack space (in WORDS, not bytes) for the current task
// 100 words = 400 bytes on ESP32 (since each word is 4 bytes)
// This prevents stack overflow crashes in your application tasks
#ifndef PERF_MIN_STACK_WORDS_CURRENT
#define PERF_MIN_STACK_WORDS_CURRENT (100U)
#endif
// Minimum unused stack space (in WORDS) for system idle tasks
// 50 words = 200 bytes - idle tasks do minimal work so need less stack
// Critical for system stability - idle task crashes = system crash
#ifndef PERF_MIN_STACK_WORDS_IDLE
#define PERF_MIN_STACK_WORDS_IDLE (50U)
#endif
// Tolerance for CPU usage percentage calculations (as a float)
// 20.0% tolerance accounts for multi-core timing complexities
// In dual-core systems, percentages don't always sum to exactly 100%
#ifndef PERF_RUNTIME_PERCENT_TOLERANCE
#define PERF_RUNTIME_PERCENT_TOLERANCE (20.0f)
#endif
/* ========================================================================
* UTILITY FUNCTIONS
* ======================================================================== */
/**
* @brief Simple delay function that yields CPU to other tasks
* @param ms Delay time in milliseconds
*
* This is better than a busy-wait loop because it allows other tasks to run.
* vTaskDelay() puts the current task to sleep and wakes it up after the specified time.
* pdMS_TO_TICKS() converts milliseconds to FreeRTOS tick units.
*/
static void wait_ms(uint32_t ms)
{
// Convert milliseconds to FreeRTOS ticks and delay the current task
// This allows other tasks to run during the delay period
vTaskDelay(pdMS_TO_TICKS(ms));
}
/* ========================================================================
* PERFORMANCE TEST FUNCTIONS
* These functions only compile when CONFIG_ENABLE_PERF_TESTS is enabled
* ======================================================================== */
#if CONFIG_ENABLE_PERF_TESTS
/* ------------------------------------------------------------------------
* MEMORY CONSUMPTION TEST
* ------------------------------------------------------------------------ */
#if CONFIG_ENABLE_MEMORY_TEST
/**
* @brief Tests system memory consumption and availability
*
* This test checks different types of memory on the ESP32:
* 1. 8-bit accessible memory (most common, used by malloc())
* 2. DMA-capable memory (required for hardware peripherals)
* 3. SPIRAM memory (external RAM, if configured)
*
* The test ensures the system has sufficient free memory for stable operation.
* Low memory can cause malloc() failures, task creation failures, or system crashes.
*/
void test_memory_consumption_basic(void)
{
/* ---- CHECK 8-BIT ACCESSIBLE HEAP ---- */
// This is the most commonly used memory type - where malloc() allocates from
// Get current free memory in the 8-bit heap
size_t free_now_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
// Get the minimum free memory since boot (worst case scenario)
// This shows if the system has been under memory pressure
size_t free_min_8bit = heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT);
// Log the results for debugging and monitoring
ESP_LOGI(TAG, "8-bit heap: free_now=%u free_min=%u", (unsigned)free_now_8bit, (unsigned)free_min_8bit);
// CRITICAL TEST: Ensure we have enough free memory
// If this fails, your application may crash due to out-of-memory conditions
TEST_ASSERT_MESSAGE(free_now_8bit >= PERF_MIN_HEAP_BYTES,
"Not enough free 8-bit heap; increase RAM or lower threshold");
/* ---- CHECK DMA-CAPABLE HEAP ---- */
// DMA memory is required for hardware peripherals like SPI, I2C, WiFi, etc.
// Some ESP32 configurations share this with 8-bit heap, others separate it
// Get current free DMA-capable memory
size_t free_now_dma = heap_caps_get_free_size(MALLOC_CAP_DMA);
// Get minimum free DMA memory since boot
size_t free_min_dma = heap_caps_get_minimum_free_size(MALLOC_CAP_DMA);
// Log DMA memory status
ESP_LOGI(TAG, "DMA heap: free_now=%u free_min=%u", (unsigned)free_now_dma, (unsigned)free_min_dma);
// Basic sanity check - ensure the API call succeeded
// Note: On some configurations, DMA heap is the same as 8-bit heap
TEST_ASSERT_GREATER_OR_EQUAL_size_t(0, free_min_dma);
/* ---- CHECK SPIRAM (EXTERNAL RAM) IF AVAILABLE ---- */
#if CONFIG_SPIRAM_SUPPORT
// SPIRAM is external RAM that can expand memory from ~300KB to several MB
// It's slower than internal RAM but great for large buffers
// Check if SPIRAM was successfully initialized at boot
if (esp_spiram_is_initialized()) {
// Get free SPIRAM memory (combined with 8-bit capability)
size_t free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
// Get minimum free SPIRAM since boot
size_t min_spiram = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
// Log SPIRAM status
ESP_LOGI(TAG, "SPIRAM: free_now=%u free_min=%u", (unsigned)free_spiram, (unsigned)min_spiram);
// Ensure SPIRAM is functioning (basic sanity check)
TEST_ASSERT_GREATER_OR_EQUAL_size_t(0, min_spiram);
}
#endif
}
#endif // CONFIG_ENABLE_MEMORY_TEST
/* ------------------------------------------------------------------------
* STACK USAGE MONITORING TESTS
* ------------------------------------------------------------------------ */
#if CONFIG_ENABLE_STACK_USAGE_TEST
/**
* @brief Helper function to convert stack words to bytes for human readability
* @param words Stack size in words (FreeRTOS native unit)
* @return Stack size in bytes
*
* FreeRTOS measures stack in "words" which are platform-specific.
* On ESP32, 1 word = 4 bytes (32-bit architecture).
* This function converts to bytes for easier understanding.
*/
static inline size_t stack_words_to_bytes(UBaseType_t words)
{
// sizeof(StackType_t) is 4 bytes on ESP32 (32-bit words)
return (size_t)words * sizeof(StackType_t);
}
/**
* @brief Tests stack usage of the currently running task
*
* Stack overflow is one of the most common causes of embedded system crashes.
* This test monitors how much stack space remains unused in the current task.
*
* IMPORTANT: "High water mark" means UNUSED stack, not USED stack!
* - High value = lots of unused stack = good
* - Low value = little unused stack = danger of overflow
*/
void test_stack_usage_current_task(void)
{
// Get the "high water mark" - minimum unused stack since task creation
// NULL parameter means "check the currently running task"
UBaseType_t high_water_words = uxTaskGetStackHighWaterMark(NULL);
// Convert to bytes for easier understanding
size_t high_water_bytes = stack_words_to_bytes(high_water_words);
// Log the results for monitoring and debugging
ESP_LOGI(TAG, "Current task high-water: %u words (%u bytes)",
(unsigned)high_water_words, (unsigned)high_water_bytes);
// CRITICAL TEST: Ensure sufficient unused stack remains
// If this fails, the task is at risk of stack overflow crash
TEST_ASSERT_MESSAGE(high_water_words >= PERF_MIN_STACK_WORDS_CURRENT,
"Current task stack high-water too low; consider increasing stack size");
}
/* ---- MULTI-CORE vs SINGLE-CORE IDLE TASK MONITORING ---- */
#if (configNUM_CORES > 1)
/**
* @brief Tests stack usage of idle tasks on multi-core ESP32
*
* In multi-core systems, each CPU core has its own idle task:
* - IDLE0: Runs on CPU core 0 when no other tasks need it
* - IDLE1: Runs on CPU core 1 when no other tasks need it
*
* Idle task stack overflow crashes the entire system!
*/
void test_stack_usage_idle_tasks(void)
{
// Loop through each CPU core (ESP32 has 2 cores: 0 and 1)
for (int cpu = 0; cpu < configNUM_CORES; ++cpu) {
// Get the idle task handle for this specific CPU core
TaskHandle_t idle = xTaskGetIdleTaskHandleForCore(cpu);
// Ensure we got a valid handle (system sanity check)
TEST_ASSERT_NOT_NULL_MESSAGE(idle, "Idle task handle is NULL for a CPU");
// Get the high water mark for this idle task
UBaseType_t hw = uxTaskGetStackHighWaterMark(idle);
// Convert to bytes for logging
size_t hw_bytes = stack_words_to_bytes(hw);
// Log idle task stack status for each core
ESP_LOGI(TAG, "Idle task (cpu %d) high-water: %u words (%u bytes)",
cpu, (unsigned)hw, (unsigned)hw_bytes);
// CRITICAL TEST: Ensure idle task has sufficient stack
// Idle task crash = system crash, so this is very important
TEST_ASSERT_MESSAGE(hw >= PERF_MIN_STACK_WORDS_IDLE,
"Idle task stack high-water too low; increase idle task stack for CPU");
}
}
#else
/**
* @brief Tests stack usage of idle task on single-core ESP32
*
* Single-core systems have only one idle task that runs when no other tasks are active.
* This is a simpler version of the multi-core test above.
*/
void test_stack_usage_idle_task(void)
{
// Get the single idle task handle (no core specification needed)
TaskHandle_t idle = xTaskGetIdleTaskHandle();
// Ensure we got a valid handle
TEST_ASSERT_NOT_NULL_MESSAGE(idle, "Idle task handle is NULL");
// Get high water mark for the idle task
UBaseType_t hw = uxTaskGetStackHighWaterMark(idle);
// Convert to bytes for logging
size_t hw_bytes = stack_words_to_bytes(hw);
// Log idle task stack status
ESP_LOGI(TAG, "Idle task high-water: %u words (%u bytes)", (unsigned)hw, (unsigned)hw_bytes);
// CRITICAL TEST: Ensure idle task has sufficient stack
TEST_ASSERT_MESSAGE(hw >= PERF_MIN_STACK_WORDS_IDLE,
"Idle task stack high-water too low; increase idle task stack");
}
#endif
#endif // CONFIG_ENABLE_STACK_USAGE_TEST
/* ------------------------------------------------------------------------
* CPU LOAD ANALYSIS TEST
* ------------------------------------------------------------------------ */
#if CONFIG_ENABLE_CPU_LOAD_TEST
/**
* @brief Analyzes CPU utilization across all cores and tasks
*
* This is the most complex test because it deals with multi-core timing statistics.
* It measures how much CPU time each task consumes and identifies performance bottlenecks.
*
* Key concepts:
* - Runtime statistics track CPU time per task using a high-resolution timer
* - Idle tasks run when no other tasks need CPU (high idle = low system load)
* - Multi-core systems have separate idle tasks per core
* - Percentages may not sum to exactly 100% due to timing complexities
*
* Requirements:
* - configGENERATE_RUN_TIME_STATS must be enabled in FreeRTOS config
* - High-resolution timer must be configured (ESP_TIMER recommended)
*/
void test_cpu_load_estimation(void)
{
/* ---- CHECK IF RUNTIME STATISTICS ARE ENABLED ---- */
#if ( configGENERATE_RUN_TIME_STATS == 1 )
/* ---- STEP 1: STABILIZATION PERIOD ---- */
// Allow the system to reach steady state before measurement
// Longer period = more accurate measurements, but slower test
wait_ms(1000); // 1 second should be sufficient for most applications
/* ---- STEP 2: GET TASK COUNT ---- */
// Find out how many tasks are currently running in the system
UBaseType_t num_tasks = uxTaskGetNumberOfTasks();
// Sanity check - ensure FreeRTOS is reporting tasks correctly
TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, num_tasks, "No tasks reported by uxTaskGetNumberOfTasks()");
/* ---- STEP 3: ALLOCATE MEMORY FOR TASK DATA ---- */
// We need an array to hold information about each task
// Add a small margin in case new tasks are created during measurement
const UBaseType_t margin = 4;
// TaskStatus_t is a FreeRTOS structure containing task information
TaskStatus_t *task_array = (TaskStatus_t *)malloc(sizeof(TaskStatus_t) * (num_tasks + margin));
// Ensure memory allocation succeeded
TEST_ASSERT_NOT_NULL_MESSAGE(task_array, "Failed to allocate task array for system state");
/* ---- STEP 4: CAPTURE SYSTEM STATE SNAPSHOT ---- */
// This is the critical measurement - atomic snapshot of all task states
unsigned long total_run_time = 0; // FreeRTOS will fill this with total CPU time
// uxTaskGetSystemState() captures all task information atomically
UBaseType_t fetched = uxTaskGetSystemState(task_array, num_tasks + margin, &total_run_time);
// Log the raw data for debugging
ESP_LOGI(TAG, "uxTaskGetSystemState fetched=%u total_run_time=%lu", (unsigned)fetched, total_run_time);
/* ---- STEP 5: VALIDATE DATA QUALITY ---- */
// Ensure the data capture was successful
TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, fetched, "uxTaskGetSystemState returned no tasks");
TEST_ASSERT_GREATER_THAN_MESSAGE(0UL, total_run_time, "Total run time is zero; ensure runtime stats timer is configured");
/* ---- STEP 6: MULTI-CORE AWARENESS ---- */
// Multi-core systems have complex timing behavior
// Each core contributes independently to the total runtime
(void)total_run_time; // Suppress unused variable warning for normalized_total_time
#if (configNUM_CORES > 1)
// Log that we're handling multi-core complexity
ESP_LOGI(TAG, "Multi-core system detected, using adjusted calculation");
#endif
/* ---- STEP 7: INITIALIZE ANALYSIS VARIABLES ---- */
double percent_sum = 0.0; // Sum of non-idle task percentages
double idle_percent_total = 0.0; // Sum of all idle task percentages
int idle_task_count = 0; // Number of idle tasks found
/* ---- STEP 8: ANALYZE EACH TASK ---- */
for (UBaseType_t i = 0; i < fetched; ++i) {
// Get the runtime counter for this task (CPU time consumed)
unsigned long run = task_array[i].ulRunTimeCounter;
// Calculate percentage: (task_time / total_time) * 100
double pct = 0.0;
if (total_run_time > 0) {
pct = ((double)run * 100.0) / (double)total_run_time;
}
// Log detailed information for each task (useful for debugging)
ESP_LOGI(TAG, "Task[%u] name='%s' handle=%p run=%lu pct=%.2f",
(unsigned)i, task_array[i].pcTaskName, task_array[i].xHandle, run, pct);
/* ---- IDENTIFY IDLE TASKS ---- */
// Idle tasks have special names and behavior
// In multi-core: "IDLE0" (core 0), "IDLE1" (core 1)
// In single-core: usually just "IDLE"
if (task_array[i].pcTaskName != NULL &&
(strcasecmp(task_array[i].pcTaskName, "IDLE0") == 0 ||
strcasecmp(task_array[i].pcTaskName, "IDLE1") == 0 ||
strstr(task_array[i].pcTaskName, "IDLE") != NULL))
{
// This is an idle task - track it separately
idle_percent_total += pct;
idle_task_count++;
ESP_LOGI(TAG, "Found idle task: %s with %.2f%% usage", task_array[i].pcTaskName, pct);
}
/* ---- SEPARATE ACTIVE vs IDLE TASKS ---- */
// Only count non-idle tasks for system load calculation
// Idle tasks run when nothing else needs CPU, so they don't represent "work"
if (task_array[i].pcTaskName == NULL ||
(strstr(task_array[i].pcTaskName, "IDLE") == NULL)) {
percent_sum += pct;
}
}
/* ---- STEP 9: CLEANUP MEMORY ---- */
// Free the allocated task array
free(task_array);
/* ---- STEP 10: ANALYZE RESULTS ---- */
// Log the analysis results
ESP_LOGI(TAG, "Non-idle task runtime sum = %.2f%%", percent_sum);
ESP_LOGI(TAG, "Total idle task runtime = %.2f%% (across %d idle tasks)", idle_percent_total, idle_task_count);
ESP_LOGI(TAG, "System appears to be %.2f%% busy", percent_sum);
/* ---- STEP 11: VALIDATE SYSTEM HEALTH ---- */
// These are relaxed validations designed for multi-core systems
// The goal is to ensure the system is reporting reasonable values
// Ensure we found idle tasks (basic system sanity check)
TEST_ASSERT_MESSAGE(idle_task_count > 0, "No idle tasks found; runtime stats may not be working");
// Ensure idle percentage makes sense (not negative)
TEST_ASSERT_MESSAGE(idle_percent_total >= 0.0, "Idle task percentage is negative");
// Ensure active task percentage is reasonable (not impossibly high)
TEST_ASSERT_MESSAGE(percent_sum >= 0.0 && percent_sum <= 200.0, "Non-idle task percentage sum is unreasonable");
// Check for system overload (too much CPU usage)
TEST_ASSERT_MESSAGE(percent_sum < 150.0, "System appears overloaded or runtime stats misconfigured");
// Log successful completion
ESP_LOGI(TAG, "CPU load test completed - system load appears reasonable");
#else
/* ---- RUNTIME STATISTICS DISABLED ---- */
// If FreeRTOS runtime statistics are not enabled, skip this test
TEST_IGNORE_MESSAGE("configGENERATE_RUN_TIME_STATS is disabled; enable to run CPU load test");
#endif // configGENERATE_RUN_TIME_STATS
}
#endif // CONFIG_ENABLE_CPU_LOAD_TEST
#endif // CONFIG_ENABLE_PERF_TESTS
} // extern "C"
/* ========================================================================
* UNITY TEST FRAMEWORK INTEGRATION
*
* This section integrates our performance tests with the Unity test framework.
* Unity provides professional test reporting, assertions, and test management.
* ======================================================================== */
#if CONFIG_ENABLE_PERF_TESTS
// Forward declaration of app_main (defined in main.cpp)
extern "C" void app_main(void);
/**
* @brief Test registration function (currently unused but kept for future expansion)
*
* This function could be used for dynamic test registration if needed.
* Currently, we use the simpler RUN_TEST() approach in unity_task().
*/
extern "C" void register_perf_tests(void)
{
// This function demonstrates how to register tests by name
// Currently not used, but available for future dynamic test loading
#if CONFIG_ENABLE_MEMORY_TEST
unity_run_test_by_name("test_memory_consumption_basic");
#endif
#if CONFIG_ENABLE_STACK_USAGE_TEST
unity_run_test_by_name("test_stack_usage_current_task");
#if (configNUM_CORES > 1)
unity_run_test_by_name("test_stack_usage_idle_tasks");
#else
unity_run_test_by_name("test_stack_usage_idle_task");
#endif
#endif
#if CONFIG_ENABLE_CPU_LOAD_TEST
unity_run_test_by_name("test_cpu_load_estimation");
#endif
}
/**
* @brief Main Unity test runner task
* @param pvParameters Task parameters (unused, required by FreeRTOS)
*
* This function runs as a FreeRTOS task and executes all performance tests.
* It's called from main.cpp when performance testing is enabled.
*
* Execution flow:
* 1. Allow system to stabilize after boot
* 2. Initialize Unity test framework
* 3. Run each enabled test function
* 4. Print final test results
* 5. Keep task alive for system stability
*/
extern "C" void unity_task(void *pvParameters)
{
// Suppress unused parameter warning (FreeRTOS requires this parameter)
(void)pvParameters;
/* ---- STEP 1: SYSTEM STABILIZATION ---- */
// Give the system a moment to finish initialization
// This ensures all components are ready before testing begins
vTaskDelay(2); // 2 FreeRTOS ticks (usually 20ms)
/* ---- STEP 2: INITIALIZE UNITY FRAMEWORK ---- */
// UNITY_BEGIN() sets up the test framework and resets counters
UNITY_BEGIN();
/* ---- STEP 3: RUN PERFORMANCE TESTS ---- */
// Each RUN_TEST() macro executes a test function and tracks results
// Tests are run conditionally based on configuration flags
#if CONFIG_ENABLE_MEMORY_TEST
// Test memory consumption and availability
RUN_TEST(test_memory_consumption_basic);
#endif
#if CONFIG_ENABLE_STACK_USAGE_TEST
// Test current task stack usage
RUN_TEST(test_stack_usage_current_task);
// Test idle task stack usage (multi-core vs single-core)
#if (configNUM_CORES > 1)
RUN_TEST(test_stack_usage_idle_tasks); // Multi-core version
#else
RUN_TEST(test_stack_usage_idle_task); // Single-core version
#endif
#endif
#if CONFIG_ENABLE_CPU_LOAD_TEST
// Test CPU utilization across all cores and tasks
RUN_TEST(test_cpu_load_estimation);
#endif
/* ---- STEP 4: FINALIZE AND REPORT RESULTS ---- */
// UNITY_END() prints the final test summary:
// - Total tests run
// - Number of failures
// - Number of ignored tests
// - Overall PASS/FAIL status
UNITY_END();
/* ---- STEP 5: KEEP TASK ALIVE ---- */
// The task must remain alive for system stability
// Deleting this task could cause issues with FreeRTOS scheduler
while(1) {
// Sleep for 1 second, then repeat
// This keeps the task alive with minimal CPU usage
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
#endif // CONFIG_ENABLE_PERF_TESTS