Skip to content

Commit bfde6dd

Browse files
feat(layout): incremental layout engine + drawlist codegen + expanded benchmarks (#151)
* feat(layout): phase 1 quick wins for incremental layout * feat(layout): phase 2 layout tree caching * feat(layout): phase 3 dirty-aware incremental layout * fix(bench): preserve backend markers in latching wrapper * feat(drawlist): generate v3 writers from spec with CI guardrails * bench: finalize strict/full-ui harness and CI perf guardrails * bench: add opentui-core as first-class framework + update benchmark docs - Add `opentui-core` to Framework type so both OpenTUI React and Core drivers run in a single benchmark invocation - Fix PTY bytesProduced validation: fall back to PTY-observed bytes when framework writes directly to fd (OpenTUI) - Add periodic GC in OpenTUI startup loops to prevent OOM - Update all 21 scenario files with opentui-core case - Rewrite BENCHMARKS.md with full 8-framework results and methodology - Update README.md Benchmarks section with cross-framework context - Update docs/benchmarks.md with new reproduction commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix biome format and import ordering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(bench): honor --opentui-driver flag for opentui framework The opentui framework entry now forwards the parsed opentuiDriver option to both checkFramework() and the worker env, so --framework opentui --opentui-driver core probes and runs the correct backend. opentui-core continues to always use core. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e7817c5 commit bfde6dd

72 files changed

Lines changed: 9261 additions & 1148 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ jobs:
5353
if: runner.os != 'Linux'
5454
run: npm install @biomejs/cli-${{ runner.os == 'macOS' && 'darwin' || 'win32' }}-${{ runner.arch == 'ARM64' && 'arm64' || 'x64' }}
5555

56+
- name: Drawlist codegen guardrail
57+
run: npm run codegen:check
58+
5659
- name: Lint
5760
run: npm run lint
5861

@@ -138,6 +141,9 @@ jobs:
138141
- name: Install
139142
run: bun install
140143

144+
- name: Drawlist codegen guardrail
145+
run: bun run codegen:check
146+
141147
- name: Lint
142148
run: bun run lint
143149

.github/workflows/prebuild.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ jobs:
5656
- name: Install
5757
run: npm ci
5858

59+
- name: Drawlist codegen guardrail
60+
run: npm run codegen:check
61+
5962
- name: Setup Rust (stable)
6063
uses: dtolnay/rust-toolchain@stable
6164
with:
@@ -141,6 +144,9 @@ jobs:
141144
- name: Install
142145
run: npm ci
143146

147+
- name: Drawlist codegen guardrail
148+
run: npm run codegen:check
149+
144150
- name: Download prebuilds
145151
uses: actions/download-artifact@v4
146152
with:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,4 @@ CODEX_*.md
181181
.last_perf_pack_full
182182
.last_perf_pack_quick
183183
*.cpuprofile
184+
packages/bench/bubbletea-bench/.bin/

AGENTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ These boundaries are strict. Violating them breaks the runtime-agnostic guarante
8686
- `@rezi-ui/node` imports from `@rezi-ui/core` only
8787
- `@rezi-ui/jsx` imports from `@rezi-ui/core` only
8888

89+
### Drawlist writer codegen guardrail (MUST for ZRDL command changes)
90+
91+
The v3/v4/v5 command writer implementation is code-generated. Never hand-edit
92+
`packages/core/src/drawlist/writers.gen.ts`.
93+
94+
When changing drawlist command layout/opcodes/field widths/offsets:
95+
96+
1. Update `scripts/drawlist-spec.ts` (single source of truth).
97+
2. Regenerate writers: `npm run codegen`.
98+
3. Verify sync guardrail: `npm run codegen:check`.
99+
4. Update `packages/core/src/drawlist/__tests__/writers.gen.test.ts` if command bytes changed.
100+
5. Update protocol docs (`docs/protocol/zrdl.md`, `docs/protocol/versioning.md`) in the same PR.
101+
102+
CI enforces this with `codegen:check`; stale generated writers are a hard failure.
103+
89104
## Testing Protocol
90105

91106
```bash
@@ -185,3 +200,4 @@ src/
185200
7. **Skipping tests after pipeline changes.** Any change to commit, reconcile, layout, or renderer files requires running the full test suite. Subtle regressions are common.
186201
8. **Breaking module boundaries.** Core must remain runtime-agnostic. Never add Node.js-specific imports (`Buffer`, `worker_threads`, `node:*`) to `@rezi-ui/core`.
187202
9. **Misconfiguring box transitions.** `ui.box` transition defaults to animating `position`, `size`, and `opacity`; use explicit `properties` filters (or `[]` to disable) when behavior should be constrained.
203+
10. **Editing generated drawlist writers by hand.** Update `scripts/drawlist-spec.ts` and run `npm run codegen` instead.

BENCHMARKS.md

Lines changed: 209 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,241 @@
11
# Benchmarks
22

3-
This document tracks terminal rendering benchmark methodology and committed result artifacts.
3+
This document tracks terminal rendering benchmark methodology, results, and committed artifacts.
44

5-
## Scope
5+
## Frameworks
66

7-
Frameworks currently covered:
8-
- Rezi native (`@rezi-ui/core` + `@rezi-ui/node`)
9-
- OpenTUI React driver (`@opentui/react` on top of `@opentui/core`, Bun runner)
10-
- OpenTUI core-imperative driver (`@opentui/core` renderables, Bun runner)
11-
- Bubble Tea (`github.com/charmbracelet/bubbletea`, Go runner)
12-
- Ink / blessed / ratatui in the terminal competitor suite
7+
| Framework | Runtime | Driver | Notes |
8+
|---|---|---|---|
9+
| **Rezi** | Node.js (worker) | `@rezi-ui/core` + `@rezi-ui/node` | Full layout engine, binary drawlist, native C renderer |
10+
| **Ink** | Node.js (worker) | React + Yoga + ANSI output | React reconciler, flexbox layout via Yoga |
11+
| **OpenTUI (React)** | Bun (subprocess) | `@opentui/react` on `@opentui/core` | React declarative driver |
12+
| **OpenTUI (Core)** | Bun (subprocess) | `@opentui/core` imperative API | Direct object mutation, no React overhead |
13+
| **Bubble Tea** | Go (subprocess) | `charmbracelet/bubbletea` | Elm architecture, string-based `View()` rendering |
14+
| **terminal-kit** | Node.js (worker) | `terminal-kit` | Low-level terminal buffer library, no widget/layout system |
15+
| **blessed** | Node.js (worker) | `blessed` | Low-level terminal UI with box model, no constraint layout |
16+
| **Ratatui** | Rust (subprocess) | `ratatui` | Native Rust terminal renderer, immediate mode |
1317

14-
`ink-compat` remains deprecated and is not part of the active matrix.
18+
`ink-compat` is deprecated and excluded from the active matrix.
1519

16-
## Latest Artifacts
20+
## Scenarios
21+
22+
The suite contains 22 scenarios in three tiers:
23+
24+
**Primitive workloads** (isolated subsystem stress):
25+
- `startup` — app create/destroy cycle
26+
- `tree-construction` — build widget tree from scratch (parameterized: 10, 100, 500, 1000 items)
27+
- `rerender` — update a single changing value in an existing tree
28+
- `content-update` — update text content across a large tree
29+
- `layout-stress` — grid layout with many cells
30+
- `scroll-stress` — scroll a 2000-item list
31+
- `virtual-list` — viewport-windowed rendering over 100K items
32+
- `tables` — 100-row, 8-column data table rendering
33+
- `memory-profile` — steady-state memory behavior
34+
35+
**Terminal-level workloads** (end-to-end render + terminal write):
36+
- `terminal-rerender`, `terminal-frame-fill` (1 and 40 dirty lines), `terminal-screen-transition`, `terminal-fps-stream`, `terminal-input-latency`, `terminal-memory-soak`, `terminal-virtual-list`, `terminal-table`
37+
38+
**Full-app workloads** (structured UI composition):
39+
- `terminal-full-ui` — composite dashboard shell with panels, status bar, data sections
40+
- `terminal-full-ui-navigation` — same shell with page routing
41+
- `terminal-strict-ui` — structured multi-panel layout (header, 3-column body, footer, status bar)
42+
- `terminal-strict-ui-navigation` — same structured layout with navigation
43+
44+
The strict scenarios exist to reduce bias when comparing Rezi (which has a layout engine) against libraries that operate closer to raw terminal buffers (blessed, ratatui, terminal-kit). In primitive scenarios these libraries skip layout entirely, which flatters their numbers on simple workloads but does not reflect real application complexity.
45+
46+
## Results (2026-02-22, all frameworks, PTY mode)
47+
48+
Single-replicate, default iteration counts. WSL host (see caveats). These numbers are directional.
49+
50+
### Relative performance vs Rezi
51+
52+
Ratio >1 means the competitor is slower. Ratio <1 means the competitor is faster.
53+
54+
**vs Ink** (21 scenarios where both participate):
55+
56+
| Scenario | Ratio |
57+
|---|---:|
58+
| tree-construction (10 items) | 206x slower |
59+
| tree-construction (100 items) | 80x slower |
60+
| tree-construction (500 items) | 49x slower |
61+
| tree-construction (1000 items) | 46x slower |
62+
| rerender | 47x slower |
63+
| content-update | 32x slower |
64+
| layout-stress | 10x slower |
65+
| scroll-stress | 12x slower |
66+
| virtual-list | 23x slower |
67+
| tables | 11x slower |
68+
| terminal-rerender | 47x slower |
69+
| terminal-frame-fill (1 dirty line) | 55x slower |
70+
| terminal-frame-fill (40 dirty lines) | 36x slower |
71+
| terminal-fps-stream | 10x slower |
72+
| terminal-input-latency | 35x slower |
73+
| terminal-full-ui | 10x slower |
74+
| terminal-strict-ui | 21x slower |
75+
76+
Rezi is faster than Ink in every measured scenario. The gap is largest on tree construction and rerender workloads (45-206x) and smallest on complex UI composition (10x).
77+
78+
**vs OpenTUI (React)** (21 scenarios):
79+
80+
Rezi faster in all 21 scenarios. Range: 1.8x to 155x. Geomean: ~10x.
81+
82+
**vs OpenTUI (Core)** (21 scenarios):
83+
84+
Rezi faster in 19/21 scenarios. Range: 1.3x to 13x. Geomean: ~2.6x.
85+
86+
OpenTUI Core wins:
87+
- `layout-stress`: Core 1.5x faster (517 vs 347 ops/s)
88+
- `tables`: Core 1.6x faster (434 vs 277 ops/s)
89+
90+
**vs Bubble Tea** (21 scenarios):
91+
92+
Rezi faster in 20/21 scenarios. Bubble Tea throughput clusters around ~120 ops/s in most scenarios due to its 8.33ms tick rate. Rezi wins are large where it exceeds this ceiling.
93+
94+
Bubble Tea wins:
95+
- `scroll-stress`: Bubble Tea 2.5x faster (120 vs 48 ops/s)
96+
97+
**vs terminal-kit / blessed / Ratatui** (subset of scenarios):
98+
99+
These three participate only in scenarios that make sense for their level of abstraction. terminal-kit and blessed are low-level buffer libraries without layout engines or widget systems. Ratatui is a native Rust renderer.
100+
101+
On **primitive** workloads (rerender, frame-fill, simple construction):
102+
- terminal-kit: 1x-13x faster than Rezi
103+
- blessed: 2x-19x faster than Rezi
104+
- Ratatui: 2x-20x faster than Rezi
105+
106+
On **complex UI** workloads (strict-ui, full-ui, fps-stream, tables, virtual-list):
107+
- Rezi is competitive or faster, because these scenarios exercise layout and composition which the primitive libraries lack
108+
109+
This is expected. terminal-kit and blessed write directly to terminal buffers without computing layout. Ratatui is compiled Rust. On workloads that actually require structured layout and widget composition, the gap narrows or reverses.
110+
111+
### Representative numbers
112+
113+
Selected scenarios showing the performance landscape across categories:
114+
115+
| Scenario | Rezi | Ink | OpenTUI React | OpenTUI Core | Bubble Tea | Ratatui |
116+
|---|---:|---:|---:|---:|---:|---:|
117+
| startup | 1.87ms (516 ops/s) | 5.62ms (112) | 8.68ms (33) | 4.92ms (38) | 9.94ms (49) | 184us (5.2K) |
118+
| tree-construction (100) | 326us (3.1K) | 26ms (38) | 36ms (27) | 2.15ms (466) | 8.33ms (120) | 696us (1.4K) |
119+
| rerender | 373us (2.7K) | 17.7ms (57) | 2.70ms (370) | 1.16ms (860) | 8.33ms (120) | 51us (19.7K) |
120+
| layout-stress | 2.88ms (347) | 28ms (36) | 33ms (30) | 1.93ms (517) | 8.33ms (120) | -- |
121+
| virtual-list (100K items) | 985us (1.0K) | 22.6ms (44) | 28.5ms (35) | 1.28ms (780) | 8.33ms (120) | -- |
122+
| terminal-strict-ui | 1.19ms (836) | 25.5ms (39) | 19.4ms (51) | 1.77ms (565) | 8.33ms (120) | 240us (4.2K) |
123+
| terminal-full-ui | 2.49ms (401) | 25.6ms (39) | 5.07ms (197) | 1.31ms (760) | 8.33ms (120) | 336us (3.0K) |
124+
125+
### Memory
126+
127+
| Framework | Typical peak RSS (UI scenarios) | Notes |
128+
|---|---|---|
129+
| Rezi | 80-210 MB | Heap ~20-120 MB depending on tree size |
130+
| Ink | 120-980 MB | Grows significantly with tree size |
131+
| OpenTUI (React) | 200 MB - 15 GB | Memory scales poorly; OOMs on tree-construction at 1000 items |
132+
| OpenTUI (Core) | 100-190 MB | Comparable to Rezi |
133+
| Bubble Tea | 7-10 MB | Go runtime baseline, very low footprint |
134+
| Ratatui | 3-16 MB | Native binary, minimal overhead |
135+
| terminal-kit | 69-83 MB | Lightweight buffer library |
136+
| blessed | 77-300 MB | Varies with screen complexity |
137+
138+
### OpenTUI: React vs Core
139+
140+
OpenTUI has two rendering paths. The React driver (`@opentui/react`) uses React reconciliation with `flushSync()`. The Core driver (`@opentui/core`) uses direct imperative object mutation.
141+
142+
Core is faster than React in all 21 scenarios. Geomean speedup: ~4x. The difference is most dramatic on memory-intensive workloads:
143+
- content-update: Core 96 ops/s vs React 4 ops/s (24x)
144+
- tree-construction (500 items): Core 104 ops/s vs React 5 ops/s (20x)
145+
- Peak RSS: Core ~180 MB vs React ~15 GB on content-update
146+
147+
Both drivers are benchmarked as separate framework entries (`opentui` and `opentui-core`).
148+
149+
## Fairness notes
150+
151+
### What is measured
152+
153+
Each benchmark iteration measures one complete render cycle: state update through final frame output. For PTY-mode benchmarks, this includes the terminal write path.
154+
155+
### Runtime differences
156+
157+
These benchmarks compare frameworks, but also inevitably compare runtimes:
158+
- Rezi, Ink, blessed: Node.js worker processes
159+
- OpenTUI (both drivers): Bun subprocesses
160+
- Bubble Tea: Go subprocess
161+
- Ratatui: Rust subprocess
162+
163+
Cross-framework numbers include runtime and toolchain effects alongside framework costs.
164+
165+
### Scenario parity
166+
167+
- Primitive scenarios (rerender, frame-fill) test narrow subsystem performance. Libraries without layout engines (terminal-kit, blessed, Ratatui) have an inherent advantage here because they do less work per frame.
168+
- Strict-ui scenarios build equivalent multi-panel layouts across all frameworks. Bubble Tea renders via lipgloss string composition rather than a widget tree, which is a different code path than the others.
169+
- Full-ui scenarios test composite dashboards with data sections, status bars, and navigation.
170+
171+
### Byte metrics
172+
173+
`bytesProduced` is framework-local and not directly comparable across frameworks. In PTY mode, `ptyBytesObserved` provides a cross-framework terminal I/O comparison.
174+
175+
### Environment
176+
177+
WSL and virtualized hosts introduce measurable scheduler jitter. Treat WSL-collected results as directional. For publication-grade numbers, use bare-metal Linux with CPU pinning and multiple replicates.
178+
179+
## Artifacts
17180

18181
Rigorous terminal-suite dataset (multi-replicate, confidence-focused):
19-
- `benchmarks/2026-02-19-terminal-v3/results.json`
20-
- `benchmarks/2026-02-19-terminal-v3/results.md`
21-
22-
Latest driver/framework matchup snapshots (quick mode, single replicate):
23-
- Rezi vs OpenTUI React: `benchmarks/2026-02-20-rezi-opentui-react-all-quick-v6/results.json`
24-
- Rezi vs OpenTUI Core: `benchmarks/2026-02-20-rezi-opentui-core-all-quick-v4/results.json`
25-
- Rezi vs OpenTUI Core vs Bubble Tea: `benchmarks/2026-02-20-rezi-opentui-bubbletea-core-all-quick-v3/results.json`
26-
27-
## Summary (2026-02-20 Quick Matchups)
28-
29-
These are directional quick runs (`--quick`, one replicate), not publication-grade confidence claims:
30-
31-
| Matchup | Cases | Rezi Faster | Opponent Faster | Geomean (opponent/Rezi) | Implied Rezi Advantage |
32-
|---|---:|---:|---:|---:|---:|
33-
| Rezi vs OpenTUI React | 21 | 21 | 0 | 0.096x | ~10.4x faster |
34-
| Rezi vs OpenTUI Core | 21 | 19 | 2 | 0.383x | ~2.6x faster |
35-
| Rezi vs Bubble Tea (3-way run) | 21 | 20 | 1 | 0.118x | ~8.5x faster |
36-
37-
Case-level notes:
38-
- OpenTUI Core wins vs Rezi in:
39-
- `layout-stress` (`~1.12x`)
40-
- `terminal-fps-stream` (`~2.41x`)
41-
- Bubble Tea wins vs Rezi in:
42-
- `scroll-stress` (`~1.67x`)
43-
- OpenTUI Core vs OpenTUI React (same scenario set):
44-
- Core is faster in `21/21` cases
45-
- Geomean core/react speedup: `~4.0x`
46-
47-
## Fairness Notes
48-
49-
- OpenTUI now has explicit driver selection in the harness:
50-
- `--opentui-driver react` (default)
51-
- `--opentui-driver core` (imperative)
52-
- Bubble Tea is executed through `packages/bench/bubbletea-bench/main.go` using Bubble Tea's normal program loop and terminal renderer.
53-
- In these runs, Bubble Tea throughput clusters around ~120 fps-equivalent in many scenarios. This behavior is visible directly in per-scenario ops/s and should be considered when interpreting throughput-style loops.
54-
- PTY mode (`--io pty`) is required for OpenTUI and Bubble Tea measurements in this suite.
182+
- `benchmarks/2026-02-19-terminal-v3/`
183+
184+
Quick matchup snapshots (directional only):
185+
- `benchmarks/2026-02-20-rezi-opentui-react-all-quick-v6/`
186+
- `benchmarks/2026-02-20-rezi-opentui-core-all-quick-v4/`
187+
- `benchmarks/2026-02-20-rezi-opentui-bubbletea-core-all-quick-v3/`
188+
189+
CI baseline (rezi-native regression tracking):
190+
- `benchmarks/ci-baseline/`
55191

56192
## Reproducing
57193

58-
### 1) Rigorous terminal suite (replicates + shuffle + affinity)
194+
### Full cross-framework suite (PTY mode)
59195

60196
```bash
61197
npm ci
62198
npm run build
63199
npm run build:native
64200
npx tsc -b packages/bench
65201

202+
# Build Ratatui bench binary
66203
cd benchmarks/native/ratatui-bench
67204
cargo build --release
68205
cd -
69206

207+
node --expose-gc packages/bench/dist/run.js \
208+
--suite all \
209+
--io pty \
210+
--output-dir benchmarks/local-all
211+
```
212+
213+
### Rigorous terminal suite (replicates + shuffle + affinity)
214+
215+
```bash
70216
node --expose-gc packages/bench/dist/run.js \
71217
--suite terminal \
72218
--io pty \
73219
--replicates 7 \
74220
--discard-first-replicate \
75221
--shuffle-framework-order \
76-
--shuffle-seed 2026-02-19-terminal-v3 \
222+
--shuffle-seed local-terminal \
77223
--cpu-affinity 0-7 \
78224
--env-check warn \
79-
--output-dir benchmarks/local-terminal-v3
225+
--output-dir benchmarks/local-terminal
80226
```
81227

82-
### 2) Rezi vs OpenTUI React (quick)
228+
### Quick matchup: Rezi vs OpenTUI
83229

84230
```bash
231+
# React driver
85232
node --expose-gc packages/bench/dist/run.js \
86233
--matchup rezi-opentui \
87-
--opentui-driver react \
88234
--io pty \
89235
--quick \
90236
--output-dir benchmarks/local-rezi-opentui-react-quick
91-
```
92-
93-
### 3) Rezi vs OpenTUI Core (quick)
94237

95-
```bash
238+
# Core driver
96239
node --expose-gc packages/bench/dist/run.js \
97240
--matchup rezi-opentui \
98241
--opentui-driver core \
@@ -101,10 +244,9 @@ node --expose-gc packages/bench/dist/run.js \
101244
--output-dir benchmarks/local-rezi-opentui-core-quick
102245
```
103246

104-
### 4) Rezi vs OpenTUI Core vs Bubble Tea (quick)
247+
### Quick matchup: Rezi vs OpenTUI Core vs Bubble Tea
105248

106249
```bash
107-
# Bubble Tea runner currently requires Go >= 1.24
108250
REZI_GO_BIN=/path/to/go \
109251
node --expose-gc packages/bench/dist/run.js \
110252
--matchup rezi-opentui-bubbletea \
@@ -114,8 +256,17 @@ node --expose-gc packages/bench/dist/run.js \
114256
--output-dir benchmarks/local-rezi-opentui-bubbletea-core-quick
115257
```
116258

259+
### Requirements
260+
261+
- Node.js 18+ with `--expose-gc`
262+
- `node-pty` for PTY mode: `npm i -w @rezi-ui/bench -D node-pty`
263+
- Bun for OpenTUI scenarios: `bun --version`
264+
- Go >= 1.24 for Bubble Tea scenarios: `go version`
265+
- Rust toolchain for Ratatui scenarios: `cargo build --release` in `benchmarks/native/ratatui-bench/`
266+
117267
## Limits
118268

119269
- PTY benchmarks include framework render + terminal write path, not terminal emulator pixel paint.
120-
- Absolute numbers are host-specific; compare within the same dataset and mode.
270+
- Absolute numbers are host-specific; compare within the same dataset, mode, and host.
121271
- Quick-mode single-replicate outputs are useful for trend checks, not confidence-grade claims.
272+
- OpenTUI React OOMs on `tree-construction` at 1000 items (inherent to React driver memory scaling).

0 commit comments

Comments
 (0)