8f657b8f74e033df9727a2e68bc0e83acfc33068
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9b8b054c2d |
perf: fast model picker + dialogs — config-load hot path, model.options off the reader thread, off-screen turns skip rendering
Third profiling round (after #66033 / #66347), targeting the composer model picker and dialog opens (worktree dialog etc.), measured over CDP on real 1000+-message sessions. Backend — model.options took 4.8s cold / 1.8s warm per call, and the desktop model pill/picker blocks on it every open: - agent/credential_pool: _load_config_safe uses load_config_readonly(). Every consumer only reads, and the per-call deepcopy was the dominant cost — list_authenticated_providers calls load_pool() per provider row, and each load_pool loaded (and deep-copied) the full config again via get_pool_strategy. - hermes_cli/config: memoize ensure_hermes_home() per home path. It runs inside the config lock on EVERY load_config(), paying ~14 mkdir/chmod syscalls per call. The fast path still re-checks that the home dir exists, so a deleted home is recreated as before; profile switches hit the new path and re-run. Tests cover both. - tui_gateway/server: add model.options to _LONG_HANDLERS. It measured seconds inline on the WS reader thread — while it ran, prompt.submit and session.interrupt sat unread (same class as #21123). Together: model.options RPC 4825/1842ms → 426/230ms (measured on the live desktop backend); build_models_payload in isolation 6.2s → 0.97s cold, 0.27s warm. Desktop — every Radix dialog/popover open forced a whole-document style recalc (Presence reads getComputedStyle on mount), which on a 1300-message transcript cost ~650-730ms per open (CPU profile: getAnimationName 483ms self). The worktree dialog (⌘⇧B) paid it on every single open: - thread/list: content-visibility:auto + contain-intrinsic-size on the per-turn group wrappers. Off-screen turns now skip style recalc, layout, and paint entirely; never-rendered turns hold a placeholder height (auto: remembered real size once rendered) so scrollbar and anchoring stay stable. Verified over CDP: worktree dialog open 656- 730ms → ~200ms on the same session; stick-to-bottom pin, scroll-to- top rendering, and sticky human bubbles all intact. Also: profile-session-switch harness accepts CDP_HTTP (Chrome tends to squat on 9222). Verification: - scripts/run_tests.sh: config, credential-pool, inventory, model-switch routing, tui_gateway protocol, profiles suites green (test_profiles has one pre-existing failure on main, unrelated); new tests for the ensure_hermes_home memo. - apps/desktop: tsc clean, eslint/prettier clean, thread + session suites green (326 tests). - E2E over CDP on the live app: numbers above, plus scroll/pin sanity. |
||
|
|
c856f36459 |
perf(desktop): kill the layout-thrash cascade on session switch (#66033)
Follow-up to #65890 (router transitions off) and #65898 (structural compare + first-paint budget): profiling the switch path on real 1000+- message sessions with a new CDP harness showed the remaining freeze is NOT markdown rendering — it's a forced-reflow cascade from mount-time layout reads interleaved with style writes across the transcript's layout effects, plus the first-paint budget cut landing too late to stop the full-budget commit. Measured on the two largest local sessions (996 and 1363 messages), main-thread longtask totals per switch: warm 2450ms -> 557ms and 1158ms -> 194ms; first paint 1690ms -> 444ms. Harness: scripts/profile-session-switch.mjs (same CDP family as profile-real-stream.mjs). - use-resize-observer: drop the synchronous initial callback and ride the observer's spec-guaranteed first delivery instead (same frame, after layout, before paint). The sync call ran while the commit's layout was dirty, so every size read in a callback forced a full reflow — with one instance per user bubble (measureClamp read scrollHeight, then WROTE --human-msg-full, re-dirtying layout for the next bubble), the switch commit thrashed for over a second. Inside RO timing the same reads are free. Composer metrics (2x getBoundingClientRect + documentElement style writes) rides the same fix. - Same class, same fix at the remaining call sites profiling surfaced: ExpandableBlock and TerminalOutput (dozens per tool-heavy transcript) now measure/pin via RO initial delivery; the tool-window and thinking-preview pins drop their sync pin() call; the thread timeline's initial active-tick compute joins its existing scroll-time rAF batching so back-to-back transcript updates coalesce. - thread/list: cut the render budget in the RENDER phase (state-from- props adjustment) instead of the post-commit layout effect. The effect-time cut was too late — on a warm switch React first built and committed the full 300-part tree, then re-rendered at 60, then bumped back to 300, so the expensive commit still happened (and on a cold switch the bump rAF usually fired while the transcript was still empty, so the prefetched messages rendered at full budget anyway). The render-phase cut restarts the component before any child renders; a second trigger handles the cold path where messages land later under the same sessionKey. - thread/list: backfill 60 -> 300 inside startTransition so the older turns' markdown+shiki render is interruptible background work instead of a synchronous freeze one frame after the switch paints. Functional Math.max so an urgent "Show earlier" click can't be rebased back down. - composer focus: skip the rAF/timeout focus retries when the element is already focused — focus() runs the full focusing steps (forcing layout) even on the active element, ~585ms per switch on a large dirty DOM. - Replace the tautological render-budget test (it re-declared the constants locally and asserted 60 < 300) with behavior tests of the now-exported buildGroups + firstVisibleGroupIndex. Verification: apps/desktop `npx tsc --noEmit` clean; full `npx vitest run` 210 files / 1763 passed; manual CDP check confirms the deferred backfill commits the full transcript, stays pinned to bottom, and "Show earlier" still pages. |