Skip to content

Fix: Preserve subtree liveness across EquatableView cache hits#64

Merged
phranck merged 7 commits into
mainfrom
issue/14-cache-liveness
Jul 22, 2026
Merged

Fix: Preserve subtree liveness across EquatableView cache hits#64
phranck merged 7 commits into
mainfrom
issue/14-cache-liveness

Conversation

@phranck

@phranck phranck commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the full issue #14 plan: cached subtrees can no longer lose descendant effects, and only provably effect-free content is ever served from the RenderCache.

  • Characterization first (EquatableEffectTests, FrameHarness end-to-end): pinned five losses across cache hits — lifecycle slots/tasks, key handlers, status-bar items, focus registrations, nested cache entries. Notable root-cause finding: the first frame's measurement pass stored into the live cache while effect sites were inert, so the same frame's main pass already hit and the subtree's effects never mounted at all.
  • Effect-delta detection + bypass: on every cache miss, EquatableView snapshots the pass's effect-registration probe (key handlers, preference writes, status-bar declarations, staged focus sections/focusables, deferred lifetime-effect records) around the rendering. Any delta flags the identity in RenderCache — flagged identities never hit, so effect-bearing subtrees render every frame and .equatable() becomes safe to apply anywhere. Measurement passes stay out of the cache entirely.
  • Nested cache liveness: RenderCache.markSubtreeActive keeps entries below a cached root alive across outer hits (applyFrameLiveness + live path).
  • Style invalidation: cache entries carry an EnvironmentFingerprint (foregroundStyle, focusIndicatorColor from the audit); a style change above an .equatable() boundary now misses instead of serving a stale buffer. Palette/appearance stay covered by the global EnvironmentSnapshot, pulsePhase stays excluded.
  • Docs: RenderCycle.md Subtree Memoization rewritten (bypass semantics, liveness guarantees, fingerprint invalidation, refreshed usage guidance).

Public API is unchanged — all new plumbing is package/internal.

Fixes #14

Test plan

  • All five characterization markers flipped green and were dropped — EquatableEffectTests now asserts the contracts directly (plus new section-survival and style-invalidation tests)
  • Old live-path marker test migrated to the end-to-end form
  • ./scripts/test-linux.sh: full warning-fatal gate green on macOS and Linux (1322 tests, zero known issues)

phranck added 7 commits July 22, 2026 02:05
- EquatableEffectTests pins issue #14 end-to-end through the RenderLoop
  pipeline: lifecycle slots/tasks, key handlers, status-bar items, focus
  registrations, and nested cache entries all vanish across
  EquatableView cache hits (withKnownIssue markers)
- Documents the first-frame hit source: the measurement pass stores into
  the live cache while effect sites are inert, so the same frame's main
  pass already hits and the subtree's effects never mount at all
- Migrate the live-path marker test from RuntimeCharacterizationTests to
  the new FrameHarness end-to-end form
- EquatableView classifies its content on every cache miss: the pass's
  effect-registration probe is snapshotted around the rendering, and any
  delta flags the identity in RenderCache so lookups always miss and the
  subtree renders every frame
- The probe (package env key, installed by RenderLoop.passEnvironment)
  sums all per-pass effect sinks: key handlers, preference writes,
  status-bar declarations, staged focus registrations, and deferred
  lifetime-effect records; new counters: PreferenceStorage.writeCount,
  StatusBarState.passRegistrationCount,
  FocusManager.stagedRegistrationCount
- Measurement passes stay out of the cache entirely: a buffer stored in
  .measure let the same frame's output pass hit a subtree whose effects
  were never recorded (they never mounted at all)
- Live path (no collectors) keeps its historical caching semantics
- EquatableEffectTests lifecycle/handler/status-bar/focus markers flip
  green and are dropped; the nested-entry marker stays for Task 3
- RenderCache.markSubtreeActive keeps every entry at or below a cached
  subtree root active without traversal (ancestor logic like
  clearAffected(by:))
- TUIContext.applyFrameLiveness marks subtree roots via
  markSubtreeActive instead of markActive; the EquatableView live path
  gains the same call
- The nested-entry characterization marker flips green and is dropped —
  EquatableEffectTests now asserts all issue #14 effect contracts
  directly
- A focus section declared without focusables inside cacheable content
  produced no probe delta, so the subtree was classified effect-free
  and the section vanished from committed Tab cycling on cache hits
- stagedRegistrationCount now counts staged sections plus focusables;
  new end-to-end test pins the section survival
- EnvironmentFingerprint (type-erased Equatable snapshot, TUIkitView)
  joins identity, view value, and size in the RenderCache key; a
  mismatch misses so a style change above an .equatable() boundary can
  never serve a stale buffer
- RenderLoop installs the per-pass fingerprint probe snapshotting the
  issue #14 audit members: foregroundStyle and focusIndicatorColor
  (pulse-derived, so cached entries inside an active focus section
  refresh every tick); palette/appearance stay covered by the global
  EnvironmentSnapshot clear, pulsePhase itself stays excluded
- Live path stores and matches nil fingerprints, keeping its historical
  cache key
- New end-to-end test: a foregroundStyle change between frames misses
  and re-stores, an unchanged style hits again
- Subtree Memoization rewrite: measure-pass exclusion, effect-bearing
  classification and bypass, liveness guarantees for hits, environment
  fingerprint in the cache key
- Refresh the when-(not)-to-use guidance: effect-bearing subtrees are
  auto-bypassed and safe, wrapping them just adds nothing
@phranck
phranck merged commit b8f98e1 into main Jul 22, 2026
6 checks passed
@phranck
phranck deleted the issue/14-cache-liveness branch July 22, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0-09] Preserve subtree liveness across EquatableView cache hits

1 participant