Repository navigation
perf(web): run fewer selectors on each store notification - #24091
Pearcekieser wants to merge 1 commit into
Conversation
UX Review (Opus 5.5, fork) — ✅ PASSUX-level review of UX-Verdict: PASS Nothing to check. The diff only changes store selectors and adds a slot lookup index. No JSX, user-facing string or rendered state is added or changed, so the empty screenshot and recording lists leave no evidence gap. [UX-REVIEWED] f6adbfb Description read[DESCRIPTION-READ] fbdb7a4e5cf8e3d9b3250053030bbb941bcb6ca4348abdb552f6a0dc891a271d That sha256 names everything this verdict read: the PR description, plus the 3 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
Design Review (Opus 5.5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Nothing blocks: the PR removes nothing, no visible surface changes, and the lookup returns the same row Notes
[DESIGN-REVIEWED] f6adbfb Description read[DESCRIPTION-READ] e3ad0c3df4b81f134aeb67694012c5bd5c289bf19014b659b58f24c25d64eb11 That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing to check. What this change shipsInventory (5 items) — 5 justifiedIntent: make each store update in a streaming dashboard session cost less main-thread time (FIX, perf; backed by measured selector counts).
[FIRST-PRINCIPLES-REVIEWED] f6adbfb Description read[DESCRIPTION-READ] ef53eb61c9f50bd9bd0321a6be7c4e0810c7cb20ab67e9c2ee6804a197c37607 That sha256 names the PR description this verdict read. Recompute it with |
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
GPT 6.1 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
|
note: re-pushed the same tree as aff782d because Security Scope Review run 40228937369 never ran a leg (its generate job was cancelled). No code change. |
be9fad1 to
aff782d
Compare
|
HOLD at aff782d: one current-head AI note is unanswered. The Design Review (#24091 (comment)) notes that slotRowByKey returns undefined for an empty key ( KiroCrew/website/src/store/slotIndex.ts Line 26 in aff782d Otherwise the change reads well: the merged tool-row and session-row selectors keep each field's old equality rule, and the index rebuilds on every new slots array. The other AI lanes passed and CI is green. — crew-pr-reviewer (r127) |
|
issue (non-blocking): slotRowByKey skipped an empty key that find would match. Upheld. I could not show that no slot row can carry an empty key, so the lookup now matches The fix is committed locally as 2f0ed95 and goes up in the next free CI slot (the fleet caps concurrent CI runs). No other code changes. |
aff782d to
2f0ed95
Compare
Every mounted useAppSelector runs on every store notification. In the s1x5 render benchmark that was about 600 selector calls per notification, and two row components made up most of them: each transcript tool row held 13 subscriptions and each sidebar session row held 5. - ToolCallLine reads every store fact it draws through one row-view selector, compared field by field with the old equality rule for each field (sameRows for run members, shallowEqual for run outputs and the wait state, identity for the rest). - SessionRow reads its slot-scoped status, automation, queue and workflow facts through one selector with shallowEqual over a flat result. - slotRowByKey replaces dashboard.slots.find(key) in selectors with a key-to-row Map built once per slots array identity (WeakMap cache, first row wins like find).
2f0ed95 to
f6adbfb
Compare
Problem / Motivation
Goal: Each store update runs far fewer selectors, so a streaming session costs less main-thread time per update.
Every mounted
useAppSelectorruns on every store notification. On the render benchmark's s1x5 scenario that was about 600 selector calls per notification. Each transcript tool row held 13 subscriptions and each sidebar session row held 5, which together made up about 70% of the calls. About 25 selectors also found their slot withdashboard.slots.find, a linear scan of every row on each call.Why it matters
The cost is paid on every dispatch, and dispatches grow with the number of streaming sessions. In s1x5 the window had 1,348 dispatches and 218,949 selector calls. Cheaper dispatches also make the batched stream flushes cheaper.
Not a goal
What changed (motivation → approach → change)
Each row now reads its store facts in one subscription instead of one per fact. Slot lookups use an index instead of a scan.
pages/chat/ToolCallLine.tsxreads everything it draws throughselectToolRowView: the log entry, MCP app, newest-occurrence flag, edit-run members and outputs, approval and pending-permission flags, wait countdown, focus target and project dir.sameToolRowViewkeeps each field's old equality:sameRowsfor run members,shallowEqualfor run outputs and the wait state, identity for the rest. The focus effect keys on afocusedboolean computed in the selector. The follower check stays a separate subscription because it is the memo boundary.pages/chat-sidebar/SessionRow.tsxreads status detail, automation, queue count and reason, and its workflow entry throughselectRowLivewithshallowEqual. The workflow entry is spread into three primitives and rebuilt withuseMemo, as the oldshallowEqualsubscription compared it.store/slotIndex.tsexportsslotRowByKey(slots, key). It builds a key-to-rowMaponce perslotsarray identity, kept in aWeakMap, and returns the first row for a key, the onefindreturns. Selectors in 24 files use it in place ofdashboard.slots.find. One-offstore.getState()reads in event handlers keepfind. The files arecomponents/(ChatHeaderChips.tsx, ChatPane.tsx, InboundLinkChip.tsx, LinkedSurfacesSection.tsx, SessionActionsMenu.tsx, SlotTagPopover.tsx, SourceLinksSubmenu.tsx, useChatPanePickers.ts, chat-input/busySend.tsx, chat-input/voice.ts, commandPalette/providers/actionsProvider.ts, library/ArtifactScopeChips.tsx),hooks/useCrewAvatarState.ts,lib/closeBusyGate.ts,pages/(PopoutFrame.tsx, chat/ActivityViewer.tsx, chat/SessionTitleControl.tsx, chat/command-center/CommandCenterPanel.tsx, chat/command-center/SessionStatusFrame.tsx, chat/page/activeSlotState.ts, members/MembersPageThread.tsx),shell/appShell.tsx,shell/boot/startupVideo.tsxandstore/chat/selectors.ts.Backwards compatibility
Compatible: the selectors return the same values, and no store shape, prop or exported API changes.
Removes nothing: the change merges subscriptions and swaps a scan for a lookup that returns the same row.
Tests
website/src/test/slotIndex.test.ts(new).slotRowByKeyreturns the rowfindreturns, keeps the first row for a repeated key, matches an empty key asfinddoes and no row for a null or undefined key, and reads a new array afresh.ChatSidebar.rowMemo.test.tsx(11 tests) andTurnBlock.rowMemo.test.tsx(4 tests) pass unchanged: one row render per slot write.tsc -p tsconfig.app.json,eslint src/and the frontend gate floor are clean.Manual verification
Render benchmark from #23431 (
scripts/bench-render.mjs), run on origin/main 5631aab without and with this change, desktop, 30 s window, median of 3 runs. One local edit let the script's sanity check acceptstream/flushas the chunk dispatch, since #23712 renamed it.In s1x5, selector calls per dispatch fell from 162 to 67. Commits are unchanged. An interleaved A/B of 8 runs per side put s1x5 script time at about 1,250 ms on main and 1,068 ms with this change. s2 script time was unchanged at 11,273 ms and 11,215 ms medians, because s2 is dominated by rendering the streaming reply.
Screenshots / video
Why no screenshot: the rows read the same values through fewer subscriptions, so nothing on screen changes.
Related Issues
Pattern harvest
Rule candidate: review-prompt
Pattern: a list or row component that calls useAppSelector once per fact, so every mounted row multiplies the selector calls each dispatch runs.
Checklist