Hazel performance analysis · ^report(14) · 2026-10-14

What the page keeps from one edit to the next

How much memory does a Hazel tab keep as you edit, and what keeps it? ^report(5) listed the forms a view takes on the page at once (its Html value, the tables of values shared with the evaluation worker, the virtual DOM tree last drawn, the DOM) and said that what memory these take had not been measured. This report measures a tab on #2597 (the full livelits branch) while one slider on the Livelit Playground's Full Body slide is moved again and again. The tab keeps about 90 MB more after every edit. The report says what that memory is, which of the page's caches and tables it is not, how that was found where the usual tool fails, and what is still open: which reference keeps it.

Short answers. One slide, Full Body, on #2597 at c4e411f9c2, in headless Chromium; after each slider edit, two forced garbage collections and the page's live JavaScript heap. The method is at the end.

  1. A tab keeps about 92 MB more after every slider edit, and does not level off. Over 15 edits of one slider, the live heap went from 399 to 1,787 MB (+1,388 MB), steadily (Figure 1). At that rate, about 25 more edits would pass 4 GB.
  2. None of the caches is what keeps it. Each was tried in a build of its own: the undo history capped at 2 states, still 92 MB per edit; the projector view cache turned off, 83; the whole-program memos held to one entry, 73; memos and undo together, 71 (Figure 2). The memos account for about 19 MB per edit while they fill up to their bounds, and stop when they are full; nothing tried touches the rest.
    Update, later on 2026-10-14: the undo history is what keeps it; the build "capped at 2" never applied its cap, which needs a setting that is off by default (Update).
  3. No long-lived table grows. A build that counts 14 of the page's tables after every edit (those keyed by ids, and HazelDB's cache) found each the same size after 8 edits as after the first, while the heap grew by 708 MB. ShareCodec's two tables of values shared with the worker rise and fall back as they reset at their cap of 200,000 entries (Figure 3).
  4. What stays is whole versions of the editor's state. V8's sampling heap profiler, over four edits, keeping only what is still alive: 390 MB, made by every stage of an edit. Types and statics 119 MB, ValueHash's hashes of objects 98, terms and syntax 64, the worker's replies 46, layout 41, other 23 (Figure 4). Not one cache's entries: each edit's whole product.
  5. Which reference keeps them is still open. A heap snapshot would show the path from a root to each object kept, but taking one kills this tab's renderer, in two builds of Chromium 155. A test page with a WeakMap of two million live entries never finished its snapshot either, and ValueHash's memo is a WeakMap of about that size; a build without it may let a snapshot through (below).
    Update, later on 2026-10-14: found, with a snapshot: the undo history (Update).
  6. Every weak table here is a JavaScript WeakMap, and none is the cause. A WeakMap's entry lives exactly as long as its key, so ValueHash's 98 MB measures how much old state is alive, not a leak of its own. The fix, once the holder is found, is an owner and a lifetime for what it holds, not a weaker reference (below).

Update, later on 2026-10-14: what keeps the versions is the undo history after all.

The page's live JS heap after each edit (MB)05001,0001,500051015slider edits (Full Body, shirt width)3998311,2981,787+92 MB per edit#2597 at c4e411f9c2; least squares, dashed
Figure 1. The page's live JavaScript heap after each slider edit on Full Body, as built: 399 MB loaded, 1,787 MB after 15 edits, about 92 MB per edit (the dashed line, least squares). Numbers in the table below.
Growth of the live heap per edit, by build (MB)As built#2597 at c4e411f9c292Undo history capped at 2from 250 states92Projector view cache offnever stores a view (6 edits)83Whole-program memos at one entryStatics.mk 16 → 1, MakeTerm 8 → 173Memos at one and undo at 2both together71every build still grows; one run of 15 edits each, except as noted
Figure 2. Growth per edit in builds that each remove a cache or hold it to one entry. Every one still grows; the dashed line is the build as it is. Numbers in the table below.
After each edit, relative to the page as loaded0×1×2×3×02468slider edits, in a build that counts its tables2.8×1×live heap, 397 → 1,105 MBShareCodec: values receivedShareCodec: hashes sent14 other tables: flat
Figure 3. A build that counts its tables, after each edit, relative to the page as loaded: the heap reaches 2.8 times its size; ShareCodec's two tables rise and reset at their cap; the 14 other tables do not grow. Numbers in the table below.
Still alive after four edits, 390 in all, by the code that made it (MB)Types and staticsConstructorMap, Typ, Statics, …119ValueHash's hashesthe memo of each object's hash98Terms and syntaxMakeTerm, IdTagged, TermBase, …64The worker's replies, decodedunmarshaled on the page46LayoutMeasured, Point, Code41OtherOptUtil, ShareCodec, HazelDB, …23every stage of an edit, not one cache: about 98 MB per edit
Figure 4. What is still alive after four edits, by the code that allocated it (V8's sampling heap profiler, live objects only), grouped by Reason source file. Numbers in the table below.

A tab, edit by edit

The slide is the Livelit Playground's Full Body: a figure drawn with the Drawing library and posed by sliders, each a user-defined livelit. The build is #2597 at c4e411f9c2, its head on 2026-10-14, in its development profile, which keeps function names (the profiler below needs them). A driver loads the slide in headless Chromium 155, with precise memory figures and garbage collection exposed, then sets the shirt-width slider to a new value and fires its input and change events, as letting go of a slider does. It waits until the picture has redrawn and four seconds more, forces two garbage collections and reads the page's live heap (performance.memory.usedJSHeapSize). The values run 67, 74, 81 and on, each 7 more, wrapping within 60 to 99, so no two edits in a row are the same.

Every edit adds between 76 and 173 MB, most about 80 to 95, with no sign of levelling off (Figure 1). This is the page's main thread only: the evaluation worker has a heap of its own, not counted here.

EditSlider set toLive heap, MBAdded, MBRedraw, s
loaded399
167483841.87
274564801.93
381657942.26
488751936.8
595831802.18
662912815.97
7691,0201071.77
8761,100817.14
9831,2181172.51
10901,298802.32
11971,379812.31
12641,458792.06
13711,533763.31
14781,7061733.87
15851,787817.47

One run. The run with the undo history capped at 2 (below), which turns out to change nothing, read within 7 MB of this one at every edit and within 1 MB at the end: that is the noise between runs here, well below the differences that matter.

Update, later on 2026-10-14: it changed nothing because its cap never ran, so these were two runs of the same program; the noise estimate stands.

Not the caches

The page has a few caches that could keep whole programs, and each was the first suspect. Each was tried in a build of its own on the same commit, measured over the same 15 edits:

BuildWhat it changesPatchEditsGrowth per edit, MBIn all, MBRedraw, s: median (range)
As builtnothing: #2597 at c4e411f9c2none1591.6+1,3882.3 (1.8–7.5)
Undo history capped at 2History.re keeps 2 states, not 250undo_history_capped_at_2.patch1591.6+1,3872.5 (2.1–7.2)
Whole-program memos at one entryStatics.mk from 16 entries to 1; MakeTerm's three memos from 8 to 1memos_at_one_entry.patch1572.9+1,10122.5 (18.2–31.3)
Memos at one and undo at 2both of the abovenone1571.3+1,09322.8 (19.4–27.3)
Projector view cache offProjectorView.ViewCache never stores a viewview_cache_never_stores.patch6 (read after 0, 3 and 6)82.9+4986.6–8.3, another driver

Redraw: from setting the slider to the picture's markup changing. The view-cache run used the DevTools-protocol driver (below), whose redraws are not comparable with the others'.

Not a table keyed by ids

If no cache keeps the versions, something else does. A read of every module-level store on the page's main thread (in src/web, src/haz3lcore, src/language and src/util at c4e411f9c2: every ref, Hashtbl, map in a ref, memo and WeakMap) found that nothing on the ordinary editing path adds a version per edit by its construction. Every store sized like a program holds one version and is replaced by the next. Nor does any version keep the one before it: CachedSyntax keeps the inputs it was made from, never an earlier CachedSyntax; the editor's old statics are arguments, never stored; an evaluation result's streaming fields are cleared when the result lands; and the evaluator's state and its incremental cache refer to each other, but replay_state unties the two, so caches do not chain.

Update, later on 2026-10-14: the read covered module-level stores, not the app's own model, where the undo history lives: a list in the model, not a store, adds one version per edit (Update).

That left tables keyed by ids: an entry is replaced when its id comes again, but if ids change from edit to edit, old entries stay. A probe build registers each such table with a counter, and the driver reads all of them after every edit (the probe is count_id_keyed_tables.patch). None grows:

TableKey: what it holdsLoadedAfter 1 editAfter 8 edits
Statics.module_livelitsa module's id: the livelits it binds, each with its elaborated definition666
LivelitProj: last_good_shapea livelit's id: its last good shape434343
LivelitProj: shape_samplesa livelit's id: shapes sampled434343
LivelitProj: computed_shapesa livelit's id: its computed shape999
LivelitProj: optimistica livelit's id: a value shown before the worker answers011
LivelitProj: warned_ephemeralids already warned about000
LivelitProj: last_good_viewa livelit's id: its last good virtual DOM, whose handlers close over the statics and the dynamics434343
LivelitProj: view_memoa livelit's id: its definition's elaboration and view424242
MakeTerm: mapthe last parse's ids to terms333
MakeTerm: term_datathe last parse's term data333
MakeTerm: projectorsthe last parse's projectors000
HazelDB's cachea saved key: its value (5,730 KB throughout)666
ProjectorView.ViewCachea projector's id: its view (never pruned)434343
AppStorea projector's id: an app's model, update and view (its gc is never called)000
ShareCodec: values received from the worker, by hashreset at 200,000 entries121,706143,082183,160 (max 185,747)
ShareCodec: hashes of values sent to the workerreset at 200,000 entries119,765140,359139,512 (max 213,234)
Live heap, MB3974801,105

One run of 8 edits on the probe build. optimistic goes from 0 to 1 at the first edit and stays there.

What stays

With every table ruled out, the question becomes what the kept memory is. V8's sampling heap profiler answers by who made it: started after two warm-up edits (HeapProfiler.startSampling, one sample per 32 KB on average, dropping whatever the garbage collector frees) and stopped after four more, it reports the objects still alive, each with the stack that allocated it. The development build's bundle marks each function with its Reason source location, so each stack's innermost Hazel frame names a source file.

Made byMBShareSource files, largest first (MB)
Types and statics11931%ConstructorMap.re 39.0, Typ.re 28.0, Statics.re 26.7, StaticsBase.re 10.9, VarMap.re 3.9, CoCtx.re 2.7
ValueHash's hashes9825%ValueHash.re 97.6
Terms and syntax6416%MakeTerm.re 19.6, IdTagged.re 14.7, TermBase.re 6.9, Skel.re 5.9, TermMap.re 4.8, Aba.re 3.8
The worker's replies, decoded4612%WorkerServer.re 46.0
Layout4110%Measured.re 32.3, Point.re 4.5, Code.re 3.7
Other236%OptUtil.re 8.7, ShareCodec.re 7.4, HazelDB.re 5.6, ListUtil.re 0.3, and the rest under 0.5 MB each
In all390100%over four edits

The 390 MB, about 98 MB per edit, is the growth itself, and it comes from every stage of an edit: the parse into terms, the statics and the types they make, the layout of the code, the evaluation worker's reply as decoded on the page, and the hashes of all of these. Nothing one cache would hold; rather, the editor's whole state as of each edit, kept. The profiler says who made what stays, not who holds it. That takes a heap snapshot.

Why not a heap snapshot

A heap snapshot (HeapProfiler.takeHeapSnapshot, the DevTools Memory panel) records every object and every reference, so it would show the chain of references from a root to the kept versions, and with it the holder. On this page it does not work:

PageResult
Full Body on #2597, as loaded (about 400 MB)the tab's renderer dies at once
Full Body after two edits (about 560 MB)the tab's renderer dies at once
A trivial pagesnapshot taken
A page holding a chain of arrays a million deepsnapshot taken
A page holding a WeakMap with two million live entriesnot finished after 400 seconds

The same in snap's Chromium 155.0.8059.39 and in Chrome for Testing's headless shell 155.0.8059.79, with the renderer's hang monitor off. The last row is the lead: ValueHash's memo is a WeakMap, and by its 98 MB in the profile it holds roughly two million entries (an estimate from bytes per entry, not a count). A build with that memo off, or bounded, may let a snapshot through, and the snapshot would name the holder.

Update, later on 2026-10-14: the lead held. With ValueHash's memo kept only within one synchronous run, a snapshot of Full Body after six edits completes in snap's Chromium: 2.3 GB of JSON in 75 s (Update).

The weak tables

Weak references are the usual suspect when memory is kept by a cache, so all of them were checked. Every weak structure in #2597 is a JavaScript WeakMap, used through js_of_ocaml; there is no OCaml Weak or Ephemeron anywhere.

WhereKey and valueVerdict
ValueHash's memo (util/ValueHash.re)any block, and its hashsmall values; its 98 MB above is a symptom of the objects living
ValueHash.Pairs (Equality.re)a term, and the term compared with it and the answer: it holds the other term stronglymade fresh for each equality check, so it goes with the check
TypHash (term/TypHash.re)a type, and its hashsmall
IdentityTable in CallMemo (dynamics/CallMemo.re)a function body, and its id-free form or its remembered calls (8 per body)bounded per body; an entry's inputs could reach an environment
IdentityTable in Ascriptionsa type, and its unrolling; a value, and a short list (reset)small
IdentityMemo (util/IdentityMemo.re)an object, and a function's result on itfine for a pure function; non-objects are not kept

A WeakMap's entry lives exactly as long as its key. That is the trouble with weak tables in general: what they keep depends on what keeps their keys, and that is written nowhere. So the fix, once the holder is found, is not a weaker reference. It is an owner and a lifetime: entries for the program shown now, dropped when the program moves on. Matthew's lesson from Adapton is the same: working within OCaml's garbage collector through weak pointers was a headache and error-prone, and the later memo system, with its demanded computation graphs, was built in Rust, with no garbage collector to work around.

Found on the way

The read of every store turned up five things that do not cause this growth but are worth a look of their own:

What next

  1. Snapshot without ValueHash's memo. Build #2597 with the memo off (or bounded), check that the growth is unchanged, and take a heap snapshot after a few edits. If it completes, its retainer paths from a kept version to a root name the holder.
  2. Find a smaller slide that grows the same way, so that a snapshot fits even with the memo: one whose slider edits a livelit's model, measured as here.
  3. Look outside the program's values: closures the browser holds on the render path (event listeners, timers, observers) keep everything they close over, and a snapshot would show them as roots.
  4. Measure the same on #2635 (livelits with modular editors) and on dev, to see whether the growth is #2597's own.
  5. Fix it with an owner and a lifetime for whatever holds the versions, and measure the same 15 edits again.

Update, later on 2026-10-14: steps 1, 3 and 4 are done and step 2 is not needed (Update): the snapshot worked; what the browser holds does not grow; #2635 keeps about 8 MB per edit and falls back, and dev has no Livelit Playground slides to measure but has the same undo history. Step 5 is Andrew's compaction, ported.

Update, later on 2026-10-14: the undo history

What keeps the versions. The page's undo history, History.Model.undo_stack in src/web/app/History.re, is a list of whole page models: before every edit, the page as it was, with every slide's editor and that editor's syntax, statics and layout. On Full Body that is about 90 MB per edit, kept by nothing else. The list is meant to stop at 250 states, but it is cut only when the setting cap_undo_stack is on, and Settings.re sets it off. So the history grows with every edit, and the build above that "capped it at 2" changed the 250 that this code never reaches.

The live heap kept after each edit, less the page as loaded (MB) 0 200 400 600 800 1,000 0 2 4 6 8 10 12 edits of the shirt-width slider #2597 as built, edit 1: +84.0 MB #2597 as built, edit 2: +171.9 MB #2597 as built, edit 3: +265.9 MB #2597 as built, edit 4: +345.7 MB #2597 as built, edit 5: +428.6 MB #2597 as built, edit 6: +508.3 MB #2597 as built, edit 7: +609.6 MB #2597 as built, edit 8: +690.3 MB #2597 as built, edit 9: +769.3 MB #2597 as built, edit 10: +886.5 MB #2597 as built, edit 11: +966.8 MB #2597 as built, edit 12: +1048.1 MB #2597 as built: +1,048 MB after 12 edits #2597, undo always capped at 2, edit 1: +83.8 MB #2597, undo always capped at 2, edit 2: +163.9 MB #2597, undo always capped at 2, edit 3: +249.7 MB #2597, undo always capped at 2, edit 4: +259.2 MB #2597, undo always capped at 2, edit 5: +259.0 MB #2597, undo always capped at 2, edit 6: +264.1 MB #2597, undo always capped at 2, edit 7: +234.3 MB #2597, undo always capped at 2, edit 8: +244.3 MB #2597, undo always capped at 2, edit 9: +253.1 MB #2597, undo always capped at 2, edit 10: +287.4 MB #2597, undo always capped at 2, edit 11: +292.5 MB #2597, undo always capped at 2, edit 12: +284.1 MB #2597, undo always capped at 2: +284 MB after 12 edits #2597 with the compaction ported, edit 1: +76.2 MB #2597 with the compaction ported, edit 2: +142.0 MB #2597 with the compaction ported, edit 3: +165.9 MB #2597 with the compaction ported, edit 4: +156.7 MB #2597 with the compaction ported, edit 5: +150.8 MB #2597 with the compaction ported, edit 6: +162.5 MB #2597 with the compaction ported, edit 7: +167.2 MB #2597 with the compaction ported, edit 8: +178.1 MB #2597 with the compaction ported, edit 9: +187.7 MB #2597 with the compaction ported, edit 10: +140.7 MB #2597 with the compaction ported, edit 11: +147.5 MB #2597 with the compaction ported, edit 12: +158.6 MB #2597 with the compaction ported: +159 MB after 12 edits #2635, which has it, edit 1: +9.2 MB #2635, which has it, edit 2: +17.5 MB #2635, which has it, edit 3: +25.7 MB #2635, which has it, edit 4: +33.7 MB #2635, which has it, edit 5: +43.5 MB #2635, which has it, edit 6: +51.4 MB #2635, which has it, edit 7: +59.3 MB #2635, which has it, edit 8: +67.5 MB #2635, which has it, edit 9: +75.4 MB #2635, which has it, edit 10: +93.6 MB #2635, which has it, edit 11: +46.0 MB #2635, which has it, edit 12: +51.9 MB #2635, which has it: +52 MB after 12 edits
The same driver as Figure 1, 12 edits per run, in snap's Chromium 155. Hover a point for its value.
BuildWhat it changesRunsKept after 12 edits, MBGrowth per edit, edits 4–12, MBRedraw, s: median (range)
As built#2597 at c4e411f9c2, nothing changed11,04889.02.41 (2.14–7.59)
Undo always capped at 2History.re cuts the history to 2 states whatever the setting (a probe, not a fix)2284 and 2844.4 and 4.22.21 (1.82–6.68); 2.29 (2.01–6.62)
Compaction portedAndrew's 6010128b7d, adapted: d41c7a0298 on #2597, measured as the same change alone on c4e411f9c21159-0.42.36 (1.92–6.43)
Compaction, the newest entry kept wholeon top of the port, the newest entry of each stack keeps its page model whole until the next is pushed: f8ae6f2b9c on #2597, measured likewise12083.52.34 (2.23–7.15)
#2635livelits with modular editors, at 9fb2bdf067: has Andrew's fix1521.60.99 (0.91–1.58)

Growth per edit is the slope from the 4th edit to the 12th, after the history has filled to its cap in the capped build. In every #2597 build redraws range from about 2 to 7 s, as in Figure 1, and do not separate the builds. #2635 was measured over 30 edits: it keeps 9 MB after the first and 94 MB after the tenth, falls back to 46 MB at the eleventh, and does so again at the 22nd; the likely cause is ShareCodec's tables starting over at their cap, not confirmed.

How it was found. In five steps, each taking one suspect off the list:

  1. Not what the browser holds. Over six edits the heap grew from 400 to 900 MB while the page's event listeners (609), DOM nodes, detached ones included (47,923), observers and timers stayed exactly the same (counted with Memory.getDOMCounters and wrappers installed before the page's own scripts).
  2. Not on #2635. The same sampled profile as Figure 4, over the same four edits, finds 37.5 MB kept on #2635: the worker's replies 14.5, ValueHash 9.8, ShareCodec 5.4, and almost nothing in types, statics, terms or layout, where #2597 keeps 390 MB.
  3. A snapshot, at last. A probe build keeps ValueHash's memo only within one synchronous run (it starts over on the next microtask). The tab still grows 68 MB per edit, and a snapshot after six edits completes: 2.3 GB of JSON in 75 seconds.
  4. Two snapshots, one session. V8 keeps an object's snapshot id for the life of the tab, so the objects in a snapshot after edit 6 whose ids a snapshot after edit 3 lacks were made by edits 4 to 6: 12.2 million objects, 257 MB. A dominator tree of the second snapshot (29 million nodes, computed in 4.6 s by a small C program) shows three copies of one 35 MB structure, Full Body's editor with its derived data. Two hang from the first and second entries of the undo history (Logged.Model.current, then undo_stack, then each entry's page model, its list of slides, and Full Body). The third is the projector view cache's one view, which is replaced, not added to.
  5. Confirmed by a build: the undo history always cut to 2 states, the same 12 edits, twice (the table above).

The fix is Andrew's. Andrew Blinn met the same growth on the modular editors, on a much larger program, and fixed it in 6010128b7d (2026-08-26, "memory: kill the exponential co_ctx leak + compact undo snapshots"): each page model saved for undo has its syntax replaced by a small shared placeholder marked out of date, its statics and evaluation state emptied, and its result reset (since d93e482166, also his, it keeps what autosave keeps); undo rebuilds them from the program. And the cap always applies. That is why #2635, which has #2469 merged in, does not grow. dev has the same undo history as #2597.

Ported to #2597 (d41c7a0298; #2597's slide list is simpler than the modular editors', and the rest follows Andrew's code, less two caches #2597 does not have, as it stands on #2635), the tab keeps 159 MB after 12 edits, level from the third. The six Undo tests pass, as changed by Andrew to check the restored program text rather than the very same model. In the browser, an edit, an undo, a redo, a second edit and another undo restore the same picture and slider value as as built does, in five runs of each, alternating between the builds (median and range; on #2597 the five with the newest entry kept whole ran later, alternating with five more as built, which agree with these; the two #2635 rows ran one after the other):

BuildRunsEdits, sUndo, sRedo, sUndo after a second edit, sRestoresConsole errors
#2597 as built56.7 (6.3–7.2)2.6 (2.6–2.6)3.0 (2.7–3.1)2.6 (2.5–2.7)yes, every step387
#2597, compaction ported56.9 (6.2–8.4)5.8 (2.0–5.9)6.1 (6.0–6.5)3.0 (2.5–6.5)yes, every step828–882
#2597, the newest entry kept whole too56.6 (6.1–7.4)2.6 (2.5–2.6)3.0 (2.5–3.0)2.6 (2.5–3.2)yes, every step387–414
#2635, as it was (653ebc4adf: Andrew's compaction)51.2 (1.1–1.4)3.6 (1.2–3.9)3.9 (3.6–4.2)3.9 (1.1–5.1)yes, every step378
#2635 with the newest entry kept whole (032fe3aca6)51.1 (1.1–1.7)0.3 (0.2–0.3)0.3 (0.2–0.3)0.3 (0.3–0.3)yes, every step120

Seconds from the key press (or the slider's change) to the picture's markup changing, read by the DevTools-protocol driver (undo_check.mjs), whose times run longer than heap_per_edit.py's, as in the caches table: compare them within this table only. On #2597, two of the as-built undos, and two of those with the newest entry kept whole, read 0.24 s or less where every other step there took 2.5 s or more: the driver catching a redraw from before the key press, so they are left out. On #2635 with the newest entry kept whole, every step reads 0.25 to 0.31 s: the redraw is there at the check's second look, 0.3 s after the key press. The console errors are one message, "HazelDOM: unrecognized attribute: Disabled", in every run, as built too; it comes about twice as often with the compaction, which fits more redraws.

What it costs. An undo now rebuilds what the saved state dropped. With the compaction, every redo took 6.0 to 6.5 seconds, about an edit's time, against 2.7 to 3.1 as built; half the undos did too (5 of 10, 5.8 to 6.5 s), and the other half took 2.0 to 3.0 s, as built does. Which undos stay fast was not examined; the whole-program memos, which remember the last few programs, may still hold the program being restored. So f8ae6f2b9c, also on #2597, keeps the newest entry of each stack whole, compacting it only when the next is pushed above it: the latest undo and redo then take what they take as built (median 2.6 s over five runs), for one page model more, 208 MB kept after 12 edits instead of 159. An undo further back still rebuilds. Andrew's note on the cap names the step after this one: undo entries that keep the program alone, not a page model.

#2635 and dev. #2635 had Andrew's compaction alone: every redo took 3.6 to 4.2 s and most undos 3.5 to 5.1 s, about three edits' time (an edit 1.2 s there, five runs); with the newest entry kept whole (032fe3aca6, on #2635 since 2026-10-15), every undo and redo is done within 0.3 s, the check's polling interval, and the memory kept over 12 edits is the same within about 1 MB (the undo times in the table above, the memory in the report's data). dev, which has the same undo history, is left to #2469's merge, which brings Andrew's fix (Matthew's call, 2026-10-15).

The update's data, in 014/data/update-2026-10-14/: each run's readings, the undo checks, the listener and node counts, #2635's profile by file, and, in holder/, the dominator reports and the tools (the snapshot index, the dominator tree in C, the drivers). The snapshots themselves, 4 GB, are not kept. In this page, under update_2026_10_14 in the report's data.

How this was put together