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.
- 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. - 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.
- 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. - 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.
- 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.
c07bdacead (median of three)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.
| Build | Scenes, first picture | Witch, first picture | When |
|---|---|---|---|
| #2597, before the worker's slices ran back to back | 297 s | 172 s | 2026-10-12 morning (finding 25) |
| #2597, after it | 301 s | finding 25 | |
| #2597, after the view's casts were fixed | 286 s | finding 28 | |
| #2597, with all three of the day's view fixes | 281 s | ^report(4) | |
#2597 at 1fa1384445, tonight | 287.5 s | 173.1 s | 2026-10-12 evening |
#2635 at 57c928cef4, tonight | 11.4, 13.2, 12.5 s | 11.8 s | 2026-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:
- The page's main thread was busy for all of it, 353 s. 97% of that was the app's update loop, one update after another.
- The page handled 2,520 messages from the worker, about 140 ms of work each. Receiving them took only 0.5 s in all. The time went to the update each one set off.
- The worker was idle 98.7% of the time. Its evaluation was over within seconds. The rest of the five minutes was the page working through the messages.
Within those updates:
| Part of the page's work | Share 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 scratch | 15% |
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:
- 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.
- 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:
- #2597's request to the worker has new fields. They were kept, and the lazy projection is forced only where a request is sent, or where a finished result is checked for reuse.
- #2597 now holds the last stream for the result message at completion. That was kept, so only the throttled sending during the evaluation came from the commit.
The copy built cleanly. Cold, the same way as above:
| Build | Scenes, first picture | Witch, first picture |
|---|---|---|
| #2597 | 287.5 s | 173.1 s |
#2597 with c07bdacead | 17.6, 16.6, 17.4 s | 19.4, 18.9, 18.6 s |
| #2635 | 11.4, 13.2, 12.5 s | 11.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:
- The adaptive cadence (
a05f8e5791): fewer messages again for a large program. The Scenes program is large, so it should help here. - The code view in cached chunks: a message redraws only the chunks whose results changed. On #2597 every message rebuilds every tile, which is most of what's left.
- Statics per item, updated rather than redone (
^report(1)).
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
- Bring
c07bdaceadto #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 asa15c6f0b65, 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. TheWorkerServer encodings,IncrSlidesandEvaluatorgroups pass (StreamInterestsis not on #2597). - Then the adaptive cadence, taken out of
a05f8e5791on its own, measured the same way. - Check that a Kids' Choice release is unchanged on the copy, as expected.
How this was put together
- Builds: #2597 at
1fa1384445, #2635 at57c928cef4, and the copy of #2597 withc07bdacead(local commit0f64f97a10), all dev builds, served locally. - Times: headless Firefox over WebDriver, a fresh profile per run, the slide opened at
/fresh. A MutationObserver in the page recorded the time (since navigation) when ansvgwith more than 40 shapes first appeared. One run at a time on one laptop. - Profiles: the Gecko profiler, started before navigation and saved a few seconds after the first picture. Saving mid-load failed: the busy page could not hand over its samples, the same busyness finding 25 met. #2597's six-minute load was sampled every 4 ms, the copy's every 1 ms. Shares are from the page's main thread. The message count is the page's
messageevents from the worker, during the whole load. - The cause was first suggested by reading: the two branches' stream code differs in exactly this commit. Then the experiment confirmed it.
- 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 in007/data/.