Skip to content

perf(app): stop the per-second shell re-render and halve bundled renderer assets - #1352

Merged
iamtoruk merged 7 commits into
getagentseal:mainfrom
ozymandiashh:perf/desktop-render-clock-and-resolution-cache
Sep 17, 2026
Merged

iamtoruk merged 7 commits into
getagentseal:mainfrom
ozymandiashh:perf/desktop-render-clock-and-resolution-cache

Conversation

@ozymandiashh

@ozymandiashh ozymandiashh commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Three quiet desktop inefficiencies, each measured before and after, each pinned by a regression test.

Update after merging main (#1459, #1462). #1459 replaced the flame art with onboarding-flame.png and deleted flame.png, so this PR's 192 px cut of the old art no longer applies and is removed; the flame parts of §3 and the table below are superseded. What remains of §3 is the splash re-encode. #1459's footer (RefreshMark) is kept as is, rendered from the leaf RefreshedAt so only it ticks. Section 2 was dropped in review (see CLI memo: dropped).

1. The shell stops re-rendering once a second

AppMain owned a wall-clock setInterval(1000) whose only consumer was the footer's "refreshed Ns ago" label, but every tick reconciled the whole tree — sidebar, hero, daily chart, activity heatmap, every table — 60 times a minute whether or not any data had changed.

The per-second tick now lives in a leaf RefreshedAt component that owns just that label. The shell re-renders on real state changes plus a 15-second day check that fires exactly when the local calendar rolls over; that rollover matters because the overview memo keys bake in a today/month boundary, so midnight must still produce one re-render to keep "Today" honest.

App.renderChurn.test.tsx pins both properties: on the base the shell re-rendered on every clock tick and on same-day checks, on this branch neither.

2. CLI resolution stops re-scanning the filesystem per request

Every read (each section poll, each prefetch warm) resolved the codeburn binary from scratch: a stat sweep over every PATH entry plus a readdir of the nvm versions tree — per request, purely to return the same path.

Resolution and the derived spawn PATH are memoized against the full set of inputs that can change the answer (CODEBURN_BIN, CODEBURN_BUNDLED_CLI, the dev-server and persisted-path overrides, CODEBURN_PATH_DIRS, PATH, NVM_DIR), so an unchanged environment costs a string compare. Safety valves:

  • a miss is never cached, so a CLI installed — or a path persisted via locate-CLI — while the app is open is still discovered on the next read;
  • a spawn error drops the memo, so a deleted binary cannot pin the app to a dead path (covered by a test that deletes the resolved binary and watches the next request find the successor);
  • __resetCliResolutionForTests() keeps one test's filesystem layout out of the next.

Measured on this machine (18 PATH entries): 26.5 µs → 2.6 µs per resolution, and the syscalls leave the steady-state path entirely.

3. Splash video halves

The cold-start splash shipped a 2.6 MB VP9 clip.

  • splash-loader.webm: re-encoded at VP9 CRF 30 — 1.29 MB, −50%, SSIM 0.994 against the source; video-only file, no audio track affected.

dist/renderer after merging main: 3,924 KB → 2,672 KB (local vite build). The JS bundle is untouched.

Proof (per review)

Render churn. Pinned by app/renderer/App.renderChurn.test.tsx: the shell's overview render count does not move across five 1 s ticks (growth ≤ 2, vs one render per tick before), rolls exactly once across the midnight boundary, and — the counterpart added for this review — the footer label still ticks every second (just now1s ago2s ago) while that same shell count stays flat. Reviewer's own measurement on his machine: AppMain renders over 10 s idle, 10 → 0.

Asset weight, per file (before merging main; the flame row is superseded by #1459's new art):

file main this PR Δ
splash.webm 2,522 KB 1,264 KB −1,257 KB
flame (880 px → 192 px cut) 696 KB 52 KB −643 KB
index.css 91 KB 90 KB ~0
index.js 591 KB 591 KB ~0
assets total 4,177 KB 2,275 KB −1,902 KB

dist/renderer: 4,228 KB → 2,328 KB locally (reviewer measured 4,200 → 2,300 on his machine).

CLI memo: dropped. Per the review, the memoization is removed entirely (b260394) — cli.ts and cli.test.ts restore to their pre-PR shape. 23 µs → 2 µs at a multi-second poll cadence was never user-visible, and the env key hashed the CLI-path file's path, not its contents, so a newly persisted path could never displace a successful resolution (the invalidation test wrote its successor to the same path and passed with a stale memo). Drop beats re-keying.

flame.png: deleted from the repo (713 KB, no importer after the cut). Main has since deleted it too, and the cut is gone with the merge.

Verification

  • tsc --noEmit clean
  • full app/ suite green after the review fixes: 1,000 passed, 4 skipped
  • npm run build green
  • churn tests verified to fail on the pre-change code and pass after
  • deliberately out of scope: bundle code-splitting (gsap ~50 kB of the 510 kB chunk) — separate PR material

Changed

  • The desktop shell stops re-rendering the whole tree once a second. AppMain owned a wall-clock setInterval(1000) that existed only so the footer could print "refreshed Ns ago", but every tick re-rendered the sidebar, the hero, the daily chart, the heatmap and every table 60 times a minute whether or not any data had changed. The per-second tick now lives in a leaf RefreshedAt component that owns just that label, and the shell itself re-renders only on real state changes plus a 15-second day check that fires exactly when the local calendar rolls over. Renderer render-count regressions are pinned by a churn test.

Fixed

  • The renderer ships half the splash video weight. The cold-start splash video is re-encoded at VP9 CRF 30 (SSIM 0.994 against the source), taking app/renderer/assets/splash-loader.webm from 2,582,836 bytes to 1,294,904 bytes — about half.

…n, halve bundled renderer assets

The desktop app's three quiet inefficiencies, measured and fixed:

- AppMain owned a 1s wall-clock interval whose only consumer was the
  footer's 'refreshed Ns ago' label, but every tick reconciled the whole
  tree - sidebar, hero, chart, heatmap, tables - 60 times a minute. The
  tick now lives in a leaf RefreshedAt component, and the shell
  re-renders on real state plus a 15s day check that fires only when the
  local calendar rolls over (the overview memo keys bake in a
  today/month boundary, so midnight must re-render exactly once).
  A churn test pins shell render counts: base re-rendered on every
  clock tick, the branch on none.

- Every read resolved the codeburn binary from scratch: a stat sweep
  over each PATH entry plus an nvm readdir, per request, per poll.
  Resolution and the derived spawn PATH are memoized against the full
  env key set that can change the answer; a miss is never cached, so a
  CLI that appears while the app is open is still discovered, and a
  spawn error drops the memo so a deleted binary cannot pin the app to
  a dead path. 26.5us -> 2.6us per resolution on this machine, and the
  syscalls leave the steady-state path entirely.

- The brand mark rendered a 880x880 713kB PNG (3.1MB RGBA decode) into
  20-76px boxes and the splash shipped a 2.6MB VP9 clip; a 192px
  lanczos cut (verified side-by-side at all three rendered sizes) and a
  CRF 30 re-encode (SSIM 0.994) cut dist/renderer from 4.0MB to 2.2MB.
@ozymandiashh
ozymandiashh force-pushed the perf/desktop-render-clock-and-resolution-cache branch from 38b7078 to 96807e7 Compare September 15, 2026 18:44

@iamtoruk iamtoruk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measured on my machine. The render churn and the asset cut both check out: AppMain renders over 10 s idle on Overview went 10 to 0, and dist/renderer went 4,200 KB to 2,300 KB with the same 19 assets. Post the proof in the PR body so it survives: render count before and after or a CPU sample, the asset size table per file, and confirmation the "refreshed Ns ago" text still ticks every second.

On the CLI memo: 23 µs to 2 µs per resolution at a multi-second poll cadence is not a user-visible win, and the memo adds stale-path surface. The env key hashes the path of the CLI-path file, not its contents, so once resolution succeeds a newly persisted path is never picked up. The invalidation test writes the successor to the same path, so it passes with a stale memo too. Either drop the memo or key on the file contents and rewrite that test to use a different path.

Also: flame.png is still in the repo at 713 KB with no importer. Delete it.

Heads up: this conflicts with a branch of mine that rewrites the same footer line and still reads now. Whoever lands second merges by hand.

No screenshots needed, nothing visible changed.

…n the footer tick

Per review on getagentseal#1352:

- The resolution memo is gone entirely (both cli.ts and its tests restore
  to their pre-PR shape): 23us -> 2us at a multi-second poll cadence was
  never user-visible, and the env key hashed the CLI-path file's path,
  not its contents, so a newly persisted path could never displace a
  successful resolution — the invalidation test wrote its successor to
  the same path and passed with a stale memo. Drop beats re-keying.
- flame.png (713 KB) is deleted from the repo; FlameMark imports the
  192px cut and nothing else referenced it.
- The churn suite gains the counterpart pin: the 'refreshed Ns ago'
  label still ticks once a second ('just now' -> '1s ago' -> '2s ago')
  while the shell's render count stays flat.
@ozymandiashh ozymandiashh changed the title perf(app): stop the per-second shell re-render, memoize CLI resolution, halve renderer assets perf(app): stop the per-second shell re-render and halve bundled renderer assets Sep 15, 2026
@ozymandiashh

Copy link
Copy Markdown
Collaborator Author

b260394 addresses the review:

  • CLI memo dropped entirely. cli.ts and cli.test.ts restore to their pre-PR shape — you were right that the env key hashed the CLI-path file's path rather than its contents, and the test's same-path successor could never catch it. Given the win was invisible at poll cadence, drop beats re-keying.
  • flame.png deleted from the repo (713 KB, unimported).
  • Proof posted in the PR body: the per-file asset table (main vs branch, same 19 assets), the churn-test render counts including the new counterpart pin — the refreshed Ns ago label ticks just now1s ago2s ago while the shell's render count stays flat — and my local dist/renderer numbers (4,228 → 2,328 KB) alongside yours.

Noted on the footer-line conflict with your branch — happy to be second and merge by hand.

@ozymandiashh

Copy link
Copy Markdown
Collaborator Author

Heads-up: pushes to this branch after 96807e7 (b260394, c98410a) have started no workflow runs at all — gh pr checks reports none, and the actions API shows zero runs for the new heads, while my other PRs from the same fork trigger normally. If a workflow approval got dropped along the way, a re-run (or an approval of the pending ones) would be appreciated; the review-fix commit is b260394, c98410a is only an empty re-trigger.

@ozymandiashh

Copy link
Copy Markdown
Collaborator Author

Merged main (#1459, #1462) by hand in cc47e20, as agreed for the footer line:

  • Footer: your RefreshMark is kept byte for byte (fixed icon, spinner only while refreshing, sr-only status). RefreshedAt now renders it, so the 1 s tick still stays out of AppMain, and the day-rollover check still recomputes the memo keys at midnight. App.renderChurn.test.tsx passes on the merge, and so does the full app suite.
  • Flame: Desktop: measured polish pass, new frame, card system, motion, onboarding #1459 replaced the art with onboarding-flame.png, so my 192 px cut of the old art no longer applies. It's deleted (8018118), and the changelog bullet now covers only the splash re-encode. After the merge, dist/renderer goes from 3,924 KB to 2,672 KB. The PR body is updated to match.

CI runs start again on the new head, all green.

App.tsx footer: keep the leaf RefreshedAt component from this branch and
main's getagentseal#1467 loading argument, so the label still shows "refreshing…" for a
restored durable snapshot until the first real refresh lands.
@iamtoruk
iamtoruk merged commit 426fea3 into getagentseal:main Sep 17, 2026
14 checks passed
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.

2 participants