feat(evaluator): implement dirty-flag tick collection loop#13
Merged
Conversation
This was referenced May 18, 2026
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 does this PR do?
Implements
EvaluatorTick, the dirty-flag collection loop described inRFC-0001 2.2 and split off from the original Evaluator issue.
EvaluatorTick<'a>is the coordination layer that consumes the atomicversion counters produced by
Signal::writeand produces, per tick, theset of
TargetIds that downstream subsystems (Atlas, Encoder) mustrecompute.
Implementation notes:
the arena's Drop Registry. Each registered signal stores a
*const ()to its slot plus two function pointers: one to read the version counter,
one to enumerate subscribers into a caller-provided
Vec. NoBox<dyn>,no vtable, no heap alloc per registration.
via
Acquireload ondirty_version. Signals whose version did notadvance contribute nothing.
dedup'd. For theexpected sizes (tens to hundreds of targets), this is more cache-friendly
than a
HashSet.Vec<TargetId>is reusedacross calls via
mem::take, so subsequent ticks of similar sizeallocate nothing.
EvaluatorTick<'a>carries the same arenalifetime as its signals; it cannot outlive them.
Linked issue
Closes #12
Refs #1
Acceptance criteria
All five criteria from the sub-issue are met and verified by tests:
(
never_written_signal_produces_no_dirty_targets).targets exactly once (
written_signal_produces_its_subscribers).subscribed target exactly once
(
multiple_writes_between_ticks_produce_each_target_once).collect_dirty()twice with no writes between returns anempty set the second time (
second_tick_with_no_writes_is_empty).Vec— scratchbuffer is reused.
Duplicate registration policy
A
debug_assert!catches duplicate signal registrations in debug builds.Release builds accept duplicates silently — the per-tick deduplication
step preserves correctness, at the cost of one wasted tracking slot per
duplicate.
Rationale: a transpiler edge case or unusual state injection should not
crash the user's application. UI engines prefer visual glitches over
hard failures. Devs see the assertion fail in development and fix it;
production stays up.
Performance
Benchmarks (release build):
tick: collect_dirty (10 signals, no writes)tick: collect_dirty (10 signals, all dirty)Projection to a realistic view of 1000 registered signals:
Existing Evaluator benchmarks are unchanged:
signal::subscriberosefrom 0.89 ns to 1.28 ns due to the
WriteGuardreentrancy protectionadded in #11, which is acceptable.
Checklist
cargo fmt --allpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspacepasses (39 unit tests, +10 in this PR)CHANGELOG.mdhas an entry under[Unreleased]Notes for reviewers
The two
pub(crate)accessors added toSignalSlot<T>(
dirty_version_refandsubscribers_ref) intentionally bypass theborrow-state guards used by the public
SignalAPI. They are soundbecause:
dirty_versionis anAtomicU64and is always safe to read from ashared reference.
subscribers_refisunsafeand documents that the caller mustguarantee no exclusive borrow of the slot is active. This is upheld
by the single-threaded invariant:
collect_dirtyruns on the Logicthread, where no
writecan be in flight simultaneously.CHANGELOG checkbox N/A — no user-visible behaviour in this crate yet.