From a6612ee2b0c437713040901ea96a7591cb4127a9 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:01:53 +0200 Subject: [PATCH] perf(l1): -D UI_PERF_L1 walks the screens and reports draw time, heap and stacks Opens each screen directly, types keys, and prints per step over USB: frames drawn/sent, render and flush time (CPU cycle counter), heap used/free/largest block, every task's stack headroom, and a screen dump. Measurement only. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/ui-new/UITask.cpp | 190 +++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 88d86db2..7d0842a7 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2380,7 +2380,188 @@ void UITask::pollHallSensor() { #endif } +#if defined(UI_PERF_L1) && defined(NRF52_PLATFORM) +// -D UI_PERF_L1: walks the main screens by itself (keys fed into the queue) and +// prints, per step over USB serial: frames drawn, frames the display skipped +// as unchanged, render/flush time (avg/max us), heap (used/free/largest +// block) and every FreeRTOS task's stack headroom. Measurement only. +#include "FreeRTOS.h" +#include "task.h" +#include +extern unsigned char __HeapBase[]; +extern unsigned char __HeapLimit[]; +extern uint32_t g_disp_flushes; +namespace perfl1 { + // Each step opens a screen directly (so the walk doesn't depend on which + // Home pages are enabled), then types keys: R/L/U/D/E/X, H = hold Enter. + enum Open : uint8_t { NONE, HOME, SETTINGS, MESSAGES, TOOLS, NEARBY, TRAIL, MAP, COMPASS, SATS, + DIAG, LIVE, LOCATOR, ADVERT, REPEATER, BOT, CLOCKTOOLS, CHANNEL0 }; + struct Step { const char* name; Open open; const char* keys; uint16_t dwell_ms; }; + static const Step STEPS[] = { + {"boot-idle", NONE, "", 3000}, + {"home", HOME, "", 2000}, + {"home-R1", NONE, "R", 1500}, {"home-R2", NONE, "R", 1500}, {"home-R3", NONE, "R", 1500}, + {"home-R4", NONE, "R", 1500}, {"home-R5", NONE, "R", 1500}, {"home-R6", NONE, "R", 1500}, + {"home-R7", NONE, "R", 1500}, {"home-R8", NONE, "R", 1500}, + {"settings", SETTINGS, "", 2000}, {"set-display", NONE, "E", 2000}, + {"set-scroll", NONE, "DDDDDDDDDDDD", 2000}, + {"messages", MESSAGES, "", 2000}, {"dm-list", NONE, "E", 2000}, + {"dm-scroll", NONE, "DDDDDDDDDDDDDDDDDDDD", 2000}, + {"dm-thread", NONE, "E", 3000}, {"dm-thread-up", NONE, "UUUU", 2000}, + {"channel", CHANNEL0, "", 3000}, {"channel-up", NONE, "UUUUUU", 2000}, + {"compose", NONE, "EE", 2000}, {"kbd-type", NONE, "RRDRRDRRE", 2000}, + {"kbd-close", NONE, "XXX", 1500}, + {"nearby", NEARBY, "", 3000}, {"nearby-scroll", NONE, "DDDDDDDDDDDDDDDDDDDD", 2000}, + {"tools", TOOLS, "", 1500}, {"trail", TRAIL, "", 2000}, {"map", MAP, "", 3000}, + {"compass", COMPASS, "", 3000}, {"sats", SATS, "", 4000}, {"sats-signal", NONE, "R", 3000}, + {"diag", DIAG, "", 4000}, {"diag-scroll", NONE, "DDDD", 3000}, + {"liveshare", LIVE, "", 1500}, {"locator", LOCATOR, "", 1500}, {"autoadvert", ADVERT, "", 1500}, + {"repeater", REPEATER, "", 1500}, {"bot", BOT, "", 1500}, {"clocktools", CLOCKTOOLS, "", 1500}, + {"home-end", HOME, "", 3000}, + {"done", NONE, "", 500}, + }; + static int step = -1; + static uint32_t step_at = 0, frames = 0, r_sum = 0, r_max = 0, f_sum = 0, f_max = 0, fl0 = 0; + static uint32_t heap_min_free = 0xFFFFFFFF; + + static uint32_t largestBlock() { // bisect the biggest malloc that succeeds + uint32_t lo = 0, hi = (uint32_t)(__HeapLimit - __HeapBase); + while (hi - lo > 64) { + uint32_t mid = (lo + hi) / 2; + void* q = malloc(mid); + if (q) { free(q); lo = mid; } else hi = mid; + } + return lo; + } + static void heap(uint32_t& used, uint32_t& freeb) { + uint32_t total = (uint32_t)(__HeapLimit - __HeapBase); + used = (uint32_t)mallinfo().uordblks; + freeb = used < total ? total - used : 0; + } + void frame(uint32_t render_us, uint32_t flush_us) { + frames++; r_sum += render_us; f_sum += flush_us; + if (render_us > r_max) r_max = render_us; + if (flush_us > f_max) f_max = flush_us; + } + static void report(const char* name) { + uint32_t used, freeb; heap(used, freeb); + if (freeb < heap_min_free) heap_min_free = freeb; + uint32_t fl = g_disp_flushes - fl0; + Serial.printf("PERF %-12s frames %3lu sent %3lu render avg %5lu max %6lu flush avg %5lu max %6lu us | heap used %lu free %lu largest %lu min %lu\n", + name, frames, fl, frames ? r_sum / frames : 0, r_max, frames ? f_sum / frames : 0, f_max, + used, freeb, largestBlock(), heap_min_free); + } + static void tasks() { + TaskStatus_t st[16]; + UBaseType_t n = uxTaskGetSystemState(st, 16, NULL); + for (UBaseType_t i = 0; i < n; i++) + Serial.printf("TASK %-10s stack free %5lu B\n", st[i].pcTaskName, (unsigned long)st[i].usStackHighWaterMark * sizeof(StackType_t)); + } + static DisplayDriver* disp = nullptr; + static void shot(const char* name) { // the 1 KB page buffer as hex, for a PNG on the host + if (!disp || !disp->getBuffer()) return; + const uint8_t* b = disp->getBuffer(); + Serial.printf("SHOT %s ", name); + for (uint16_t i = 0; i < disp->getBufferSize(); i++) Serial.printf("%02x", b[i]); + Serial.println(); + } + static char keyOf(char c) { + switch (c) { case 'R': return KEY_RIGHT; case 'L': return KEY_LEFT; case 'U': return KEY_UP; + case 'D': return KEY_DOWN; case 'E': return KEY_ENTER; case 'X': return KEY_CANCEL; case 'H': return KEY_CONTEXT_MENU; } + return 0; + } + static const char* pending = nullptr; + static uint32_t next_key_at = 0; + // Returns a key to enqueue (0 = none). + Open open_now = NONE; + char tick() { + uint32_t now = millis(); + if (step >= (int)(sizeof(STEPS) / sizeof(STEPS[0]))) return 0; + if (pending && *pending) { + if ((int32_t)(now - next_key_at) < 0) return 0; + next_key_at = now + 300; + char k = keyOf(*pending++); + if (!*pending) { step_at = now; frames = r_sum = r_max = f_sum = f_max = 0; fl0 = g_disp_flushes; } + return k; + } + if (step >= 0 && (int32_t)(now - step_at) < (int32_t)STEPS[step].dwell_ms) return 0; + if (step >= 0) { report(STEPS[step].name); shot(STEPS[step].name); } + step++; + if (step >= (int)(sizeof(STEPS) / sizeof(STEPS[0]))) { tasks(); Serial.println("PERF end"); return 0; } + if (step == 0) tasks(); + pending = STEPS[step].keys; + open_now = STEPS[step].open; + step_at = now; frames = r_sum = r_max = f_sum = f_max = 0; fl0 = g_disp_flushes; + next_key_at = now; + return 0; + } +} +// micros() ticks in ~1 ms steps here (RTC-based), so time with the CPU cycle counter. +static inline uint32_t perfUs() { + static bool on = false; + if (!on) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; on = true; } + return DWT->CYCCNT; // cycles; differences stay right across the 67 s wrap +} +#define PERF_T0() uint32_t _pf0 = perfUs() +#define PERF_T1() uint32_t _pf1 = perfUs() +#define PERF_T2() perfl1::frame((_pf1 - _pf0) / (SystemCoreClock / 1000000), (perfUs() - _pf1) / (SystemCoreClock / 1000000)) +#else +#define PERF_T0() +#define PERF_T1() +#define PERF_T2() +#endif + void UITask::loop() { +#if defined(UI_PERF_L1) && defined(NRF52_PLATFORM) + { static uint32_t start = millis(); + perfl1::disp = _display; + { static bool sz = false; + if (!sz && millis() - start > 7000) { sz = true; + Serial.printf("SIZE UiCore %u Splash %u Home %u Settings %u Messages %u Tools %u Ringtone %u Bot %u Admin %u Nearby %u Dash %u AutoAdv %u LiveSh %u Locator %u Trail %u Compass %u Sats %u Diag %u Repeater %u ClockTools %u Kbd %u UITask %u StaticPool32 %u Packet %u\n", + sizeof(UiCore), sizeof(SplashScreen), sizeof(HomeScreen), sizeof(SettingsScreen), sizeof(MessagesScreen), sizeof(ToolsScreen), + sizeof(RingtoneEditorScreen), sizeof(BotScreen), sizeof(AdminScreen), sizeof(NearbyScreen), sizeof(DashboardConfigScreen), + sizeof(AutoAdvertScreen), sizeof(LiveShareScreen), sizeof(LocatorScreen), sizeof(TrailScreen), sizeof(CompassScreen), +#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW) + sizeof(SatellitesScreen), +#else + 0u, +#endif + sizeof(DiagnosticsScreen), sizeof(RepeaterScreen), sizeof(ClockToolsScreen), sizeof(KeyboardWidget), sizeof(UITask), + sizeof(StaticPoolPacketManager), sizeof(mesh::Packet)); + TaskStatus_t st[16]; uint32_t tot = 0; + UBaseType_t n = uxTaskGetSystemState(st, 16, NULL); + (void)n; (void)tot; + } } + if (millis() - start > 8000) { + char k = perfl1::tick(); + if (k) enqueueKey(checkDisplayOn(k)); + if (perfl1::open_now != perfl1::NONE) { + checkDisplayOn(0); + switch (perfl1::open_now) { + case perfl1::HOME: gotoHomeScreen(); break; + case perfl1::SETTINGS: gotoSettingsScreen(); break; + case perfl1::MESSAGES: gotoMessagesScreen(); break; + case perfl1::TOOLS: gotoToolsScreen(); break; + case perfl1::NEARBY: gotoNearbyScreen(); break; + case perfl1::TRAIL: gotoTrailScreen(); break; + case perfl1::MAP: gotoMapScreen(); break; + case perfl1::COMPASS: gotoCompassScreen(); break; + case perfl1::SATS: gotoSatellitesScreen(); break; + case perfl1::DIAG: gotoDiagnosticsScreen(); break; + case perfl1::LIVE: gotoLiveShareScreen(); break; + case perfl1::LOCATOR: gotoLocatorScreen(); break; + case perfl1::ADVERT: gotoAutoAdvertScreen(); break; + case perfl1::REPEATER: gotoRepeaterScreen(); break; + case perfl1::BOT: gotoBotScreen(); break; + case perfl1::CLOCKTOOLS: gotoClockTools(); break; + case perfl1::CHANNEL0: openChannelHistory(0); break; + default: break; + } + perfl1::open_now = perfl1::NONE; + _next_refresh = 0; + } + } } +#endif pollConnection(); // BLE link state -> hasConnection() (see UITaskBase) drainCoreEvents(); // react to what the Core filed during mesh processing (alerts, wake, sounds) #if UI_HAS_JOYSTICK @@ -2641,13 +2822,17 @@ void UITask::loop() { _display->turnOff(); } else if (_locked && _unlock_kb && millis() >= _next_refresh) { // While the prompt is up the password keyboard replaces the lockscreen view + PERF_T0(); _display->startFrame(); _kb.beginFrame(); int delay_millis = _kb.render(*_display); if (millis() < _alert_expiry) renderAlertOverlay(); // "Wrong PIN", and a ringing alarm + PERF_T1(); _display->endFrame(); + PERF_T2(); _next_refresh = millis() + delay_millis; } else if (_locked && millis() >= _next_refresh && home) { + PERF_T0(); _display->startFrame(); if (curr && curr != home && (millis() - ui_started_at < BOOT_SCREEN_MILLIS)) { // Boot splash is still up on a boot-locked device @@ -2659,8 +2844,11 @@ void UITask::loop() { // Alert overlay on top — without this a ringing alarm on a locked device // played its melody against a screen that never said what was ringing. if (millis() < _alert_expiry) renderAlertOverlay(); + PERF_T1(); _display->endFrame(); + PERF_T2(); } else if (!_locked && millis() >= _next_refresh && curr) { + PERF_T0(); _display->startFrame(); _kb.beginFrame(); int delay_millis = curr->render(*_display); @@ -2681,7 +2869,9 @@ void UITask::loop() { } else { _next_refresh = millis() + delay_millis; } + PERF_T1(); _display->endFrame(); + PERF_T2(); } #if AUTO_OFF_MILLIS > 0 #ifdef KEEP_DISPLAY_ON_USB