Runaway-source diagnostic for settle()/expect() fixpoint timeouts#25
Merged
Conversation
When a model never reaches a fixpoint (a non-converging reactive cascade — a
node.forEach(Observed{}) / node.onChange whose source emits a non-isSame value
each evaluation, or sits in a feedback loop), settle()/expect() correctly time
out, but the diagnostic listed EVERY active registration, hiding the offender
(see the parallel-apple swift-model-104-…-settle-hang handoff thread — it took
several debugging rounds to find the culprit).
Now the drive counts per-call-site reactive-body deliveries and, on a timeout,
prepends a callout naming the registration that fired far more than a one-shot
AND was still firing at the timeout — the non-isSame/feedback source — with fix
guidance. Covers both forEach and onChange (onChange routes through forEach and
forwards the user's source location).
- ModelAccess.reactiveBodyFired(_:) — no-op default; gated so counting is zero
cost outside .modelTesting (production ModelAccess.current is nil/non-test).
- TestAccess records per-FileAndLine (count, lastFireNs) under a dedicated lock.
- _forEachImpl increments on each delivery in both loop paths.
- settleDiagnostics() prepends the runaway callout; non-runaway stalls (genuine
deadlock / parked task) keep identical output (no snapshot churn).
Diagnostic-only — the 120s watchdog and all settle semantics are unchanged.
Verified: a non-isSame→non-Equatable-env cascade now reports
"⚠️ likely runaway: … fired 984716× and was still firing at the timeout."
Full suite green (no regression from the hot-path hook). CHANGELOG + design-note
Update 26. Targets 1.0.5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-diagnostic # Conflicts: # CHANGELOG.md
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.
Runaway-source diagnostic for
settle()/expect()fixpoint timeoutsWhen a model never reaches a fixpoint under the executor-drive — a non-converging reactive cascade (
node.forEach(Observed { … })/node.onChangewhose source emits a non-isSamevalue each evaluation, or sits in a feedback loop) —settle()/expect()correctly time out with "model never reached a fixpoint." Previously the diagnostic listed every active registration, hiding the offender (it took several debugging rounds to find the culprit in a real consumer — see theswift-model-104-…-settle-hanghandoff thread on the parallel-apple side).Now the drive counts per-call-site reactive-body deliveries and, on a timeout, prepends a callout naming the registration that fired far more than a one-shot AND was still firing at the timeout — the non-
isSame/feedback source — with fix guidance:Properties
forEachandonChange(onChange routes throughforEachand forwards the user's source location).ModelAccess.reactiveBodyFired(a no-op whenModelAccess.currentis nil or a non-test access), so it runs only under.modelTesting.settle()/expect()semantics are unchanged.Implementation
ModelAccess.reactiveBodyFired(_:)— no-op default;TestAccessoverrides to count perFileAndLine(count,lastFireNs) under a dedicated lock._forEachImplincrements on each delivery in both loop paths (cancelPrevioustrue/false).settleDiagnostics()prepends_runawayDiagnosticLine()when one call site fired ≥ a threshold and was still firing within the last second.Validation
isSame→non-Equatable-env cascade now reports⚠️ likely runaway: … fired 984716× and was still firing at the timeout(manually verified — a true runaway hangs to the 120s watchdog by design, so this isn't a CI test).scripts/test), no regression from the hot-path hook.Targets 1.0.5. CHANGELOG + design-note Update 26 included.
🤖 Generated with Claude Code