Raxol. Harness. CadencePolicy
(Raxol v2.6.1)
View Source
The streaming-flush cadence policy: a pure decision function that tells a caller whether to paint now, wait, or stand aside for input.
Design laws
Same discipline as Raxol.Harness.StallDetector: this module owns no
processes, no timers, no clocks. The caller passes timestamps in
(decide/4,5) and owns the actual scheduling (Process.send_after/3
or equivalent) -- identical inputs always produce identical outputs,
which is what makes this testable with plain integers instead of
Process.sleep/1.
Input wins, within a budget.
decide/4,5checksinput_pending?first, before even asking whether the cadence window is open -- a live agent session must never let a token flood delay keystroke handling. But the win is bounded: aftermax_consecutive_yields/0consecutive yields (~one frame at the 1ms retry) the decision falls through to the cadence rules, so continuous input can never starve rendering indefinitely either.First paint is unconditional. A stream's first delta (
last_flush_ms == nil) flushes immediately. Perceived latency matters most at the start of a response; there is no prior cadence to respect yet.Deterministic coalescing. Every decision inside the same cadence window derefs to the same deadline (
last_flush_ms + interval), not to "interval from now". This is what lets a burst of hundreds of deltas collapse into one scheduled flush instead of one timer reset per delta.
Summary
Functions
Decides what a caller holding pending_count buffered deltas should
do right now.
The number of pending items to drain into a single flush batch: the
lesser of pending_count and the configured max drain.
The number of pending items a caller must shed (from the OLDEST end)
to get back under the :max_pending watermark: max(pending_count - max_pending, 0).
The minimum interval between token flushes, in milliseconds.
The recheck delay, in milliseconds, after yielding to pending input.
The maximum consecutive :yield_to_input verdicts between flushes.
The maximum number of pending items drained into a single flush batch.
The default pending-queue watermark above which the oldest deltas are shed.
Types
@type verdict() :: :flush_now | {:defer, pos_integer()} | :yield_to_input
Functions
@spec decide( now_ms :: integer(), last_flush_ms :: integer() | nil, pending_count :: pos_integer(), input_pending? :: boolean(), opts :: keyword() ) :: verdict()
Decides what a caller holding pending_count buffered deltas should
do right now.
now_ms and last_flush_ms are caller-supplied timestamps on the
same clock (last_flush_ms is nil when nothing has flushed yet).
pending_count must be positive -- calling this with nothing pending
is a caller bug and crashes loudly via FunctionClauseError rather
than silently returning a no-op verdict.
Decision order (the order IS the design, not an implementation detail):
input_pending?true ANDyields_since_flush < max_consecutive_yields->:yield_to_input-- input is scheduled ahead of token flushes, even when the cadence gate is fully open or this is the very first delta. Once the yield budget is exhausted, the decision FALLS THROUGH to the cadence rules below (so continuous input holds a flush for at mostmax_consecutive_yields x input_yield_retry_ms~= one frame before forward progress is forced; a defer landing with the budget still exhausted falls through again and flushes at the window edge).last_flush_msisnil->:flush_now-- the first delta of a stream paints immediately.now_ms - last_flush_ms >= interval->:flush_now.- otherwise ->
{:defer, remaining_ms}, whereremaining_msis always>= 1by construction (case 3 already claimed the>= intervalregion).
Options
:flush_interval_ms-- override the module's default cadence interval. This is both the deterministic-test seam (set it to0to force:flush_nowon every decision) and the per-instance config seam.:yields_since_flush(default0) -- how many consecutive:yield_to_inputverdicts the caller has already acted on since its last flush. This is one piece of CALLER STATE deliberately carried in opts to preserve the positional-arg shape of this function; the policy itself stays stateless. Callers reset their counter to0on every flushed batch.:max_consecutive_yields-- override the module's default yield budget.
@spec drain_count(pending_count :: non_neg_integer(), opts :: keyword()) :: non_neg_integer()
The number of pending items to drain into a single flush batch: the
lesser of pending_count and the configured max drain.
The configured cap is clamped to at least 1: a batch that drains
nothing makes no forward progress, and StreamCadence's forced full
drain loops until pending hits zero -- a zero cap would spin it
forever. Zero pending still drains zero; only the cap has a floor.
Options
:max_drain_per_flush-- override the module's default per-flush cap (clamped to a minimum of 1).
@spec drop_count(pending_count :: non_neg_integer(), opts :: keyword()) :: non_neg_integer()
The number of pending items a caller must shed (from the OLDEST end)
to get back under the :max_pending watermark: max(pending_count - max_pending, 0).
Zero at or below the watermark. Callers drop from the queue front -- the newest deltas are the live tail's value; the oldest are already history.
Options
:max_pending-- override the module's default watermark.
@spec flush_interval_ms() :: pos_integer()
The minimum interval between token flushes, in milliseconds.
@spec input_yield_retry_ms() :: pos_integer()
The recheck delay, in milliseconds, after yielding to pending input.
@spec max_consecutive_yields() :: pos_integer()
The maximum consecutive :yield_to_input verdicts between flushes.
@spec max_drain_per_flush() :: pos_integer()
The maximum number of pending items drained into a single flush batch.
@spec max_pending() :: pos_integer()
The default pending-queue watermark above which the oldest deltas are shed.