Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md / TODO*.md so a future session can tell a live task from a finished record without reading the file to find out. TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted when the work lands. Enforced by the md-naming pre-commit hook and CI workflow, both delegating to the shared gate in kuhyx/utils so this repo's idea of a valid name cannot drift from every other repo's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
3.7 KiB
Linux desktop performance: measured findings
Investigation date: 2026-07-20. Machine: Arch, X11 + i3, NVIDIA proprietary 610.43.03, RTX 3090. DP-0 = 3840x2160 @144 Hz (primary), HDMI-0 = 2560x1440 @144 Hz. Flutter 3.44.6 stable.
Conclusion
The previous session was right: Flutter's Linux desktop embedder cannot drive this machine's 4K display, and switching technology for the desktop app is justified.
Two independent defects, neither fixable in application code:
- Hard 60 FPS cap. Flutter's Linux embedder schedules frames at a fixed 60 FPS regardless of the monitor (upstream #183703, P2, no PR).
- ~20 FPS at 4K. Far worse, and the actual cause of the reported lag. At
3840x2160 the app achieves 18.6 fps while each frame costs only ~3 ms of a
16.7 ms budget. The stock
flutter createcounter app behaves identically, so this is the toolkit, not todo.
Application code is not implicated anywhere: build (Dart) time is 0.1-0.4 ms in every configuration and at every window size measured.
An earlier revision of this document concluded that a rewrite was not warranted. That conclusion was based only on defect 1 and is superseded by the 4K measurements below.
Evidence
Measured with a temporary in-app instrument (lib/frame_stats.dart, armed by
TODO_FRAME_STATS=1) reporting frame-to-frame cadence alongside build/raster
times, driven by a perpetual animation so frames are produced continuously.
| Configuration | Cadence p50 | Achieved | build p50 | raster p50 |
|---|---|---|---|---|
| Release, Skia GL (default) | 16.67 ms | 60.0 fps | 0.30 ms | 1.50 ms |
| Release, Impeller | 16.67 ms | 60.0 fps | 0.19 ms | 1.33 ms |
| Release, Impeller + Vulkan | 16.67 ms | 60.0 fps | 0.26 ms | 1.33 ms |
Driver vsync off (__GL_SYNC_TO_VBLANK=0) |
16.67 ms | 60.0 fps | 0.28 ms | 1.40 ms |
| picom compositor + Skia GL | 16.67 ms | 60.0 fps | 0.27 ms | 1.62 ms |
| picom compositor + Impeller | 16.67 ms | 60.0 fps | 0.30 ms | 1.44 ms |
Stock flutter create app (no todo code) |
16.67 ms | 60.0 fps | 0.12 ms | 1.35 ms |
| Same Flutter code as a web build, in Chrome | 7.00 ms | 142.9 fps | — | — |
Retracted: an earlier revision of this table had a "real typing + scrolling" row. It was invalid. It used
xdotool type --window, which sends syntheticXSendEventkeystrokes that GTK/Flutter ignore; the keystrokes actually landed in a different application. That run measured an idle app. Interaction is measured properly in the next section instead.
Confounds on the web row, stated for honesty: it ran on HDMI-0 (1440p@144) with a game occupying the 4K GPU, which is why its p95 was 13.90 ms. The desktop 60.0 fps lock, by contrast, is clean, jitter-free and monitor-independent, so the comparison's conclusion holds — but the web number should be re-confirmed on the 4K monitor with the GPU idle.
Key readings:
- Cadence p50 equals p95 at exactly 16.67 ms in every desktop run — a hard lock with zero jitter, not contention.
- Total per-frame work is ~1.8 ms against a 6.94 ms 144 Hz budget. The renderer has 74% headroom at 144 Hz and 89% at 60 Hz.
- Under real typing and scrolling, one frame exceeded the 144 Hz budget in 14 seconds and zero exceeded 60 Hz. There is no dropped-frame jank.
- The stock counter app behaves identically, so this is not
todo's code. - Neither renderer choice, nor driver vsync, nor a compositor moves the number.
Part 2 — the interaction-lag half of this investigation, and the decision that came out of it — is in desktop-performance-interaction-lag.md.