fix(runtime): skip the behavior scan while the context is quiescent - #536
Merged
Merged
Conversation
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Contributor
Author
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
HuiJun
marked this pull request as ready for review
September 22, 2026 20:39
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…rs to run Co-Authored-By: jason.han <hanhuijun@gmail.com>
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.
What and why
Since the dynamic object lifecycle work (#468), every outermost start (
holdDrivenWork) and everynextRunnableBehaviorcall walks all ofctx.objectBehaviorscallinghasPendingWork(). A context holding many instantiated objects with idle exhibited machines therefore pays O(#behaviors) perCheckConstraintOn/ feature write, making batches quadratic. ProfilingBenchmarkBatchConstraintsondevelopshowed 34% of CPU inhasPendingWorkand 7 ms → 260 ms versus v0.8.1.The fix is a dependency-aware quiescence memo on
Context:workis bumped bypostFrom, every clock move (now routed throughsetClock), everyStateExecutorevent enqueue,beginExecutorRun/previewExecutorRunon entry and leave,objectBehaviorsappends/removals, held-image materialization, and byrollbackJournal/runCapture.restore, so a snapshot, probe or transaction restore drops the memo. Each full scan installs apendingMemoasctx.polling, so when any pending check read object data (notePollReadsData, the existingviamechanism) the recorded quiescence also keys onctx.writes; a held per-machinependingSignalmemo that read data reports that to the enclosing scan. Data-free scans stay immune to feature writes, which is what keeps a plain write cheap. Filtered scans (run boundaries, held behaviors) never record quiescence.The run bookkeeping a store sets up is also allocated only when there is something to run:
holdDrivenWorkallocates its held map on the first pending behavior (aholdingDrivenflag now carries the outer-start sentinel),nextRunnableBehaviorinstalls a poll memo only when its scan range is non-empty, andrunState.extentCandidatesis created at its write site.How it was verified
quiescence_test.go: a posted signal, a clock advance past anaftertrigger and a direct executor run each break quiescence; a snapshot restore after an idle scan re-delivers the restored message; a held memo that read data reports the read to an enclosing poll and no longer covers a write; an idle store on a behavior-free object stays within its allocation budget (AllocsPerRun).go test -count=1 ./internal/exec/runtime,go test -race ./...(full suite),./tests/grpc ./tests/corpus ./tests/identitywith all four corpora required — all pass, no baseline moved.go vet,gofmt -l,changelog.py checkclean.BatchConstraints245 ms → 9.0 ms (v0.8.1 7.4 ms);SetFeatureValueNoDependents1.5 µs / 28 allocs → 386 ns / 9 allocs (v0.8.1 143 ns / 1). The remaining allocations are the eagerrunState/scheduler and the run-bracket closures every top-level store still creates.Checklist
make testandmake lintpass locallychanges/unreleased/<slug>.<section>.md, not as an edit toCHANGELOG.mdmake docs-countsrun if a gate count moved (compliance rows need nothing: the census is counted at docs build)F4,K5) in the body, docs, or changelog