Allow compact histograms in private radio timing builds

This commit is contained in:
mikecarper committed 2026-10-23 12:03:49 -07:00
1 parent fd2c99b890
commit 601f3ee622
3 files changed
+76 -7

No files matched your search

+2 -1
View File
@@ -48,7 +48,8 @@ inline bool handle(const char* suffix, const char* args, int verb,
|| (timing && (sscanf(args, "%u %u %c", &direction, &stage, &extra) != 2
|| direction > 1 || stage >= profiletiming::StageCount))
|| (hist && (sscanf(args, "%u %u %u %c", &direction, &stage, &page, &extra) != 3
|| direction > 1 || stage >= profiletiming::StageCount || page > 1))) {
|| direction > 1 || stage >= profiletiming::StageCount
|| page >= profiletiming::HistogramCount / 8))) {
snprintf(reply, capacity, "Error: get radio.fast.hw|hil; time D S; hist D S P");
return true;
}
+12 -5
View File
@@ -10,6 +10,10 @@
#include <stdint.h>
#include "NrfRadioIrqObserver.h"
#ifndef MESH_HIL_PROFILE_HISTOGRAM_COUNT
#define MESH_HIL_PROFILE_HISTOGRAM_COUNT 16
#endif
namespace mesh {
namespace profiletiming {
@@ -20,7 +24,9 @@ enum Stage : uint32_t {
enum Result : uint32_t { Success = 0, Busy = 1, Failure = 2 };
static constexpr uint32_t Magic = 0x4d435054; // MCPT
static constexpr uint32_t Version = 3;
static constexpr uint32_t HistogramCount = 16;
static constexpr uint32_t HistogramCount = MESH_HIL_PROFILE_HISTOGRAM_COUNT;
static_assert(HistogramCount == 8 || HistogramCount == 16,
"Private profile timing supports eight or sixteen histogram bins");
static constexpr uint32_t HistogramWidthUs = 64;
#if defined(NRF52_PLATFORM)
static constexpr uint32_t ClockDivisor = 64;
@@ -33,7 +39,8 @@ static constexpr uint32_t ClockHz = ClockDivisor * 1000000;
// MCU; sums are split low/high words rather than ABI-dependent uint64_t.
struct StageStats {
uint32_t count, sum_low_us, sum_high_us, min_us, max_us, last_us;
uint32_t histogram[HistogramCount]; // final bucket includes >= 960 us
// Final bucket includes >= (HistogramCount - 1) * HistogramWidthUs.
uint32_t histogram[HistogramCount];
};
struct DirectionStats {
uint32_t successes, busy, failures, last_result, last_mask;
@@ -190,10 +197,10 @@ inline void finish(Result result, uint32_t now_ticks) {
++capture.sequence;
}
static_assert(sizeof(Capture) == 40 + 2 * (20 + StageCount * (8 + 88)),
static_assert(sizeof(Capture) == 40 + 2 * (20 + StageCount * (32 + 4 * HistogramCount)),
"Unexpected profile timing ABI");
static_assert(StageCount == 11 && sizeof(Capture) == 2192,
"Profile timing version 3 requires eleven stages and 2192 bytes");
static_assert(StageCount == 11 && sizeof(Capture) == (HistogramCount == 16 ? 2192 : 1488),
"Profile timing version 3 requires eleven stages and matching bin metadata");
} // namespace profiletiming
} // namespace mesh
+62 -1
View File
@@ -13,6 +13,12 @@ ROOT = Path(__file__).resolve().parents[1]
class Esp32SpiABDiagnosticsTests(unittest.TestCase):
def test_commands_reach_diagnostics_through_production_cli(self):
self.check_production_cli()
def test_eight_bin_capture_preserves_stages_sums_and_rejects_a_second_page(self):
self.check_production_cli(8)
def check_production_cli(self, histogram_count=None):
fixture = (ROOT / 'test/fixtures/radio_profiles/switch_tuning_test.cpp').read_text()
fixture = fixture.rsplit('\nint main() {', 1)[0]
fixture += r'''
@@ -29,12 +35,42 @@ int main() {
mesh::esp_spiab::recordClock(0x1000,8888888);
assert(!strcmp(f.cmd("get radio.fast.hw"),"> divider=0x00001000 hz=8888888 changes=1"));
assert(strstr(f.cmd("get radio.fast.time 1 10"),"d=1 s=10 count=0"));
#if MESH_HIL_PROFILE_HISTOGRAM_COUNT == 8
static_assert(sizeof(mesh::profiletiming::Capture)==1488,"compact HIL capture");
assert(strstr(f.cmd("get radio.fast.hist 0 10 1"),"Error:"));
assert(strstr(f.cmd("get radio.fast.hist 0 10 0"),"p=0 bins=0,0,0,0,0,0,0,0"));
assert(strstr(f.cmd("get radio.fast.hil"),"bytes=1488"));
assert(strstr(f.cmd("get radio.fast.hil"),"stages=11 bins=8 width=64"));
#else
assert(strstr(f.cmd("get radio.fast.hist 0 10 1"),"p=1 bins=0,0,0,0,0,0,0,0"));
assert(strstr(f.cmd("get radio.fast.hil"),"bytes=2192"));
assert(strstr(f.cmd("get radio.fast.hil"),"stages=11 bins=16 width=64"));
#endif
assert(strstr(f.cmd("set radio.fast.hw 8",false),"read-only"));
assert(strstr(f.cmd("get radio.fast.time 2 10"),"Error:"));
assert(!strcmp(f.cmd("get radio.fast.buffer"),"> on"));
assert(f.radio.attempts==0 && f.fs.files.empty());
// Compact histograms coarsen only the tail. All eleven stage rows and
// full-resolution sums/min/max remain unchanged for the same real sample.
using namespace mesh::profiletiming;
static_assert(StageCount==11,"all timing stages retained");
begin(0,1,100);mark(Admission,132);finish(Success,1000);
const auto& stats=mesh_hil_profile_timing.direction[0].stage[Whole];
assert(stats.count==1 && stats.sum_low_us==900 && stats.sum_high_us==0);
assert(stats.min_us==900 && stats.max_us==900 && stats.last_us==900);
const unsigned bucket=900/HistogramWidthUs;
assert(stats.histogram[bucket<HistogramCount?bucket:HistogramCount-1]==1);
assert(strstr(f.cmd("get radio.fast.time 0 10"),"count=1 sum_hi=0 sum_lo=900"));
#if MESH_HIL_PROFILE_HISTOGRAM_COUNT == 8
assert(strstr(f.cmd("get radio.fast.hist 0 10 0"),"bins=0,0,0,0,0,0,0,1"));
for(unsigned size=0;size<=160;++size) {
char guarded[162];memset(guarded,0x55,sizeof(guarded));
assert(mesh::esp_spiab::handle("hist","0 10 0",0,guarded+1,size));
assert(guarded[0]==0x55 && guarded[size+1]==0x55);
if(size) assert(memchr(guarded+1,0,size));
}
#endif
assert(f.radio.attempts==0 && f.fs.files.empty());
}
'''
with tempfile.TemporaryDirectory() as directory:
@@ -44,9 +80,15 @@ int main() {
encoding='ascii')
cpp, binary = work / 'cli.cpp', work / 'cli'
cpp.write_text(fixture, encoding='ascii')
extra_flags = []
if histogram_count is not None:
extra_flags.append('-DMESH_HIL_PROFILE_HISTOGRAM_COUNT='+str(histogram_count))
if os.name != 'nt':
extra_flags += ['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie']
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
'-Wall', '-Wextra', '-DESP32_PLATFORM=1', '-DMESH_HIL_PROFILE_TIMING=1',
'-DMESH_HIL_ESP_SPI_AB=1', '-include', str(prelude),
'-DMESH_HIL_ESP_SPI_AB=1', *extra_flags, '-include', str(prelude),
'-I', str(ROOT / 'test/fixtures/radio_profiles'),
'-I', str(ROOT / 'test/fixtures/radio_profiles/mocks'),
'-I', str(ROOT / 'test/mocks'), '-I', str(ROOT / 'src'),
@@ -55,6 +97,25 @@ int main() {
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
subprocess.run([str(binary)], check=True)
def test_histogram_geometry_is_private_and_rejects_unsupported_counts(self):
source = ('#include <cstdint>\ninline uint32_t micros() { return 0; }\n'
'#include <helpers/radiolib/ProfileSwitchTiming.h>\nint main() {}\n')
for count in (0, 7, 9, 32):
with self.subTest(count=count):
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++11',
'-DMESH_HIL_PROFILE_TIMING=1', '-DMESH_HIL_PROFILE_HISTOGRAM_COUNT='+str(count),
'-x', 'c++', '-fsyntax-only', '-I', str(ROOT / 'src'), '-'],
input=source, capture_output=True, text=True)
self.assertNotEqual(built.returncode,0)
self.assertIn('supports eight or sixteen histogram bins',built.stderr)
source = ('#include <helpers/radiolib/ProfileSwitchTiming.h>\n'
'#ifdef MESH_HIL_PROFILE_HISTOGRAM_COUNT\n#error HIL geometry leaked\n#endif\n'
'int main() {}\n')
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++11',
'-x', 'c++', '-fsyntax-only', '-I', str(ROOT / 'src'), '-'],
input=source, capture_output=True, text=True)
self.assertEqual(built.returncode,0,built.stderr)
def test_live_divider_after_transaction_and_read_only_bounded_snapshots(self):
fixture = r'''
#include <cassert>