From 601f3ee622621189b4acaa229639c4ef5be910f2 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 23 Oct 2026 12:03:49 -0700 Subject: [PATCH] Allow compact histograms in private radio timing builds --- src/helpers/radiolib/ESP32SpiABDiagnostics.h | 3 +- src/helpers/radiolib/ProfileSwitchTiming.h | 17 ++++-- test/test_esp32_spi_ab_diagnostics.py | 63 +++++++++++++++++++- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/src/helpers/radiolib/ESP32SpiABDiagnostics.h b/src/helpers/radiolib/ESP32SpiABDiagnostics.h index 4754a883d..b70320401 100644 --- a/src/helpers/radiolib/ESP32SpiABDiagnostics.h +++ b/src/helpers/radiolib/ESP32SpiABDiagnostics.h @@ -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; } diff --git a/src/helpers/radiolib/ProfileSwitchTiming.h b/src/helpers/radiolib/ProfileSwitchTiming.h index 47436620b..c50bdb77a 100644 --- a/src/helpers/radiolib/ProfileSwitchTiming.h +++ b/src/helpers/radiolib/ProfileSwitchTiming.h @@ -10,6 +10,10 @@ #include #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 diff --git a/test/test_esp32_spi_ab_diagnostics.py b/test/test_esp32_spi_ab_diagnostics.py index 1820b04f5..4e7ac5fbc 100644 --- a/test/test_esp32_spi_ab_diagnostics.py +++ b/test/test_esp32_spi_ab_diagnostics.py @@ -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\ninline uint32_t micros() { return 0; }\n' + '#include \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 \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