mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 02:30:52 +03:00
feat(examples): add cache access counters example
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# 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')
|
||||
Reference in New Issue
Block a user