Hazel performance analysis · ^report(11) · 2026-10-13
Naming what a slider leaves alone
How close could Hazel get to evaluating only what a slider change touches? ^report(10) found, for one slider on each of three Livelit Playground slides, that 75 to 84% of the steps Hazel evaluates after the edit are in calls whose inputs did not change, with calls named by their position in the evaluation. This report asks the same of every slider on those slides, and asks how the answer depends on how the calls are named, taking the design space of names from the merge sort in Fumola, Matthew's Adapton language, where one computation is named three ways.
Short answers. Preliminary, like 010: counts of evaluation steps, from an oracle that watches and reuses nothing, with every check free. Three measures (below): the picture diff, the shapes an edit changes, a diff of the result; the trace distance, the fewest steps any update could evaluate again, a diff of the computation; and an update's cost, the steps a given update does evaluate again, with its cache and its names. The last two are shares of a run from scratch, and they need not follow the first.
- A slider's picture diff can be empty, or most of the picture. Of 63 edits, one per slider, 22 have an empty picture diff and 6 change how many shapes there are; among the rest the picture diff runs from 2 to 85% (Figure 1).
- The picture's representation sets its diff. Hazel's pictures are flat lists of shapes in absolute coordinates, so moving a part changes every shape in it and hung from it, up to 113 shapes for one slider. In a picture of groups with transforms, every slider edit that keeps the number of shapes would change at most 14 nodes, at best, except Scenes' red eyes: 26 shapes all scaled alike, 25 nodes in a tree of groups, one with a shared symbol (Figure 5).
- What Hazel reuses today depends on where a slider's dependents sit. On Full Body and Witch, the whole figure is one binding that depends on every slider, so every slider edit costs today's incremental run 85 to 95% of a run from scratch, even one with an empty picture diff. On Scenes, the figure's sliders cost 5%: the woods are their own binding, and Hazel reuses them. The sliders the woods read cost 92%, and the blink rate, which the picture never reads, nothing.
- An update keyed by position would cost little: below the picture diff or within ten points of it, except on the eye sliders. On top of Hazel's own reuse, it would cost 0.0 to 1.7% of a run from scratch for the 22 edits with an empty picture diff. Of the 35 others, 15 would cost less than their picture diff and 8 at most five points more; 12 would cost more: 8 eye-slider edits, by 30 to 38 points, and 4 others, by 6 to 10 points. The eye sliders are the exception, the clearest case of the trace distance parting from the picture diff: their picture diff is 2 to 14%, and both their estimated trace distance and the cost of an update keyed by position are 32 to 51% (below).
- How calls are named matters only when an edit changes which calls are made. On the 57 edits that only change values, an update keyed by position costs at most 0.1 points more than one keyed by content, the estimate of the trace distance; on the 6 that change which calls are made, it costs 1.3 to 4.9 points more (Figure 3).
- Naming a call for its operand is the best identity name we tried, as naming a merge for the pair it compares is in Fumola: on the structural edits it costs 0.0 to 1.3 points more than content keys, against 1.3 to 4.9 points by position, and on value edits the same as position. Naming a call for its caller's inputs is the worst: it renames everything under a caller whose inputs moved.
- Reads instead of mentions add little, and lists as named cells a little more: recording reads lowers the update cost by at most 0.8 points of a run from scratch; treating lists as named cells lowers it further by 0.6 points at the median, and by at most 5.1 points, on the Witch slide, whose picture has 133 shapes (Figure 2).
- Every scheme's control is clean: the same program evaluated twice matches every call under every name, once names that would collide are told apart by their order.
At a glance
Picture diff, trace distance and update cost
An edit can be measured by what it does to the picture, and by what it does to the computation that draws the picture. The second is found by comparing the edit's evaluation with the one before it step by step: the steps of the new run that match steps of the old one, the same work with the same inputs, need not be taken again. Three quantities follow:
- Picture diff: the shapes of the picture the edit changed, as a share of the picture's shapes, found by comparing the two pictures shape by shape (as in
^report(10)). It is a diff of the result, not of the computation, and counts shapes, not steps. - Trace distance: the fewest steps any update could evaluate again, under the best matching of the new run's trace to the old one's, in the sense of Acar and colleagues' work on self-adjusting computation. It belongs to the program and the edit, not to any cache. It cannot be measured directly; here it is estimated from above, by matching whole calls by their content (any call before with the same code and the same inputs), and, more finely, by matching each call's own steps by what it reads.
- Update cost: the steps an actual update evaluates again, given its cache, how its cache names what it keeps, and how it decides what to evaluate again. Hazel's update today caches top-level bindings by the editor's ids; the updates measured here add a memo of calls keyed by position, by caller, by operand or by content. An update's cost is never below the trace distance; the gap is what its caching and its names lose.
The picture diff and the trace distance are different diffs, and they need not follow each other. They would, if every shape were computed on its own at a cost in proportion to the shape: the diagonal of Figure 1. A slider can feed computation that sits between it and the picture, a fit or a check over many parts, and then the trace distance exceeds the picture diff: the eye sliders, whose outline every check of the mouth against the eyes walks again (below). Or it can change many shapes that are cheap to compute again, and then the trace distance falls short of it: the shirt's width. For example, Full Body's eye size: a picture diff of 7 of 49 shapes (14%), an estimated trace distance of 51% of a run from scratch; and Full Body's shirt width: a picture diff of 39 of 49 shapes (80%), an estimated trace distance of 21% of a run from scratch.
^report(10) measured all three without naming them: its "shapes changed" is the picture diff; its "steps a memo by name skips" are a run's steps less an update's cost, and its "left to run" is the update cost of a memo keyed by position.
Every slider
Each slider on the Full Body, Witch and Scenes slides was moved by a fifth of its range (down, if up would leave the range), with its literal's id kept, as a slider's commit keeps it. How far a slider moves does not set its picture diff; which shapes depend on it does. Moved by one instead, Full Body's shirt width, arm length and hand size change the same 39, 30 and 24 of its 49 shapes. Each slide was cut at its picture, as in ^report(10), and evaluated before the edit, then after it twice: from scratch, and incrementally against the first run's cache, as the eval worker does. Every call of the two runs after the edit was compared with the run before.
| Slide | Slider | Edit | Picture diff | Hazel's update, steps | Update cost, by position | by operand | by content |
|---|---|---|---|---|---|---|---|
| Scenes | figure size | 100 → 138 | 23 of 570 | 27,982 | 5% | 5% | 5% |
| Scenes | across | 0 → 120 | 23 of 570 | 27,983 | 5% | 5% | 5% |
| Scenes | up, down | 0 → 76 | 23 of 570 | 27,984 | 5% | 5% | 5% |
| Scenes | leg size | 100 → 156 | 0 of 570 | 489,105 | 0% | 0% | 0% |
| Scenes | hut nearer | 0 → 20 | 18 of 570 | 489,105 | 3% | 3% | 3% |
| Scenes | eye size | 70 → 90 | 26 of 570 | 489,105 | 10% | 10% | 10% |
| Scenes | blinks a minute | 30 → 78 | 0 of 570 | 41 | 0% | 0% | 0% |
| Full Body | shirt width | 66 → 88 | 39 of 49 | 108,558 | 21% | 21% | 21% |
| Full Body | shirt height | 146 → 202 | 9 of 49 | 108,048 | 5% | 5% | 5% |
| Full Body | neck | 28 → 40 | 9 of 49 | 105,197 | 9% | 9% | 9% |
| Full Body | shoe size | 100 → 158 | 4 of 49 | 104,189 | 3% | 3% | 3% |
| Full Body | arms (up / down) | 55 → 127 | 30 of 49 | 104,193 | 17% | 17% | 17% |
| Full Body | arm length | 95 → 213 | 30 of 49 | 104,189 | 17% | 17% | 17% |
| Full Body | arm width | 16 → 27 | 6 of 49 | 104,189 | 2% | 2% | 2% |
| Full Body | hand size | 14 → 30 | 24 of 49 | 104,189 | 23% | 23% | 23% |
| Full Body | human fingers | 5 → 25 | 49 → 129 shapes | 184,214 | 52% | 51% | 50% |
| Full Body | alien fingers | 3 → 23 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | star points | 5 → 25 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | star turn (% to the next point) | 0 → 20 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | claws | 4 → 24 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | claw length | 100 → 276 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | hand angle | 0 → 72 | 24 of 49 | 110,025 | 20% | 20% | 20% |
| Full Body | eye size | 50 → 117 | 7 of 49 | 104,025 | 51% | 51% | 51% |
| Full Body | eye gap | 38 → 78 | 4 of 49 | 102,429 | 45% | 45% | 45% |
| Full Body | eye shape (round / narrow) | 0 → 20 | 2 of 49 | 104,189 | 42% | 42% | 42% |
| Full Body | eye angle | 0 → 24 | 2 of 49 | 104,029 | 42% | 42% | 42% |
| Full Body | iris size | 0 → 30 | 49 → 51 shapes | 112,424 | 56% | 54% | 54% |
| Full Body | eye whites | 0 → 30 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | shine size | 45 → 75 | 2 of 49 | 104,189 | 6% | 6% | 6% |
| Full Body | smile | 85 → 145 | 8 of 49 | 104,189 | 15% | 15% | 15% |
| Full Body | mouth open | 0 → 60 | 49 → 54 shapes | 134,282 | 29% | 28% | 27% |
| Full Body | spiky teeth | 5 → 8 | 0 of 49 | 104,189 | 1% | 1% | 1% |
| Full Body | tongue length | 100 → 158 | 0 of 49 | 104,189 | 2% | 2% | 2% |
| Full Body | tongue width | 100 → 144 | 0 of 49 | 104,189 | 2% | 2% | 2% |
| Witch | shirt width | 66 → 88 | 113 of 133 | 176,681 | 37% | 37% | 37% |
| Witch | shirt height | 146 → 202 | 85 of 133 | 176,171 | 19% | 19% | 19% |
| Witch | neck | 28 → 40 | 17 of 133 | 171,878 | 15% | 15% | 15% |
| Witch | arms (up / down) | 55 → 127 | 30 of 133 | 170,874 | 17% | 17% | 17% |
| Witch | arm length | 95 → 213 | 30 of 133 | 170,870 | 17% | 17% | 17% |
| Witch | arm width | 16 → 27 | 6 of 133 | 170,870 | 6% | 6% | 6% |
| Witch | hand size | 14 → 30 | 24 of 133 | 170,870 | 21% | 21% | 21% |
| Witch | human fingers | 5 → 25 | 133 → 213 shapes | 256,735 | 44% | 44% | 42% |
| Witch | alien fingers | 3 → 23 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | star points | 5 → 25 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | star turn (% to the next point) | 0 → 20 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | claws | 4 → 24 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | claw length | 100 → 276 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | hand angle | 0 → 72 | 24 of 133 | 176,706 | 19% | 19% | 19% |
| Witch | eye size | 50 → 117 | 11 of 133 | 170,706 | 42% | 42% | 42% |
| Witch | eye gap | 38 → 78 | 4 of 133 | 169,110 | 34% | 34% | 34% |
| Witch | eye shape (round / narrow) | 0 → 20 | 2 of 133 | 170,870 | 32% | 32% | 32% |
| Witch | eye angle | 0 → 24 | 2 of 133 | 170,710 | 32% | 32% | 32% |
| Witch | iris size | 0 → 30 | 133 → 135 shapes | 179,251 | 50% | 45% | 45% |
| Witch | eye whites | 0 → 30 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | shine size | 45 → 75 | 2 of 133 | 170,870 | 9% | 9% | 9% |
| Witch | witch nose length | 100 → 174 | 4 of 133 | 170,870 | 13% | 13% | 13% |
| Witch | cat size | 100 → 298 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | broom size | 100 → 298 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | smile | 85 → 145 | 16 of 133 | 170,870 | 19% | 19% | 19% |
| Witch | mouth open | 0 → 60 | 133 → 138 shapes | 201,329 | 28% | 24% | 23% |
| Witch | spiky teeth | 5 → 8 | 0 of 133 | 170,870 | 1% | 1% | 1% |
| Witch | tongue length | 100 → 158 | 0 of 133 | 170,870 | 2% | 2% | 2% |
| Witch | tongue width | 100 → 144 | 0 of 133 | 170,870 | 2% | 2% | 2% |
- 22 edits have an empty picture diff, changing no shape at all: the alien fingers, stars and claws of hands that are human; the spikes of teeth that are not spiky, and the tongue; the whites of eyes with no iris; the witch's cat and broom; Scenes' leg size and blink rate. On Full Body and Witch, they still cost Hazel's update 85 to 92% of a run from scratch, since the binding that builds the figure depends on the slider, whatever it draws; on Scenes, the leg size costs 92%, and the blink rate, which the picture never reads, nothing. Keyed by position, an update would cost 0.0 to 1.7%.
- Some change most of the picture: Witch's shirt width, 113 of 133 shapes; Full Body's shirt width, 39 of 49 shapes; Witch's shirt height, 85 of 133 shapes; Full Body's arms (up / down), 30 of 49 shapes.
- 6 edits change how many shapes there are (Full Body's human fingers, Full Body's iris size, Full Body's mouth open, Witch's human fingers, Witch's iris size, Witch's mouth open), and with them which calls are made. These are the edits where names matter (below).
Naming, a design space
A memo, like the paper's demanded computation graph, needs a name for everything it keeps: a name that finds the same work in the next run, and that no other work in the same run claims. Hazel's evaluation needs names in several places, and for each there is a space of choices:
| What needs a name | Hazel today | Choices |
|---|---|---|
| The input: a slider's literal | The editor's tile id, kept while the tile is edited | Keep it. It is what the user edits, so it needs a name that stays put while its value changes: Fumola's rule for a pointer name. |
| A call: a memo entry | None: calls are not cached | By position (the call path and a count), by the caller (its site and inputs), by its operand (its site and its argument), by content (its code and every input). Measured below. |
| A list cell evaluation builds (the picture's list of shapes) | A fresh id at every step | By index, by the cell it came from (the paper's map forks an output cell's name from its input cell's), or by content (^report(9)'s Merkle names). |
| A shape | A fresh id, and a name string the program gives it ("eye", "red eye") | The program's own name, told apart from others of the same name by order: names carried by the data, as Fumola's elements carry symbols. |
A function that captures the slider (Scenes' map is given one) | A fresh id | Its definition site with the names of what it captures; by content, it changes whenever the slider does. |
A worked example: naming a merge
Fumola's merge sort (levelTree.fumola, with its notes on naming) builds a merge network over a balanced tree of the input list and names each cell of each merge in one of three ways. The computation and its answer are the same under all three; only the names differ, and with them what an edit to the input list can reuse:
| Fumola's scheme | A merge cell is named for | Closest Hazel scheme, for calls |
|---|---|---|
pathwise | its position in the recursion: the path from the root of the tree | by position: the call path and a count (^report(10)'s) |
inductive | the tree node it belongs to and the element that won the comparison, in a namespace per node | by the caller: the caller's site and inputs, made unique by order |
cartesian | the pair of elements compared, which no other cell of the network compares | by operand: the call site and the argument's value |
In Fumola, a position survives only as long as the shape it addresses: under an edit that reshapes the tree, pathwise leaves several times as many cells unmatched as the others, and cartesian, whose name is nearly the content of the comparison, bounds the worst case. The notes read all three as approximations of content addressing with identity keys, and give a rule: what is edited gets a pointer name, a name that stays put while its value changes, and what is derived gets a structural name, found again by content. Hazel's picture is derived through and through, with one pointer at its root, the slider.
Two properties make a scheme usable. A name must find the same work in the next run, and it must be unique in a run: Fumola's cartesian names are unique because a merge network never compares the same two elements twice. Hazel's drawing code makes no such promise: the same helper is called from the same site with the same argument under different callers, so names by caller and by operand collide: in one run of Full Body, 1,375 calls share a name by operand with an earlier call, and 609 by caller. As inductive opens a namespace per tree node, each name here is told apart from the others of the same name by its order in the run; a name by position is unique without that.
Four outcomes
Fumola's notes compare namings with a microscope that only names can have. Each call after the edit is equal (a call before had its name and the same inputs), notEqual (its name, other inputs), or onlyRight (a name new to this run); a call before whose name is gone is onlyLeft. A memo can reuse only the equal; notEqual is a call that keeps its identity and changes its value, which a demanded graph could repair in place. Content keys cannot have notEqual at all: under them a changed call is simply a different one.
- The smile: by position, 2,927 calls are equal, 595 keep their name with other inputs, and 42 are new; by caller, 704 are new and as many gone, the calls under every caller whose inputs moved; by operand, 2,927 are equal and the changed calls show as new names (508) rather than notEqual (129), since a changed argument is a changed name.
- The fingers: by position, 1,981 calls are new, since twenty fingers shift every count after them; by operand, 3,012 calls are equal against 2,949 by position.
Reads instead of mentions
^report(10)'s oracle took a call's inputs to be the values of every variable its body mentions. The paper records instead what a thunk actually read, each read with what it saw. Here every variable a call's own steps look up, and the result of every call it makes, is recorded with the hash of its value; a call whose reads all saw what the call of its name saw before is clean, and skips its own steps, while its callees are judged on their own reads. This is the paper's demanded graph, re-checked, in steps.
Summed over the 63 edits, after today's reuse, an update that matches each call's own steps by what it reads would cost 1,678,586 steps, against 1,696,510 for one that compares what the bodies mention: 1.1% less. A function whose body mentions a slider's value almost always reads it.
Lists as named cells
A walk over a list takes the rest of the list at each step, and a rest that holds a changed element is itself changed (^report(10)). The paper's remedy is a name for each cell, so that a step reads one cell and the rest is a reference whose name does not change with what it holds. Here every list a call reads is compared by its head alone, with the rest stood in for by such a name. That is an upper bound on what cell names could save: right for a function that walks a list cell by cell, too generous for one that hands the whole list to a builtin, as @ does.
Over the same edits, with lists as named cells an update would cost at most 1,509,299 steps, 10.1% less than reads. Under reads, a call that walks a list already runs again only its own steps; the work it calls for each element is judged on its own reads, and skipped where the element did not change. Cells save the walks' own steps, and over a picture's long lists those add up: most of the saving is on the Witch slide, where all six of the largest gains are.
Why the eye sliders cost so much
The eye sliders are where the trace distance parts most from the picture diff: Full Body's eye size: a picture diff of 7 of 49 shapes (14%), an estimated trace distance of 51% of a run from scratch; Full Body's eye gap: a picture diff of 4 of 49 shapes (8%), an estimated trace distance of 45% of a run from scratch; Full Body's eye shape (round / narrow): a picture diff of 2 of 49 shapes (4%), an estimated trace distance of 42% of a run from scratch; Full Body's eye angle: a picture diff of 2 of 49 shapes (4%), an estimated trace distance of 42% of a run from scratch; Witch's eye size: a picture diff of 11 of 133 shapes (8%), an estimated trace distance of 42% of a run from scratch; Witch's eye gap: a picture diff of 4 of 133 shapes (3%), an estimated trace distance of 34% of a run from scratch; Witch's eye shape (round / narrow): a picture diff of 2 of 133 shapes (2%), an estimated trace distance of 32% of a run from scratch; Witch's eye angle: a picture diff of 2 of 133 shapes (2%), an estimated trace distance of 32% of a run from scratch. The eyes feed geometry the face computes around them, and the diagnosis below names it. The largest part is the check that the mouth clears the eyes: clear_of_eyes asks eye_below at 41 points along the lip, and each time below_poly's helper go walks every edge of the eye's outline. A new eye size is a new outline, so every one of those walks runs again, whatever the update's names. Then the head grows to fit the eyes (eye_need) and the eyes are drawn again (round_eye): parts of the picture diff.
Added later on 2026-10-13: this is by design. For a small face or body, the head is meant to fit the eyes, so its geometry, and everything connected to it, the hair and the rest, depends on eye size. The eye sliders' trace distance is that dependence, not work an update could avoid.
Full Body, eye size: the functions whose calls run again, by their own steps, and the variables whose values differ most often among those calls.
| Function | Calls | Own steps | Differing |
|---|---|---|---|
go | 943 | 29,765 | go 557, first 557, qs 536, x2 495, x1 495 |
round_eye | 77 | 6,339 | x 77, r 77, px 72, py 67, y 22 |
shift_points | 174 | 5,724 | vs 162, tl 150, v 144, y 140, x 100 |
eye_need | 50 | 4,418 | eye_need 50, eye_half_w 50, eye_half_h 50, eye_dx 50, u 46 |
clear_of_eyes | 42 | 2,882 | eye_below 42, clear_of_eyes 42 |
scale_points | 36 | 1,634 | sy 36 |
skinned | 26 | 1,183 | ps 25, tl 24, p 7 |
on_head | 56 | 1,044 | ps 52, p 50, tl 48 |
Full Body, eye gap: the functions whose calls run again, by their own steps, and the variables whose values differ most often among those calls.
| Function | Calls | Own steps | Differing |
|---|---|---|---|
go | 943 | 28,685 | go 531, first 531, qs 512, x2 490, x1 490 |
round_eye | 77 | 6,339 | x 77 |
eye_need | 50 | 4,418 | eye_need 50, eye_dx 50, u 48, px 48, here 24 |
shift_points | 112 | 3,680 | x 104, vs 104, v 104, tl 96 |
clear_of_eyes | 42 | 2,882 | eye_below 42, clear_of_eyes 42 |
skinned | 24 | 1,091 | ps 23, tl 22, p 4 |
on_head | 56 | 1,044 | ps 52, p 52, tl 48 |
to_head | 52 | 988 | u 52 |
Witch, eye size: the functions whose calls run again, by their own steps, and the variables whose values differ most often among those calls.
| Function | Calls | Own steps | Differing |
|---|---|---|---|
go | 943 | 29,765 | go 557, first 557, qs 536, x2 495, x1 495 |
shift_points | 253 | 8,346 | vs 237, tl 221, v 219, y 215, x 100 |
round_eye | 77 | 6,339 | x 77, r 77, px 72, py 67, y 22 |
dressed | 110 | 5,030 | ps 109, tl 108, p 11 |
skinned | 110 | 4,699 | ps 109, tl 108, p 11 |
eye_need | 50 | 4,418 | eye_need 50, eye_half_w 50, eye_half_h 50, eye_dx 50, u 46 |
hat_verts | 79 | 3,131 | head_lift 79, hat_verts 79 |
clear_of_eyes | 42 | 2,882 | eye_below 42, clear_of_eyes 42 |
Witch, eye gap: the functions whose calls run again, by their own steps, and the variables whose values differ most often among those calls.
| Function | Calls | Own steps | Differing |
|---|---|---|---|
go | 943 | 28,685 | go 531, first 531, qs 512, x2 490, x1 490 |
round_eye | 77 | 6,339 | x 77 |
dressed | 100 | 4,546 | ps 99, tl 98, p 4 |
eye_need | 50 | 4,418 | eye_need 50, eye_dx 50, u 48, px 48, here 24 |
skinned | 100 | 4,245 | ps 99, tl 98, p 4 |
shift_points | 112 | 3,680 | x 104, vs 104, v 104, tl 96 |
clear_of_eyes | 42 | 2,882 | eye_below 42, clear_of_eyes 42 |
on_head | 56 | 1,044 | ps 52, p 52, tl 48 |
How the picture's representation sets its diff
Measured later on 2026-10-13: ^report(12) built Full Body as a picture of groups with transforms and measured it: every slider edit that changes the picture changes at most 14 nodes (this section estimated 14 at best), and the tree, flattened, is today's picture exactly. Not flattened, the trace distance follows: moving a part costs about what an edit that changes nothing costs.
The picture diff counts shapes of the picture as Hazel's Drawing library represents it: a flat list of polygons and paths, every point in absolute coordinates. The library places a part by moving every one of its points (shift_points, on_head, to_head, among the largest functions that run again in ^report(10) and above). So moving a part changes every shape in it, and every shape hung from it: the witch's shirt width changes 113 of 133 of her shapes.
In a picture made of groups with transforms, as with SVG's <g transform> or a scene graph, a part keeps its shapes in its own coordinates, and moving, scaling or turning it changes one transform. To estimate what that would change, every shape an edit changed was compared with its old self: moved (every point translated alike), scaled (and maybe moved), rotated (and maybe scaled and moved), each one transform of the shape's old points; or reshaped, its geometry itself changed. Shapes changed by one same transform would share one changed node, and the others count one each. That is the best case, with the groups lined up with the transforms the edit applies.
| Slide | Slider | Shapes changed | moved | scaled | rotated | reshaped | Distinct transforms | Nodes changed, with transforms |
|---|---|---|---|---|---|---|---|---|
| Full Body | shirt width | 39 | 36 | 1 | 0 | 2 | 5 | 7 |
| Full Body | arms (up / down) | 30 | 0 | 0 | 30 | 0 | 2 | 2 |
| Full Body | arm length | 30 | 24 | 0 | 0 | 6 | 2 | 8 |
| Full Body | hand angle | 24 | 0 | 0 | 24 | 0 | 2 | 2 |
| Full Body | hand size | 24 | 0 | 12 | 0 | 12 | 2 | 14 |
| Full Body | neck | 9 | 8 | 0 | 0 | 1 | 1 | 2 |
| Full Body | shirt height | 9 | 8 | 0 | 0 | 1 | 2 | 3 |
| Full Body | smile | 8 | 6 | 0 | 0 | 2 | 1 | 3 |
| Full Body | eye size | 7 | 0 | 4 | 0 | 3 | 2 | 5 |
| Full Body | arm width | 6 | 0 | 0 | 0 | 6 | 0 | 6 |
| Full Body | eye gap | 4 | 4 | 0 | 0 | 0 | 2 | 2 |
| Full Body | shoe size | 4 | 0 | 4 | 0 | 0 | 2 | 2 |
| Full Body | eye angle | 2 | 0 | 0 | 2 | 0 | 2 | 2 |
| Full Body | eye shape (round / narrow) | 2 | 0 | 0 | 0 | 2 | 0 | 2 |
| Full Body | shine size | 2 | 0 | 2 | 0 | 0 | 2 | 2 |
| Scenes | eye size | 26 | 0 | 26 | 0 | 0 | 25 | 25 |
| Scenes | figure size | 23 | 0 | 16 | 0 | 7 | 1 | 8 |
| Scenes | across | 23 | 23 | 0 | 0 | 0 | 1 | 1 |
| Scenes | up, down | 23 | 22 | 0 | 0 | 1 | 1 | 2 |
| Scenes | hut nearer | 18 | 0 | 18 | 0 | 0 | 1 | 1 |
| Witch | shirt width | 113 | 54 | 56 | 0 | 3 | 8 | 11 |
| Witch | shirt height | 85 | 84 | 0 | 0 | 1 | 3 | 4 |
| Witch | arms (up / down) | 30 | 0 | 0 | 30 | 0 | 2 | 2 |
| Witch | arm length | 30 | 24 | 0 | 0 | 6 | 2 | 8 |
| Witch | hand angle | 24 | 0 | 0 | 24 | 0 | 2 | 2 |
| Witch | hand size | 24 | 0 | 12 | 0 | 12 | 2 | 14 |
| Witch | neck | 17 | 16 | 0 | 0 | 1 | 1 | 2 |
| Witch | smile | 16 | 14 | 0 | 0 | 2 | 1 | 3 |
| Witch | eye size | 11 | 0 | 4 | 0 | 7 | 2 | 9 |
| Witch | arm width | 6 | 0 | 0 | 0 | 6 | 0 | 6 |
| Witch | eye gap | 4 | 4 | 0 | 0 | 0 | 2 | 2 |
| Witch | witch nose length | 4 | 0 | 3 | 0 | 1 | 3 | 4 |
| Witch | eye angle | 2 | 0 | 0 | 2 | 0 | 2 | 2 |
| Witch | eye shape (round / narrow) | 2 | 0 | 0 | 0 | 2 | 0 | 2 |
| Witch | shine size | 2 | 0 | 2 | 0 | 0 | 2 | 2 |
- With groups, transforms and shared symbols, every slider edit is O(1). Every slider edit that keeps the number of shapes would change at most 14 nodes, against up to 113 shapes in the flat list, except the red eyes (next). The shirt's sliders move the parts hung from it: the witch's shirt width, 113 shapes, would be 11 nodes, her shirt height 85 shapes 4. The angle sliders turn parts: the arms, 30 shapes, would be 2 nodes, one per arm.
- The red eyes on Scenes are 26 shapes, every one scaled by the same factor about its own centre (25 distinct transforms, since each eye's centre differs). In a tree of groups each eye's own transform changes; in a picture that defines the red eye once and places it again and again (a shared symbol, as SVG's
<use>), one definition would. - What a representation cannot shrink is a shape whose geometry itself changes: Full Body's hand size, 12 of 24, Witch's hand size, 12 of 24, Scenes's figure size, 7 of 23, Witch's eye size, 7 of 11.
- The computation would shrink too, though this is not measured here. The work of moving every point of a part to place it is the work transforms replace: in a tree of groups, placing a part computes one transform, whatever the part holds. So the representation would bear on the trace distance as well as on the picture diff; measuring that is the first step below.
What next
- A picture of groups with transforms in the Drawing library, and shared symbols for repeated shapes: then measure its picture diff, its trace distance and its update cost on the same edits. Of everything here, it would change the most. Done later on 2026-10-13: for Full Body, in
^report(12). - Edits with diffs in between: a slider's picture diff does not depend on how far it moves, so other edits are needed for the middle of Figure 1: a
^choiceswitched (human hands to alien ones), or one vertex of a^polygonseditor moved, which changes one shape, or the few anchored to it. - Time a memo keyed by operand. It finds what content keys find on value edits and most of it on the others, and its name costs one hash of the argument, already there with
^report(9)'s Merkle names kept on the values. Build it at the call boundary, with names told apart by order, and time it against today's run. - Names carried by the data, for the places where operand names collide: a shape's own name, and list cells named from the cells they came from, as the paper's
mapdoes and as Fumola's elements carry symbols. - Look at what the eye sliders' work computes, and whether its answers could be cut off sooner: the calls whose inputs change but whose results do not. Closed later on 2026-10-13: not needed. The dependence is by design, Matthew notes: for a small face or body, the head is meant to fit the eyes, so its shape, and everything connected to it, the hair and the rest, depends on eye size.
- Edits beyond sliders: typing in a definition, adding a shape, reordering a list. These are the edits that reshape, where Fumola's measurements say names matter most.
How this was put together
- Build: #2635 at
c4e64060f1, with^report(10)'s oracle extended on a local branch, not pushed: the reads of each call (variable lookups and callee results), a localized and an operand name for each call, and names told apart by order. - Edits: every
Drawing.^slideron the three slides, found by itslet's name; its model is the literal the slider's outer call is applied to. Each moved by a fifth of its range. - Runs: node, the test build; each slide cut at its picture, analyzed with
Statics.mk_unmemoized, evaluated withEvaluator.evaluateonce before the edits, again with no edit (the control, from scratch and incrementally), and twice for each edit. Steps are counted as the evaluator takes them; they are deterministic, so one run of each suffices, and every control is clean. - Fumola: the merge sort and its notes at commit
667a34d5f2. - The numbers, for programs too: every table in this report, and what its numbers measure, is also in this page as JSON, in its
<script type="application/json" id="report-data">element, so a reader or an agent that fetches the page alone has the dataset. The same, with any raw files that survive, is in011/data/.