Repository navigation
Conversation
Refs CNCKitchen#91 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PERF_RUNS repeats each tap (toggles alternate, tabs go round) and reports the median with the profiler off plus one profiled run for the hot list; the UI appears column is gone. Rows now carry budgets (links, uploadMB, contexts, longMs, inpMs) checked under PERF_ASSERT=1. PERF_SCENARIO=rich fits elements, dimensions, a section and probes through the in-page command registry on the dev server. Production hot lists are named through the build's source maps. New rows: thickness view-bar key, bands, histograms, range slider, tolerance, first show of the reference, element target, brush stroke. The spec's baseline section records the v2 medians. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
inpMs budgets are round(0.7 x median) of the saved baseline, stated once next to them; a budgeted row that took no sample, a rich project that did not come out whole, and a budgeted tap no input event reached now fail the run; the profiler is stopped in a finally; the first tap is run 1 even when it failed; the browser closes in a finally; the first-show row says only INP first is the first show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tab tap re-renders the bar for the highlight alone, and each render ran the fold loop, which forces the page's layout once per fold. useFit now skips a render-driven fit when the bar's text, child count and class are as the last fit left them; the resize and font-ready paths always fit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
From the fourth fold on only the open tab shows its word, so a tab tap changes the row's width with no change of text, child count or class. The signature now names the open tab, so such a render fits again. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tab or map tap re-ran paintField over every vertex and re-uploaded the colour buffer even when nothing the map is read from had changed. The paint effect now remembers, per colour buffer, the version, scale and ramp it last painted under (paintKey) and skips the pass when they match; RegionColors skips laying the same field on again, and the scene its upload, until a layer changes or the array is repainted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
applyTransform baked the alignment into the scan's positions and then built the bounds tree over them from scratch on the main thread. The triangles are the same after a rigid move, so refitAfterRigid rewrites the boxes in place instead; it renews the bounding box and sphere with it, and builds a tree only where the geometry has none. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ger says rebuild Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section view's first tap compiled six programs and searched the part's closed shells on the main thread. The programs are now drawn once in idle time into a one-pixel scissor, in a throwaway scene lit like the stage, and their materials kept so the live ones hit the program cache; the closed shells are found by the mesh worker as a part loads and primed into the cap's cache, keyed by the geometry asked about. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker's closed mask was per triangle in its own order, while a bounds tree reorders the render index before the part is shown and the reference's surface keeps an index of its own: a part with closed and open shells got another part's mask. The worker now answers a vertex at a time over the render layout (its vertices, then its shading copies), which no tree reorders, and the main thread reads each triangle through its index as it stands. A warm job now compiles one program at most, and the repaint and disposal hold if a job throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening the split view or the 2D Measure tab again made a new WebGL context (and, for the split, a 6.8 MB upload and a shader link). Both viewports are now mounted at their first opening, hidden and paused while closed, and shown again with the camera link and the flat sheet re-laid. The reference half is released when its reference geometry is replaced or cleared. OrthoViewport ignores a 0x0 container once it has a size, and gives its context back on dispose. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…hout re-laying The kept split state held the old reference geometry until the split was next opened; it is now dropped as soon as the reference changes while the split is closed. A kept 2D viewport shown again is only unpaused and resized unless its sheet was laid anew since (image, subject or section cut), so the image is not uploaded again and the user's pan and zoom survive a tab round trip. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A tab or the map's switch rebuilt every element overlay, re-ran evaluateDimensions outside the workspace that shows its output, and disposed and recreated the overlay materials, which released their shader programs to be compiled again on the next tap into 3D Measure. - Overlays.updateOverlays draws nothing when the input says what is drawn already (compared by content, since it is built afresh each time) and reports whether it drew. - An element's materials live as long as the element, drawn or not, with colour and opacity set in place; the callout lines and the selection strokes share one material each. - The same for a finished section's materials, and the volume layer's two. - evaluateDimensions runs only in the measure workspace. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ThicknessPanel, MarkTools, the split picker and its pick tools read their whole store; each now picks only the fields it draws. The hint ladder was built by every usePulse caller; a HintsProvider above the app builds it once and usePulse/useHintChip read it from context. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The element map was a pass over every scan vertex on the main thread, a long task on a phone that the next tap waited behind. It now goes to the mesh worker as the motion did: a request keyed on the scan token (StaleScan on a scan moved or replaced, one retry, then the main thread), the render thread's normals always sent as they are not the ones its sharp edges were shaded with, its positions only where the worker's may not match. Requests coalesce to the newest, a stale reply is dropped, and the status can never be left at running. The panel, view bar and hover keep the map in hand while it is re-measured so a dragged slider is not unmounted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The map against an element lands a worker round trip after its inputs move, so a command that read it in the frame after saw "running" and answered "no map", an empty list of patches, or a report pairing the old figures with the new settings. mapSettled (a frame, then not running) is awaited by each reader: hotspots, feature patches, export, the relief and height reads, the picture's map note, session state and the report. set_target throws at once when the map could not be measured instead of waiting out 30 s. Copy report is off while the map is measured, an unmounted hook no longer settles, and a worker error falling back to the main thread is logged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useSceneSync and useElementField subscribe to some seventy store fields; called from the Workbench every write to one re-rendered the whole workspace. They now live in a memoised SceneSync that draws nothing, mounted after the viewport. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewport loop asked for a frame 60 times a second per viewport even with nothing to draw (1305 frame callbacks in 3 s idle on the probe). It now sleeps after 30 idle frames; invalidate, canvas pointer/touch/ wheel, window keys, resize, tab shown, SpaceMouse attach, animateLook, un-pause and a linked view's invalidate wake it. keepAwake holds it for the deviation motion, a queued stroke and a pending section dressing. The canvas size is cached in resize() (0x0 never cached over a real size), so worldPerPixel, the frame, the frustum and the ticks read no layout; followView no longer forces a layout pass per frame. spreadLabels measures a label again only when its text or classes change; the navigator's rect cache also expires per task; hover hit tests use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- labelSpread: sizes re-measured after web fonts finish loading; labels cut away by the section view are left out, and nothing is measured with the Labels key off. - nextFrame wakes the viewport (SceneManager.wakeFrame) before asking for its own frame, so the viewport's frame is queued first, as before the loop could sleep. - shown needs both sides and is read-only; onTick / SceneLayer.tick docs say ticks stop while asleep; keepAwake's belt-and-braces noted. - Tests: CameraLink wake/dispose, labels measured at nothing not kept, cut-away and Labels-off skipped, nextFrame wake order. - scripts/e2e-sleep.mjs (npm run e2e:sleep): idle frames, wheel and pinch after sleep, the deviation motion, the split view link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan's bounds tree was the longest single task in opening it. The mesh worker now builds it (indirect, over copies of the render positions and index, crease copies included) once the part is in place, and the viewport lays it over the geometry with MeshBVH.deserialize, setIndex off: the drawn index is never reordered. A tree landing on a geometry laid out again, given a tree of its own or taken off the stage is turned away; one landing after a rigid move is refitted. Until it lands, the pointer tests a frame makes skip the scan (sparingRays) while clicks fall back to three's raycast, and the tree's arrival wakes the loop to look at the hover and the pins again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference is laid out with its tree built in the mesh worker, as the scan is, and its pending tree is forgotten when it is taken away. While a part's tree is on its way it hides nothing behind it either: the hover steps see no cursor and an orbit pivot finds nothing on the stage. The split picker hears the tree land and looks at its cursor again; the reverse network's drag step is a frame's test and spares the part too. Every tree built here as a fallback is indirect, and one from the worker that cannot be laid over the geometry is built here instead, with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring keyframes animated border-color and box-shadow, which repaint every frame while a hint shows. The ring now only changes opacity and the halo is its own pseudo-element with a static shadow that scales and fades. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SceneSync's effects run ahead of the Workbench's since Task 12, and with them the paint ran before Mesh Tools swapped its copy in or out: a map coming back to Deviation or Thickness was laid on the copy and the scan came back bare. The swap is a layout effect now, ahead of every plain effect. e2e-mesh-flip checks the round trip for both maps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
session.state and report.get waited strictly for the element map, and a map queued behind a long job in the mesh worker made a read fail after 30 s. They now answer anyway, the map said to be running; the commands that act on the map still wait strictly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A warming run now disposes the keepers of kinds the stage no longer has (a scan closed, a reference cleared), which outlived them before. A scan laid out again (resplit, replaceScan) forgets a tree still on its way for the layout it replaces, as disposeMesh does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A brush dab on a scan whose bounds tree was still being built off the thread was dropped without a word. It is held now, as the ray it was cast along, and laid when the tree lands (or on the next dab, if the tree came another way), so a view turned meanwhile does not move it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Commands wait for the element map only when it is the map they read: with the reference map shown, the element map re-measured behind it no longer stalls or times out hotspots, export, heightmap and feature patches. set_target still waits, as it asked for that map itself. - view.render waits for the map only in the deviation workspace, within the picture's own time budget, and past it pictures the map as still being measured instead of failing the call. - SceneSync compares the plugin runtime by what scene sync reads of it (pluginReads), so Mesh Tools and Reverse no longer re-render it on every Workbench render. - The orbit pivot keeps finding parts that have their bounds tree while another part's tree is still being built; it stays off only where that part's box could lie in front of the hit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #91.
Taps that open or change a panel — Deviation, Map, Split, Section view, the element target and facing limit — were slow to show their result, most visibly on phones. This branch removes the main-thread work behind that, on phone and desktop alike. The findings are in
docs/superpowers/specs/2026-11-04-mobile-perf-findings.mdand the plan indocs/superpowers/plans/2026-11-04-mobile-perf-fixes.md.What changed
scripts/perf-probe.mjstakes medians over several runs, has a richer test scenario, and checks budgets (PERF_ASSERT=1). It also counts frames drawn while idle.clientWidthreads (forced layout) are gone.Commands that read the element map now wait for it to settle (
mapSettled). A command waiting a frame wakes the sleeping viewport first.Results
4x CPU throttle, 288k-triangle scan, BASE
5763504vs HEAD. Production build unless marked dev.The full per-row tables (INP, longest task, links, upload MB, contexts) are in the probe output.
Verification
npm run build(tsc + vite build): passes.npx vitest run --maxWorkers=8: 3223 passed, 24 skipped.npm run e2e:ci: all checks pass except the six that need the MCP server's dependencies (npm --prefix mcp ci), which aren't installed on the machine I ran them on (e2e-agent,e2e-agent-warning,e2e-agent-mesh,e2e-agent-reverse,e2e-note-from-file,e2e-reverse-notes).e2e-agentande2e-agent-warningfail the same way on the base commit.e2e-patch-networkskipped (fixture missing).e2e:sleepchecks the view wakes after going idle for wheel, pinch, animation and split view.e2e-mesh-flipgained a check that a map survives switching through Mesh Tools with an edited copy.ballbar.stl,block-marius.stl,side bracket left.stl,fridgeBracket.ply). They were run with a substitute model on both BASE and HEAD, with identical results.PERF_ASSERT=1 PERF_RUNS=5: bare scenario on the production build passes. The rich scenario on the dev build has two rows over budget; both were already over at BASE:Known trade-offs and follow-ups
src/app/SceneSync.tsx.memo).Real-device check (not done yet)
On a phone, with Chrome remote debugging (Android) or Safari Web Inspector (iPhone), Performance panel: tap Surface Deviation, Map, Split and Section, and confirm no task runs over 100 ms.
🤖 Generated with Claude Code