Repository navigation
Measurement tools: main-thread trace + offscreen replay page, left out of builds unless MEASUREMENT_TOOLS - #2126
Conversation
… page) Draft for open-205 §9.1. Not for merge as is. - ?feature=mainThreadTrace (src/utils/mainThreadTrace.js): records long animation frames (split into pre-render work and rendering), long tasks, rAF cadence, input delay and load milestones from page start. A Mark / Summary / Download overlay; the trace carries no model name, URL or path. - /spike/offscreen/ (tools/spikes/offscreen/, built beside public/spike/offscreen/index.html): replays a trace's non-render main-thread blocks, or the section-5 synthetic pump, while drawing one BatchedMesh scene on main or in a worker with fence pacing; reports frame intervals, input-to-frame latency and how much the busy work stretched. A many-views test measures live-context limits, first-show and re-show times for one worker per view against one context with many scenes, for the multi-session UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
✅ Deploy Preview for bldrs-share-prod ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Here's the results on my MacBook Air M2 16GB OSX Sonoma 14.3.1, Brave 1.99.55, Chromium 157.0.8103.39 |
From the first real-GPU run (M2, Brave): - One frame in flight capped every run at half the display rate even when idle: a frame's fence had not signalled by the next rAF, because the browser presents it asynchronously. Default pacing is now `fence2` (at most two frames in flight); `fence` stays as an option. Completions are tracked per frame, in order. - The PSB trace showed a 5.8 s frame gap with no main-thread work and no input, almost certainly a hidden tab. The trace now records hidden/visible, and summaries leave out intervals spanning a hidden period (counted as hiddenGaps). - The replay page gains a "between Marks" window, and results and traces record navigator.userAgentData brands, since Brave masks the GPU name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
…r-worker-gpu-spike
Measures what one repaint of the still view costs on the user's GPU, to decide the hover-highlight overlay (draw only the highlight over a cached frame). Its "Repaint probe" button forces 60 repaints and times each two ways: CPU time inside three's render calls, and GPU time from the draw's return until a WebGL2 fence placed after it signals, polled from a MessageChannel loop. Triangles and draw calls are summed per frame because three resets renderer.info on every render call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
…r-worker-gpu-spike
…s and #2135 Adds §5b describing the measurement tools kept on draft PR #2126 (main-thread trace, repaint probe, replay page, many-views test), how to rebuild and use them, and their two gaps (minified attribution; no N-session harness). Links §9.1's outcome to the measured long-task targets on #2030 and the shared GPU budget follow-up (#2135). Router row updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
…sion to defer the render worker (#2134) * Pin conway 1.1630.750 (styled-items prep from record bytes) conway#750 replaces populateStyledItemsMap's per-item entity walk with a read of each styled item's target straight from the record bytes, shared by IFC and AP214. In Node on a 231 MB IFC that pass drops from 1.6-2.6 s to ~120 ms and the longest synchronous block in a deferred open from 2.0-3.1 s to ~0.6 s (open-205 #2030, target 3). The only other change since the 1.1626.740 pin is conway#747 (setup tooling); the wasm bundle is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * PLAYBOOK: why a symlinked or partial node_modules fails the single-three check The worktree section claimed a symlinked node_modules makes singleThreeInstance.test.js see two copies of three. It sees none: the test runs `find node_modules ...`, and without -L find does not descend into a symlink given as its starting point, so the guard behind the muted "Multiple instances of Three.js" warning fails on source that is fine. A partly hardlinked copy fails differently (the deferred-BVH picking tests loaded two copies of three). A full `cp -al` of a sibling's tree plus its .husky/_ shim passes, at almost no disk cost, when the dependencies match. Router row updated to say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * Residency: rank OCCUPANCY from the viewer's camera, not a field it lacks ResidencyControl handed ResidencyController a camera getter reading `viewer.context.ifcCamera.perspectiveCamera`. `viewer.context` is the ThreeContext facade, which has no `ifcCamera` (that lives on the legacy context it wraps), so the getter always returned null and OCCUPANCY fell back to ranking parts by world-space radius alone, ignoring the eye. It now reads `context.getCamera()`. This changes what the at-rest slider keeps at a given percent under the default metric, and what Auto keeps while moving. The ranking is still taken when a set is chosen, not re-ranked as the camera orbits; the design doc's Known limits now says so, and notes that the owner's 25% PSB measurement was by size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * Residency: a disposed viewer's camera reads as none, not a throw Review follow-up on the OCCUPANCY camera fix. A disposed ThreeContext throws from getCamera once its legacy context is dropped, and the store can hold the old viewer until the next setViewer, so a re-rank in that gap would now throw where the old (always-null) getter did not. Guard it, with a test that goes red without the guard. Doc: note that the eye is the camera position, which is right for a perspective view but not an orthographic one, and rewrap Known limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * Motion residency E2E: assert eviction only on frames painted while the camera moved The drag tests asserted that every frame painted during a drag drew fewer triangles than the still view. CI (and ~1 in 8 local runs under parallel load) saw an occasional full-count frame mid-drag. Recording the camera's last change per frame showed why: under CPU load Playwright's pointer moves arrive 200+ ms apart, the drag stretches from ~0.4 s to ~4 s, and a pause that long is rest by MotionResidency's own rule (REST_MS plus REST_FRAMES), so the full set is drawn again until the next move restarts motion. The product is right; the test assumed a drag never stalls. The recorder now notes, per painted frame, how long since the last camera-controls `update` (the event that starts and extends motion), and the two "fewer while moving" checks look only at frames inside REST_MS of a change, where rest is impossible. A full frame there still fails. Measured locally: 7 of 24 failures before; 48 of 48 passes after (whole spec, 8x per form factor, parallel workers). With motion start broken, all six cases still go red on their triangle assertions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * Motion residency E2E: time a frame from before its draw, not after Review follow-up. sinceMoveMs was taken after the frame's first render call, so a slow render counted toward "time since the camera moved", and on a slow runner every drag frame could exceed REST_MS and trip the "painted while the camera moved" guard. Read the clock before the draw instead. It is still after MotionResidency's rest check in frame(), so it never reads less than the controller's own and a rest frame can't pass as one in motion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * offscreen-rendering: real-GPU spike results, and the go/no-go rule Adds §5a with the owner's M2 measurements from Share#2126: a render worker against a busy main (R1/R2, 4.3x frame rate, input still main-bound), 20 views (per-view contexts vs one shared), Share's own main thread on PSB (hover was the largest block, since fixed by #2127-#2129), and the repaint probe (big models are GPU- and triangle-bound, which residency fixes and a worker doesn't). Re-opens §9.1 as "whether to start": re-capture Share on main, replay R1 vs R2, go if render on main still costs visible frames, otherwise defer to the multi-session UI. Status, §1, §10 and the router row updated to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * offscreen-rendering: record the re-capture on main, and defer the render worker Adds the 2026-11-10 re-capture to §5a: PSB as a GLB cache hit (two tasks, 1.3 s and 1.0 s, the first being the hit's synchronous hydration) and as a first load (18 long tasks, input otherwise fine), a Seestrasse trace (main effectively free), and 8 Seestrasse-sized views (switch costs one frame per context, two shared). A worker removes the stalls but costs ~54 ms of input latency in every phase and ~15% of idle frames; splitting the tasks fixes both. §9.1 is amended to ask first whether a cheaper fix removes a block, and records the outcome: deferred, split the tasks under #2030, revisit with the multi-session UI. Status, §1, §10 and the router row updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH * offscreen-rendering: §5b reusable tools on #2126; link #2030's targets and #2135 Adds §5b describing the measurement tools kept on draft PR #2126 (main-thread trace, repaint probe, replay page, many-views test), how to rebuild and use them, and their two gaps (minified attribution; no N-session harness). Links §9.1's outcome to the measured long-task targets on #2030 and the shared GPU budget follow-up (#2135). Router row updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH --------- Co-authored-by: Claude <noreply@anthropic.com>
|
Kept open as a toolbox, not for merge. The spike's conclusion is recorded in The branch was brought up to date with main at Generated by Claude Code |
…r-worker-gpu-spike
…rement tools The spike's tools land on main at their current paths, so the next measurement doesn't need a long-lived draft branch: `?feature=mainThreadTrace` (src/utils/mainThreadTrace.js, off by default, ~4 KB gzipped in the bundle; the build doesn't code-split, so a lazy import would be inlined anyway), its repaint probe, and the /spike/offscreen/ replay and many-views page (tools/spikes/offscreen/, public/spike/offscreen/index.html, noindex). README, the lint and build comments, offscreen-rendering.md §5b and the router row now describe them as checked in rather than as a draft PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
…ncludes them Per the owner: check the tools in, but don't ship them. A build-time switch, MEASUREMENT_TOOLS (vars.prod.js, default false, inherited by every config), now gates both: - the main-thread tracer: index.jsx installs it through an import() inside a branch the define folds away, so esbuild drops the module (verified: no tracer strings in docs/index.js, ~10.5 KB smaller; present again with the switch on); - the /spike/offscreen/ page: build.js builds its bundles and copies its index.html only with the switch. The HTML moves from public/ (copied whole into every build) to tools/spikes/offscreen/, beside its source. The README now opens with how to include them: locally (MEASUREMENT_TOOLS=true yarn build-prod / serve) and on a deploy preview (a throwaway branch setting it under [context.deploy-preview.environment]). offscreen-rendering.md §5b and the router row say the same. A defines test pins the default to off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
Review follow-up. A long animation frame names an event-listener invoker by its element (`BUTTON#some-id.onclick`), so an id rode into the trace. Keep the tag and handler, replace the id with `#…`; the trace promises no model names, and an id is page content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH
Codex Review Summary: ❌ FailedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |

What and why
This checks in the tools from the open-205 §9.1 render-worker spike, so the next measurement doesn't need a long-lived draft branch. They're left out of normal builds, as the owner asked, and turned on by a build-time switch. Their findings are in
design/new/offscreen-rendering.md§5a (merged in #2134); §5b covers how to use them again.What's included:
?feature=mainThreadTrace(src/utils/mainThreadTrace.js, 16 unit tests). From page start it records long animation frames (split into non-render and render work), long tasks, its own rAF cadence, input delay, load milestones and tab visibility. An overlay offers Mark, Summary, Repaint probe and Download trace.renderer.render, and GPU up to a WebGL2 fence./spike/offscreen/(tools/spikes/offscreen/), the replay page. It plays a trace's non-render blocks, or a synthetic pump, against aBatchedMeshscene rendered on main or in a worker. Its Many views test compares a context per view with one shared context. The page isnoindex.How they stay out of production:
MEASUREMENT_TOOLSis a define intools/esbuild/vars.prod.js,falseby default and inherited by every config.src/index.jsxloads it through animport()inside a branch the define folds away, so esbuild drops the module.build-prod: no tracer strings indocs/index.js, and the bundle is ~10.5 KB smaller. With the switch on, they're back.tools/esbuild/build.jsbuilds its two bundles and copies itsindex.htmlonly with the switch.public/, which every build copies whole, totools/spikes/offscreen/, beside its source.docs/spike/without the switch; all four files with it.tools/esbuild/defines.test.jspins the default to off.Including them when needed (the top of
tools/spikes/offscreen/README.md):MEASUREMENT_TOOLS=true yarn build-prod(or… yarn serve).netlify.tomladds[context.deploy-preview.environment] MEASUREMENT_TOOLS = "true", never merged. Or set the variable for deploy previews in Netlify's settings, then unset it afterwards.Also changed:
.eslintrc.cjsturnsno-magic-numbersoff fortools/spikes/**, because scene constants read better inline.CLAUDE.md/AGENTS.mdrouter row and offscreen-rendering.md §5b describe the tools as checked in and gated.History: this was the draft spike PR, kept open as a toolbox. Its measurements are recorded in #2134 and on #2092 and #2030. The branch has been brought up to date with main.
Tests
mainThreadTrace.js(percentiles, privacy of script attribution including the id drop, LoAF splitting, replay plans, windows, hidden-tab gaps, the repaint probe), plus the defines default.test-cipassed (coverage thresholds included).build-prodwas run with the switch off and on, with the outputs checked as above./spike/offscreen/loads and completes a Many views run.yarn servepicks the switch up.buffered.BUTTON#id.onclicknow keeps the tag and handler but drops the id (#…), tested.Reported Conditions
None. These are dev-only measurement tools, absent from normal builds, and they add no failure path to the app.
🤖 Generated with Claude Code
https://claude.ai/code/session_017nj2dXLdxTaS5V1HZS5gsH