Hazel performance analysis · ^report(7) · 2026-10-12

Five minutes to the first picture

On #2597, the full livelits branch, the Livelit Playground's Scenes slide has taken about five minutes to draw its first picture, and the Witch slide almost three. #2635 draws both in about 12 seconds. ^report(4) reported this, and put it down to #2597 lacking the speedups of #2469 (modular editors). This report asks whether it is really that long, and why, and tries a fix.

Short answers.

  1. Yes, it really is that long. Cold, in a fresh headless Firefox, the Scenes slide's first picture came 287.5 s after the page was opened on #2597, and the Witch slide's 173.1 s. On #2635 they came in 11.4 s and 11.8 s. Earlier measurements agree, and today's three fixes to the picture's view (^report(4)) did not change it.
  2. Why: the page redraws everything for every message from the worker, and #2597's worker sends one after every slice of its evaluation. Opening Scenes, the page received 2,520 messages, and each cost it about 140 ms. About three quarters of that time went to rebuilding the code editor's view of the whole program, and most of the rest to statics, from scratch. The worker was idle 98.7% of the time. It had finished, and the page took five minutes catching up.
  3. The fix already exists, in one commit from #2469: c07bdacead, by Andrew Blinn. The worker sends at most one partial result every 100 ms, and the page stops re-projecting the whole program for each one. In a copy of #2597 with only this commit, Scenes took 16.6–17.6 s instead of 287.5 s, and Witch 18.6–19.4 s instead of 173.1 s (three runs each). The page received 86 messages instead of 2,520.
  4. What's left, 5–7 s against #2635, is #2469's other work: a cadence that slows as programs grow, a code view built in cached chunks, and statics per item. Those have not been measured one at a time.
  5. This is not the slowness ^report(1) found. There, a Kids' Choice slider release took 3.76 s on #2597 against 0.35 s on #2635, because #2597 runs statics on the whole program for every release. This commit does not address that.
287.5 s
#2597, the Scenes slide's first picture
17.4 s
the same, with c07bdacead (median of three)
12.5 s
#2635, which has all of #2469 (median of three)
2,520 → 86
worker messages the page handled while opening Scenes

Is it really that long?

Each run opened the slide in a fresh headless Firefox at /fresh, so nothing saved was read. A watcher in the page noted the moment a picture with more than 40 shapes appeared, so the time is exact even while the page is too busy to answer the test script. One heavy job ran at a time.

BuildScenes, first pictureWitch, first pictureWhen
#2597, before the worker's slices ran back to back297 s172 s2026-10-12 morning (finding 25)
#2597, after it301 sfinding 25
#2597, after the view's casts were fixed286 sfinding 28
#2597, with all three of the day's view fixes281 s^report(4)
#2597 at 1fa1384445, tonight287.5 s173.1 s2026-10-12 evening
#2635 at 57c928cef4, tonight11.4, 13.2, 12.5 s11.8 s2026-10-12 evening

So it is about five minutes for Scenes and three for Witch, and has been all day, whatever was fixed in the picture's view.

Where the five minutes go

Hazel evaluates a program in a web worker, and the page draws it on the main thread (^report(5)). The worker evaluates in slices of 5,000 steps, and after each slice it can send the page the partial results it has so far, so that probes and livelits fill in while the rest is still running. Every message the page receives is an action: the page updates its model, recalculates, and redraws.

A profile of the whole load on #2597, from opening the slide until its first picture, sampled every 4 ms:

Within those updates:

Part of the page's workShare of the main thread
Rebuilding the code editor's view of the whole program (main_view)77%
— of which, turning every tile of the program into the view (of_segment)58%
Recalculating (calculate)19%
— of which, statics on the whole program, from scratch15%

The Scenes slide's program is large, so each of these is expensive. Done 2,520 times, it comes to five minutes. That is also why the day's fixes didn't move it: they made the picture's view cheaper, and this is the code editor's view, redrawn for every message.

The fix, from #2469

Andrew Blinn found and fixed the same thing on the modular-editors branch on 2026-08-25, in c07bdacead, "eval streaming: kill the post-load main-thread burn". On a generated 1,000-item program, the slide was drawn at about 5 s, and the main thread then worked for 40 s more on 307 stream messages. The commit does two things:

  1. The worker sends at most one stream message every 100 ms. Partial results it holds back pile up in its outbox and go in the next message. When the evaluation finishes, everything left goes at once.
  2. The page's projection of the program for the worker is made lazy. It folds over the whole program, and it was computed on every update, including each stream message's, which never used it.

His measurement: 307 messages down to 28, and the main thread's work after opening the slide down from 45.5 s to 5.4 s. A day later, a05f8e5791 made the interval grow with the program, from 100 ms up to 1 s, because a fixed 100 ms still swamped a 2,000-item program (46.7 → 10.2 s). ^report(5) credited the throttle to that second commit alone; it now has a correction.

#2635 is #2597 plus dev plus #2469, so it has both commits. #2597 has neither.

The fix, tried on #2597

c07bdacead was brought onto a copy of #2597 at 1fa1384445, on a local branch (perf/2597-stream-throttle, not pushed). It had two small conflicts, both because #2597 had changed the same code since:

The copy built cleanly. Cold, the same way as above:

BuildScenes, first pictureWitch, first picture
#2597287.5 s173.1 s
#2597 with c07bdacead17.6, 16.6, 17.4 s19.4, 18.9, 18.6 s
#263511.4, 13.2, 12.5 s11.8 s

A profile of the copy's load, the same way but sampled every 1 ms: the page handled 86 messages from the worker instead of 2,520, and its main thread ran the app's code for about 31 s instead of 353. Both of those are profiled times, which run long: under the profiler the first picture came at 348 s on #2597 and 29.5 s on the copy, against 287.5 and about 17 s without it. What remains is the same kind of work, about 60% of it the code editor's view.

What's left

The copy is still 5–7 s behind #2635: Scenes about 17 s against 12.5, Witch about 19 s against 11.8. #2469 has more that bears on a load like this:

These have not been measured one at a time. a05f8e5791 also carries several unrelated changes, so its cadence would have to be taken out on its own.

Not the other #2597 slowness

^report(1) found that a Kids' Choice slider release takes about 3.76 s on #2597 and 0.35 s on #2635. That is a different cost: on every release #2597 runs statics on the whole program from scratch (3.5 s profiled), where #2469 runs them per item. This report's fix is about how often the page redraws while a program loads, so it should leave a release's time as it was. That has not been checked.

What next

  1. Bring c07bdacead to #2597 itself, as tried here, with the worker tests (WorkerServer encodings, StreamInterests) and the slides' load times as the check. It is one commit with two small conflicts. Done later on 2026-10-12: it is on #2597 as a15c6f0b65, with the same resolution measured here. Scenes now takes 16.7 and 16.4 s to its first picture on #2597, and Witch 18.8 s. The WorkerServer encodings, IncrSlides and Evaluator groups pass (StreamInterests is not on #2597).
  2. Then the adaptive cadence, taken out of a05f8e5791 on its own, measured the same way.
  3. Check that a Kids' Choice release is unchanged on the copy, as expected.

How this was put together