Skip to content

Extract the attribution duty cycle into DutyCycle - #136

Merged
Pixnop merged 4 commits into
devfrom
refactor/duty-cycle
Oct 4, 2026
Merged

Pixnop merged 4 commits into
devfrom
refactor/duty-cycle

Conversation

@Pixnop

@Pixnop Pixnop commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Per-mod attribution runs on a duty cycle: by default it waits 10 seconds, then profiles a burst of 10 ticks after discarding one warm-up tick. That cycle lived inside TickAttribution. The Stratum behavior timings (#6) need the same schedule, so it moves into a DutyCycle class both features can use. Two copies of a state machine would drift apart. This is a pure refactoring: attribution's behaviour, config, commands, logs and served metrics do not change.

What moved

  • DutyCycle owns the schedule and nothing else: the clamps, the idle, warm-up and burst counters, and the enabled, interval and burst-length state. Apply(enabled, burstTicks, intervalSeconds) reconfigures it and drops a burst in progress. OnTick(elapsedSeconds) says what the current tick is: Idle, Start, WarmUp, Sample or LastSample.
  • TickAttribution keeps the folding. It folds the tree on Sample and LastSample, publishes on LastSample, and its Profiling is the cycle's InBurst.
  • AttributionMetrics, PulseCommands, the commands and the logs are untouched.
  • Start and WarmUp have no caller beyond attribution's step check today. They map onto what the Stratum feature will do: request its recording lease on Start, take the first snapshot on WarmUp, and take the second snapshot and release the lease on LastSample.

How it was checked

  • The implementer ran the old TickAttribution and the new pair on 20,000 random sequences of 400 operations, about 8,000,000 comparisons. Every burst and every observable property matched after every operation.
  • An independent review compared the code line by line and reran that comparison with hostile inputs (negative, NaN and infinite elapsed times) over 2,000,000 more operations, again identical. The one divergence it found is unobservable. If the owner lookup throws mid-fold, the new cycle has already counted the sample. But AttributionMetrics switches attribution off and forces the profiler off on any exception, so nothing reads that state.
  • The review's two fixes are in the last commit:
    • The start of a burst repeated three resets that Restart already does. Neither copy could be pinned by a mutation, so Restart now owns them, with one mutation each.
    • The cap and floor tests compared against the constants themselves. They now assert the 300 ticks and 1 second the README documents.

Tests

  • Pulse.Tests passes 422 tests, against 410 on dev. Of the 24 TickAttribution tests, 4 moved to DutyCycleTests (15 tests) with their assertions, and 21 remain, one of them new. No test was deleted.
  • Before the review fixes, the scenarios passed 30 of 30 and 14 of 14, including hot toggling, toggling off mid-burst, config reload and the 6 attribution scenarios.
  • tools/mutation-check.sh kills 135 of 135 mutations, with none inert:
    • 2 patterns realigned onto DutyCycle.cs;
    • 13 added with the extraction;
    • 4 added after the review: the sample count and the warm-up flag that Restart resets, the documented burst cap, and the clear at the end of a published burst.

Pixnop added 4 commits October 4, 2026 22:34
The idle, warm-up and burst schedule that drove per-mod attribution now
lives in a class of its own, so a second feature can run on the same
schedule instead of keeping a copy that would drift. DutyCycle owns the
clamp, the interval, the warm-up and the burst length, and says what
each tick is for: idle, start, warm-up, sample or last sample.
TickAttribution folds a tree on the sample steps and keeps the rest of
what it had, with the same constructor, the same Apply and the same
properties, so AttributionMetrics and the commands are untouched.

Behaviour is unchanged. The schedule tests moved to DutyCycleTests,
where they run against the schedule alone, and the attribution tests
that check what attribution does with each step stay where they were.
The two mutation patterns that pointed at the moved code now point at
DutyCycle.cs.
The schedule is the part every burst of every measurement will now run
on, so each way it can drift gets a mutation in tools/mutation-check.sh:
the idle branch, counting seconds rather than ticks, the interval
boundary, the burst length boundary, the end of a burst, what Apply
drops, and the three clamps. Attribution's own use of the steps gets
two more, and a third for a reload that has to take the half-folded
burst with it.

Four tests back them. A reload mid-interval restarts the interval from
the reload, which nothing checked before: the mutant that kept the idle
time already counted survived the old suite. The interval is counted in
seconds, so the class itself fails when it counts ticks. A reload that
leaves the cycle on drops the burst in progress the way a switch-off
does. And attribution drops what a cut-short burst had folded, which
used to be covered only because the burst end and the reload shared one
Restart.
The start of a burst reset the idle time, the sample count and the warm-up flag that Restart had already reset when the previous burst ended or the cycle was applied, so neither copy could be pinned by a mutation. Restart now owns them, with a mutation each. The cap and floor tests compared against the constants themselves, so a moved cap passed; they now assert the 300 ticks and 1 second the README documents. The burst end's ClearBurst gets the mutation Apply's already had, and the duty cycle mutations sit in one block.
@Pixnop
Pixnop merged commit 7e5e577 into dev Oct 4, 2026
8 checks passed
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