From 5a1f8eb8f4ebd5c998ecbe3757410493fff235c2 Mon Sep 17 00:00:00 2001 From: GlassOnTin Date: Sun, 29 Mar 2026 10:58:50 +0100 Subject: [PATCH] Add serial-triggered performance profile test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debug command 'P' runs a standardized 5-test benchmark: 1. Idle frame (nothing dirty) — measures LVGL overhead 2. Full frame invalidation + render — measures render + flush 3. Data update cycle — measures label formatting cost 4. Navigate all 5 tiles — measures tile transition cost 5. 10-frame burst — measures sustained frame rate Results reported as JSON with build timestamp for regression tracking. Python tool: ./scripts/screenshot.py profile Baseline (Mar 29 2026, hwcdc, unprovisioned): Idle: 914µs, Full: 80ms (37ms flush + 42ms render), Data update: 2ms, Per-tile: 75ms, Per-frame: 80ms, Main loop: 331µs idle --- Gui.h | 96 ++++++++++++++++++++++++++++++++++++++++++- scripts/screenshot.py | 34 +++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/Gui.h b/Gui.h index 7e1c928..892a7eb 100644 --- a/Gui.h +++ b/Gui.h @@ -798,7 +798,8 @@ bool gui_init() { // 'M' (0x4D) — Metrics: responds RWSM + JSON stats // 'I' (0x49) — Invalidate: force full screen redraw // 'L' (0x4C) — Log toggle: start/stop IMU logging to SD card -// 'F' (0x46) — File download: 1 byte name length + filename, sends file over serial +// 'F' (0x46) — File list: lists files on SD card +// 'P' (0x50) — Profile: runs standardized performance test, reports JSON results #define GUI_CMD_PREFIX_LEN 3 static const uint8_t gui_cmd_prefix[] = {0x52, 0x57, 0x53}; // "RWS" @@ -929,6 +930,99 @@ static void gui_cmd_execute() { break; } + case 'P': { // Standardized performance profile test + Serial.write(hdr, 4); + if (display_blanked) display_unblank(); + + uint32_t p_t0, p_t1; + uint32_t p_idle_render = 0, p_idle_flush = 0; + uint32_t p_full_render = 0, p_full_flush = 0; + uint32_t p_nav_total = 0; + uint32_t p_data_update = 0; + uint32_t p_frames = 0; + uint32_t p_multi_total = 0; + + // Test 1: Idle frame (nothing dirty) + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); // clear any pending + gui_flush_us_last = 0; + gui_frame_count = 0; + p_t0 = micros(); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + p_t1 = micros(); + p_idle_render = p_t1 - p_t0; + p_idle_flush = gui_flush_us_last; + + // Test 2: Full invalidation + render + lv_obj_invalidate(lv_screen_active()); + gui_flush_us_last = 0; + p_t0 = micros(); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + p_t1 = micros(); + p_full_render = p_t1 - p_t0; + p_full_flush = gui_flush_us_last; + + // Test 3: Data update cycle + gui_last_data_update = 0; // force update + p_t0 = micros(); + gui_update_data(); + p_t1 = micros(); + p_data_update = p_t1 - p_t0; + + // Test 4: Navigate to each tile and back (5 transitions) + p_t0 = micros(); + lv_tileview_set_tile(gui_tileview, gui_tile_radio, LV_ANIM_OFF); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + lv_tileview_set_tile(gui_tileview, gui_tile_gps, LV_ANIM_OFF); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + lv_tileview_set_tile(gui_tileview, gui_tile_msg, LV_ANIM_OFF); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + lv_tileview_set_tile(gui_tileview, gui_tile_set, LV_ANIM_OFF); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + p_t1 = micros(); + p_nav_total = p_t1 - p_t0; + + // Test 5: Rapid frame burst (10 full frames) + p_t0 = micros(); + for (int i = 0; i < 10; i++) { + lv_obj_invalidate(lv_screen_active()); + lv_tick_inc(LV_DEF_REFR_PERIOD + 1); + lv_timer_handler(); + } + p_t1 = micros(); + p_multi_total = p_t1 - p_t0; + + Serial.printf("{\"test\":\"profile\",\"build\":\"%s %s\"," + "\"idle_us\":%lu,\"idle_flush_us\":%lu," + "\"full_us\":%lu,\"full_flush_us\":%lu," + "\"data_update_us\":%lu," + "\"nav_5tile_us\":%lu," + "\"burst_10frame_us\":%lu," + "\"avg_frame_us\":%lu," + "\"loop_us\":%lu," + "\"heap\":%lu,\"psram\":%lu}\n", + __DATE__, __TIME__, + p_idle_render, p_idle_flush, + p_full_render, p_full_flush, + p_data_update, + p_nav_total, + p_multi_total, p_multi_total / 10, + gui_loop_us_last, + (uint32_t)esp_get_free_heap_size(), + (uint32_t)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + Serial.flush(); + break; + } + case 'I': { // Invalidate — force full redraw if (gui_screen) lv_obj_invalidate(gui_screen); if (display_blanked) display_unblank(); diff --git a/scripts/screenshot.py b/scripts/screenshot.py index 0ed0bc9..c1ef1e3 100755 --- a/scripts/screenshot.py +++ b/scripts/screenshot.py @@ -153,6 +153,35 @@ def cmd_navigate(s, screen): print(f"Navigate → {screen} ({col},{row})") +def cmd_profile(s): + """Run standardized performance profile test""" + send_cmd(s, ord('P')) + buf = b"" + deadline = time.time() + 30 # profile test takes several seconds + while time.time() < deadline: + chunk = s.read(max(1, s.in_waiting or 1)) + if chunk: + buf += chunk + magic = PREFIX + b"P" + idx = buf.find(magic) + if idx >= 0: + nl = buf.find(b"\n", idx + 4) + if nl >= 0: + import json + data = json.loads(buf[idx + 4:nl]) + print(f"Build: {data.get('build', '?')}") + print(f"Idle frame: {data['idle_us']:>8} µs (flush: {data['idle_flush_us']} µs)") + print(f"Full frame: {data['full_us']:>8} µs (flush: {data['full_flush_us']} µs)") + print(f"Data update: {data['data_update_us']:>8} µs") + print(f"Nav 5 tiles: {data['nav_5tile_us']:>8} µs ({data['nav_5tile_us']//5} µs/tile)") + print(f"Burst 10 frame: {data['burst_10frame_us']:>8} µs ({data['avg_frame_us']} µs/frame)") + print(f"Main loop: {data['loop_us']:>8} µs") + print(f"Heap free: {data['heap']:>8} bytes") + print(f"PSRAM free: {data['psram']:>8} bytes") + return + print(f"Timeout ({len(buf)} bytes)") + + def cmd_invalidate(s): send_cmd(s, ord('I')) print("Invalidated — full redraw requested") @@ -217,6 +246,9 @@ def main(): sub.add_parser("invalidate", aliases=["inv"]) + sub.add_parser("profile", aliases=["p"], + help="Run standardized performance test") + sub.add_parser("log", aliases=["l"], help="Toggle IMU logging to SD card") @@ -242,6 +274,8 @@ def main(): cmd_navigate(s, args.screen) elif args.command in ("invalidate", "inv"): cmd_invalidate(s) + elif args.command in ("profile", "p"): + cmd_profile(s) elif args.command in ("log", "l"): cmd_log(s) elif args.command in ("files", "f"):