Skip to content

Turing USB: coalesce widget updates into rate-limited full-frame sends (~340% -> ~5% CPU) - #1049

Open
davidfdr wants to merge 1 commit into
mathoudebine:mainfrom
davidfdr:perf/turing-usb-coalesce-frames
Open

Turing USB: coalesce widget updates into rate-limited full-frame sends (~340% -> ~5% CPU)#1049
davidfdr wants to merge 1 commit into
mathoudebine:mainfrom
davidfdr:perf/turing-usb-coalesce-frames

Conversation

@davidfdr

Copy link
Copy Markdown

Problem

The Turing USB protocol (REVISION: TUR_USB, HW rev 1.x models incl. 8.8"/9.2") only supports full-screen image uploads — there is no partial-update command. LcdCommTuringUSB.DisplayPILImage() therefore encoded and sent the entire framebuffer once per widget refresh. With a typical theme refreshing ~20 widgets per second, and _encode_png() using compress_level=9 (~540 ms CPU per 1920x480 photographic frame), the monitor pinned 3-4 CPU cores on an 8-core Ryzen (9.2" model, theme with photo background).

Profiled with py-spy: virtually all samples were in ImageFile._encode_tile under DisplayPILImage, spread across the stat refresh threads.

Fix

  • DisplayPILImage() now only pastes the widget into current_state (under a lock) and marks the frame dirty — no encode/send per widget.
  • A dedicated daemon thread (TuringUSB_Sender) sends the composed frame at most every 0.5 s, so any number of widget updates coalesce into <= 2 uploads/sec. Visually nothing changes (stat intervals are >= 1 s).
  • A device lock serializes USB access; previously the stat threads wrote to the device concurrently with no synchronization.
  • Streamed frames are encoded with compress_level=1 (~25 ms, still lossless, still well under the 1 MiB payload limit). upload_file() storage uploads keep level 9.
  • Frames are kept RGBA: converting to RGB makes the firmware render the frame incorrectly (tested on the 9.2" PID 0x0092).

Measurements (9.2" model, 1920x480 theme with photographic background)

CPU (of one core)
before ~340%
per-widget sends coalesced ~55%
+ compress_level=1 ~5%

Encode benchmark for one full RGBA frame: level 9 = 538 ms, level 1 = 25 ms (625 KiB).

🤖 Generated with Claude Code

The Turing USB protocol only supports full-screen image uploads, so
every widget refresh (each text value, bar, ...) encoded and sent the
whole framebuffer. With a typical theme refreshing ~20 widgets per
second, and PNG compress_level=9 costing ~540 ms CPU per 1920x480
photographic frame, the monitor pinned 3-4 CPU cores.

Instead of sending from every DisplayPILImage call, only paste the
widget into current_state (under a lock) and mark the frame dirty.
A dedicated daemon thread sends the composed frame at most every
0.5 s. A device lock serializes USB access, which was previously
unsynchronized across the stat refresh threads.

Also encode streamed frames with PNG compress_level=1 (~25 ms,
still lossless). Frames must stay RGBA: the device firmware does
not render RGB PNGs correctly.

On an 8-core Ryzen with the 9.2" model this drops CPU usage from
~340% to ~5% of one core with no visible change on the display.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant