Fix: Preserve subtree liveness across EquatableView cache hits#64
Merged
Conversation
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.EquatableEffectTests,FrameHarnessend-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.EquatableViewsnapshots 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 inRenderCache— 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.RenderCache.markSubtreeActivekeeps entries below a cached root alive across outer hits (applyFrameLiveness+ live path).EnvironmentFingerprint(foregroundStyle,focusIndicatorColorfrom the audit); a style change above an.equatable()boundary now misses instead of serving a stale buffer. Palette/appearance stay covered by the globalEnvironmentSnapshot,pulsePhasestays excluded.RenderCycle.mdSubtree 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
EquatableEffectTestsnow asserts the contracts directly (plus new section-survival and style-invalidation tests)./scripts/test-linux.sh: full warning-fatal gate green on macOS and Linux (1322 tests, zero known issues)