mirror of
https://github.com/espressif/esp-idf.git
synced 2026-08-18 06:35:35 +03:00
feat(esp_trace): add compiler-instrumented function tracing support
This commit is contained in:
156
examples/system/function_tracing/pytest_function_tracing.py
Normal file
156
examples/system/function_tracing/pytest_function_tracing.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user