docs(skills): record the measurement traps this investigation hit

Every wrong turn in this work came from trusting a number whose validity had
not been established: a throttled renderer reporting zero rendering work, a
trace category that was never enabled reporting zero long tasks, a scenario
that rendered nothing reporting a perfectly quiet profile, and an 'after'
measured without a matching 'before', which made a no-op change look like a
fix.

The performance skill now puts measurement validity ahead of measurement,
requires a baseline from the unchanged build through the identical scenario,
directs native work to the timeline trace rather than the sampler, requires
unvalidated changes to be reverted and recorded as rejected, and says when to
stop optimising a path that is already inside budget. It also points at the
repository's capture commands.

The sync skill gains the invariant behind the cache-thrash loop: an entry
acquired during render but protected only after commit is unprotected for the
whole render pass, capacity should be a soft target, eviction must not run on
the acquisition path, and raising a limit relocates a cliff instead of removing
it.
This commit is contained in:
Bohdan Triapitsyn
2026-08-03 17:05:56 +03:00
parent 262b1eb18a
commit d3576117da
2 changed files with 122 additions and 0 deletions
@@ -97,6 +97,28 @@ For streaming-frequency work, also load `performance-engineering`.
- Key runtime-scoped caches by runtime identity when IDs or paths can collide.
- Clean optimistic and local cache state after partial failures.
### Never Evict What Is In Use
An entry acquired during render but protected only after commit is unprotected
for the whole render pass. Eviction that runs on acquisition therefore disposes
entries that are actively mounting; the next render recreates them in a loading
state, which issues another fetch, which repeats forever. The symptom is an
endless request loop and sawtoothing listeners, heap, and CPU, and it appears
only once live entries outnumber the limit, so it never reproduces on a small
workspace.
- Define what protects an entry from eviction, and prove that protection is in
place before eviction can observe the entry, not one commit later.
- Treat capacity as a soft target. Overflowing briefly is always cheaper than
evict/recreate cycles; bound the cache with idle-time eviction instead.
- Never run an eviction scan on the acquisition path. Coalesce it into one
deferred pass so a render mounting many entries scans once, not once per
entry.
- Keep explicit lifecycle edges, such as the last consumer releasing an entry,
synchronous. Deferring those changes an observable contract.
- Raising a limit is a workaround, not a fix. It relocates the cliff and hides
the loop from everyone whose workload is smaller than the new number.
## Persisted Snapshot Ordering
When state exists in memory and one or more persistent stores, define an explicit authority and ordering protocol:
@@ -136,5 +158,6 @@ Cover the relevant lifecycle, not only static state:
- New session lookup assumes SSE already indexed it.
- Optimistic data has no shadow entry or rollback.
- Snapshot-difference cleanup treats its first startup snapshot as a disappearance event.
- Eviction runs on the acquisition path, or a cache limit is raised in response to a request loop.
- Missing or malformed persistence becomes authoritative empty state.
- Debounced writes are canceled on owner/lifecycle change without completing against the captured owner or an explicit durability/data-loss contract.