Repository navigation
Open a long document with half the main-thread work, and type into it at well over twice the speed - #806
Merged
Conversation
… holds An open document asks the server for the person's machines every 3 seconds, for the document's machine choice every 15 and for its investigations every 20, for as long as the page stays open. Each answer was published as a new object whether or not it said anything new, so the room, which holds all three through useSyncExternalStore and a state setter, rendered again, and with it Chat, whose messages are parsed again and whose layout effect reads the page's layout, and the editor's previews. On a large document that was one task of 350 to 500 ms starting within 120 ms of each answer, 2.85 s of every 17 s at rest in a trace of the page after it had opened. The machine feed, the investigations store and the machine choice now keep what they hold when an answer is structurally the same (`sameJson`) and publish a first answer, a change, a failure not already shown and the recovery from one. What the page changes itself, the optimistic favorite and removal and the optimistic choice, still publishes at once. The machine choice also records where a read began in the order of `nextReading`, which the room compares with the socket's last word to decide whether the list is current; that place moves only when a number was minted since the last read (`afterRead`), the only case in which the comparison can change, so an unchanged read of the choice renders nothing either. What stays the same: every list, state and error the page draws, and when it draws them. Two existing tests of the feed counted the answers a poll published (a repeated answer was heard); they now say that a change is heard and a repetition is not. Nothing here touches the document, Yjs, drafts, comments or decisions. Two things that are not a poll of the machines made the test count more commits than it should, and it now keeps clear of both. First, it started each focus round on the request of the last, not on its answer: a focus asks for the machine list twice and the choice once, and `answered > asked + round` was met by the surplus requests of earlier rounds, so its three focus events came 6 ms apart. A read of the choice that the next focus overtook before its answer was acted on is not the read the hook follows (`afterRead` cannot show that nothing was minted between the held read and this one), so the room drew twice for it, RoomWorkspace and then the editor's layers: two more commits than counted, in 3 of 5 and then 1 of 6 runs alone at one worker and 12 of 30 on an unminified client. Each round now waits for its own new answers, counted once the page has them, and for the page to be idle. Second, the Projects socket tells every page that has the repository open whenever any document in it changes (`sidebar:catalogue`) or has a decision answered (`sidebar:decisions`), the other tests' documents included; the page reads the list again 250 ms later and draws it twice over, which is four more commits than counted whenever a neighbouring spec renames or creates a document in the second the test measures (41 against 45). The page's socket now passes on only what is said of this document, as `e2e/document-intent.e2e.ts` already keeps the list to a test's own documents. A read overtaken by another still draws the room once, and a change to another document still draws the sidebar; what the test claims, and the page does, is for polls, which come seconds apart. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7) Assistant-verification: bun test apps/web/src/machine-summary.test.ts, machine-choice.test.ts and experiments/store.test.ts failed before the change for the reason in the title (the feed published 3 times for 3 identical answers where 1 was expected; the investigations store likewise; afterRead did not exist) and passed after it: 34 pass, 0 fail Assistant-verification: e2e/poll-quiet.e2e.ts (a stubbed machine list and machine choice answered the same again on each window focus, once the page has been quiet for 1.2 s, React commits counted through the DevTools hook) failed on the unmodified client of 341a2b3 in 3 of 3 runs (8 to 10 more commits after three identical answers) and passed on this tree in 30 of 30 runs at 8 workers; a first version without the wait for quiet flaked once beside other specs, because the page's own settling renders come in a burst a second and a half after it opens, and the test also found that afterRead needed the previous read's place, which it now has Assistant-verification: bun test apps/web passed: 1118 pass, 2 skip, 0 fail across 122 files; bun run types and bun run ci passed (dprint, oxlint with 0 errors and the 74 warnings origin/main has, token, design contract and design record checks) Assistant-verification: playwright on machine-choice, machines, experiments, planner-picker and poll-quiet with CI=1 on a fresh database in chopin-stage1-pg and GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=commit.gpgsign GIT_CONFIG_VALUE_0=false in that run's environment only passed: 88 passed and 1 flaky, "the sidebar's Machines menu sets the default machine and leads to the page" (toHaveURL on /machines after page.goto("/")), which passes on retry and also failed 1 of 12 runs on the unmodified client Assistant-verification: measurement with the enriched seed of the local harness (59 SeeCode diagrams, 42 fenced blocks, 18 decisions, 82-entry Chat), warm, 1x CPU, 25 s window, median of 3: first line 1.58 s (1.54 s before), longest task 1.63 s (1.72 s), long tasks 34 (43), blocked 7.20 s (11.22 s) and total blocking time 5.46 s (9.07 s) in the window, 4.81 s (6.55 s) blocked before the diagrams are in; a re-measure of the unmodified client on the same machine gave 44 tasks, 11.22 s and 9.02 s Assistant-verification: e2e/poll-quiet.e2e.ts CI=1 --retries=0 on a fresh database: its first version failed at its count of commits (line 104, always more by 2) in 3 of 5 and in 1 of 6 runs alone at --workers=1 on the minified client and in 12 of 30 on an unminified one (apps/web built with --minify false); beside machine-choice, machines, experiments, chat-pin and chat-anchoring at --workers=6 (three repeats of each) it failed with 4 more (41 against 45) in 4 of 18 repeats over five such runs, instrumented copies of it among them; an in-page timeline of one of those showed `sidebar:catalogue` for another document at 2379 ms, the Projects list read again at 2630 ms and four commits from 2634 ms, and, for the overlap, three failing runs showed the last round's choice read starting within 0 to 6 ms of the reply to the one before it, then two commits after the later read's reply; a throwaway spec that renames another document inside the measured window made 4 more commits in 5 of 5 runs, and 0 more in 5 of 5 with the socket passing on this document alone; with the overlap fixed alone it passed 30 of 30 on the unminified client and 60 of 60 at --workers=6 by itself, and this version passed 10 of 10 alone on the minified client and 9 of 9 repeats in three full runs of the mixed set above (156 passed in each) User-preference: Chopin must feel fast; no lag when resizing or switching documents Co-authored-by: Alex Lavaee <lavaman131@github.com>
…nstead of playing it Every SeeCode diagram mounted playing from its first step, on screen or not. Each step rewrites its step counter and swaps its Play and Pause icon inside the editor's editable root, and every such change wakes Lexical's mutation observer, whose update reads the DOM selection and forces a layout of the whole page. A document with 59 diagrams, nearly all of them screens below the reader, spent the seconds after it opened in a train of about 20 tasks of 100 ms each, 2.1 s of Lexical's flushes in all, for diagrams nobody could see. The editor now tells a diagram, through a new `near` prop, whether it was within a screen of the reader when it mounted: the same margin and scroller that decide when a Mermaid drawing is made, read once from an IntersectionObserver, so no layout is forced to find out (`watchPlaced`, `useNearAtMount`). A diagram that was not near opens paused on its last step, the state playing ends in and the one reduced motion opens in (`initialPlayback(steps, reduced, near)`); one that had already begun to play when the browser said so settles there in one update (`settle`), and every far diagram settles in the same commit. One in view plays as it always has. A diagram nobody places, the gallery's and the charts', plays as before, and one whose source changes mounts again and is placed again, so an edit the reader is looking at still plays. What changes for a reader is only that a diagram they scroll to within the first seconds shows its finished drawing instead of its entrance mid-way. A one-off check, not kept, took a stage screenshot of such a diagram on this tree and on its parent after the parent's diagram had played to the end, in the same document and window: the two files are byte-identical. By reading the code, not by listening with a screen reader: a far diagram's polite status line, "Step N of N", is now there when the diagram is placed, or from its first render where it is placed before it mounts, rather than appearing when it finishes playing a few seconds later. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7) Assistant-verification: bun test packages/diagrams/src/playback.test.ts and packages/editor/src/widgets/near-viewport.test.ts failed before the change (settle and watchPlaced did not exist) and passed after it; the whole of packages/diagrams, packages/editor and apps/web passed: 2178 pass, 0 fail across 242 files Assistant-verification: e2e/diagram-playback.e2e.ts (three diagrams, the third after 150 paragraphs; every text the step counters showed, read at each change) failed on the parent commit's client (the far diagram counted "1 / 5, 2 / 5, 3 / 5, 4 / 5, 5 / 5") and passed on this tree, 5 of 5 runs; it also checks that the two in view still count every step and that the far one, scrolled to, has the same parts in the same state as the first after it played Assistant-verification: playwright on seecode, diagram-lazy, code, wide-content, visual-handoff and diagram-playback with CI=1 on a fresh database in chopin-stage1-pg and GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=commit.gpgsign GIT_CONFIG_VALUE_0=false in that run's environment only passed: 43 passed; the diagram gallery config (e2e/diagram-gallery, vite dev on port 8955 with CHOKIDAR_USEPOLLING=1 because the machine's inotify instances were exhausted) passed: 17 passed, 5 skipped by the config's projects Assistant-verification: bun run types and bun run ci passed (dprint, oxlint with 0 errors and the 74 warnings origin/main has, token, design contract and design record checks, with no design-contract hash exception for the files this changes) Assistant-verification: measurement with the enriched seed of the local harness, warm, 1x CPU, 25 s window, median of 3, against the parent commit's 34 tasks, 7.20 s blocked and 5.46 s total blocking time: 26 long tasks, 5.92 s blocked and 4.62 s total blocking time in the window, longest task 1.51 s (1.63 s), first line 1.57 s (1.58 s), tasks after the open phase 12 and 1.20 s (21 and 2.39 s); against the unmodified client of 341a2b3 (43 tasks, 11.22 s, 9.07 s, longest 1.72 s, first line 1.54 s) that is 17 tasks, 5.3 s and 4.5 s fewer; the open phase itself (before the diagrams are in) is unchanged at about 4.7 s User-preference: Chopin must feel fast; no lag when resizing or switching documents Co-authored-by: Alex Lavaee <lavaman131@github.com>
…ps writing it again on every render
A drawn Mermaid diagram, a formula and the full-screen copy of a diagram are markup set with `dangerouslySetInnerHTML={{ __html: html }}`. React 19 compares that prop by identity, so each render's new object makes it set `innerHTML` again: the whole drawing is parsed and replaced, and because it sits inside the editor's editable root Lexical's mutation observer wakes and reads the page's layout, though the markup is the very string it was. The previews render again on every editor update (`PreviewPlugin` rebuilds its block list in each update listener call, so every `Preview` and every `Diagram` under it renders), so each keystroke anywhere in the document, each remote edit and each selection change redrew every drawn diagram and formula, and each poll answer did the same through the room. In a trace of a large real document the one drawn Mermaid preview was written 20 times in 20 s, 10 flushes of 111 to 249 ms.
`useInnerHtml` gives React the same object for the same string, the full-screen copy keeps its own object while it is open, and the formula's two lines move into a `Formula` component because hooks cannot follow the early returns of `Rendered`. The cards of decisions draw their diagrams with the same `Diagram`, so they stop being rewritten too. A redraw with different markup still replaces the drawing.
What stays the same: the markup, the DOM and what is drawn. The test types 40 keys into a paragraph beside a drawn diagram and a formula and finds the very same nodes afterwards, written into by nobody; a unit test is not possible here because `bun test` has no DOM to count assignments in.
Assistant-model: Claude Sonnet 5.5
Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7)
Assistant-verification: e2e/drawing-stable.e2e.ts (40 keys typed into a paragraph beside a drawn Mermaid diagram and a display formula; each drawing marked beforehand, and every record written inside them counted) failed on the parent commit's client for all three reasons (the diagram's SVG replaced, the formula replaced, 256 write records) and passed on this tree, 4 of 4 runs
Assistant-verification: bun run types and bun run ci passed (dprint, oxlint with 0 errors and the 74 warnings origin/main has, token, design contract and design record checks, no design-contract hash exception for this file); bun test packages/diagrams packages/editor apps/web passed: 2178 tests, 2 skipped, 0 failed
Assistant-verification: playwright on code, decision-markdown, decision-option-previews, diagram-lazy, drawing-stable, seecode, wide-content, diagram-playback and poll-quiet with CI=1 on a fresh database in chopin-stage1-pg and GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=commit.gpgsign GIT_CONFIG_VALUE_0=false in that run's environment only passed: 51 passed and 1 flaky, the first version of poll-quiet (fixed at its cause in the first commit of this branch afterwards: 30 of 30); responsive-content, which holds the Mermaid preview and display math geometry, passed on the fixtures server: 11 passed
Assistant-verification: measurement with the enriched seed of the local harness, 85 keys 25 ms apart typed into the first paragraph of the settled document, three runs each: the parent commit's client blocked the page for 40.4 s (39.5 to 40.6), with 145 long tasks, a longest of 0.97 s and 163 writes into the drawings; this tree for 20.9 s (17.5 to 23.9), with 133, 0.58 s and none
Assistant-verification: the same measurement as the other two commits (open, warm, 1x CPU, 25 s, median of 3) shows no change from this commit: 6.06 s blocked and 4.81 s total blocking time against 5.92 s and 4.62 s on the parent, inside the run-to-run spread, because after the first two commits nothing renders the room while the page is at rest; the open phase and the first line (1.61 s) are likewise unchanged
Assistant-verification: an init script that records every container Mermaid adds to the page found exactly one mermaid.render for the drawn block in an open plus 20 s on the unmodified client, on the parent and on this tree, so the repeated renderMermaid runs a coverage count suggested are not real: what repeated on the unmodified client were innerHTML rewrites of the finished drawing (9 bursts of 23 or 46 records, none after this commit)
User-preference: Chopin must feel fast; no lag when resizing or switching documents
Co-authored-by: Alex Lavaee <lavaman131@github.com>
…s they mount A document with 59 SeeCode diagrams spent most of the 1.5 s task that mounts them on two things. Each diagram was routed about three and a half times: once when its block checked that the source can be drawn, once when the diagram first rendered, and again at the width its column turned out to have, which draws the unconstrained layout once more before it looks for a compact one. And each diagram read its stage's width in a layout effect, before the first paint. A diagram also writes as it mounts (the classes that show its steps), so the next diagram's read had to lay the page out again: 86 layouts in a trace of one open, 800 ms, most of them for a page whose only change was one more diagram. `renderDiagram` now keeps the drawings it has made, by the specification and the settings, in a bounded cache (400 drawings and 8 million characters of markup, oldest used first out), so the same diagram at the same width is the same result object wherever it is asked for, and the compact layouts tried for a narrow column are made once for it. Callers read a result and never change it. The measure waits for the commit after the one that changed the drawing, by bumping a counter in a layout effect, so every diagram that mounted or changed together has written what it will before any of them reads, and their reads share one layout. That commit is still made before the first paint, as the measure always was, so the zoom controls of a diagram that fits are never on a painted frame. What stays the same: every drawing, the controls a diagram offers and when it first shows them, and what the Planner and the server's admission checks are told about a diagram that cannot be drawn (they call the same function and read the same problems). The wide diagrams still get their compact layout for a narrow column. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7) Assistant-verification: bun test packages/diagrams/test/render-once.test.ts failed 4 of 6 before the change (the same diagram read again from the document, a diagram that fits its column at three widths, a wide one drawn compact for 500 px, and a chart fitted to 500 px each gave a new equal result object instead of the one made before; the other two, a diagram that cannot be drawn and a changed diagram with a bound on what is kept, pass either way) and passed 6 of 6 after it Assistant-verification: e2e/diagram-open.e2e.ts (a document of sixty SeeCode diagrams, a third wider than the column and compactable, a third wider that stays wide, a third narrow; reduced motion so nothing plays; the browser's own LayoutCount from the first byte to a settled page) failed on the parent commit's client with 113 layouts against the 60 it allows, and passed on this tree with 38 (37 with 8 diagrams, so the page's own layouts are about 37 and no longer grow with the diagrams; the parent's grew 47, 88, 113, 138 for 8, 40, 60, 80); its second test, which samples every frame for zoom controls over a diagram that fits, passes on both and guards against reading the width after the first paint Assistant-verification: bun test packages/diagrams packages/editor apps/web and apps/server/src/agent/visual-catalog.test.ts passed: 2184 pass, 2 skip, 0 fail; bun run types and bun run ci passed (dprint, oxlint with 0 errors and the 75 warnings origin/main has, token, design contract and design record checks, no design-contract hash exception for these files) Assistant-verification: playwright on seecode, diagram-lazy, code, wide-content, visual-handoff, diagram-playback, drawing-stable, diagram-open, decision-option-previews and decision-markdown with CI=1 on a fresh database in chopin-stage1-pg and GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=commit.gpgsign GIT_CONFIG_VALUE_0=false in that run's environment only passed: 54 passed Assistant-verification: measurement with the enriched seed of the local harness (59 SeeCode diagrams, 42 fenced blocks, 18 decisions, 82-entry Chat), 1x CPU, 25 s window, median of 3, against the parent commit's numbers; warm: longest task 1.17 s (1.49 s), open phase blocked 4.30 s (4.73 s) and TBT 3.60 s (4.08 s), whole window 26 long tasks, 5.54 s blocked (6.06 s) and TBT 4.24 s (4.81 s), first line 1.57 s (1.61 s); cold: whole window 5.36 s blocked (5.87 s), longest 1.16 s (1.48 s); switch: whole window 5.25 s (5.85 s), longest 1.15 s (1.36 s); the task that mounts the diagrams went from 1.5 s to 1.04 to 1.17 s User-preference: Chopin must feel fast; no lag when resizing or switching documents Co-authored-by: Alex Lavaee <lavaman131@github.com>
…ut, not in the commit that mounted it A fenced block asks the stylesheet whether to wrap or scroll its lines. The answer is a custom property that a container query decides, so reading it with `getComputedStyle` makes the browser lay the page out first. The read was in a layout effect of the block's view, and the views of a document mount in about twenty-five groups of one to five (a trace of 42 blocks logged every read: the first of each group took about 45 ms and the rest of the group nothing). Each group's read came after the previous group's coloured copy had been written, so each one laid the whole document out again: about twenty layouts and most of a second in one 1.2 s task, which also held back everything queued behind it (Chat's last message and the last diagram arrived a second later than they needed to). The view's own ResizeObserver, which was already there to follow a resize, now delivers the first answer as well. An observer's callback runs after the frame's own layout, so the read costs nothing, and the renderer was never mounted before the answer, so nothing changes in what is drawn: a phone still never highlights a block twice and never paints scrolling lines and then wraps them. The coloured copy of a block is mounted in the render after that callback instead of in the commit that mounted its view, which puts it a frame later (the copy is highlighted asynchronously and draws nothing before that in any case; the frame in between was not compared pixel by pixel). A block in a pane that is not displayed has no box and so no answer until it is shown; it used to be mounted anyway. What stays the same: the switch's value and where it comes from, wrapping on a phone and scrolling elsewhere, the switch following a resize, and the preview not being mounted again when it changes. The design contract keeps a hash of this file for the dynamic props it forwards to its view (`CodeView` to `<View>`); that spread is untouched, so the exception's hash is renewed after reading the change. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7) Assistant-verification: e2e/code-overflow.e2e.ts (forty fenced blocks, Rust and Gherkin; every read of the wrap switch recorded with whether it happened inside a ResizeObserver callback; every block answered and drawn) failed on the parent commit's client with 40 reads made outside a ResizeObserver callback, where each lays the page out, and passed on this tree with none Assistant-verification: playwright on code-overflow, code, wide-content, decision-option-previews and responsive-content with CI=1 on a fresh database in chopin-stage1-pg and GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=commit.gpgsign GIT_CONFIG_VALUE_0=false in that run's environment only passed: 52 passed, and the three failures were in responsive-content.e2e.ts, which needs the fixtures server; run on it (ALT_FIXTURES) the file passed: 11 passed (the phone wrap and the return to scrolling are in wide-content: "code wraps on a phone and keeps one preview when it stops" passed) Assistant-verification: bun test packages/editor passed: 953 pass, 0 fail; bun run types passed; bun run ci passed after the exact design-contract hash of packages/editor/src/widgets/code-view.tsx was renewed (the reviewed case, unresolved JSX props from CodeView to View, is the spread of the component's props and was not touched); dprint, oxlint with 0 errors and the 75 warnings the parent has, token and design record checks pass Assistant-verification: measurement with the enriched seed of the local harness, 1x CPU, 25 s window, median of 3, against the parent commit's numbers; warm: longest task 0.92 s (1.17 s), open phase 13 long tasks, 3.21 s blocked (4.30 s) and TBT 2.53 s (3.60 s), whole window 25 long tasks, 4.25 s blocked (5.54 s) and TBT 3.00 s (4.24 s), Chat's last message at 2.56 s (3.57 s), last diagram at 4.17 s (5.35 s), first line 1.55 s (1.57 s); cold: whole window 4.64 s blocked (5.36 s), longest 0.92 s (1.16 s); switch: whole window 4.20 s (5.25 s), longest 0.89 s (1.15 s) User-preference: Chopin must feel fast; no lag when resizing or switching documents Co-authored-by: Alex Lavaee <lavaman131@github.com>
…newest line in the frame instead of in the commit Two costs sat in every render of Chat's transcript. The room renders for polls, presence and, as the typing numbers below show, each key typed into the document, for reasons that change no message, and every one of them rendered every message again: each message's Markdown was parsed again, which in a trace of the unmodified client was about 5% of the blocked time. And a layout effect that ran after every render moved the transcript to its newest line with `scrollTop = scrollHeight`, a read of layout in the commit that had just changed the transcript, so the browser laid the page out for the read and again for the frame (five forced layouts, 220 ms, in a trace of one open, two of them 120 ms inside the task that builds the document). `MessageMarkdown` is now memoized and compared by what the message says (`sameMessageMarkdown`: its text, class, mention and references, the references by value because a message to the Planner builds a new list of them each time). The move to the newest line now waits for the animation frame, where one render or twenty make one layout and the frame's own layout serves it, and each render that follows the reader replaces the move that was waiting. Whether the reader is following is decided as it always was, before the render's growth can move them, from a new flag that is true from the last time they were near the newest line until they scroll up: the existing 48-pixel test (`pinned`) is still what the page reads, but it gives way whenever a row grows under a reader who has not moved, which in the frame between a move and the scroll event it causes would have lost a reader at the bottom of a streaming message. A jump to a message, which stops following, cancels the move that was waiting so the frame cannot undo it. What stays the same: a reader at the bottom follows new rows, a streaming row and a card that grows; one who has scrolled up is left where they are; a reader's own message comes into view wherever they were; a jump to a message and the highlight on it. Where the observer already moves a reader at the bottom after a resize, nothing changed. This commit does not make opening a document faster by itself (the numbers are in the last lines); it makes every later render of the room, typing in the document first among them, cheaper. What a reader sees does change in one place, and a test had come to rely on its absence. Every render of a message made a new `a` component for its Markdown, so every render of Chat mounted the message's links again, and the link that had opened a child document was a detached node by the time the child closed: `parentFocusTarget` found its opener gone and fell back to the document's heading. With the message memoized the link stays, so closing a child returns focus to the link that opened it, which is the opener focus `docs/architecture.md` says a close restores and what `e2e/research-child-motion.e2e.ts` expects of a research card. `e2e/research-child-surface.e2e.ts` had expected the heading since 9b975fa moved its opener from a sidebar row to a Chat reference link, with a comment that Chat's links can remount; the five specs that close a child, and the lines after their first check, now expect the link that was pressed. Bisecting the branch's six commits found this one: the five specs passed on origin/main and on the commits before this one, and failed in 25 of 25 runs from here. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7) Assistant-verification: bun test apps/web/src/chat/markdown.test.ts failed before the change (sameMessageMarkdown was not exported) and passed after it: 17 pass, 0 fail Assistant-verification: e2e/chat-pin.e2e.ts, three tests: with the DevTools hook counting, for each commit, the Markdown parsers that rendered (mounted subtrees whole, the rest only where a fiber and its alternate differ), a transcript of 14 messages and a room render caused by a changed machine list failed on the parent commit's client (70 parses against the 42 of opening, 28 more for a render that changed no message) and passed on this tree (no more); with a MutationObserver's pending records checked at every read of the transcript's scroll height, the same page failed on the parent with 32 reads made in the commit that changed the transcript and passed with none; the third test (another member's message follows a reader at the bottom, leaves one scrolled up where they are, and the reader's own message comes into view from wherever they were) passes on both and guards the behaviour Assistant-verification: playwright on chat-pin, chat-anchoring, chat-composer, chat-commands, chat-mentions, chat-references, chat-phone and poll-quiet with CI=1 on a fresh database in chopin-stage1-pg and GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=commit.gpgsign GIT_CONFIG_VALUE_0=false in that run's environment only passed: 34 passed; decision-evidence and decision-prose, the specs that jump to a chat message from a decision's evidence, belong to the conversation-plan project (a scripted harness with the conversation plan on) that this setup cannot start and fail the same way on the parent commit, so a jump to a message is covered by reading the code and not by a browser test here Assistant-verification: bun test apps/web packages/editor passed: 2070 pass, 2 skip, 0 fail; bun run types passed; bun run ci passed after the exact design-contract exception of apps/web/src/chat/markdown.tsx was renewed (the reviewed boundary, the component's className into its root div, is untouched; only the owner's name in the case, now MessageMarkdown inside MessageMarkdown, and the file's hash changed); oxlint reports the 75 warnings the parent has Assistant-verification: measurement of typing, the enriched seed of the local harness, the settled document, 85 keys 25 ms apart into its first paragraph, three runs each, this tree against its parent: 14.9 s blocked (14.0 to 16.5) against 23.3 s (22.4 to 23.8), total blocking time 8.6 s against 16.6 s, longest task 0.42 s against 0.70 s, 126 long tasks against 136 Assistant-verification: measurement of the open, warm, 1x CPU, 25 s window, median of 3, parent against this tree: no change beyond the run-to-run spread (first line 1.55 s and 1.53 s, longest task 0.92 s and 0.90 s, open phase 3.21 s and 3.15 s blocked, whole window 4.25 s and 4.26 s blocked, Chat's last message 2.56 s and 2.44 s), because the transcript is rendered a few times while a document opens and its forced layouts only move to the next reader of layout; the gain is in what the room renders after it Assistant-verification: e2e/research-child-surface.e2e.ts, the five specs that close a child (the root's Chat stays live, Lift keeps the child, an in-app child preserves and restores its mounted parent, a delayed sibling route, a direct child keeps direct-entry history), CI=1 --workers=1 --retries=0 on a fresh database: on origin/main bc31c50 25 passed of 25 (--repeat-each=5); the specs as they were, on this tree, 25 failed of 25, each at its first check that the document heading has focus; git bisect over the branch's six commits ran the five specs twice each at 33581b15 and b2dc41d7 (10 passed of 10 at both) and named this commit the first bad one; a probe that marks the pressed Chat link found it detached while the child was open on bc31c50, with focus on the heading after the close, and on this tree the same node, connected, with focus on it; the five specs as edited here: 50 passed of 50 (--repeat-each=10) User-preference: Chopin must feel fast; no lag when resizing or switching documents Co-authored-by: Alex Lavaee <lavaman131@github.com>
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.
Open a long document with half the main-thread work, and type into it at well over twice the speed
Opening a long document spent most of its time on forced layouts and on work for content nobody was looking at. Six commits remove the largest causes found by profiling a large document on the hosted Chopin. Nothing a person sees or does changes, apart from one disclosed difference: a far-off SeeCode diagram opens on its last step instead of playing its entrance animation while nobody can see it.
What was found
A read-only trace and CPU profiles of opening a large real document on the hosted Chopin (295 blocks, 59 SeeCode diagrams, 42 fenced blocks, 18 decisions, an 82-entry Chat) put Layout at 55% and style recalculation at 12% of the main thread's blocked time. The cost was not the network, Yjs or the size of the update. Each forced layout of that page took about 105 ms, because the editor lays out every fenced block and diagram again. Something forced one whenever the DOM inside the editor changed (Lexical's mutation observer reads the selection at the start of each update it starts), or whenever code read geometry right after a render. The causes, ranked by measured cost:
innerHTMLwritten again on every render, because React re-applies a new{ __html }object.The reported "10 s open with a 9 s task" was not reproduced at full speed on the profiling machine. It matches a machine about six times slower with the same structure. The local seed was enriched until it reproduces the production open to within about 10%.
The commits
Each commit has a test that failed on its parent for the reason the commit describes: a pure decision extracted for
bun test, or a browser spec.Measured
These numbers use the enriched local seed of the large document, warm, at 1x CPU, over a 25 s window, as the median of 3 runs on one machine.
The first line did not move. Before it appears, React mounts the whole document in one commit, followed by about six forced layouts of 35 to 100 ms each. Removing those readers one at a time moves the cost to the next reader. Mounting off-screen blocks later would address it, but this PR doesn't include that. Neither does a Chat change that held back drawing the Chat's history, tried and dropped.
Risks and notes
research-child-surfacechecks that had been written around the fallback now expect the opener, as they did before the opener moved into the Chat. The heading fallback for an opener that really is gone no longer has a browser test.poll-quiet.e2e.ts, this PR's own test, waits for each round's answers, and listens only to its own document's sidebar pushes. Before, it failed now and then because rounds overlapped, and because a neighbouring test changed the shared repository.Assistant-model: Claude Opus 5.5
Assistant-workflow: goal (run ad147134-2902-4536-9b05-f7ad233d7cb7)
Assistant-duration: about 5h converged, estimated 4-6h
Assistant-verification: bun run types and bun run ci passed on the final head
Assistant-verification: full bun test with PostgreSQL (fresh database on 5442) passed: 7993 tests, 0 fail
Assistant-verification: whole CI=1 Playwright suite before the last fixes: 995 passed, 6 failed (poll-quiet, and five research-child-surface focus checks); after them, research-child-surface passed 50 of 50 and poll-quiet 10 of 10 alone and 3 runs of 156 with its neighbours at 6 workers, and planner-picker 41 of 41
Assistant-verification: each of the six commits has a test that failed on its parent; the research-child-surface change was bisected to the Chat Markdown commit, and a probe showed the opener link stays connected on this head and detaches on main
User-preference: Chopin must feel fast; no lag when resizing or switching documents
Co-authored-by: Alex Lavaee lavaman131@github.com