|
| 1 | +# Real desktop renderer profile |
| 2 | + |
| 3 | +## Scope |
| 4 | + |
| 5 | +- Revision: `60e23fee161982d572fafbf6cc0df63ac8ac87cc` |
| 6 | +- Runtime: production Electron preview from the current `dev` build |
| 7 | +- Data: the live `opencode.db` used by Beta, queried read-only for targets active in the preceding 24 hours |
| 8 | +- Contents are never written to reports. Reports contain rank labels, counts, sizes, durations, and source locations only. |
| 9 | +- The database is shared by channels, so rows cannot be attributed exclusively to the Beta executable. |
| 10 | + |
| 11 | +The final attributed run covered 26 active sessions: 7 roots and 19 child/subagent sessions. The selected root-session targets were: |
| 12 | + |
| 13 | +| Rank | Serialized bytes in window | Messages | Parts | User turns | |
| 14 | +| --- | ---: | ---: | ---: | ---: | |
| 15 | +| p50 | 13,211 | 6 | 14 | 3 | |
| 16 | +| p95/max | 7,665,253 | 421 | 1,883 | 51 | |
| 17 | + |
| 18 | +The p95 and maximum were the same root session. The harness intentionally runs it twice, so those rows are warm/cold repetitions rather than independent samples. |
| 19 | + |
| 20 | +## Confirmed main-thread work |
| 21 | + |
| 22 | +### 1. Response parsing and synchronous continuation |
| 23 | + |
| 24 | +The largest consistently attributed script entry starts at `Response.text.then` in `packages/sdk/js/src/v2/gen/client/client.gen.ts:171-172`. The client reads the complete body as text and calls synchronous `JSON.parse` on the renderer. The LoAF duration also includes the promise microtasks that synchronously ingest the parsed result and update the UI. |
| 25 | + |
| 26 | +- Large-session navigation: 139-391 ms attributed to this entry point, including 38-88 ms forced style/layout. |
| 27 | +- History pagination: 388 ms attributed, including 64 ms forced style/layout. |
| 28 | +- Home: two response continuations took 77 ms and 61 ms, including 22 ms forced style/layout. |
| 29 | + |
| 30 | +This is the first boundary to split or move off-thread. The measurement does not imply that all attributed time is `JSON.parse`; it includes downstream work in the same promise checkpoint. |
| 31 | + |
| 32 | +### 2. Timeline measurement and scroll reconciliation |
| 33 | + |
| 34 | +History loading was the heaviest repeatable workflow. |
| 35 | + |
| 36 | +| Metric | Clean-run range | |
| 37 | +| --- | ---: | |
| 38 | +| Long tasks | 6-9 | |
| 39 | +| Total long-task time | 763-1,097 ms | |
| 40 | +| Worst long task | 138-309 ms | |
| 41 | +| Workflow time | 4.7-5.3 s | |
| 42 | + |
| 43 | +The attributed run found: |
| 44 | + |
| 45 | +- TanStack Virtual `ResizeObserver`: 173 ms, including 42 ms forced layout. |
| 46 | +- TanStack Virtual scroll handler: 160 ms. |
| 47 | +- `ScrollView` scroll handler at `packages/ui/src/components/scroll-view.tsx:173-202`: 65 ms, including 64 ms forced layout. |
| 48 | +- The timeline's virtualizer and resize anchoring are configured at `packages/app/src/pages/session/timeline/message-timeline.tsx:413-486`. |
| 49 | + |
| 50 | +The next optimization target is the response-to-projection-to-measurement pipeline, especially reducing synchronous DOM reads and repeated resize/scroll reactions while prepending history. |
| 51 | + |
| 52 | +### 3. Renderer-side Shiki WASM startup |
| 53 | + |
| 54 | +A small-session navigation loaded `@shikijs/engine-oniguruma` in the renderer and spent 58-62 ms evaluating its WASM module. Session Markdown parsing/highlighting and Pierre highlighting have workers, but renderer-side Shiki/Pierre initialization is still reachable. The bundled path is created by `getSharedHighlighter(... preferredHighlighter: "shiki-wasm")` and should be audited separately from worker execution. |
| 55 | + |
| 56 | +### 4. Home startup and geometry |
| 57 | + |
| 58 | +Across clean runs, Home produced 3-4 long tasks, 277-394 ms total, with a 127-172 ms maximum. |
| 59 | + |
| 60 | +Attribution showed: |
| 61 | + |
| 62 | +- Main module evaluation: 137 ms. |
| 63 | +- Two response continuations: 77 ms and 61 ms. |
| 64 | +- 22 ms forced style/layout. |
| 65 | +- CPU samples in `ScrollView.updateThumb` and `home-scroll-controller.ts:56-69` (`getComputedStyle`, every header's `offsetTop`, and `scrollTop`). |
| 66 | + |
| 67 | +### 5. Review opening |
| 68 | + |
| 69 | +The current real workspace review was not a major hotspot. Clean runs ranged from zero to two long tasks, with a 0-106 ms maximum. The attributed run had a 35 ms delegated click handler and 11 ms forced style/layout. This corpus had one diff viewer and does not validate large-review behavior. |
| 70 | + |
| 71 | +## Markdown renderer conclusion |
| 72 | + |
| 73 | +No current real text part exceeded 19,466 characters in the initial corpus audit. In the desktop traces: |
| 74 | + |
| 75 | +- Marked, KaTeX, and Shiki parsing did not appear as renderer CPU hotspots because they run in the Markdown worker. |
| 76 | +- Markdown worker response handling appeared as a 43 ms entry in one run. |
| 77 | +- Renderer `postMessage`, `innerHTML`, HTML parsing, sanitization-related DOM parsing, and token DOM updates appeared in low-single-digit samples and accumulated tens of milliseconds, but none was independently responsible for the observed long tasks. |
| 78 | +- Checksumming did not appear above the 1 ms CPU reporting threshold. |
| 79 | + |
| 80 | +For this real 24-hour corpus, Markdown's remaining renderer stages are secondary to response ingestion and timeline layout. This does not establish safety for an exceptional multi-megabyte text part. |
| 81 | + |
| 82 | +## Reviewed but not measured |
| 83 | + |
| 84 | +The source audit also found scale-sensitive renderer paths that this real workflow did not exercise enough to assign runtime numbers: |
| 85 | + |
| 86 | +- Terminal buffer serialization/restoration over up to 10,000 rows. |
| 87 | +- In-file search text-node scans, DOM `Range` creation, and match geometry. |
| 88 | +- Large file/diff preprocessing and virtualized file DOM work. |
| 89 | +- Command-palette filtering and unvirtualized result rendering. |
| 90 | +- Draft recursive serialization and blob hashing. |
| 91 | +- Generic persisted-state parse/merge/stringify. |
| 92 | +- Large review trees and large diff switching. |
| 93 | + |
| 94 | +No synthetic data was introduced merely to force these paths. They remain candidates for a future profile when corresponding real data exists. |
| 95 | + |
| 96 | +## Reproduce |
| 97 | + |
| 98 | +From `packages/app`: |
| 99 | + |
| 100 | +```powershell |
| 101 | +$env:OPENCODE_PROFILE_DIAGNOSTICS = "0" |
| 102 | +$env:OPENCODE_PROFILE_OUTPUT = "C:\tmp\opencode\markdown-profile-results\run" |
| 103 | +bun run e2e/performance/real-desktop-profile.ts |
| 104 | +``` |
| 105 | + |
| 106 | +The script builds and launches Electron, points it at the shared real database, enables Electron 42's `AlwaysLogLOAFURL` feature for custom-protocol script attribution, profiles the workflows, and terminates the complete process tree in `finally`. |
| 107 | + |
| 108 | +For Chrome traces and a 1 ms CPU sampler: |
| 109 | + |
| 110 | +```powershell |
| 111 | +$env:OPENCODE_PROFILE_DIAGNOSTICS = "1" |
| 112 | +$env:OPENCODE_PROFILE_OUTPUT = "C:\tmp\opencode\markdown-profile-results\diagnostic" |
| 113 | +bun run e2e/performance/real-desktop-profile.ts |
| 114 | +``` |
| 115 | + |
| 116 | +Diagnostic timing is perturbed by tracing and sampling. Use clean runs for blocking-time numbers and diagnostic runs only for attribution. |
| 117 | + |
| 118 | +Useful outputs from this audit: |
| 119 | + |
| 120 | +- `C:\tmp\opencode\markdown-profile-results\loaf-positions\renderer-profile.json` |
| 121 | +- `C:\tmp\opencode\markdown-profile-results\diagnostic\renderer-profile.json` |
| 122 | +- `C:\tmp\opencode\markdown-profile-results\diagnostic\traces` |
| 123 | + |
| 124 | +## LoAF attribution |
| 125 | + |
| 126 | +Electron custom protocols normally produce an empty `PerformanceLongAnimationFrameTiming.scripts` array. Electron 42 supports custom-protocol attribution behind `--enable-features=AlwaysLogLOAFURL`; the harness enables it through an environment-guarded `app.commandLine.appendSwitch` and verifies it with an 80 ms calibration callback. |
| 127 | + |
| 128 | +References: |
| 129 | + |
| 130 | +- https://developer.chrome.com/docs/web-platform/long-animation-frames |
| 131 | +- https://www.electronjs.org/docs/latest/api/command-line-switches#chromium-features-relevant-to-electron-apps |
| 132 | +- https://github.com/electron/electron/pull/49706 |
0 commit comments