Skip to content

feat(evaluator): implement dirty-flag tick collection loop#13

Merged
Briany4717 merged 4 commits into
mainfrom
feat/evaluator-tick-cycle
May 18, 2026
Merged

feat(evaluator): implement dirty-flag tick collection loop#13
Briany4717 merged 4 commits into
mainfrom
feat/evaluator-tick-cycle

Conversation

@Briany4717

@Briany4717 Briany4717 commented May 17, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Implements EvaluatorTick, the dirty-flag collection loop described in
RFC-0001 2.2 and split off from the original Evaluator issue.

EvaluatorTick<'a> is the coordination layer that consumes the atomic
version counters produced by Signal::write and produces, per tick, the
set of TargetIds that downstream subsystems (Atlas, Encoder) must
recompute.

Implementation notes:

  • Type erasure via monomorphised function pointers — same pattern as
    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. No Box<dyn>,
    no vtable, no heap alloc per registration.
  • Pull model — the tick polls each registered signal once per tick
    via Acquire load on dirty_version. Signals whose version did not
    advance contribute nothing.
  • Deduplication — output is sorted by raw ID then dedup'd. For the
    expected sizes (tens to hundreds of targets), this is more cache-friendly
    than a HashSet.
  • Scratch buffer reuse — the internal Vec<TargetId> is reused
    across calls via mem::take, so subsequent ticks of similar size
    allocate nothing.
  • Lifetime-boundEvaluatorTick<'a> carries the same arena
    lifetime 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:

  • A signal that has never been written produces no dirty targets
    (never_written_signal_produces_no_dirty_targets).
  • A signal written once between ticks produces all its subscribed
    targets exactly once (written_signal_produces_its_subscribers).
  • A signal written N times between ticks still produces each
    subscribed target exactly once
    (multiple_writes_between_ticks_produce_each_target_once).
  • Calling collect_dirty() twice with no writes between returns an
    empty set the second time (second_tick_with_no_writes_is_empty).
  • No allocations on the hot path beyond the returned Vec — scratch
    buffer 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):

Operation Result
tick: collect_dirty (10 signals, no writes) 40 ns/op
tick: collect_dirty (10 signals, all dirty) 165 ns/op

Projection to a realistic view of 1000 registered signals:

  • All clean (common case): ~4 µs/tick → 0.024% of the 60 FPS budget.
  • All dirty (worst case): ~16 µs/tick → 0.1% of the 60 FPS budget.

Existing Evaluator benchmarks are unchanged: signal::subscribe rose
from 0.89 ns to 1.28 ns due to the WriteGuard reentrancy protection
added in #11, which is acceptable.

Checklist

  • cargo fmt --all passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace passes (39 unit tests, +10 in this PR)
  • New public items have doc comments
  • If this changes behavior visible to users, CHANGELOG.md has an entry under [Unreleased]
  • If this changes the architecture, an RFC is linked or opened alongside this PR

Notes for reviewers

The two pub(crate) accessors added to SignalSlot<T>
(dirty_version_ref and subscribers_ref) intentionally bypass the
borrow-state guards used by the public Signal API. They are sound
because:

  1. dirty_version is an AtomicU64 and is always safe to read from a
    shared reference.
  2. subscribers_ref is unsafe and documents that the caller must
    guarantee no exclusive borrow of the slot is active. This is upheld
    by the single-threaded invariant: collect_dirty runs on the Logic
    thread, where no write can be in flight simultaneously.

CHANGELOG checkbox N/A — no user-visible behaviour in this crate yet.

@Briany4717 Briany4717 marked this pull request as ready for review May 17, 2026 23:21
Copilot AI review requested due to automatic review settings May 17, 2026 23:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@Briany4717 Briany4717 merged commit c827845 into main May 18, 2026
10 of 11 checks passed
@Briany4717 Briany4717 deleted the feat/evaluator-tick-cycle branch May 18, 2026 00:27
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.

feat(evaluator): implement dirty-flag tick collection loop

2 participants