Repository navigation
epic(perf): steady 120 Hz without drops (profile findings, Sep 2026) #987
Description
Goal
Steady 120 Hz without drops (and headroom for 144 Hz): every frame ≤ 8.3 ms build and ≤ 8.3 ms raster, no multi-frame hitches on common actions.
How it was measured
Profile builds on Linux (Hyprland, 120 Hz monitor, AMD iGPU, Impeller GLES), two harnesses added in this investigation:
benchmark/app_perf_bench.dart: the real shell driven through the Demo Playground with a throwaway data directory and in-memory secrets. Scenarios: idle, sidebar toggle, run a 5000-row query, grid vertical / horizontal scroll, hover, typing, tabs, connections tree with 1500 tables (expand / scroll / collapse). Reports FrameTiming, presented-frame intervals from the raster timeline, and optionally CPU profile, allocations and engine timeline per scenario.benchmark/grid_perf_bench.dart: the grid alone (5000×120), modesscroll,edit,select,dialog,rerun.
Caveats found along the way: recording the Dart timeline stream allocates heavily inside the framework and inflates GC; the in-process VM service client itself costs CPU; FrameTiming.buildDuration does not include flushSemantics.
Results (real app, window 2056×2616)
| Scenario | build p50 / max | raster p50 | Verdict |
|---|---|---|---|
| Idle | nothing drawn | — | ok |
| Sidebar toggle | 4.0 / 7.1 ms | 0.8 ms | too heavy for 144 Hz → #984 |
| Run 5000-row query | — / 54–75 ms | 1.0 ms | one big hitch → #982, #981 |
| Grid vertical scroll | 1.4 / 3.4 ms | 0.8 ms | ok |
| Grid horizontal scroll | 0.1 / 12–20 ms | 0.8 ms | window-shift hitches → #983 |
| Hover over grid | 0.1 / 0.3 ms | 0.8 ms | ok |
| Typing in editor | 2.6 / 5.0 ms | 0.9 ms | ok |
| Tab switch | — / 6.8–7.9 ms | 0.9 ms | tight → #985 |
| Tree 1500 tables: expand / scroll / collapse | ≤ 5.9 / ≤ 2.8 / ≤ 1.8 ms | 1.2–1.3 ms | ok |
All scenarios present at exactly 60 fps on Linux regardless of cost → #980 (engine).
Children
- perf(linux): app is capped at 60 fps because the GTK embedder has no vsync #980 Linux capped at 60 fps (GTK embedder has no vsync; upstream Use vsync in Linux embedder flutter/flutter#192342)
- perf(grid): semantics tree is rebuilt for every visible cell (~17 ms per result set, hidden from FrameTiming) #981 Grid semantics rebuilt per cell (~17 ms per new result set, hidden from FrameTiming)
- perf(grid): first frame of a new result set spends ~35 ms in layout #982 Grid first frame of a new result set: ~35 ms layout
- perf(grid): column-window shift during horizontal scroll builds heavy cells (12-50 ms frames) #983 Grid column-window shift builds heavy cells (12–50 ms)
- perf(shell): sidebar toggle re-lays out offstage workspace layers and LayoutBuilders every frame #984 Sidebar toggle lays out offstage layers and LayoutBuilders every frame
- perf(workspace): switching SQL tabs builds up to 8 ms in one frame #985 SQL tab switch up to 8 ms build
Added after the Oct 2026 performance review:
- perf(sql-workspace): results wait for a schema lookup on every query; cache table schema #1171 SQL results wait for a schema lookup on every query
- perf(sql-workspace): transaction refresh after every statement probes the server and rebuilds all tabs #1172 Transaction refresh after every statement probes the server and rebuilds all tabs
- ci(bench): ERD, charts/groupings and multi-tab SQL scenarios in the app benchmark #1173 Bench scenarios for ERD, charts / groupings, multi-tab SQL
- ERD and charts: perf(erd): router rebuilds the obstacle grid and re-routes every edge on each drag move #1168, perf(erd): hover rebuilds and repaints the whole canvas; no viewport culling; first layout on the UI thread #1169, perf(results): charts and groupings parse and aggregate all rows on the UI thread on every rebuild #1170 (tracked in epic(viz): ER diagram, charts and plans: correct on real data, consistent with the UI kit #1166)
Already fixed in this cycle: #875, #879, #885, #972 (grid rebuilds), #874, #876, #877, #878, #886.
Related
#723 (connections tree @ 120 Hz: measured within budget with 1500 tables), #739 (QA sign-off).
Activity
Merge pull request #988 from QueryaHub/perf/120hz-investigation
ZhuchkaTriplesix commentedon Sep 27, 2026
Status update — 2026-09-27
Went through all six children this cycle. Summary of what shipped, with measured before/after where it exists, and honest notes on where a fix didn't move the needle or wasn't attempted at all.
Closed
GestureDetector+MouseRegionper visible cell). Benchmarked (grid_perf_bench.dart MODE=select, 5000×120): build p50 3.53→2.88ms, total p50 11.69→9.09ms, frames>8.33ms 312/372→264/371. Secondary-tap/context-menu/tooltip hoisting and a full custom-RenderObject rewrite remain out of scope.VerticalSplitPanecaching, from earlier in this cycle).Still open
LazyOffstage, which skips re-laying-out hiddenQueryaSwitchingBodylayers every animation frame (verified correct with a dedicated test). Benchmarked honestly: no measurable difference in the bundledsidebar_togglescenario (build p50 3.5ms before/after), because the dominant cost there is the visible tree's ownLayoutBuilderrebuilds reacting to the changing width, not the hidden layer's layout. That remaining piece needs restructuring_MainContentSplitStateso the workspace's laid-out width doesn't change during the toggle at all (reserve final width immediately, animate the reveal via clip/transform) — a bigger, riskier change to a central screen (main_screen.dart), not attempted yet.flutter/flutter#192342), which is likely an engine/embedder-level issue rather than something fixable from app code. Worth a scoped investigation before committing effort, but that investigation hasn't happened yet.Net effect
Every scenario in the original profile table has a closed or measurably-improved issue against it except the sidebar toggle (partially improved, real bottleneck untouched) and the Linux vsync cap (untouched, may not be fixable here at all). Given the remaining work is either high-risk-for-uncertain-gain (#984's Row restructure) or possibly not our issue to fix (#980's embedder cap), pausing here rather than continuing to grind on partial increments — flagging both for a deliberate decision on whether/how to pursue them next.
ZhuchkaTriplesix commentedon Oct 12, 2026
Re-measurement on
dev/main(2026-10-12)benchmark/app_perf_bench.dart, profile build, Linux, 120 Hz display, window 2056x2616, 5000 rows x 20 cols, tree of 1500 tables.Reading:
PROFILE=trueto see what is left after perf(grid): semantics tree is rebuilt for every visible cell (~17 ms per result set, hidden from FrameTiming) #981/perf(grid): first frame of a new result set spends ~35 ms in layout #982.erd_open_200scenario times out on currentdev; tracked separately (see the new issue).ZhuchkaTriplesix commentedon Oct 12, 2026
Follow-up: where the
run_heavy_queryhitch comes from (call-path analysis, same machine and build)What was measured, and what turned out to be noise:
debugProfileBuildsEnabled / LayoutsEnabled / PaintsEnabledthe timeline for the 5000 x 20 run showsBUILD9.0 ms total (30 builds) andLAYOUT53.4 ms in 7 frames, i.e. the ~50 ms frame is layout, not build. (These flags add per-object timeline events, so leaf numbers such asRenderParagraph43 ms for 242 paragraphs are inflated; a clean paragraph layout is 10-20 us, see below.)TextPainter.layoutis 10-20 us per paragraph for ASCII / Cyrillic / Latin-extended in every font-family variant (monospace, a concrete installed family, a missing family, the 10-family editor stack). Only CJK is slow (~400 us, 40x); filed separately.FcPatternGetString,FcStrCmpIgnoreCase,FcPatternFilter) in the CPU profile comes from that fallback path; replacing'monospace'by a concrete family in the grid changed the max build only from 47-52 ms to 38-45 ms (n = 2 each), so it is not the main cost.run_heavy_queryscenario shows ~35 young-generation collections on the UI thread totalling ~200 ms (~5.7 ms each, i.e. a frame each at 120 Hz) plus 30-60 ms of old-generation work. That points at the allocation volume of the result path (List<List<Object?>>, per-cell strings), not at layout; see perf(database): CompactResultDataset.fromRawRows formats every cell during type detection; split it and sample #1548, perf(results): carry the columnar dataset to sort / filter / export instead of List<List<String>> (memory 68 -> 17 MiB at 50k x 20) #1549, perf(results): build typed columns directly from the driver stream instead of collecting List<List<Object?>> first #1552._OneByteString.==/hashCodeandLinkedHashMaplookups are caused by the VM-service client itself (the call chains end inVmService._processMessage/createServiceObject).row[col]lookups cost 1.8 ms for 5000 x 20 (row.valuesis not faster).Note on Linux fps: a profile build from
flutter runuses the stock engine (60 fps). Release builds splice the vsync-patched engine (scripts/linux/apply_patched_engine.sh), verified on the installed package.ZhuchkaTriplesix commentedon Oct 12, 2026
Memory finding from the same session as the CPU work: every result grid that is shown stays alive for the rest of the session (~5 MB per run at 5000 x 20; 6 re-runs -> 6 live grid states and datasets), and closed tabs retain their editor, results tab, grid and dataset. Root cause and the two-line fix are in #1555 (P1). Caches in
lib/were audited and are bounded (highlighter pair cache 12, intern pool 4096, connection pools LRU, Redis history 200);_gridSchemaCacheis the only one without a cap but is cleared on writes and bounded by table count.