Skip to content

(sidebar): fetch projects once and skip unchanged sidebar renders - #412

Closed
paulo-jay wants to merge 1 commit into
devsuitup:mainfrom
paulo-jay:worktree-perf-sidebar-single-fetch
Closed

paulo-jay wants to merge 1 commit into
devsuitup:mainfrom
paulo-jay:worktree-perf-sidebar-single-fetch

Conversation

@paulo-jay

Copy link
Copy Markdown

Why

Measured on a live instance with 6 Claude sessions: the renderer averaged ~33 % of a core and the main process ~16 %, with spikes to 110 %.

Every JSONL append from a live session sends projects-changed (throttled to 1.5 s). On each one, the renderer:

  • called get-projects twice, once with archived sessions hidden and once with them shown. Each call read the whole session cache (SELECT * over 5k+ rows), rebuilt the project list and cloned it across IPC;
  • then rebuilt the full sidebar (~22k nodes) and ran morphdom over it, about once a second.

Change

  • One build, two views. get-projects returns { projects, allProjects } from a single build: gatherProjectInputs() does every read once, and assembleProjects(inputs, showArchived) produces each view. The renderer can't derive the archived-hidden view on its own, because a project whose sessions are all archived is listed through the empty-directory pass.
  • Skipping an unchanged render. The projects-changed reload skips the sidebar rebuild when a signature of everything the sidebar displays is unchanged, and refreshes only the time labels. modified is rounded to the minute, so a live session that only appends costs one render a minute instead of one a second.
  • Every other caller still renders unconditionally.

Known trade-offs

  • Computing the signature costs ~30–40 ms for ~5.4k sessions, measured in jsdom. That is well below the render it avoids.
  • While renders are skipped, the slug-group header time and the status age text can lag by up to a minute.
  • Renderer state the sidebar reads has to be listed in sidebarRenderSignature by hand.

The rationale is in .ai/contexts/session-cache.md ("Sidebar refresh cost"), and ipc-bridge.md is updated.

Tests

test/sidebar-refresh-single-fetch.test.js (10 tests) checks:

  • one fetch per loadProjects;
  • that the shared build matches the two separate builds, including an all-archived project, an archived parent's subagent and an empty directory;
  • a skip when nothing displayed changed, and a re-render for each displayed field that changes.

test/get-projects-cold-start-reconcile.test.js is adapted to the new shape. task check passes.

Not yet verified live with task test-pr.

Every JSONL append from a live session made the renderer call get-projects twice (archived hidden and shown), each one reading the whole session cache, and then rebuild the full sidebar about once a second. get-projects now returns both views from one build, and the projects-changed reload skips the sidebar rebuild when nothing it displays has changed, refreshing only the time labels.
@devsuitup

Copy link
Copy Markdown
Owner

Reviewing e313f7f (adversarial review in progress).

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Adversarial review at e313f7f.

I could not break the core of this one. The only get-projects consumer is loadProjects in public/app.js, both on this branch and on current main, so the new { projects, allProjects } shape breaks nothing. The archived view matches the old per-flag build, including an all-archived project, which survives through the empty-dir pass. The two views share session objects inside one structured-clone message, so the payload is not doubled. I traced the skip signature against the render code: busy, attention and response-ready come through the localPtyStates snapshot, remote host status through the project fields, and filters, search and the pty sets through the state blob. Local run of 10 related test files: 49/49 pass. CI has not run. Changes requested, mostly hygiene.

Must fix

  1. Rebase onto main. merge-tree reports three text conflicts:
    • the main.js destructure at ~473, where main adds isIndexingFinished; keep it next to buildProjectViewsFromCache;
    • the get-indexing-state handler that main adds just above get-projects (~1053);
    • preload.js:12, where main adds getIndexingState.
      public/app.js, public/sidebar.js and session-cache.js also changed on main and auto-merge, so re-run the tests on the merged tree.
  2. CHANGELOG or no-changelog. One or the other is required. The PR states one visible side effect: the group-header time and the status-age text can lag by up to a minute while renders are skipped. I would give that a one-line entry. If you think it does not deserve one, say so and the maintainer will add the label, since your account cannot.

Should fix

  1. A skipped item is never repaired after a rename. While a .session-rename-input is open, onBeforeElUpdated keeps that item from being updated (sidebar.js ~1046). renderProjects still stores lastSidebarRenderSignature as if the item had been rendered (~1109).
    • Scenario: rename is open on session X, X's title or modified changes, then the user cancels. X shows the old label until the next real data change. Before this PR, the next render about a second later fixed it.
    • Fix: leave the signature null when an item was skipped for an open rename input.
  2. The signature is a hand-maintained list of what buildSessionItem and buildSubagentItem read. It covers today's inputs. A future input that is not added there leaves the sidebar silently stale on a projects-changed reload. Three PRs queued ahead each add sidebar state: #395 (remote attention), #400 (setSessionMcpState) and #374 (the bg badge). Whichever lands second has to extend the signature. List the contract in .ai/contexts/session-cache.md. A test that fails when a builder reads state the signature ignores would be better still.

Nits

  1. Stale comments still describe the two-call behaviour: session-cache.js ~360 and ~632, and test/build-projects-cold-scan-fallback.test.js ~87.
  2. The signature is computed twice on every render that runs, which adds cost rather than saving it. The 30–40 ms for ~5.4k sessions was measured in jsdom; a quick live check with task test-pr before merge would confirm it. annotateRemoteAttachable and mergePlaceholderSessions still run once per view: redundant, but harmless.

@paulo-jay

Copy link
Copy Markdown
Author

Closing for now: this change will be folded into a single perf PR with the other optimizations, opened once tested live.

@paulo-jay paulo-jay closed this Oct 2, 2026
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