init
This commit is contained in:
23
software design/components/perf_tests/CMakeLists.txt
Normal file
23
software design/components/perf_tests/CMakeLists.txt
Normal 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
|
||||
)
|
||||
@@ -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.
|
||||
26
software design/components/perf_tests/Kconfig
Normal file
26
software design/components/perf_tests/Kconfig
Normal 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
|
||||
574
software design/components/perf_tests/test/perf_tests.cpp
Normal file
574
software design/components/perf_tests/test/perf_tests.cpp
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user