Files
esp-idf/examples/system/cache_counters/pytest_cache_counters.py
2026-09-11 11:30:29 +02:00

148 lines
5.1 KiB
Python

# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
# Names of the units observing core 0 instruction and data traffic, per target
INST_UNIT = {
'esp32c5': 'l1-cache-ibus',
'esp32c6': 'l1-cache-ibus',
'esp32c61': 'l1-cache-ibus',
'esp32h2': 'l1-cache-ibus',
'esp32h4': 'l1-cache-inst-core0',
'esp32p4': 'l1-icache-core0',
'esp32s31': 'l1-cache-inst-core0',
}
DATA_UNIT = {
'esp32c5': 'l1-cache-dbus',
'esp32c6': 'l1-cache-dbus',
'esp32c61': 'l1-cache-dbus',
'esp32h2': 'l1-cache-dbus',
'esp32h4': 'l1-cache-data-core0',
'esp32p4': 'l1-dcache-core0',
'esp32s31': 'l1-cache-data-core0',
}
# The one workload every target runs. Reading .rodata goes through the cache on
# every chip, and 128 KB exceeds the data cache everywhere, so each pass has to
# fetch the lines from flash again.
FLASH_PHASE = 'flash rodata, 128 KB x 10 passes'
def expect_counter_row(dut: Dut, unit: str) -> dict:
"""Match one row of the esp_cache_cnt_dump() table, return the counter values.
Counters which the unit does not provide (printed as '-') are returned as None.
"""
num = r'(\d+|-)'
match = dut.expect(rf'{unit} +{num} +{num} +{num} +{num} +[0-9.]+%')
def field(idx: int) -> int | None:
value = match.group(idx).decode()
return None if value == '-' else int(value)
return {
'accesses': field(1),
'line_fills': field(2),
'writebacks': field(3),
'conflicts': field(4),
}
def check_phase(dut: Dut, workload: str, nonzero: dict) -> None:
"""Expect one 'Workload: ...' line followed by a counter table.
nonzero maps unit names (in dump order) to the list of counters which
must read non-zero after the workload.
"""
dut.expect_exact(f'Workload: {workload}')
for unit, counters in nonzero.items():
row = expect_counter_row(dut, unit)
for counter in counters:
assert row[counter], f'{workload}: expected {unit} {counter} > 0, got {row[counter]}'
@pytest.mark.generic
@idf_parametrize('config', ['default'], indirect=['config'])
@idf_parametrize('target', ['esp32c6', 'esp32h2'], indirect=['target'])
def test_cache_counters(dut: Dut) -> None:
# No PSRAM: only the flash workload runs. Its code is fetched from flash as
# well, so the instruction unit counts accesses too.
check_phase(
dut,
FLASH_PHASE,
{
INST_UNIT[dut.target]: ['accesses'],
DATA_UNIT[dut.target]: ['accesses', 'line_fills'],
},
)
dut.expect_exact('Cache counters example done')
@pytest.mark.generic
@idf_parametrize('config', ['psram'], indirect=['config'])
@idf_parametrize('target', ['esp32c5', 'esp32c61', 'esp32h4', 'esp32s31'], indirect=['target'])
def test_cache_counters_psram(dut: Dut) -> None:
inst = INST_UNIT[dut.target]
data = DATA_UNIT[dut.target]
check_phase(dut, FLASH_PHASE, {inst: ['accesses'], data: ['accesses', 'line_fills']})
check_phase(dut, 'PSRAM, 8 KB x 500 passes (fits in cache)', {inst: ['accesses'], data: ['accesses']})
# A working set larger than the cache must be fetched from PSRAM on every pass
check_phase(dut, 'PSRAM, 512 KB x 5 passes (exceeds cache)', {inst: ['accesses'], data: ['accesses', 'line_fills']})
dut.expect_exact('Cache counters example done')
@pytest.mark.generic
@idf_parametrize('config', ['psram'], indirect=['config'])
@idf_parametrize('target', ['esp32p4'], indirect=['target'])
def test_cache_counters_psram_esp32p4(dut: Dut) -> None:
# 128 KB is larger than the L1 data cache but fits in L2, so L1 refills on
# every pass while L2 only has to fill once
check_phase(
dut,
FLASH_PHASE,
{
'l1-icache-core0': ['accesses'],
'l1-dcache-core0': ['accesses', 'line_fills'],
'l2-cache-data': ['accesses'],
},
)
# Internal SRAM is reached through the L1 cache on this chip, and the whole
# working set stays in it, so the L1 data unit counts accesses but no fills
check_phase(
dut,
'internal SRAM, 8 KB x 1000 passes',
{
'l1-icache-core0': ['accesses'],
'l1-dcache-core0': ['accesses'],
},
)
check_phase(
dut,
'PSRAM, 48 KB x 100 passes (fits in L1)',
{
'l1-icache-core0': ['accesses'],
'l1-dcache-core0': ['accesses'],
},
)
# Larger than the L1 data cache: every pass refills L1 from L2
check_phase(
dut,
'PSRAM, 192 KB x 20 passes (fits in L2)',
{
'l1-dcache-core0': ['accesses', 'line_fills'],
'l2-cache-data': ['accesses'],
},
)
# Larger than the L2 cache: lines are continuously fetched from PSRAM
check_phase(
dut,
'PSRAM, 2 MB x 5 passes (exceeds L2)',
{
'l1-dcache-core0': ['accesses', 'line_fills'],
'l2-cache-data': ['accesses', 'line_fills'],
},
)
dut.expect_exact('Cache counters example done')