Skip to content

Commit 3e9c0d4

Browse files
arul28claude
andauthored
perf: end-to-end optimization pass across desktop main + renderer (#219)
* perf: end-to-end optimization pass across desktop main + renderer Profile-driven cleanup of CPU/memory/IPC/render hot paths uncovered while running the Electron app under stress. No product behavior changes — just removing waste and tightening contracts. Main process - aiIntegrationService: trim redundant work and tighten auth detection paths. - authDetector: cache + dedupe lookups; expanded test coverage. - ipc/registerIpc: validation + redaction hardening on the IPC surface. - ptyService: terminal session lifecycle fixes (stale-id handling, prune). - rebaseSuggestionService: cache TTL/LRU + rate-bucket prune. - projectIconResolver: fewer disk hits, deterministic fallbacks. - syncService, memory/embedding services: drop redundant work, fix worker shutdown, add coverage. - cursorModelsDiscovery / droidModelsDiscovery: shared discovery cleanup. Preload / renderer - preload: large reorganization of the IPC bridge surface (~+800 lines). - AppShell, TopBar, TabNav, ChatGitToolbar, AgentChatPane, AgentChatComposer, LanesPage, WorkspaceGraphPage, TerminalView, PaneTilingLayout: avoid avoidable re-renders, polling, log spam. - appStore: state slice cleanup + tests. - terminalAttention: lifecycle correctness. Tooling - Add .claude/commands/optimize.md (the /optimize skill used to drive this pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ship: iter 1 — fix CI failures + address CodeRabbit review CI fixes (test-ade-cli, test-desktop shards 1 + 3) - AgentChatComposer: revert BorderBeam wrapper conditional that was unmounting/remounting the composer subtree on turnActive flip, swallowing onChange events. Always render the wrapper, drive with active prop. (test-desktop (1) submit-recovery, 2 tests) - memoryService.evaluateWriteGate: re-add `tier IN (1,2)` to dedupe scan. The optimization had let candidate (tier-3) episodes dedupe against each other, which broke procedural promotion when 3 distinct episodes shared signals. Tier-2 promoted writes still fall under the filter, so the policy change for `resolveAgentMemoryWritePolicy` remains intact. (test-desktop (3) proceduralLearningService, 3 tests) - adeRpcServer.test: update one assertion to match the intentional new promoted/tier-2 contract; load-bearing scopeOwner/runId-from-env assertions unchanged. (test-ade-cli, 1 test) Review fixes (13/14 CodeRabbit comments — preserves optimization intent) - aiIntegrationService: shallowCliAuth keys off !shouldProbeCliModels so refreshOpenCodeInventory runs a full auth pass. - registerIpc.timePhase: takes a thunk + Promise.resolve().then.catch so synchronous throws degrade to []/null fallbacks. - autoRebaseService: hasAuthoritativeLaneSet guard prevents wiping persisted statuses for lanes outside a caller-supplied subset. - rebaseSuggestionService: normalize primary lane refs before origin/<branch> lookup; scope cache + in-flight promise to default request shape only (force/lanes/refreshRemoteTracking bypass shared cache). - knowledgeCaptureService: low-signal courtesy regex now also requires !hasDurablePrFeedbackSignals && wordCount<10, preserving "Thanks but always X..." style actionable guidance. - projectIconResolver: drop negative-icon caching (no invalidation signal exists for added icon files). - ptyService.onData: early return on entry.disposed so late chunks cannot mutate post-teardown state. - syncService: force=true bypasses cache only, not in-flight dedupe. - preload.createKeyedShortIpcCache: bounded LRU-ish cap (256 entries, touch-on-access) for high-cardinality keys. - preload IPC fanout: per-callback try/catch so one throwing subscriber does not starve later subscribers. - preload agentChatEventFanout: beforeDispatch clears agentChatSummaryCache so background events invalidate the 1s cache. - WorkspaceGraphPage.refreshGraphLanes: includeStatus=true (skip conflict/rebase phases instead) so dirty/behind chips stay current without reverting to a full snapshot. Dismissed (1/14) - authDetector.ts skip-path "skipped probes shouldn't claim authenticated": false positive. The `authenticated:true, verified:false` shape is an intentional presence sentinel pinned by the colocated test (authDetector.test.ts:144-150); combined with the CLI-probe full-auth fix above, the only consumer treats it as a fast-path hint, not a verified credential. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ship: iter 2 — guard createShortIpcCache .finally against force-call races capy-ai caught a real concurrency bug introduced by the optimization pass: the generic createShortIpcCache helper unconditionally cleared its shared `promise` ref in .finally(), so when a `force: true` call overwrote `promise` mid-flight, the older request's settle would null out the new request. A subsequent non-force caller then saw `promise === null` and started a redundant IPC instead of reusing the in-flight forced load. Fix mirrors the pattern already used by the bespoke aiStatusCache 30 lines below: capture the request reference, only null `promise` if it still equals our request. Addresses capy-ai comment 3166962638. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent da32d09 commit 3e9c0d4

45 files changed

Lines changed: 2526 additions & 576 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/optimize.md

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
---
2+
name: optimize
3+
description: Profile a large ADE feature end-to-end, add temporary or permanent telemetry, run the Electron app, find real CPU/memory/IPC/render hot paths, fix them, and verify with stress tests.
4+
---
5+
6+
# /optimize — Performance Steward
7+
8+
You are the performance steward for ADE. Use this after a large feature lands, when the app works but might be too heavy for normal laptops.
9+
10+
**Argument:** `$ARGUMENTS` — optional feature or surface hint, for example `/optimize Work and Lanes`, `/optimize iOS simulator drawer`, or `/optimize graph route`.
11+
12+
Your job is not to make the app feel stripped down. Preserve the product's intent and interaction quality while removing waste, polling, avoidable rendering, memory spikes, runaway logs, redundant IPC, and expensive cold-start behavior.
13+
14+
---
15+
16+
## Operating mode
17+
18+
Run autonomously. Do not stop at a plan. Read, instrument, run, measure, fix, and verify. Ask the user only if a required action would spend money, use an expensive model, destroy data, or cannot be inferred from repo context.
19+
20+
Be concrete. Every optimization should be backed by at least one of:
21+
22+
- A live log finding.
23+
- Process CPU/RSS/GPU evidence.
24+
- A renderer/DOM/animation finding.
25+
- A repeated IPC or polling pattern.
26+
- A testable code path that clearly does unnecessary work.
27+
28+
Do not make speculative cleanup the main result. If you cannot reproduce a suspected issue, leave a short note and move to the next measurable surface.
29+
30+
---
31+
32+
## Phase 0: Understand the surface
33+
34+
1. Read the relevant docs before editing:
35+
- `AGENTS.md` or the prompt-provided project instructions.
36+
- `docs/ARCHITECTURE.md` if the change crosses app/service boundaries.
37+
- Feature docs under `docs/features/**` that match `$ARGUMENTS`.
38+
- Existing playbooks if the surface involves PRs, lanes, missions, computer use, sync, or release.
39+
40+
2. Inspect the changed surface:
41+
- `git status --short`
42+
- `git diff --stat`
43+
- `git diff -- <relevant files>`
44+
- `rg` for the feature's IPC channels, services, hooks, intervals, observers, animations, and route components.
45+
46+
3. Identify the likely hot paths:
47+
- Renderer route mount and tab switches.
48+
- Work/Lanes session lists and terminal panes.
49+
- IPC polling and fan-out calls.
50+
- Main-process services doing filesystem, git, SQLite, model discovery, sync, or embedding work.
51+
- Hidden drawers or panes that still run effects while closed.
52+
- Infinite CSS animations, WebGL/canvas surfaces, resize loops, observers, and timers.
53+
- Startup, project switch, and route navigation cold paths.
54+
55+
Keep a working list of surfaces to test. Prefer a small number of realistic flows over broad shallow poking.
56+
57+
---
58+
59+
## Phase 1: Establish observability
60+
61+
Before making performance edits, make sure the app can tell you what is happening.
62+
63+
1. Look for existing instrumentation:
64+
- IPC begin/done/summary logs.
65+
- Route change logs.
66+
- Service phase summaries.
67+
- PTY/session output summaries.
68+
- Renderer debug logs.
69+
- Cache hit/miss or model discovery summaries.
70+
71+
2. If logs are missing, add narrow instrumentation first:
72+
- For IPC handlers: log channel, duration, slow count, failure count, and top callers when available.
73+
- For expensive service methods: log phase timings, input size/counts, cache hits, and result counts.
74+
- For terminal/session output: log chunks, batches, bytes/chars, listener count, active session count.
75+
- For renderer effects: log route mount/unmount or ready state only when the structural signature changes, not every render.
76+
77+
3. Instrumentation rules:
78+
- Summaries beat per-item spam.
79+
- Redact user prompts, secrets, command input, tokens, and file contents.
80+
- Add logs behind existing logger/debug patterns.
81+
- Avoid permanent noisy logs. If a log is only useful during this run, remove it before finishing or gate it behind an existing dev/debug flag.
82+
83+
---
84+
85+
## Phase 2: Run the app and attach to the real Electron surface
86+
87+
Use the local desktop app as the source of truth.
88+
89+
1. Start the dev app from `apps/desktop`:
90+
91+
```bash
92+
npm run dev
93+
```
94+
95+
2. Keep the dev terminal visible. Watch for:
96+
- `dev launcher using http://localhost:5173`
97+
- `DevTools listening on ws://127.0.0.1:9222`
98+
- `window.loading_url`
99+
- `renderer.route_change`
100+
- `ipc.invoke.summary`
101+
- Feature-specific summary logs.
102+
103+
3. Attach to Electron, not Safari:
104+
- Prefer the `Electron` app entry when using computer-use.
105+
- Confirm the window URL contains `localhost:5173`.
106+
- If DevTools is the focused target, switch to the ADE page before evaluating DOM or interacting.
107+
108+
4. If using CDP/agent-browser, target the ADE tab:
109+
110+
```bash
111+
agent-browser --cdp 9222 tab
112+
agent-browser --cdp 9222 tab <ADE-tab-index>
113+
```
114+
115+
5. Collect baseline process data:
116+
117+
```bash
118+
pgrep -fl "Electron . --remote-debugging-port=9222|Electron Helper|vite --port 5173|tsup --watch|esbuild --service"
119+
ps -axo pid,ppid,%cpu,%mem,rss,comm,args
120+
```
121+
122+
Use process names carefully:
123+
- Main Electron process: app services, SQLite, IPC handlers.
124+
- Renderer helper: React route work, DOM rendering, terminal rendering.
125+
- GPU helper: WebGL/canvas/compositing/animation pressure.
126+
- Network utility: fetch/WebSocket behavior.
127+
128+
Close extra DevTools targets before trusting memory numbers. DevTools can inflate RSS and CPU.
129+
130+
---
131+
132+
## Phase 3: Navigate and profile the feature
133+
134+
Run realistic flows with logs open.
135+
136+
1. Sweep relevant tabs/routes:
137+
- Work
138+
- Lanes
139+
- Files
140+
- Run
141+
- Graph
142+
- PRs
143+
- Review
144+
- History
145+
- Automations
146+
- Missions
147+
- Settings
148+
149+
If `$ARGUMENTS` names a surface, spend most time there but still check adjacent routes that stay mounted or subscribe to the same data.
150+
151+
2. For each route, record:
152+
- Cold navigation IPC summary.
153+
- Idle IPC summary after 10-20 seconds.
154+
- Main/renderer/GPU CPU and RSS.
155+
- `document.getAnimations({ subtree: true })`.
156+
- Number of canvases/WebGL/xterm instances if relevant.
157+
- Obvious repeated logs, repeated effects, or repeated identical IPC calls.
158+
159+
Useful renderer probes:
160+
161+
```js
162+
JSON.stringify({
163+
href: location.href,
164+
hidden: document.hidden,
165+
visibility: document.visibilityState,
166+
animations: document.getAnimations({ subtree: true }).map((a) => ({
167+
state: a.playState,
168+
tag: a.effect?.target?.tagName,
169+
cls: String(a.effect?.target?.className).slice(0, 160),
170+
text: a.effect?.target?.textContent?.slice(0, 80),
171+
})),
172+
xterms: document.querySelectorAll(".xterm").length,
173+
xtermCanvases: document.querySelectorAll(".xterm canvas").length,
174+
canvases: document.querySelectorAll("canvas").length,
175+
})
176+
```
177+
178+
3. Watch for these patterns:
179+
- Same IPC call every second or every render.
180+
- Multiple identical IPC calls during mount.
181+
- A route refreshing full decorated snapshots when it only needs counts.
182+
- Hidden panels polling, probing devices, or reading files.
183+
- Model discovery or provider probing blocking composer open.
184+
- Large transcript reads on route mount.
185+
- Fit/resize loops in terminals.
186+
- Infinite status animations keeping GPU/compositor awake.
187+
- WebGL/canvas rendering where DOM/static rendering is enough.
188+
- Cache misses for data that changes rarely, like project icons, auth status, model inventories, or GitHub status.
189+
190+
---
191+
192+
## Phase 4: Stress the real workflow
193+
194+
For ADE, always stress Work and Lanes unless the feature is completely unrelated. Most users live there.
195+
196+
1. Work tab stress:
197+
- Open or reuse a lane.
198+
- Create a shell session and run a bounded output stream.
199+
- Keep the terminal visible so renderer cost is real.
200+
201+
Example shell stress:
202+
203+
```bash
204+
node -e 'let i=0; const t=setInterval(()=>{process.stdout.write("ade-stress "+(++i)+" abcdefghijklmnopqrstuvwxyz0123456789\n"); if(i>=3000){clearInterval(t); process.exit(0)}},2)'
205+
```
206+
207+
2. Lanes tab stress:
208+
- Navigate to Lanes while a session is running or immediately after heavy terminal output.
209+
- Observe whether Lanes does full status snapshots, rebase suggestions, git/diff reads, or presence updates repeatedly.
210+
- Confirm idle logs calm down.
211+
212+
3. Chat stress:
213+
- Prefer a cheap model only: Haiku, a mini Codex/OpenAI model, or the cheapest available local/dev model.
214+
- Do not use expensive models for performance testing.
215+
- If cheap model availability cannot be confirmed, use shell/session stress instead and note why.
216+
- Start multiple chats only when the user explicitly asked for multi-agent load or the feature depends on parallel chats.
217+
218+
4. Computer-use/iOS/simulator stress:
219+
- Only stress if the feature touches those panels.
220+
- Closed drawers should not probe devices, fetch previews, or run screenshot loops.
221+
- Open drawer, measure, close drawer, measure again.
222+
223+
5. Memory checks:
224+
- Use `ps`/Activity Monitor-style process sampling repeatedly.
225+
- On macOS, `vmmap <pid> -summary` can help explain big RSS spikes.
226+
- Distinguish DevTools memory from actual app memory by closing DevTools targets and rechecking.
227+
228+
6. Cleanup after stress:
229+
- Stop or dispose test PTYs/sessions you created.
230+
- Do not kill user-created sessions unless they are clearly from the test.
231+
- Stop the dev server before finishing unless the user asked to keep it running.
232+
233+
---
234+
235+
## Phase 5: Fix the highest-impact causes
236+
237+
Prefer fixes in this order:
238+
239+
1. Remove runaway work:
240+
- Stop polling when hidden, closed, or unfocused.
241+
- Deduplicate identical in-flight calls.
242+
- Debounce or throttle high-frequency refresh.
243+
- Narrow full refreshes to runtime-only or count-only queries when possible.
244+
245+
2. Reduce IPC and main-process load:
246+
- Cache cold data with clear invalidation.
247+
- Coalesce event streams, especially PTY data.
248+
- Avoid repeated resize/write/status no-ops.
249+
- Add service-level phase summaries so future regressions are visible.
250+
251+
3. Reduce renderer and GPU load:
252+
- Remove infinite decorative/status animations in persistent chrome.
253+
- Make expensive renderers opt-in when the default can be cheaper.
254+
- Do not mount hidden heavy panels if they can lazy-mount.
255+
- Avoid re-render logs/effects that depend on unstable object identities.
256+
- Virtualize large lists or cap expensive previews when needed.
257+
258+
4. Reduce memory pressure:
259+
- Lazy-load embedding/model/device work.
260+
- Bound caches and transcripts.
261+
- Avoid retaining full snapshots or logs in renderer state when summaries are enough.
262+
- Reuse cached project/model/provider metadata with invalidation.
263+
264+
5. Preserve UX:
265+
- Keep controls responsive.
266+
- Keep clear status feedback, but use static state where animation is not essential.
267+
- Do not remove core functionality to make numbers look better.
268+
- If an expensive feature is valuable, make it lazy, cached, or opt-in.
269+
270+
---
271+
272+
## Phase 6: Verify
273+
274+
Run the smallest meaningful checks first, then broaden.
275+
276+
Desktop checks to choose from:
277+
278+
```bash
279+
npm --prefix apps/desktop run typecheck
280+
npm --prefix apps/desktop run test -- <targeted test files>
281+
npm --prefix apps/desktop run test
282+
npm --prefix apps/desktop run build
283+
npm --prefix apps/desktop run lint
284+
```
285+
286+
For IPC/preload/type changes, verify all synced surfaces:
287+
- main handler
288+
- shared IPC/type
289+
- preload exposure
290+
- renderer caller
291+
- tests/mocks
292+
293+
For renderer performance changes:
294+
- Re-run the route sweep.
295+
- Re-run Work/Lanes stress if touched.
296+
- Confirm `document.getAnimations()` does not show persistent unnecessary animations.
297+
- Confirm process CPU returns near idle after stress.
298+
- Confirm logs do not show repeated full refreshes or identical calls.
299+
300+
For terminal changes:
301+
- Verify output is not lost on fast exit.
302+
- Verify output streams while visible.
303+
- Verify resize still works.
304+
- Verify cleanup/dispose flushes pending data.
305+
306+
For cache changes:
307+
- Verify cold call and warm call behavior.
308+
- Verify invalidation when the underlying file/config/state changes.
309+
- Keep cache bounded.
310+
311+
---
312+
313+
## Final report
314+
315+
End with a concise report:
316+
317+
1. Surfaces tested.
318+
2. Hot paths found, with concrete measurements or log evidence.
319+
3. Fixes made.
320+
4. Before/after observations.
321+
5. Validation commands and results.
322+
6. Residual risks or future optimization targets.
323+
7. Cleanup performed, including whether the dev server is stopped.
324+
325+
If you found a suspected issue but did not fix it, say exactly why: not reproducible, too risky, needs product decision, or requires credentials/model spend.

apps/ade-cli/src/adeRpcServer.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2657,9 +2657,9 @@ describe("adeRpcServer", () => {
26572657
expect.objectContaining({
26582658
scope: "mission",
26592659
scopeOwnerId: "run-from-env",
2660-
status: "candidate",
2661-
tier: 3,
2662-
confidence: 0.6,
2660+
status: "promoted",
2661+
tier: 2,
2662+
confidence: 1,
26632663
})
26642664
);
26652665
expect(fixture.runtime.memoryService.addSharedFact).toHaveBeenCalledWith(

apps/desktop/src/main/main.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ import { createBatchConsolidationService } from "./services/memory/batchConsolid
9999
import { createEmbeddingService } from "./services/memory/embeddingService";
100100
import { createEmbeddingWorkerService } from "./services/memory/embeddingWorkerService";
101101
import { createHybridSearchService } from "./services/memory/hybridSearchService";
102-
import { createMemoryService } from "./services/memory/memoryService";
102+
import { createMemoryService, type Memory } from "./services/memory/memoryService";
103103
import { createProjectMemoryFilesService } from "./services/memory/memoryFilesService";
104104
import { createMemoryLifecycleService } from "./services/memory/memoryLifecycleService";
105105
import { createMemoryBriefingService } from "./services/memory/memoryBriefingService";
@@ -232,6 +232,10 @@ function isBackgroundTaskEnabled(enableFlag?: string): boolean {
232232
);
233233
}
234234

235+
function shouldEmbedMemory(memory: Pick<Memory, "status" | "pinned">): boolean {
236+
return memory.status === "promoted" || memory.pinned === true;
237+
}
238+
235239
const episodicSummaryEnabled = isBackgroundTaskEnabled(
236240
"ADE_ENABLE_EPISODIC_SUMMARY",
237241
);
@@ -1603,17 +1607,6 @@ app.whenReady().then(async () => {
16031607
onEvent: (event) =>
16041608
emitProjectEvent(projectRoot, IPC.lanesRebaseSuggestionsEvent, event),
16051609
});
1606-
// Prime suggestions once on init so the UI can show them without waiting for a head change.
1607-
void rebaseSuggestionService
1608-
.listSuggestions()
1609-
.then((suggestions) =>
1610-
emitProjectEvent(projectRoot, IPC.lanesRebaseSuggestionsEvent, {
1611-
type: "rebase-suggestions-updated",
1612-
computedAt: new Date().toISOString(),
1613-
suggestions,
1614-
}),
1615-
)
1616-
.catch(() => {});
16171610

16181611
const githubService = createGithubService({
16191612
logger,
@@ -2061,7 +2054,7 @@ app.whenReady().then(async () => {
20612054
debouncedSyncMemoryDocs();
20622055
},
20632056
onMemoryUpserted: (event) => {
2064-
if (event.created || event.contentChanged) {
2057+
if ((event.created || event.contentChanged) && shouldEmbedMemory(event.memory)) {
20652058
embeddingWorkerServiceRef?.queueMemory(event.memory.id);
20662059
}
20672060
},
@@ -2082,7 +2075,10 @@ app.whenReady().then(async () => {
20822075
onStatus: (event) =>
20832076
emitProjectEvent(projectRoot, IPC.memoryConsolidationStatus, event),
20842077
onMemoryInserted: (memoryId) => {
2085-
embeddingWorkerServiceRef?.queueMemory(memoryId);
2078+
const memory = memoryService.getMemory(memoryId);
2079+
if (memory && shouldEmbedMemory(memory)) {
2080+
embeddingWorkerServiceRef?.queueMemory(memoryId);
2081+
}
20862082
},
20872083
});
20882084
batchConsolidationServiceRef = batchConsolidationService;

0 commit comments

Comments
 (0)