This commit is contained in:
2026-01-19 16:19:41 +01:00
commit edd3e96591
301 changed files with 36763 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# components/perf_tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
# Build lists conditionally depending on Kconfig
set(PERF_SRCS "")
# Dependencies needed by external components that link to perf_tests (public)
set(PERF_PUBLIC_REQUIRES freertos esp_timer esp_system heap log)
# Dependencies needed only for compiling perf_tests's own sources (private)
# Always include unity since the source file includes it conditionally
set(PERF_PRIVATE_REQUIRES unity)
if(CONFIG_ENABLE_PERF_TESTS)
list(APPEND PERF_SRCS "test/perf_tests.cpp")
endif()
# Single registration call
idf_component_register(
SRCS ${PERF_SRCS}
INCLUDE_DIRS "."
REQUIRES ${PERF_PUBLIC_REQUIRES} # Public dependencies
PRIV_REQUIRES ${PERF_PRIVATE_REQUIRES} # Private dependencies
)

View File

@@ -0,0 +1,708 @@
# ESP32 Performance Tests - Complete Technical Documentation
## Table of Contents
1. [Overview](#overview)
2. [System Architecture](#system-architecture)
3. [Configuration System](#configuration-system)
4. [Test Functions Detailed Analysis](#test-functions-detailed-analysis)
5. [Unity Test Framework Integration](#unity-test-framework-integration)
6. [Memory Management Analysis](#memory-management-analysis)
7. [Stack Usage Monitoring](#stack-usage-monitoring)
8. [CPU Load Analysis](#cpu-load-analysis)
9. [Build System Integration](#build-system-integration)
10. [Troubleshooting Guide](#troubleshooting-guide)
---
## Overview
The ESP32 Performance Tests system is a comprehensive monitoring solution that evaluates three critical aspects of embedded system health:
- **Memory Consumption**: Monitors heap usage and availability
- **Stack Usage**: Tracks stack utilization for tasks
- **CPU Load**: Analyzes processor utilization across cores
### Key Features
- **Real-time Monitoring**: Live system performance analysis
- **Multi-core Support**: Handles ESP32's dual-core architecture
- **Configurable Thresholds**: Adjustable limits for different environments
- **Unity Integration**: Professional test framework with detailed reporting
- **Conditional Compilation**: Tests only compile when enabled
---
## System Architecture
### File Structure
```
components/perf_tests/
├── CMakeLists.txt # Build configuration
├── test/
│ └── perf_tests.cpp # Main test implementation
└── Kconfig # Configuration options
main/
├── main.cpp # Application entry point
└── CMakeLists.txt # Main component build config
```
### Compilation Flow
```mermaid
graph TD
A[sdkconfig] --> B[CONFIG_ENABLE_PERF_TESTS]
B --> C{Tests Enabled?}
C -->|Yes| D[Include Unity Framework]
C -->|No| E[Normal Application Mode]
D --> F[Compile Test Functions]
F --> G[Register Tests with Unity]
G --> H[Run Performance Tests]
```
---
## Configuration System
### Primary Configuration Flags
#### `CONFIG_ENABLE_PERF_TESTS`
- **Purpose**: Master switch for performance testing
- **Location**: `sdkconfig`
- **Effect**: When enabled, replaces normal application with test suite
- **Default**: `y` (enabled)
#### `CONFIG_ENABLE_MEMORY_TEST`
- **Purpose**: Controls memory consumption testing
- **Tests**: Heap usage, DMA memory, SPIRAM (if available)
- **Default**: `y`
#### `CONFIG_ENABLE_STACK_USAGE_TEST`
- **Purpose**: Controls stack monitoring tests
- **Tests**: Current task stack, idle task stacks
- **Default**: `y`
#### `CONFIG_ENABLE_CPU_LOAD_TEST`
- **Purpose**: Controls CPU utilization analysis
- **Requirements**: `configGENERATE_RUN_TIME_STATS=1`
- **Default**: `y`
### Threshold Configuration
```cpp
// Memory threshold - minimum free heap considered healthy
#define PERF_MIN_HEAP_BYTES (1024U)
// Stack thresholds - minimum unused stack (in words, not bytes)
#define PERF_MIN_STACK_WORDS_CURRENT (100U) // Current task
#define PERF_MIN_STACK_WORDS_IDLE (50U) // Idle tasks
// CPU load tolerance - acceptable deviation from 100%
#define PERF_RUNTIME_PERCENT_TOLERANCE (20.0f)
```
---
## Test Functions Detailed Analysis
### 1. Memory Consumption Test (`test_memory_consumption_basic`)
#### Purpose
Validates that the system has sufficient free memory for stable operation.
#### Step-by-Step Execution
**Step 1: Check 8-bit Heap**
```cpp
size_t free_now_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t free_min_8bit = heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT);
```
- `heap_caps_get_free_size()`: Returns current free memory in bytes
- `MALLOC_CAP_8BIT`: Specifies 8-bit accessible memory (most common)
- `heap_caps_get_minimum_free_size()`: Returns lowest free memory since boot
- **Why Important**: Shows both current state and worst-case scenario
**Step 2: Validate 8-bit Heap**
```cpp
TEST_ASSERT_MESSAGE(free_now_8bit >= PERF_MIN_HEAP_BYTES,
"Not enough free 8-bit heap; increase RAM or lower threshold");
```
- Ensures minimum threshold is met
- Fails test if memory is critically low
- Provides actionable error message
**Step 3: Check DMA-Capable Heap**
```cpp
size_t free_now_dma = heap_caps_get_free_size(MALLOC_CAP_DMA);
size_t free_min_dma = heap_caps_get_minimum_free_size(MALLOC_CAP_DMA);
```
- `MALLOC_CAP_DMA`: Memory accessible by DMA controllers
- Critical for peripherals like SPI, I2C, WiFi
- May overlap with 8-bit heap on some configurations
**Step 4: SPIRAM Check (Optional)**
```cpp
#if CONFIG_SPIRAM_SUPPORT
if (esp_spiram_is_initialized()) {
size_t free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
// ... validation
}
#endif
```
- Only runs if SPIRAM (external RAM) is configured
- `esp_spiram_is_initialized()`: Checks if SPIRAM is available
- `MALLOC_CAP_SPIRAM`: Targets external SPIRAM memory
#### Expected Results
- **Healthy System**: >200KB free heap
- **Warning Level**: 50-200KB free heap
- **Critical Level**: <50KB free heap
---
### 2. Stack Usage Test (`test_stack_usage_current_task`)
#### Purpose
Monitors stack consumption to prevent stack overflow crashes.
#### Understanding Stack Monitoring
**What is Stack High Water Mark?**
- The "high water mark" is the maximum stack usage since task creation
- Measured in "words" (typically 4 bytes each on ESP32)
- Lower values indicate higher stack usage (less remaining)
**Step-by-Step Execution**
**Step 1: Get Current Task Stack Info**
```cpp
UBaseType_t high_water_words = uxTaskGetStackHighWaterMark(NULL);
```
- `NULL` parameter means "current task"
- Returns unused stack space in words
- **Critical**: This is REMAINING stack, not USED stack
**Step 2: Convert to Human-Readable Format**
```cpp
size_t high_water_bytes = stack_words_to_bytes(high_water_words);
```
- Converts words to bytes for easier understanding
- `sizeof(StackType_t)` accounts for platform differences
**Step 3: Validate Stack Health**
```cpp
TEST_ASSERT_MESSAGE(high_water_words >= PERF_MIN_STACK_WORDS_CURRENT,
"Current task stack high-water too low; consider increasing stack size");
```
- Ensures sufficient stack remains
- Prevents stack overflow crashes
- Suggests solution in error message
#### Stack Usage Interpretation
- **High Water Mark = 1000 words**: 1000 words (4KB) unused - GOOD
- **High Water Mark = 100 words**: 100 words (400 bytes) unused - WARNING
- **High Water Mark = 10 words**: 10 words (40 bytes) unused - CRITICAL
---
### 3. Idle Task Stack Test (`test_stack_usage_idle_tasks`)
#### Purpose
Monitors system-critical idle task stacks across all CPU cores.
#### Multi-Core Considerations
**Why Monitor Idle Tasks?**
- Idle tasks run when no other tasks are active
- Stack overflow in idle tasks crashes the entire system
- Each CPU core has its own idle task
**Step-by-Step Execution**
**Step 1: Iterate Through CPU Cores**
```cpp
for (int cpu = 0; cpu < configNUM_CORES; ++cpu) {
```
- `configNUM_CORES`: Compile-time constant (2 for ESP32)
- Ensures all cores are checked
**Step 2: Get Idle Task Handle**
```cpp
TaskHandle_t idle = xTaskGetIdleTaskHandleForCore(cpu);
```
- `xTaskGetIdleTaskHandleForCore()`: Gets idle task for specific core
- Returns handle to system-managed idle task
- **Note**: Uses newer API (old `xTaskGetIdleTaskHandleForCPU` deprecated)
**Step 3: Check Stack Usage**
```cpp
UBaseType_t hw = uxTaskGetStackHighWaterMark(idle);
```
- Same principle as current task check
- Applied to system idle task
**Step 4: Validate Each Idle Task**
```cpp
TEST_ASSERT_MESSAGE(hw >= PERF_MIN_STACK_WORDS_IDLE,
"Idle task stack high-water too low; increase idle task stack for CPU");
```
- Lower threshold than user tasks (idle tasks do less work)
- Critical for system stability
#### Single-Core Fallback
```cpp
#else
void test_stack_usage_idle_task(void) {
TaskHandle_t idle = xTaskGetIdleTaskHandle();
// ... same validation logic
}
#endif
```
- Handles single-core ESP32 variants
- Uses simpler API without core specification
---
### 4. CPU Load Test (`test_cpu_load_estimation`)
#### Purpose
Analyzes CPU utilization across all cores and tasks to identify performance bottlenecks.
#### Prerequisites
```cpp
#if ( configGENERATE_RUN_TIME_STATS == 1 )
```
- Requires FreeRTOS runtime statistics enabled
- Must be configured in `sdkconfig`: `CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y`
- Uses high-resolution timer for accurate measurements
#### Step-by-Step Execution
**Step 1: Stabilization Period**
```cpp
wait_ms(1000); // Increased wait time for more stable measurements
```
- Allows system to reach steady state
- Accumulates meaningful runtime statistics
- Longer period = more accurate measurements
**Step 2: Get Task Count**
```cpp
UBaseType_t num_tasks = uxTaskGetNumberOfTasks();
TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, num_tasks, "No tasks reported");
```
- Counts all active tasks in system
- Validates FreeRTOS is functioning
- Used to allocate appropriate array size
**Step 3: Allocate Task Array**
```cpp
const UBaseType_t margin = 4;
TaskStatus_t *task_array = (TaskStatus_t *)malloc(sizeof(TaskStatus_t) * (num_tasks + margin));
```
- `TaskStatus_t`: FreeRTOS structure containing task information
- `margin`: Extra space in case tasks are created during measurement
- Heap allocation safer than stack for large arrays
**Step 4: Capture System State**
```cpp
unsigned long total_run_time = 0;
UBaseType_t fetched = uxTaskGetSystemState(task_array, num_tasks + margin, &total_run_time);
```
- `uxTaskGetSystemState()`: Atomic snapshot of all task states
- `total_run_time`: Total CPU time across all cores since boot
- `fetched`: Actual number of tasks captured
**Step 5: Validate Data Quality**
```cpp
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");
```
- Ensures data capture succeeded
- Validates runtime statistics are working
- Prevents division by zero errors
**Step 6: Multi-Core Awareness**
```cpp
#if (configNUM_CORES > 1)
ESP_LOGI(TAG, "Multi-core system detected, using adjusted calculation");
#endif
```
- Acknowledges dual-core complexity
- Runtime statistics behave differently on multi-core systems
- Each core contributes to total runtime independently
**Step 7: Calculate Task Percentages**
```cpp
for (UBaseType_t i = 0; i < fetched; ++i) {
unsigned long run = task_array[i].ulRunTimeCounter;
double pct = ((double)run * 100.0) / (double)total_run_time;
```
- `ulRunTimeCounter`: CPU time consumed by this task
- Percentage calculation: (task_time / total_time) × 100
- Double precision for accuracy
**Step 8: Identify Idle Tasks**
```cpp
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))
```
- `pcTaskName`: Task name string
- `strcasecmp()`: Case-insensitive string comparison
- `strstr()`: Substring search for "IDLE"
- Handles both "IDLE0", "IDLE1" and generic "IDLE" names
**Step 9: Separate Idle vs Active Tasks**
```cpp
// Only count non-idle tasks for the sum check
if (task_array[i].pcTaskName == NULL ||
(strstr(task_array[i].pcTaskName, "IDLE") == NULL)) {
percent_sum += pct;
}
```
- Idle tasks run when nothing else needs CPU
- Active task percentage shows actual system load
- Idle percentage shows available capacity
**Step 10: System Health Validation**
```cpp
TEST_ASSERT_MESSAGE(idle_task_count > 0, "No idle tasks found");
TEST_ASSERT_MESSAGE(percent_sum < 150.0, "System appears overloaded");
```
- Ensures idle tasks exist (system sanity check)
- Validates reasonable CPU usage levels
- Relaxed thresholds for multi-core systems
#### CPU Load Interpretation
**Typical Results:**
- **Idle Tasks**: 85-95% (system mostly idle)
- **Active Tasks**: 5-15% (normal background activity)
- **Unity Test**: 3-8% (test execution overhead)
- **IPC Tasks**: 1-3% (inter-processor communication)
**Warning Signs:**
- **No Idle Time**: System overloaded
- **>50% Active Load**: High CPU usage
- **Unbalanced Cores**: One core much busier than other
---
## Unity Test Framework Integration
### Test Registration System
#### Unity Task Function
```cpp
extern "C" void unity_task(void *pvParameters)
{
vTaskDelay(2); // Allow system to settle
UNITY_BEGIN();
// Run individual tests
RUN_TEST(test_memory_consumption_basic);
RUN_TEST(test_stack_usage_current_task);
// ... more tests
UNITY_END();
// Keep task alive
while(1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
```
**Step-by-Step Execution:**
1. **Task Delay**: `vTaskDelay(2)` allows system initialization to complete
2. **Unity Begin**: `UNITY_BEGIN()` initializes test framework
3. **Test Execution**: `RUN_TEST()` macro executes each test function
4. **Unity End**: `UNITY_END()` prints final results and statistics
5. **Task Persistence**: Infinite loop keeps task alive for system stability
#### Main Application Integration
```cpp
extern "C" void app_main(void)
{
ESP_LOGI(TAG, "Performance tests enabled - starting Unity test suite");
// Create Unity test task
xTaskCreate(unity_task, "unity", 8192, NULL, 5, NULL);
// Keep main task alive
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
```
**Parameters Explained:**
- `unity_task`: Function to execute
- `"unity"`: Task name (for debugging)
- `8192`: Stack size in bytes (8KB)
- `NULL`: No parameters passed to task
- `5`: Task priority (medium priority)
- `NULL`: No task handle returned
---
## Memory Management Analysis
### ESP32 Memory Architecture
#### Internal RAM Types
1. **DRAM (Data RAM)**: General purpose data storage
2. **IRAM (Instruction RAM)**: Code execution, interrupt handlers
3. **RTC RAM**: Retains data during deep sleep
#### Memory Capabilities
- `MALLOC_CAP_8BIT`: 8-bit accessible memory (most common)
- `MALLOC_CAP_32BIT`: 32-bit aligned memory (faster access)
- `MALLOC_CAP_DMA`: DMA controller accessible memory
- `MALLOC_CAP_SPIRAM`: External SPIRAM memory
### Memory Test Deep Dive
#### Heap Fragmentation Considerations
```cpp
size_t free_now = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t free_min = heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT);
```
**Key Insights:**
- `free_now > free_min`: Memory was more fragmented earlier
- `free_now == free_min`: Current state is worst case
- Large difference suggests memory fragmentation issues
#### SPIRAM Integration
```cpp
if (esp_spiram_is_initialized()) {
size_t free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
```
**SPIRAM Benefits:**
- Expands available memory from ~300KB to several MB
- Slower access than internal RAM
- Ideal for large buffers, images, audio data
---
## Stack Usage Monitoring
### FreeRTOS Stack Management
#### Stack Growth Direction
- ESP32 stacks grow downward (high to low addresses)
- Stack pointer starts at high address, decreases with usage
- Stack overflow occurs when pointer goes below allocated region
#### High Water Mark Calculation
```cpp
UBaseType_t high_water_words = uxTaskGetStackHighWaterMark(NULL);
```
**Internal Process:**
1. FreeRTOS fills unused stack with pattern (0xa5a5a5a5)
2. Periodically scans from stack bottom upward
3. Finds first non-pattern word (maximum usage point)
4. Returns remaining unused words
#### Stack Size Recommendations
- **Minimal Tasks**: 2KB (512 words)
- **Standard Tasks**: 4KB (1024 words)
- **Complex Tasks**: 8KB+ (2048+ words)
- **Unity Test Task**: 8KB (handles test framework overhead)
---
## CPU Load Analysis
### FreeRTOS Runtime Statistics
#### Timer Configuration
```cpp
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
```
**Timer Options:**
- **ESP Timer**: High-resolution microsecond timer
- **CPU Clock**: Cycle-accurate but overflow-prone
- **Custom Timer**: User-defined timer source
#### Multi-Core Considerations
**Dual-Core Behavior:**
- Each core runs independently
- Idle tasks per core (IDLE0, IDLE1)
- Runtime statistics accumulate per core
- Total runtime = sum of both cores
**Percentage Calculation Challenges:**
- Single-core: percentages sum to ~100%
- Dual-core: percentages can sum to ~200%
- Our solution: separate idle vs active task analysis
#### Task Types Analysis
**System Tasks:**
- `IDLE0/IDLE1`: Core idle tasks (run when nothing else active)
- `main`: Main application task
- `unity`: Test execution task
- `ipc0/ipc1`: Inter-processor communication
**Typical Load Distribution:**
```
IDLE0: 91.94% (CPU0 mostly idle)
IDLE1: 97.26% (CPU1 mostly idle)
unity: 5.14% (test execution)
main: 1.09% (minimal main task work)
ipc0: 1.77% (core communication)
ipc1: 1.83% (core communication)
```
---
## Build System Integration
### CMakeLists.txt Configuration
#### Component Registration
```cmake
# Set up dependencies conditionally
set(PERF_SRCS "")
set(PERF_PUBLIC_REQUIRES freertos esp_timer esp_system heap log)
set(PERF_PRIVATE_REQUIRES unity) # Always include unity
if(CONFIG_ENABLE_PERF_TESTS)
list(APPEND PERF_SRCS "test/perf_tests.cpp")
endif()
idf_component_register(
SRCS ${PERF_SRCS}
INCLUDE_DIRS "."
REQUIRES ${PERF_PUBLIC_REQUIRES}
PRIV_REQUIRES ${PERF_PRIVATE_REQUIRES}
)
```
**Key Points:**
- `PERF_SRCS`: Source files (conditional)
- `REQUIRES`: Public dependencies (always available)
- `PRIV_REQUIRES`: Private dependencies (internal use only)
- Unity always included to avoid compilation issues
#### Main Component Integration
```cmake
set(MAIN_REQUIRES sensor_manager actuator_manager event_system)
set(MAIN_PRIV_REQUIRES esp_common unity)
if(CONFIG_ENABLE_PERF_TESTS)
# Unity dependency already included above
endif()
```
### Conditional Compilation Strategy
#### Preprocessor Guards
```cpp
#if CONFIG_ENABLE_PERF_TESTS
// Test code only compiled when enabled
#include "unity.h"
// ... test functions
#endif
```
**Benefits:**
- Zero overhead when tests disabled
- Clean separation of test vs production code
- Compile-time optimization
---
## Troubleshooting Guide
### Common Issues and Solutions
#### 1. "unity.h: No such file or directory"
**Cause**: Unity component not properly linked
**Solution**:
```cmake
# In CMakeLists.txt
PRIV_REQUIRES unity
```
#### 2. "0 Tests 0 Failures 0 Ignored"
**Cause**: Tests not registered with Unity framework
**Solution**: Ensure `unity_task()` function calls `RUN_TEST()` macros
#### 3. CPU Load Test Fails with >100% Usage
**Cause**: Multi-core runtime statistics accumulation
**Solution**: Use separate idle vs active task analysis (already implemented)
#### 4. Stack Overflow in Tests
**Cause**: Insufficient stack size for Unity test task
**Solution**:
```cpp
xTaskCreate(unity_task, "unity", 8192, NULL, 5, NULL);
// ^^^^ Increase this value
```
#### 5. Memory Test Fails Immediately
**Cause**: System already low on memory
**Solutions**:
- Reduce `PERF_MIN_HEAP_BYTES` threshold
- Optimize application memory usage
- Enable SPIRAM if available
#### 6. Runtime Statistics Not Working
**Cause**: FreeRTOS runtime stats disabled
**Solution**:
```
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
```
### Performance Optimization Tips
#### Memory Optimization
1. **Use SPIRAM**: For large data structures
2. **Optimize Heap Usage**: Avoid frequent malloc/free
3. **Static Allocation**: Use static variables when possible
4. **Memory Pools**: Pre-allocate fixed-size blocks
#### Stack Optimization
1. **Right-Size Stacks**: Don't over-allocate
2. **Monitor Usage**: Regular high water mark checks
3. **Avoid Deep Recursion**: Use iteration instead
4. **Local Variable Management**: Minimize large local arrays
#### CPU Optimization
1. **Task Priorities**: Balance workload across cores
2. **Yield Points**: Add `vTaskDelay()` in long loops
3. **Interrupt Efficiency**: Keep ISRs short
4. **Core Affinity**: Pin tasks to specific cores when beneficial
---
## Conclusion
This performance testing system provides comprehensive monitoring of ESP32 system health. The tests are designed to catch issues early in development and provide actionable feedback for optimization.
### Key Benefits
- **Proactive Monitoring**: Catch issues before they cause crashes
- **Quantitative Analysis**: Precise measurements vs. guesswork
- **Multi-Core Awareness**: Proper handling of ESP32's dual-core architecture
- **Professional Framework**: Unity provides industry-standard test reporting
- **Configurable Thresholds**: Adaptable to different application requirements
### Best Practices
1. **Run Tests Regularly**: Include in CI/CD pipeline
2. **Monitor Trends**: Track performance over time
3. **Adjust Thresholds**: Tune limits based on application needs
4. **Document Results**: Keep performance baselines
5. **Investigate Failures**: Don't ignore test failures
The system is designed to grow with your project, providing valuable insights throughout the development lifecycle.

View File

@@ -0,0 +1,26 @@
menu "Performance Tests"
config ENABLE_PERF_TESTS
bool "Enable performance tests (CPU, memory, stack)"
default y
help
Enable building and running performance-oriented Unity tests
(CPU load, memory consumption, and stack usage).
if ENABLE_PERF_TESTS
config ENABLE_CPU_LOAD_TEST
bool "Enable CPU load test"
default y
config ENABLE_MEMORY_TEST
bool "Enable memory consumption test"
default y
config ENABLE_STACK_USAGE_TEST
bool "Enable stack usage test"
default y
endif # ENABLE_PERF_TESTS
endmenu

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