memory: the cache that aged the thing it cached

Two lessons from the thumbnail work, the second of which nearly shipped.

We parked progressive loading on a count of images and the cost was in bytes.
'Measure the real booth before optimising it' was followed and still gave the
wrong answer, because we measured the dimension that was easy to measure rather
than the one the user feels.

And a cache living inside the thing it describes can age that thing. Excluding
every path under the cache dir passed its own test and was still wrong: creating
the directory touches the BOOTH's own mtime, which is what _newest_mtime seeds
from. The contents were excluded; the existence was the leak. Had it reached the
Desk, one index load would have pushed every booth's expiry out and the TTL
would never have fired again.
This commit is contained in:
vh
2026-09-23 10:58:21 -07:00
parent ff35023377
commit 70bfff15cf
2 changed files with 57 additions and 0 deletions
@@ -0,0 +1,56 @@
# The cache that aged the thing it cached
_2026-09-23 · booth_
**Thumbnails: 77.5 MB → 0.78 MB on the biggest gallery, ~100 MB → 1.12 MB on
the Desk.** The operator found the defect in about a minute of using the live
redesign. Two lessons, and the second nearly shipped.
## ⚠ LESSON 1 — we parked it on a count and the cost was in the bytes
ROADMAP parked progressive loading on *"the largest gallery is 66 images; at
that size a lazy grid is almost certainly fine"*, and the parking-lot row said
*"270 `<img loading=lazy>` may be fine."* **Both count IMAGES. Neither weighs
BYTES.** 66 is a fine count sitting on 77.5 MB of 1024×1024 PNGs rendered at
250px — roughly 16× the pixels that reach the screen.
The rule "measure the real booth before optimising it" was followed and still
produced the wrong answer, because **we measured the dimension that was easy to
measure rather than the one that determines the experience.** Before parking
anything on a measurement again, ask what the user would actually feel.
## ⚠ LESSON 2 — a cache INSIDE the thing it describes can age it, and excluding the cache is not enough
`.thumbs/` lives inside the booth on purpose, so it is swept with the booth and
can never outlive what it describes. But `_newest_mtime` — which feeds
`is_expired`, which feeds `rmtree` — walks the booth, so **the server writing a
cache on a mere view counted as the operator touching the booth.**
**The first fix passed its own test and was still wrong.** Excluding every path
under `THUMB_DIR` does not help, because **creating the directory touches the
BOOTH DIRECTORY's own mtime**, and `_newest_mtime` SEEDS from exactly that. The
cache's contents were excluded; its *existence* was the leak.
The fix restores the booth's stamp across the `mkdir`. That cannot hide real
activity: any file an agent adds is counted by its own mtime in the same walk,
so the directory stamp is only a seed.
**The blast radius if it had reached the Desk.** The Desk pulls a preview
thumbnail per booth, so ONE INDEX LOAD would have pushed EVERY booth's expiry
out — the TTL would never fire again and nothing would ever sweep. Caught by
design-dev from the outside, hours before the strip landed; verified after the
fix on the live set: **29 booths, two Desk loads, 22 caches generated, 0 clocks
moved.**
`.viewed` counting as activity is different and deliberate — that is a record of
a *person* looking, which U4 says is activity. **A server-written derived cache
is machinery, like the `.lock` sidecars already excluded.** Any future cache
inherits this: exclude it from the clock AND preserve the parent's stamp.
## A third, smaller one: the dot-namespace was only skin deep
`booth_items` and `zip_booth` both tested `p.name.startswith(".")` — the FILE's
name — so `.thumbs/a.png` (name `a.png`) would have rendered as a gallery item
and shipped inside every zip download. CLAUDE.md invariant 2 promises a dotfile
costs nothing in item counts, galleries or zips; **that was true only at the top
level.** Both now skip every dot-prefixed path COMPONENT.