Skip to content

task_handler: keep the main thread alive under heavy UI load (stacked on #13) - #15

Open
bitcoin3us wants to merge 2 commits into
MicroPythonOS:integrationfrom
bitcoin3us:fix/task-handler-fairness
Open

bitcoin3us wants to merge 2 commits into
MicroPythonOS:integrationfrom
bitcoin3us:fix/task-handler-fairness

Conversation

@bitcoin3us

Copy link
Copy Markdown

Stacked on #13 (single elapsed-based tick source); only the last commit is new here.

Problem

TaskHandler._timer_cb reschedules _task_handler as soon as the previous pass has finished. When a pass (LVGL refresh, timers, callbacks) takes longer than the 2 ms timer period, which any real UI load does (video playback, big animations), the passes run back-to-back and the main thread only executes between them: the REPL stops draining serial input, host tools time out mid-protocol and abort, and touch and app tasks starve.

Change

Each pass records its duration and end time; the timer callback then waits until at least a quarter of the last pass's duration has elapsed since it ended before scheduling the next one. Short passes are unaffected; long ones now leave the main thread at least ~20% of the CPU.

Measured (Waveshare ESP32-S3-Touch-LCD-3.5, 160x120 MJPEG at 30 fps, the heaviest UI load I have)

Before After
Serial input drained during playback 228 B/s, host writes block 365 B/s, no blocking
Console after the clip (harness run) unreachable until a physical reset responsive
Playback, 160x120 @ 15 fps clip 13.9 fps 14.1 fps
Playback, 320x240 @ 8 fps clip 7.8 fps 7.3 fps

Idle drain rate is unchanged (~1.8 KB/s either way).

🤖 Generated with Claude Code

With thanks to the scientists and engineers who did the hard, unglamorous work that got us here.

bitcoin3us and others added 2 commits September 16, 2026 13:49
LVGL time ran ~1.8x faster than wall clock on ESP32 builds: _timer_cb
added the nominal period on every machine.Timer tick while
_task_handler also added the elapsed time on every scheduled pass (plus
a third increment covering the FINISHED callbacks). Every lv timer,
animation, scroll throw and long-press threshold therefore fired early;
a 12 fps lv.timer produced 14 frames per second.

Measured on a Waveshare ESP32-S3-Touch-LCD-3.5 (MicroPythonOS 0.18):
lv.tick_get() advanced 1.834 ms per wall-clock ms when idle. With
TaskHandler.disable() (timer path only) the ratio was exactly 1.000,
and under load the timer path alone lost ~20% of ticks because
scheduled callbacks coalesce.

Make _timer_cb the only place ticks are added, and have it add the real
elapsed milliseconds since its previous run instead of the nominal
period, so LVGL time equals wall time regardless of load. Verified
1.000 idle and under flash-read load; a stalled scheduler now catches
up instead of losing time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… load

When a handler pass (lv.task_handler plus the STARTED/FINISHED callbacks)
outlasts the timer period, the timer callback rescheduled the next pass
immediately, so under sustained LVGL load (video playback, heavy
animations) the main thread ran only between back-to-back passes:
the REPL stopped draining serial input, host tools timed out
mid-protocol, touch and app tasks starved.

Record how long each pass took and when it ended, and have the timer
callback wait until at least a quarter of that duration has elapsed
before scheduling the next pass. Passes shorter than the timer period
are unaffected; long ones now leave the main thread at least ~20% of
the time.

Measured on a Waveshare ESP32-S3-Touch-LCD-3.5 during 160x120 MJPEG
playback at 30 fps: serial input drain went from 228 B/s with host
writes blocking to 365 B/s without blocking, a clip that reliably left
the console unreachable now completes with the console responsive, and
playback rates are unchanged within noise (14.1 vs 13.9 fps at
160x120/15 fps; 7.3 vs 7.8 at 320x240/8 fps).

Co-Authored-By: Claude Fable 5.1 <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