Bayesian Diff Strategy Selection
What goes wrong with a naive approach
The render loop has three diff strategies with different cost curves:
- Full — scan every cell, emit only the ones that changed. Cheap when change rate is small; O(N) scan dominates.
- DirtyRow — skip clean rows entirely via the dirty bitmap; emit per-row runs. Cheap when changes cluster in few rows.
- FullRedraw — skip the scan; emit every cell unconditionally. Cheap only when almost everything changed.
A hand-tuned “if change_pct > 0.4 use FullRedraw else DirtyRow” works until the workload drifts. When a dashboard settles into a steady state the threshold is wrong in one direction; during a scroll burst it is wrong in the other. The p99 frame time breathes with the workload and no amount of knob-twiddling stabilizes it.
The diff engine needs (a) an estimator of the current change rate, (b) an explicit cost model, and (c) a conservative mode that degrades gracefully when uncertainty is high.
Mental model
Every frame produces one Bernoulli observation per cell (changed or not). Over a rolling window the rate is well approximated by a Beta posterior — conjugate to Bernoulli, closed form, finite-sample sensible.
Because workloads drift, use exponential decay on the posterior counts: recent frames matter more than the entire history.
For each strategy compute expected cost as a linear function of . Pick . When the posterior variance is high (cold start, regime change) pick the strategy that minimizes the 95th-percentile cost instead of the mean — the conservative mode.
The diff engine is a Bayesian cost-minimiser. The posterior over change rate is the state; the three cost curves are the decision rule; exponential decay lets the estimator follow the workload.
The math
Posterior over change rate
Updates under exponential decay with factor :
where is the cell count and the observed change count for the current frame. Default (half-life ≈ 14 frames).
Posterior mean and variance:
Cost model
Let = total cells, = row count, = posterior mean.
Default coefficients: , ,
. These are measured once on the
presenter/bench harness and held constant — they describe the
terminal, not the workload.
Decision rule
Conservative (95th-percentile) mode
When (default 0.02):
The 95th-percentile call picks the strategy with the cheapest worst-plausible cost rather than the cheapest expected cost.
Worked example — the three regimes
Idle dashboard
p ≈ 0.002, Var(p) small → C_full = N + 6·0.002N = 1.012N
C_dirty = N + 0.1·0.002R ≈ 1.0N
C_redraw = 6N
pick: DirtyRowRust interface
use ftui_render::diff_strategy::{DiffStrategyPicker, DiffStrategyConfig, Strategy};
let mut picker = DiffStrategyPicker::new(DiffStrategyConfig {
c_scan: 1.0, c_emit: 6.0, c_row: 0.1,
decay: 0.95,
conservative_var_threshold: 0.02,
conservative_quantile: 0.95,
});
// Each frame: observe, then pick.
picker.observe(changes, total_cells);
let Strategy { kind, expected_cost, posterior_mean, posterior_var }
= picker.pick(rows, total_cells);The picker returns both the chosen strategy and the posterior
summary so the renderer can log it without recomputing. Link out to
/render/diff for how Strategy feeds into the
actual diff pass.
How to debug
Every decision emits a diff_decision line:
{"schema":"diff_decision","strategy":"DirtyRow",
"posterior_mean":0.047,"posterior_var":0.0014,
"expected_cost":1024.5,"conservative":false,
"alpha":3.1,"beta":62.0}FTUI_EVIDENCE_SINK=/tmp/ftui.jsonl cargo run -p ftui-demo-showcase
# Watch how often each strategy wins:
jq -c 'select(.schema=="diff_decision") | .strategy' /tmp/ftui.jsonl \
| sort | uniq -cIf FullRedraw never wins on scroll-heavy workloads, the
coefficient is probably too large for your terminal
— re-run presenter/bench and update the config.
Pitfalls
Decay too slow, strategy lags. With the posterior half-life is ~70 frames, so a regime change takes a full second of 30 fps to propagate. Keep unless you’re consciously suppressing noise at the cost of reactivity.
Coefficients are terminal-specific. is much higher on a ssh-tunnelled terminal than a local iTerm2. Run the calibration on representative hardware; a laptop-tuned constant will pick DirtyRow on machines where FullRedraw is actually cheaper.
Cross-references
/render/diff— the diff implementation that consumes the picked strategy.- Conformal frame gating — downstream safety layer that catches mispicks.
- Heuristic vs. principled — the side-by-side on the overview page uses this exact snippet.