Scheduling — Smith’s Rule Plus Aging
What goes wrong with a naive approach
The effect queue runs background work (Cmd::perform, async
subscriptions, telemetry flushes) alongside rendering. Three naive
scheduling policies all fail in predictable ways:
- FIFO — one slow effect blocks all the light ones behind it. A telemetry flush that takes 200 ms stalls a trivial cursor repaint.
- SRPT (shortest remaining processing time) — optimal for average completion time in the unweighted case. But a 10-second export job never runs while short jobs keep arriving. Starvation.
- Priority queues with a static order — nothing ever bubbles up from low priority; long-waiting low-priority jobs die of old age.
The right policy is a weighted SRPT with an aging bonus:
- Order primarily by Smith’s rule — , the weighted inverse remaining time. This is optimal for weighted mean completion time on a single machine.
- Add a wait-time bonus so no job waits forever. The aging term is a linear function of the time spent in the queue.
The combination gives optimal throughput for a given weight vector and a provable worst-case wait bound.
Mental model
Every queued job carries three numbers:
- — importance (weight; default 1.0).
- — remaining-time estimate (default 10 ms).
- wait — time since enqueue.
Compute priority as:
Pick the maximum. On each tick decrement for the running job; other jobs accumulate more wait.
- Light, important jobs surface instantly because is large.
- Heavy, unimportant jobs would starve under pure SRPT, but the aging term drags them upward linearly until they run.
This is not a made-up recipe. Smith’s rule is a textbook result (Smith, 1956) for minimising weighted flow time on a single machine; the aging term is an explicit trade that buys bounded starvation at a known throughput cost.
The math
Priority function
The schedule at time is .
Starvation bound
For a job with minimum weight , the worst-case wait time before it runs is:
where is the longest job in the queue and is the aging factor. Halving doubles the bound; doubling halves it but costs throughput because more short jobs are starved by aged long jobs.
Throughput vs. fairness trade
recovers pure Smith’s rule (optimal throughput, unbounded starvation). recovers FIFO (bounded starvation, terrible throughput). FrankenTUI picks , empirically a sweet spot on TUI workload traces.
Defaults
| Parameter | Default |
|---|---|
| (aging factor) | 0.1 |
| default | 1.0 |
| default | 10 ms (estimate for an unknown effect) |
Worked example — three jobs
Queue after enqueueing in order A, B, C:
A: w=1, r=100 ms, wait=0 → priority = 0.01
B: w=1, r=5 ms, wait=0 → priority = 0.20 ← picked
C: w=2, r=10 ms, wait=0 → priority = 0.20 ← tied with B; id tiebreakB runs first. Five ms later:
A: w=1, r=100, wait=5 → 0.01 + 0.5 = 0.51 ← aged up
C: w=2, r=10, wait=5 → 0.20 + 0.5 = 0.70 ← still firstC runs next. After another 10 ms the queue has:
A: w=1, r=100, wait=15 → 0.01 + 1.5 = 1.51 ← pickedA is heavy and low-weight but the aging term lifted it above short- job dominance. Nothing starves.
Rust interface
use ftui_runtime::queueing_scheduler::{QueueingScheduler, QueueingConfig, Job};
let mut sched = QueueingScheduler::new(QueueingConfig {
aging_factor: 0.1,
weight_default: 1.0,
estimate_default_ms: 10.0,
});
sched.enqueue(Job::new("persist")
.with_weight(1.0)
.with_estimate_ms(100.0));
sched.enqueue(Job::new("paint-cursor")
.with_weight(1.0)
.with_estimate_ms(2.0));
let job = sched.pick(); // weighted SRPT with agingThe runtime owns the scheduler; effects enqueue into it from the
Cmd executor. See /runtime/rollout/effect-queue
for how the scheduler plugs into the runtime’s effect pipeline.
How to debug
Every pick emits a queue_select line:
{"schema":"queue_select","job_idx":17,"id":"persist-state",
"priority":0.51,"weight":1.0,"remaining_ms":100,
"wait_time_ms":15,"aging_boost":1.50}FTUI_EVIDENCE_SINK=/tmp/ftui.jsonl cargo run -p ftui-demo-showcase
# Which jobs are being aged heavily?
jq -c 'select(.schema=="queue_select")
| [.id, .aging_boost]' /tmp/ftui.jsonl | sort -k2 -n | tail -20A job with aging_boost dominating weight/remaining_ms every
pick is essentially FIFO-ing — likely the result of a wrong
estimate; raise its weight or fix the estimator.
Pitfalls
Bad estimates corrupt the schedule. Smith’s rule is
optimal in the idealised model where is known. Estimates
drift; the evidence sink’s remaining_ms field is worth auditing
periodically against actual runtime.
Aging is additive, not multiplicative. Doubling halves the wait bound but also doubles the “importance” of old jobs linearly. Go too high and every pick is aged-dominated — effectively FIFO. is the sensible band.
Cross-references
/runtime/rollout/effect-queue— how the scheduler is wired into the effect executor./runtime/overview— the bigger runtime picture.- Input fairness — Jain’s index intervention that protects input from rendering starvation.