mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
157 lines
6.7 KiB
Rust
157 lines
6.7 KiB
Rust
//! G3: the renderer's wait for the terminal lock, under flood.
|
|
//!
|
|
//! The plan originally required "reader hold < 16.7 ms". That requirement was
|
|
//! struck: measured under a 180 MB/s flood, reader hold is p50 1 us while
|
|
//! renderer *acquire* is p50 4245 us. Hold time passes trivially while the
|
|
//! window is visibly stuck, because 0.389% of feeds carry 96.4% of the lock
|
|
//! time and the p50 hold never sees them. What a human feels is the wait, so
|
|
//! that is what is gated here.
|
|
//!
|
|
//! F1 is the fence being tested. It is a memory bound *and* a latency fence:
|
|
//! it turns one ~2 MiB parser release into ~64 KiB pieces, and the renderer's
|
|
//! wait falls with it.
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use buzz_terminal::damage::Encoder;
|
|
use buzz_terminal::fences::Fences;
|
|
use buzz_terminal::{SharedTerminal, Size, Terminal};
|
|
|
|
/// One frame at 60 Hz. No acquire may exceed this: a single wait this long is
|
|
/// a dropped frame regardless of how good the distribution looks.
|
|
///
|
|
/// Unlike the p95 below, this bound **cannot be protected by headroom**, and
|
|
/// that asymmetry is why this test is `#[ignore]`d and run only in release on
|
|
/// an idle host. A quantile discards its worst samples by construction, so it
|
|
/// degrades gracefully as a machine gets noisy; a maximum over `FRAMES` samples
|
|
/// is a single observation, and any one scheduler preemption exceeds it. There
|
|
/// is no budget that makes the max arm robust to contention -- the tail it
|
|
/// catches belongs to the scheduler, not to this code.
|
|
///
|
|
/// Measured on one 16-core host at `FRAMES = 200`: at load average ~6 the gate
|
|
/// passes; at ~31 it fails with p95 65535 us / max 164889 us. A run at ambient
|
|
/// load produced p95 1023 us -- 4x *inside* budget -- while max alone blew at
|
|
/// 38150 us.
|
|
///
|
|
/// So the repair for a flake here is to fix the host, never to raise this
|
|
/// number. Raising it is the one change that silently removes the only assert
|
|
/// that catches the user-visible failure: a hitch is a max-event, and a
|
|
/// p95-only gate passes a run containing a 38 ms stall.
|
|
const FRAME_MICROS: u64 = 16_667;
|
|
|
|
/// p95 budget. Measured at 127 us with F1 on -- 31x of headroom, which is the
|
|
/// margin that lets *this* arm tolerate a loaded machine without becoming a
|
|
/// coin flip. The reasoning covers the quantile only; see `FRAME_MICROS`.
|
|
const P95_MICROS: u64 = 4_000;
|
|
|
|
/// Frames sampled per arm. Counted rather than timed: sample count under a
|
|
/// wall-clock budget is a function of how slow the arm is, so a duration-based
|
|
/// loop gives the *unfenced* arm the fewest samples -- fewest exactly where the
|
|
/// tail being measured lives. Counting frames makes both arms the same
|
|
/// experiment.
|
|
const FRAMES: u32 = 200;
|
|
|
|
/// A ~2 MiB synchronized update, closed, replayed in PTY-sized reads.
|
|
///
|
|
/// The payload's *shape* is the load-bearing part, and it cost me a wrong
|
|
/// result to learn it. An earlier version poured 8 KiB blocks of `A` into an
|
|
/// update that was never closed. It floods just as many bytes per second, and
|
|
/// it does not discriminate F1 at all: measured p95 63 us fenced vs 63 us
|
|
/// unfenced. Plain `A` overwrites one line at a few ns per byte, so even a
|
|
/// 2 MiB release is a short lock hold.
|
|
///
|
|
/// What makes a release expensive is work per byte -- SGR state changes and
|
|
/// `\r\n` line feeds that push rows into scrollback. With that payload the same
|
|
/// experiment separates by 129x. So this gate is sensitive to input shape and
|
|
/// not merely to input rate, which is why the control below is not optional.
|
|
fn flood(shared: &SharedTerminal, stop: &AtomicBool) {
|
|
let mut payload: Vec<u8> = b"\x1b[?2026h".to_vec();
|
|
while payload.len() < (2 << 20) {
|
|
payload.extend_from_slice(b"\x1b[1;32mbuzz\x1b[0m substrate line of output 0123456789\r\n");
|
|
}
|
|
payload.extend_from_slice(b"\x1b[?2026l");
|
|
while !stop.load(Ordering::Relaxed) {
|
|
for chunk in payload.chunks(8192) {
|
|
if stop.load(Ordering::Relaxed) {
|
|
return;
|
|
}
|
|
shared.feed_fully(chunk);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Render at 60 Hz for the duration of the flood, and report the renderer
|
|
/// plane's acquisition latencies.
|
|
fn measure(fences: Fences) -> buzz_terminal::AcquireStats {
|
|
let size = Size {
|
|
columns: 200,
|
|
screen_lines: 50,
|
|
scrollback: 10_000,
|
|
};
|
|
let (term, _actions) = Terminal::new(size, fences);
|
|
let shared = Arc::new(SharedTerminal::new(term));
|
|
let stop = Arc::new(AtomicBool::new(false));
|
|
|
|
let writer = {
|
|
let (shared, stop) = (Arc::clone(&shared), Arc::clone(&stop));
|
|
thread::spawn(move || flood(&shared, &stop))
|
|
};
|
|
|
|
// Don't measure the ramp: let the flood reach steady state, then clear.
|
|
thread::sleep(Duration::from_millis(200));
|
|
shared.renderer_acquire().reset();
|
|
|
|
let mut encoder = Encoder::new();
|
|
for _ in 0..FRAMES {
|
|
shared.render(&mut encoder);
|
|
thread::sleep(Duration::from_micros(FRAME_MICROS));
|
|
}
|
|
let stats = shared.renderer_acquire().snapshot();
|
|
|
|
stop.store(true, Ordering::Relaxed);
|
|
writer.join().expect("flood thread panicked");
|
|
|
|
assert_eq!(stats.acquisitions, FRAMES as u64, "meter lost samples");
|
|
stats
|
|
}
|
|
|
|
/// G3: with F1 on, the renderer's wait stays inside a frame -- and the
|
|
/// unfenced control shows the fence is what puts it there.
|
|
///
|
|
/// Both arms live in one `#[test]` on purpose. As separate tests they run
|
|
/// concurrently by default, each with its own flood thread, so each arm's
|
|
/// measurement includes the other arm's CPU load and the control's ratio
|
|
/// becomes a race between two floods rather than a statement about F1.
|
|
#[test]
|
|
#[ignore = "native performance gate; run release-mode on a known-idle host"]
|
|
fn g3_renderer_acquire_stays_within_frame_budget() {
|
|
let fenced = measure(Fences::ALL);
|
|
let p95 = fenced.percentile_micros(0.95);
|
|
assert!(
|
|
p95 <= P95_MICROS,
|
|
"renderer acquire p95 {p95} us over the {P95_MICROS} us budget (max {} us, n={})",
|
|
fenced.max_micros,
|
|
fenced.acquisitions
|
|
);
|
|
assert!(
|
|
fenced.max_micros <= FRAME_MICROS,
|
|
"renderer waited {} us for the terminal lock -- a dropped frame (p95 {p95} us, n={})",
|
|
fenced.max_micros,
|
|
fenced.acquisitions
|
|
);
|
|
|
|
// The control. Without it this gate could pass because the fixture never
|
|
// contended -- green over an experiment that did not run.
|
|
let unfenced = measure(Fences::OSC_ONLY);
|
|
assert!(
|
|
unfenced.max_micros > fenced.max_micros.max(1) * 4,
|
|
"unfenced renderer max {} us vs fenced {} us -- F1 is not what holds \
|
|
renderer latency down, and this gate is measuring something else",
|
|
unfenced.max_micros,
|
|
fenced.max_micros
|
|
);
|
|
}
|