Merge branch 'feat/enable_function_tracing_v6.0' into 'release/v6.0'

Enable function tracing (-finstrument-functions) (v6.0)

See merge request espressif/esp-idf!52656
This commit is contained in:
Alexey Gerenkov
2026-09-22 22:17:19 +08:00
36 changed files with 1562 additions and 274 deletions
@@ -27,6 +27,18 @@ examples/system/tracing/esp_trace_custom_library:
- esp_trace
- freertos
examples/system/tracing/function_tracing:
disable_test:
- if: IDF_TARGET == "esp32h21"
temporary: true
reason: lack of runners
- if: IDF_TARGET == "esp32h4"
temporary: true
reason: lack of runners
depends_components:
- esp_trace
- app_trace
examples/system/tracing/gcov:
disable_test:
- if: IDF_TARGET == "esp32h21"
@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(function_tracing)
@@ -0,0 +1,138 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- |
# Example: Compiler-Instrumented Function Tracing (function_tracing)
This example shows how to trace function entry/exit automatically with the `esp_trace` compiler-instrumented function tracing feature, and how to control **which** code is instrumented.
When a source file is built with the GCC flag `-finstrument-functions`, the compiler inserts a call at the start and end of every function. Those calls go to hooks provided by `esp_trace`, which forward the events to the active trace encoder (SystemView here). The events carry the raw function and call-site addresses. SystemView shows the enter/exit sequence with those addresses. Resolving the addresses to function names is done separately against the ELF file (for example with `addr2line` or a custom tool).
Unlike RTOS-aware tracing (task switches, semaphores, etc.), function tracing needs no manual trace points. You add one compile flag to a component and the whole call flow is captured.
## What this example demonstrates
- Instrument a whole component (`components/ft_demo`) by adding `-finstrument-functions` to its build.
- Instrument only selected sources (`main`) by setting the flag per source file.
- `-finstrument-functions-exclude-file-list` — exclude whole files as a comma-separated list (`ft_demo_hot.c`, `ft_demo_quiet.c`).
- `-finstrument-functions-exclude-function-list` — exclude a single function by name (`ft_demo_secret`).
The workload (`example_workload()`) calls four things every iteration:
| Call | Instrumented? | Why |
| --------------------- | ------------- | ------------------------------------------------ |
| `ft_demo_run()` | yes | component is instrumented, appears as enter/exit |
| `ft_demo_secret()` | no | excluded by `-...-exclude-function-list` |
| `ft_demo_hot_loop()` | no | its file is excluded by `-...-exclude-file-list` |
| `ft_demo_quiet_path()`| no | its file is the 2nd `-...-exclude-file-list` entry |
So in the captured trace you should see `ft_demo_run -> ft_demo_level1 -> ft_demo_level2`, but **not** `ft_demo_secret`, `ft_demo_hot_loop` or `ft_demo_quiet_path`.
The same workload task is created pinned to each core, so on a dual-core target the multi-core capture shows function tracing on both cores (one task per core). On a single-core target it is one task on core 0.
## Project layout
```
function_tracing/
├── main/
│ ├── function_tracing_example_main.c # workload + trace setup
│ └── CMakeLists.txt # instruments this source only
├── components/
│ └── ft_demo/
│ ├── ft_demo.c # instrumented call graph + excluded-by-name function
│ ├── ft_demo_hot.c # excluded-by-file (1st exclude-file-list entry)
│ ├── ft_demo_quiet.c # excluded-by-file (2nd exclude-file-list entry)
│ └── CMakeLists.txt # instruments the whole component + exclude flags
├── sdkconfig.defaults # enables function tracing
├── sdkconfig.ci.jtag # apptrace over JTAG
├── SYSVIEW_FreeRTOS.txt # event names for the function-trace module
└── gdbinit
```
## Instrumenting your code
A component is instrumented from its own `CMakeLists.txt`, after `idf_component_register()`, by adding `-finstrument-functions` to its build. Guard it with the config so the flag is absent when the feature is off — otherwise the `__cyg_profile_*` hooks are undefined at link time:
```cmake
# whole component (see components/ft_demo/CMakeLists.txt)
if(CONFIG_ESP_TRACE_FUNCTION_TRACE)
target_compile_options(${COMPONENT_LIB} PRIVATE -finstrument-functions)
endif()
# or only specific sources (see main/CMakeLists.txt)
if(CONFIG_ESP_TRACE_FUNCTION_TRACE)
set_source_files_properties(my_file.c PROPERTIES
COMPILE_OPTIONS "-finstrument-functions")
endif()
```
Instrumentation is applied per component, so you trace only your own code. ESP-IDF internals are not instrumented (this keeps event volume manageable and avoids tracing code that runs with the flash cache disabled).
To exclude individual files or functions from an otherwise-instrumented component, add the GCC blocklist flags:
```cmake
if(CONFIG_ESP_TRACE_FUNCTION_TRACE)
target_compile_options(${COMPONENT_LIB} PRIVATE
-finstrument-functions
-finstrument-functions-exclude-file-list=ft_demo_hot,ft_demo_quiet
-finstrument-functions-exclude-function-list=ft_demo_secret)
endif()
```
Each list is a comma-separated set of substrings matched against the source file path / function name, so a single entry can cover several files and you can list several at once.
## How recording starts
This example records when the **host** starts the SystemView session over JTAG (OpenOCD `mon esp sysview_mcore start`). Because `CONFIG_ESP_TRACE_FUNCTION_TRACE_AUTO_START` is enabled, no explicit `esp_trace_function_trace_start()` call is needed. Recording follows the encoder's recording state.
## Build, run and capture (JTAG + OpenOCD)
1. Connect a JTAG interface and [run OpenOCD](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/jtag-debugging/index.html#run-openocd).
2. Build and flash with the JTAG config:
```
idf.py set-target <target>
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci.jtag" build flash monitor
```
3. Start tracing automatically from GDB using the provided `gdbinit` (it breaks at `app_main` and runs `mon esp sysview_mcore start`):
```
riscv32-esp-elf-gdb -x gdbinit build/function_tracing.elf
```
Replace the GDB binary with the one matching your target (e.g. `xtensa-esp32-elf-gdb`). Trace data is written to `/tmp/function_tracing.svdat`.
This example uses `esp sysview_mcore`, which captures all cores into a single file in SEGGER's official multi-core format. It requires SystemView **v3.60 or later**. On a dual-core target the one file holds both cores. For older SystemView versions, use `mon esp sysview start <core0_file> [core1_file]` instead to write a separate file per core.
4. When enough data is captured, stop tracing:
```
mon esp sysview_mcore stop
```
## Viewing in SystemView and naming the events
Open the `.svdat` file in the SEGGER SystemView application.
The function-trace events arrive as numeric module event IDs. To show them as names, copy this example's `SYSVIEW_FreeRTOS.txt` into your SystemView installation directory (or merge its entries):
```
512 function_enter func=%p call_site=%p
513 function_exit func=%p call_site=%p
```
`func` is the traced function start address. `call_site` is the caller return address. SystemView records both as raw addresses. Map them to function names offline against the ELF file (for example with `addr2line`).
The IDs are not chosen by the application: SystemView assigns each registered module an `EventOffset` (the first module gets `512`) and events are `EventOffset + index`. In this example the function-trace module is the only one registered, so it occupies `512-513`.
## Configuration
Function tracing is configured under **Component config → ESP Trace Configuration → Function Tracing** in `menuconfig`. Each option has built-in help. This example enables it and relies on the default `CONFIG_ESP_TRACE_FUNCTION_TRACE_AUTO_START` so recording follows the host session. File/function exclusion uses the `-finstrument-functions-exclude-*` compile flags (see [Instrumenting your code](#instrumenting-your-code)).
For the full feature description and option reference, see the [Compiler-Instrumented Function Tracing guide](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/tracing/function-tracing.html).
## Limitations
- Instrumentation adds overhead to every traced function call and increases code size and stack usage.
- Do not instrument code that runs with the flash cache disabled (IRAM ISRs, SPI flash operations). Keep instrumentation scoped to your own components.
@@ -0,0 +1,2 @@
512 function_enter func=%p call_site=%p
513 function_exit func=%p call_site=%p
@@ -0,0 +1,15 @@
idf_component_register(SRCS "ft_demo.c" "ft_demo_hot.c" "ft_demo_quiet.c"
INCLUDE_DIRS "include")
# Opt the whole component into compiler-instrumented function tracing by adding
# -finstrument-functions to its build. The guard keeps the flag out when the
# feature is off, otherwise the __cyg_profile_* hooks would be undefined at link.
# The exclude-list flags skip specific files / functions:
# - ft_demo_hot.c and ft_demo_quiet.c are excluded by file (comma-separated)
# - ft_demo_secret is excluded by function name
if(CONFIG_ESP_TRACE_FUNCTION_TRACE)
target_compile_options(${COMPONENT_LIB} PRIVATE
-finstrument-functions
-finstrument-functions-exclude-file-list=ft_demo_hot,ft_demo_quiet
-finstrument-functions-exclude-function-list=ft_demo_secret)
endif()
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Function Tracing Example - demo library
*/
#include "ft_demo.h"
/* noinline keeps each function as a distinct call so the compiler inserts
* enter/exit hooks instead of inlining them away. */
static uint32_t __attribute__((noinline)) ft_demo_level2(uint32_t v)
{
return v * 3u + 1u;
}
static uint32_t __attribute__((noinline)) ft_demo_level1(uint32_t v)
{
return ft_demo_level2(v) + ft_demo_level2(v + 1u);
}
void ft_demo_run(uint32_t iteration)
{
volatile uint32_t r = ft_demo_level1(iteration);
(void)r;
}
void __attribute__((noinline)) ft_demo_secret(void)
{
/* Excluded from instrumentation by name, so no enter/exit is recorded even
* though this file is compiled with -finstrument-functions. */
__asm__ volatile("");
}
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Function Tracing Example - demo library (excluded file)
*/
#include "ft_demo.h"
/* This whole file is listed in the component's -finstrument-functions-exclude-file-list
* flag, so none of its functions are instrumented even though the component opts
* in. Use this for hot paths you do not want to trace. */
uint32_t ft_demo_hot_loop(uint32_t n)
{
uint32_t acc = 0;
for (uint32_t i = 0; i < n; i++) {
acc += i * i;
}
return acc;
}
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Function Tracing Example - demo library (second excluded file)
*/
#include "ft_demo.h"
/* A second file added to the component's -finstrument-functions-exclude-file-list
* as a separate, comma-separated entry. It shows the exclude list accepts more
* than one substring. Like ft_demo_hot.c, none of its functions are instrumented. */
uint32_t ft_demo_quiet_path(uint32_t n)
{
uint32_t acc = 1;
for (uint32_t i = 1; i <= n; i++) {
acc = (acc * i) % 1000u;
}
return acc;
}
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Function Tracing Example - demo library
*/
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Runs a small nested call graph (ft_demo_run -> level1 -> level2). These
* functions are instrumented and appear as function_enter/function_exit. */
void ft_demo_run(uint32_t iteration);
/* Instrumented file, but excluded by name through the component's
* -finstrument-functions-exclude-function-list flag: it produces no events. */
void ft_demo_secret(void);
/* Lives in ft_demo_hot.c, which is excluded as a whole through the component's
* -finstrument-functions-exclude-file-list flag: it produces no events. */
uint32_t ft_demo_hot_loop(uint32_t n);
/* Lives in ft_demo_quiet.c, a second comma-separated entry in the component's
* -finstrument-functions-exclude-file-list flag: it also produces no events. */
uint32_t ft_demo_quiet_path(uint32_t n);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,13 @@
set pagination off
target remote :3333
mon reset halt
maintenance flush register-cache
b app_main
commands
mon esp sysview_mcore start file:///tmp/function_tracing.svdat
c
end
c
@@ -0,0 +1,9 @@
idf_component_register(SRCS "function_tracing_example_main.c"
PRIV_REQUIRES ft_demo
INCLUDE_DIRS ".")
# Demonstrate file-level instrumentation. It instruments only this source.
if(CONFIG_ESP_TRACE_FUNCTION_TRACE)
set_source_files_properties(function_tracing_example_main.c PROPERTIES
COMPILE_OPTIONS "-finstrument-functions")
endif()
@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Function Tracing Example
*/
#include <inttypes.h>
#include <stdio.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_trace.h"
#include "ft_demo.h"
static const char *TAG = "function-tracing";
static void __attribute__((noinline)) example_workload(uint32_t iteration)
{
ft_demo_run(iteration); /* traced: ft_demo_run -> level1 -> level2 */
ft_demo_secret(); /* excluded by function name: not traced */
ft_demo_hot_loop(64); /* excluded by file: not traced */
ft_demo_quiet_path(8); /* excluded by file (2nd list entry): not traced */
}
static void example_task(void *arg)
{
(void)arg;
uint32_t iteration = 0;
while (1) {
example_workload(iteration);
ESP_LOGI(TAG, "workload iteration %" PRIu32 " on core %d", iteration, xPortGetCoreID());
iteration++;
vTaskDelay(pdMS_TO_TICKS(200));
}
}
#if CONFIG_ESP_TRACE_TRANSPORT_APPTRACE
#include "esp_app_trace.h"
esp_trace_open_params_t esp_trace_get_user_params(void)
{
static esp_apptrace_config_t app_trace_config = APPTRACE_CONFIG_DEFAULT();
esp_trace_open_params_t trace_params = {
.core_cfg = NULL,
.encoder_name = "sysview",
.encoder_cfg = NULL,
.transport_name = "apptrace",
.transport_cfg = &app_trace_config,
};
return trace_params;
}
#endif
void app_main(void)
{
ESP_LOGI(TAG, "Hello from function_tracing example!");
/* Recording is host-driven: it starts when the SystemView host (OpenOCD
* "mon esp sysview_mcore start") begins the session. No explicit start call
* is needed because CONFIG_ESP_TRACE_FUNCTION_TRACE_AUTO_START is enabled. */
for (int core = 0; core < CONFIG_FREERTOS_NUMBER_OF_CORES; core++) {
char name[configMAX_TASK_NAME_LEN];
snprintf(name, sizeof(name), "ft_workload%d", core);
xTaskCreatePinnedToCore(example_task, name, 4096, NULL, 5, NULL, core);
}
}
@@ -0,0 +1,6 @@
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=6.0'
espressif/esp_sysview: ^1
@@ -0,0 +1,156 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import json
import os.path
import re
import shutil
import subprocess
import sys
import time
import typing
import pexpect
import pytest
from pytest_embedded_idf import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
if typing.TYPE_CHECKING:
from conftest import OpenOCD
# Function-trace events are a SystemView module. The first (only) registered module
# gets EventOffset 512, so enter uses ID 512 and exit uses ID 513.
FT_EVENT_ENTER = 512
FT_EVENT_EXIT = 513
# Demo source compiled with instrumentation. Its functions must be traced.
INSTRUMENTED_SOURCE = 'ft_demo.c'
# Demo sources excluded by file. Their functions must never be traced.
EXCLUDED_SOURCES = ['ft_demo_hot.c', 'ft_demo_quiet.c']
def _encode_event_id(event_id: int) -> bytes:
"""Encode a SystemView event ID the way SEGGER_SYSVIEW does (base-128, LSB first)."""
out = bytearray()
while True:
b = event_id & 0x7F
event_id >>= 7
out.append(b | 0x80 if event_id else b)
if not event_id:
return bytes(out)
def _toolchain_prefix(binary_path: str) -> str:
with open(os.path.join(binary_path, 'project_description.json')) as f:
return str(json.load(f)['monitor_toolprefix'])
def _validate_function_trace_manual(trace_log: str) -> None:
"""Fallback validation when no toolchain is available to decode the capture.
The module description string is not in a JTAG capture (it is recorded before
recording is enabled and never re-sent), so check for the enter/exit event IDs.
"""
with open(trace_log, 'rb') as f:
content = f.read()
enter = content.count(_encode_event_id(FT_EVENT_ENTER))
exit_ = content.count(_encode_event_id(FT_EVENT_EXIT))
assert enter > 0 and exit_ > 0, f'no function enter/exit events in {trace_log} (enter={enter}, exit={exit_})'
def _validate_function_trace(trace_log: str, idf_path: str, elf_file: str, binary_path: str) -> None:
"""Decode function-trace events with sysviewtrace_proc.py and check instrumented
functions are traced while excluded sources are not. Fall back to counting
enter/exit packets when the target toolchain is not available."""
toolchain = _toolchain_prefix(binary_path)
if shutil.which(f'{toolchain}addr2line') is None:
print(f'addr2line not found for {toolchain}, using manual validation')
_validate_function_trace_manual(trace_log)
return
proc_script = os.path.join(idf_path, 'tools', 'esp_app_trace', 'sysviewtrace_proc.py')
result = subprocess.run(
[sys.executable, proc_script, '-b', elf_file, '-t', toolchain, '-i', 'func', f'file://{trace_log}'],
capture_output=True,
text=True,
)
report = result.stdout + result.stderr
assert result.returncode == 0, f'sysviewtrace_proc.py failed:\n{report}'
print(f'{report}')
m = re.search(r'Processed (\d+) function trace events\.', report)
assert m and int(m.group(1)) > 0, f'no function trace events decoded:\n{report}'
assert INSTRUMENTED_SOURCE in report, f'instrumented functions ({INSTRUMENTED_SOURCE}) not traced:\n{report}'
for excluded in EXCLUDED_SOURCES:
assert excluded not in report, f'excluded source {excluded} was traced:\n{report}'
def _validate_trace_data(trace_log: str, target: str, dual_core: bool) -> None:
"""Validate the multi-core capture contains SystemView trace data for each core."""
with open(trace_log, 'rb') as f:
content = f.read()
for idx in range(2 if dual_core else 1):
search_str = f'N=FreeRTOS Application,D={target},C=core{idx},O=FreeRTOS'.encode()
assert search_str in content, f'SysView core{idx} trace data not found in {trace_log}'
def _test_function_tracing_jtag(openocd_dut: 'OpenOCD', idf_path: str, dut: IdfDut) -> None:
# Single multi-core capture file (esp sysview_mcore).
trace_log = os.path.join(dut.logdir, 'function_tracing.svdat')
dual_core = not dut.app.sdkconfig.get('ESP_SYSTEM_SINGLE_CORE_MODE') or dut.target == 'esp32s3'
# Prepare gdbinit file pointing at this run's capture file
gdb_logfile = os.path.join(dut.logdir, 'gdb.txt')
gdbinit_orig = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'gdbinit')
gdbinit = os.path.join(dut.logdir, 'gdbinit')
with open(gdbinit_orig) as f_r, open(gdbinit, 'w') as f_w:
for line in f_r:
if line.startswith('mon esp sysview_mcore start'):
f_w.write(f'mon esp sysview_mcore start file://{trace_log}\n')
else:
f_w.write(line)
time.sleep(1) # Wait for the USJ port to be ready
dut.expect_exact('function-tracing: Hello from function_tracing example!', timeout=5)
with (
openocd_dut.run() as openocd,
open(gdb_logfile, 'w') as gdb_log,
pexpect.spawn(
f'idf.py -B {dut.app.binary_path} gdb --batch -x {gdbinit}',
timeout=60,
logfile=gdb_log,
encoding='utf-8',
codec_errors='ignore',
) as p,
):
p.expect_exact('hit Breakpoint 1, app_main ()')
# dut has been restarted by gdb since the last dut.expect()
dut.expect(re.compile(rb'function-tracing: workload iteration \d+'), timeout=30)
# Let function-trace samples accumulate while recording.
time.sleep(1)
openocd.write('esp sysview_mcore stop')
openocd.apptrace_wait_stop()
_validate_trace_data(trace_log, dut.target, dual_core)
_validate_function_trace(trace_log, idf_path, dut.app.elf_file, dut.app.binary_path)
@pytest.mark.jtag
@idf_parametrize('config', ['jtag'], indirect=['config'])
@idf_parametrize('target', ['esp32', 'esp32c2', 'esp32s2'], indirect=['target'])
def test_function_tracing_jtag(openocd_dut: 'OpenOCD', idf_path: str, dut: IdfDut) -> None:
_test_function_tracing_jtag(openocd_dut, idf_path, dut)
@pytest.mark.usb_serial_jtag
@idf_parametrize('config', ['jtag'], indirect=['config'])
@idf_parametrize(
'target',
soc_filtered_targets('SOC_USB_SERIAL_JTAG_SUPPORTED == 1'),
indirect=['target'],
)
@idf_parametrize('port', ['/dev/serial_ports/ttyUSB-esp32'], indirect=['port'])
def test_function_tracing_usj(openocd_dut: 'OpenOCD', idf_path: str, dut: IdfDut) -> None:
_test_function_tracing_jtag(openocd_dut, idf_path, dut)
@@ -0,0 +1,3 @@
CONFIG_ESP_TRACE_TRANSPORT_APPTRACE=y
CONFIG_APPTRACE_DEST_JTAG=y
CONFIG_APPTRACE_BUF_SIZE=32768
@@ -0,0 +1,12 @@
# 1ms tick period
CONFIG_FREERTOS_HZ=1000
# Enable SystemView tracing by default
CONFIG_ESP_TRACE_ENABLE=y
CONFIG_ESP_TRACE_LIB_EXTERNAL=y
CONFIG_ESP_TRACE_TS_SOURCE_ESP_TIMER=y
CONFIG_SEGGER_SYSVIEW_EVT_TASK_START_EXEC_ENABLE=y
CONFIG_SEGGER_SYSVIEW_EVT_TASK_STOP_EXEC_ENABLE=y
CONFIG_SEGGER_SYSVIEW_EVT_TASK_CREATE_ENABLE=y
# Compiler-instrumented function tracing
CONFIG_ESP_TRACE_FUNCTION_TRACE=y