Skip to content

Fix: Derive the image placeholder instead of mutating state in traversal - #63

Merged
phranck merged 2 commits into
mainfrom
issue/27-image-placeholder-derivation
Jul 22, 2026
Merged

Fix: Derive the image placeholder instead of mutating state in traversal#63
phranck merged 2 commits into
mainfrom
issue/27-image-placeholder-derivation

Conversation

@phranck

@phranck phranck commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • _ImageCore no longer mutates its phase/lastSource state boxes while rendering. The placeholder for a changed source is derived from lastSource != source, so it still appears in the same frame as the source change — without any state write inside the traversal window.
  • The mounted loading task is now the only writer of both boxes: it commits the phase first, then the source, and skips the commit entirely when cancelled. A stale load can therefore never pair with a newer source (renders in between derive the placeholder).
  • Root cause surfaced by [P0-08d] Document the phase model and diagnose body side effects #58: the previous resetLoadingPhaseIfNeeded wrote lastSourceBox on every frame, so every frame containing an image emitted a body-side-effect RuntimeDiagnostic (plus a superfluous invalidation). Rendering an image is now diagnostic-free.
  • New ImageRenderPhaseTests cover both contracts end-to-end via FrameHarness (which gained an injectable TUIContext for deterministic loaders); mountLoadingTask now reads its dependencies from the render context (SwiftLint function_parameter_count).

Refs #27 — delivers the "Rendering does not mutate image phase merely by reading the same source" acceptance criterion; the remaining transport/validation/dedup work stays in #27.

Test plan

  • ImageRenderPhaseTests: RED verified against the old behavior (diagnostic on every frame), GREEN after the fix
  • Placeholder still shows in the same frame as a source change (asserted against the frame's terminal diff)
  • ./scripts/test-linux.sh: full warning-fatal gate green on macOS and Linux (1317 tests, 1 expected known issue for [P0-09] Preserve subtree liveness across EquatableView cache hits #14)

phranck added 2 commits July 22, 2026 01:40
- _ImageCore no longer writes its phase/lastSource state boxes while
  rendering; the placeholder for a changed source is derived from
  lastSource != source, so it still shows in the same frame
- The mounted loading task is now the only writer: it commits phase
  first, then lastSource, and skips the commit when cancelled, so a
  stale load can never pair with a newer source
- Since #58 every image frame emitted a body-side-effect
  RuntimeDiagnostic (unconditional lastSource write); rendering is now
  diagnostic-free
- Add ImageRenderPhaseTests and an injectable TUIContext on FrameHarness
- mountLoadingTask reads lifecycle, loader, cache, timeout, and pixel
  limit from context.environment instead of taking nine parameters
  (SwiftLint function_parameter_count)
@phranck
phranck merged commit 21f140a into main Jul 22, 2026
6 checks passed
@phranck
phranck deleted the issue/27-image-placeholder-derivation branch July 22, 2026 00:00
wadetregaskis added a commit to wadetregaskis/TUIkit that referenced this pull request Sep 2, 2026
…open

PRs phranck#60-phranck#62 are the rest of the render-phase work: per-pass collectors,
an ordered pending-effect log committed from the final traversal, and a
body-mutation diagnostic. Deferred as a real backlog item — it is the
only design that closes the gap a338e81f pins, since the header
correction cannot know which walk is final until after both have run.
Notes record the order to do it in and that it wants Stress --bench on
both sides, given the live perf concern.

PR phranck#63 is queued, and honestly so. Their diagnostic immediately caught
_ImageCore writing its lastSource box on every render; ours does the same
unconditional write, and StateBox.didSet invalidates without comparing.
The suspicion is that a screen with an Image never lets the loop idle.

That is a suspicion, not a finding. A unit test failed to discriminate —
the sink defers to the main-actor frame boundary, so a synchronous
renderToBuffer never reaches the point where anything moves — and the
probe was deleted rather than left asserting something it does not test.
Tools/Profiling/idle_cpu.py exists for exactly this question and is the
right instrument; the one-line fix can follow the measurement rather than
stand in for it.

Co-Authored-By: Claude <[email protected]>
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.

1 participant