Skip to content

Commit dde9276

Browse files
authored
perf(renderer): optimize long sessions and add event log
Merge renderer long-session guards, Cowork event log storage, and performance validation docs.
1 parent d532bea commit dde9276

12 files changed

Lines changed: 1051 additions & 119 deletions
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Phase 7 Performance Final Validation
2+
3+
Date: 2026-06-02
4+
5+
Branch: `codex/perf-phase-4-5-7-renderer-eventlog-final`
6+
7+
## Scope
8+
9+
This report closes the local Phase 7 review for the first performance optimization track. It validates the local implementation state for Phase 0 through Phase 7, records remaining gaps, and decides whether DB or heavy event processing must move to a worker in this stage.
10+
11+
## Local Phase Status
12+
13+
| Phase | Local status | Commit(s) | Notes |
14+
|---|---|---|---|
15+
| Phase 0 - Metrics baseline | Completed | `7e412c0`, `dfd9c39` | Startup, IPC, DB, settings-capable metrics and log export snapshot. |
16+
| Phase 1 - Startup staging | Completed | `56ce52f` | T0/T1/T2 split and background services. |
17+
| Phase 2 - IPC subscribe and coalescing | Completed | `ad7fe54`, `fdc6c71`, `abe4496` | Session subscriptions and stream message coalescing. |
18+
| Phase 3 - DB split and batching | Completed | `070adda`, `60569c3` | Session meta/recent message split, paging, indexes, config transaction. |
19+
| Phase 4 - Renderer heavy rendering | Completed | `1b6b61b` | Long content, long code, tool logs, and large diffs are deferred or collapsed. |
20+
| Phase 5 - Event Log MVP | Completed | `395cd65` | Append-only `cowork_events`, replay helpers, event timeline export summary. |
21+
| Phase 6 - Settings performance | Completed locally | `b8198e5`, `a9d8cae` | Settings lazy diagnostics and slow settings IPC channel attribution. |
22+
| Phase 7 - Final validation | Completed locally | this commit | This report. |
23+
24+
## Verification Run Locally
25+
26+
- `npm test -- renderingGuards`
27+
- `npm test -- coworkEventStore`
28+
- `npm run compile:electron`
29+
- `npm run build`
30+
- Targeted ESLint checks for Phase 4 and Phase 5 changed files
31+
32+
Results:
33+
34+
- Renderer guard tests passed.
35+
- Event log store tests passed.
36+
- Electron main/preload TypeScript compilation passed.
37+
- Production build passed.
38+
- Targeted ESLint checks had no new errors. Existing warnings in large legacy files remain outside this phase.
39+
40+
## Requirement Alignment
41+
42+
Startup:
43+
44+
- Phase 0 added timing capture.
45+
- Phase 1 split foreground startup from background services.
46+
- Remaining validation: measure cold startup and first interactive time after all phase branches are merged into the same base.
47+
48+
IPC and streaming:
49+
50+
- Phase 0 records IPC event rate and payload size.
51+
- Phase 2 adds session subscriptions and coalesces message updates.
52+
- Remaining validation: run a high-frequency stream fixture and confirm per-session IPC rate and payload size in `performance-snapshot.json`.
53+
54+
Database:
55+
56+
- Phase 0 records slow DB operations.
57+
- Phase 3 splits full session loading into meta and paged messages, adds indexes, and batches config saves.
58+
- Phase 5 adds append-only events without switching UI loading to event replay.
59+
- Remaining validation: confirm no main-process DB operation over 100ms during long-session open and streaming fixture runs.
60+
61+
Renderer:
62+
63+
- Phase 4 defers long markdown, long code blocks, large diffs, and long tool output.
64+
- Full list virtualization was not implemented in Phase 4 because recent-window loading plus heavy-content deferral is lower risk and covers the primary blocker first.
65+
- Remaining validation: run the 1000-message fixture and long diff fixture in `electron:dev`.
66+
67+
Settings:
68+
69+
- Phase 6 adds settings performance attribution and reduces eager loading.
70+
- Remaining validation: measure settings open and first tab load timings after merge, especially with OpenClaw missing.
71+
72+
Event log:
73+
74+
- Phase 5 adds append-only event storage, idempotent source event IDs, replay helpers, and a redacted timeline summary in log export.
75+
- UI still reads the existing message view by default, preserving old-session compatibility.
76+
77+
## Deviations
78+
79+
- Full message-list virtualization is not implemented. This is acceptable for the local Phase 4 scope because the current session already uses paged recent messages and the heavy-rendering blockers are now deferred.
80+
- Mermaid-specific viewport lazy rendering is not implemented separately. It remains covered indirectly by long markdown collapse and should move to P1 only if fixture testing still shows Mermaid-specific stalls.
81+
- Permission and runtime metric event types exist in the Event Log API, but not every runtime adapter emits every event type yet. This keeps Phase 5 low risk and leaves broader telemetry wiring for follow-up work.
82+
- The remote PR workflow is not complete in this local branch: per-phase PR creation, six-minute review polling, merging, and pulling latest `main` still need to happen.
83+
- Final numeric P0 performance targets cannot be marked globally complete until all phase branches are merged and measured in one unified codebase.
84+
85+
## Workerization Decision
86+
87+
Decision: do not move DB or event reduction to a worker in this first stage.
88+
89+
Rationale:
90+
91+
- Phase 3 removed the primary synchronous full-session hot path for normal session open by adding meta and paged message reads.
92+
- Phase 5 event replay is available for debug and migration, but the UI is not using raw event reduction as a hot-path renderer input.
93+
- Phase 0 metrics can already identify DB operations over 100ms after merge, so workerization can be triggered by measured regressions rather than added speculatively.
94+
95+
P1 trigger:
96+
97+
- If `performance-snapshot.json` shows repeated DB operations over 100ms during startup, settings open, long-session open, or streaming, create a utility-process DB service design.
98+
- If event replay or timeline reduction becomes a user-facing hot path and blocks the main process, move reducer work to a worker or utility process.
99+
100+
## Required Follow-Up Before Final Merge
101+
102+
- Create PRs directly on `origin` for the phase branches.
103+
- Wait six minutes after each PR, then read all review comments, review threads, PR comments, and check annotations.
104+
- Fix actionable review feedback and push.
105+
- Merge PRs in order and pull latest `main`.
106+
- Re-run `npm run compile:electron`, `npm run build`, and targeted tests on merged `main`.
107+
- Run `npm run electron:dev` with startup, stream, 1000-message, long diff, and settings scenarios.
108+
- Export logs and inspect `performance-snapshot.json` plus `event-timeline-summary.json`.

src/main/coworkEventStore.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import Database from 'better-sqlite3';
2+
import { afterEach, beforeEach, expect, test } from 'vitest';
3+
4+
import { CoworkEventStore, RuntimeEventType } from './coworkEventStore';
5+
6+
let db: Database.Database;
7+
let store: CoworkEventStore;
8+
9+
beforeEach(() => {
10+
db = new Database(':memory:');
11+
db.exec(`
12+
CREATE TABLE cowork_sessions (
13+
id TEXT PRIMARY KEY,
14+
title TEXT NOT NULL
15+
);
16+
CREATE TABLE cowork_events (
17+
id TEXT PRIMARY KEY,
18+
session_id TEXT NOT NULL,
19+
source TEXT NOT NULL,
20+
source_event_id TEXT,
21+
type TEXT NOT NULL,
22+
payload_json TEXT NOT NULL,
23+
created_at INTEGER NOT NULL,
24+
FOREIGN KEY (session_id) REFERENCES cowork_sessions(id) ON DELETE CASCADE,
25+
UNIQUE (source, source_event_id)
26+
);
27+
CREATE INDEX idx_cowork_events_session_created
28+
ON cowork_events(session_id, created_at, id);
29+
CREATE INDEX idx_cowork_events_type_created
30+
ON cowork_events(type, created_at);
31+
INSERT INTO cowork_sessions (id, title) VALUES ('session-1', 'Test');
32+
`);
33+
store = new CoworkEventStore(db);
34+
});
35+
36+
afterEach(() => {
37+
db.close();
38+
});
39+
40+
test('returns the existing event when source event id is appended twice', () => {
41+
const first = store.appendEvent({
42+
id: 'event-1',
43+
sessionId: 'session-1',
44+
source: 'runtime',
45+
sourceEventId: 'source-1',
46+
type: RuntimeEventType.SessionCreated,
47+
payload: { title: 'first' },
48+
createdAt: 100,
49+
});
50+
const second = store.appendEvent({
51+
id: 'event-2',
52+
sessionId: 'session-1',
53+
source: 'runtime',
54+
sourceEventId: 'source-1',
55+
type: RuntimeEventType.SessionCreated,
56+
payload: { title: 'second' },
57+
createdAt: 200,
58+
});
59+
60+
expect(second).toEqual(first);
61+
expect(store.listEvents('session-1')).toHaveLength(1);
62+
});
63+
64+
test('reduces final message and delta events into a replayed message view', () => {
65+
store.appendEvents([
66+
{
67+
id: 'event-1',
68+
sessionId: 'session-1',
69+
source: 'runtime',
70+
sourceEventId: 'message-1',
71+
type: RuntimeEventType.MessageFinal,
72+
payload: {
73+
message: {
74+
id: 'message-1',
75+
type: 'assistant',
76+
content: 'draft',
77+
timestamp: 100,
78+
sequence: 1,
79+
metadata: { isStreaming: true },
80+
},
81+
},
82+
createdAt: 100,
83+
},
84+
{
85+
id: 'event-2',
86+
sessionId: 'session-1',
87+
source: 'runtime',
88+
type: RuntimeEventType.MessageDelta,
89+
payload: {
90+
messageId: 'message-1',
91+
content: 'final',
92+
metadata: { isStreaming: false, isFinal: true },
93+
},
94+
createdAt: 200,
95+
},
96+
]);
97+
98+
expect(store.rebuildMessageView('session-1')).toEqual([{
99+
id: 'message-1',
100+
type: 'assistant',
101+
content: 'final',
102+
timestamp: 100,
103+
sequence: 1,
104+
metadata: { isStreaming: false, isFinal: true },
105+
}]);
106+
});
107+
108+
test('does not turn runtime metric events into messages', () => {
109+
store.appendEvent({
110+
sessionId: 'session-1',
111+
source: 'performance',
112+
type: RuntimeEventType.RuntimeMetric,
113+
payload: { durationMs: 42 },
114+
createdAt: 100,
115+
});
116+
117+
expect(store.reduceEventsToMessages('session-1')).toEqual([]);
118+
expect(store.getTimelineSummary()).toMatchObject({
119+
totalSampled: 1,
120+
byType: {
121+
[RuntimeEventType.RuntimeMetric]: 1,
122+
},
123+
});
124+
});

0 commit comments

Comments
 (0)