Repository navigation
Add Ghostty comparison benchmarks and speed up the builtin engine - #61
Merged
Marc-André Moreau (mamoreau-devolutions) merged 1 commit intoOct 26, 2026
Conversation
Benchmarks: - Add a `vt` bench mode that feeds identical bytes to the builtin engine, raw libghostty-vt and DT's Ghostty engine in one process, checks the screens match, and reports a conservative faster/overlap/slower verdict. - Add 13 deterministic workloads modeled on ghostty-gen and vtebench, plus --frame-kb to include per-frame screen reads. - Add scripts/Invoke-TerminalBenchmarks.ps1 (one process per workload, ratio-based -Baseline regression gate, optional upstream ghostty-bench) and docs/performance-benchmarks.md. Builtin engine (now at or above raw libghostty-vt on 8 of 13 workloads): - Store cells packed (16 bytes, no GC references) with combining text and hyperlinks in per-line side arrays; the public Cell is unchanged. - Never write logically blank cells: recycling scrollback lines and erasing to end of line are O(1). - Bulk paths for printable ASCII (AVX2 where available), well-formed UTF-8 runs, plain CSI sequences and C0 controls, each checked against the per-character path by differential tests. - Precomputed width and grapheme-join tables for planes 0 and 1. Ghostty engine: - Project the render state lazily and only for rows Ghostty reports dirty, instead of re-projecting the whole screen after every feed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Marc-André Moreau (mamoreau-devolutions)
deployed
to
publish-dry-run
October 26, 2026 19:02 — with
GitHub Actions
Active
Marc-André Moreau (mamoreau-devolutions)
deployed
to
publish-dry-run
October 26, 2026 19:02 — with
GitHub Actions
Active
Marc-André Moreau (mamoreau-devolutions)
deployed
to
publish-dry-run
October 26, 2026 19:03 — with
GitHub Actions
Active
Marc-André Moreau (mamoreau-devolutions)
deployed
to
publish-dry-run
October 26, 2026 19:03 — with
GitHub Actions
Active
Marc-André Moreau (mamoreau-devolutions)
merged commit Oct 26, 2026
f45b95b
into
master
54 checks passed
Marc-André Moreau (mamoreau-devolutions)
deleted the
claude/dt-ghostty-perf-benchmarks-cf1a9f
branch
October 26, 2026 19:24
Marc-André Moreau (mamoreau-devolutions)
added a commit
that referenced
this pull request
Oct 27, 2026
## Summary The builtin engine now beats raw `libghostty-vt` on all 13 `vt` benchmark workloads, up from 8 of 13. The five that were behind or tied are now ahead: | Workload | Before (#61) | After | | --- | ---: | ---: | | cjk | 0.77× | 1.12× | | emoji | 0.80× | 1.21× | | utf8 | 0.77× | 1.36× | | cursor-motion | 0.90× | 1.09× | | scroll-region | 0.96× | 1.11× | Ratios are builtin speed ÷ Ghostty speed in the same run, on one Windows x64 laptop. Screens are identical to Ghostty's on every workload except `styled`, whose known one-row difference is unchanged. Full table and methodology: `docs/performance-benchmarks.md`. This PR also fixes windows ending up larger than the screen, or off it, after a display is disconnected. One commit per change, in order. Each perf commit builds and passes the Core tests on its own. ### Builtin engine - **`PrintRunes` append loop**: the loop moves into a small static method. Each rune is one OR into precomputed template bits and one 16-byte store, with widths from a table where ambiguous widths are already resolved. - **Short prints** (cursor-motion, `yes`): the print templates are cached until attributes, protection or shell-integration state change. A single printable byte is one store. The DEC graphics check is inline. - **CSI dispatch**: parameters are parsed straight into the arrays the engine receives, and the parser calls the sealed engine directly. Plain sequences skip the special cases and go straight to the switch on the final byte. - **Combining text as interned IDs**: one `CombiningTable` per buffer. Lines store `int` IDs in an array that is reused when the line is recycled. A cluster seen before allocates nothing, and the table compacts so it cannot grow without bound. Plain combining marks and ZWJ skip the per-character path. utf8 allocation fell from 9.4 MB to under 0.1 MB per MiB. - **Vectorized UTF-8 decode**: runs of 3- or 4-byte sequences are decoded four at a time from 16-byte `Vector128` loads, so Arm64 gets it too. They follow the scalar decoder's validity rules, and anything else falls back to it. CJK decoding is about 3× faster. Every new path is checked against the per-character or byte-by-byte path by differential tests. Each check was also deliberately broken to confirm a test fails. ### Benchmarks - `Invoke-TerminalBenchmarks.ps1 -MinMs` passes `--min-ms` to the bench. This laptop's CPU leaves turbo about 2.7 s into sustained load. With 500 ms samples, runs landed on both sides of that drop and spreads reached 50–180%. 3 s samples keep them around 5–20%. - `docs/performance-benchmarks.md` has the round 5 results, the engine changes, new per-stage costs and what remains. It also records that prefetching recycled scrollback lines was tried and measured slower. ### Window placement Windows moves windows off a disconnected display without shrinking them, so the terminal could overhang the laptop screen with its caption buttons out of reach. A saved position could also point at a display that is gone. `WindowPlacement.Fit` leaves a window alone when the displays cover all of it, so a window spanning two connected displays stays put. Otherwise it shrinks the window and moves it onto the display it overlaps most, or the nearest one. `MainWindow` applies it: - at launch, when a saved or requested position is restored, before maximizing; - on `Screens.Changed`; - when the window returns to Normal. A maximized or full-screen window keeps its state; its saved normal bounds are fitted instead. Windows Terminal only checks the top-left corner at launch and never shrinks. ## Testing - Core tests: 1,191 passed. - App tests: 632 passed, 9 of them new for `WindowPlacement`. - Headless UI tests: 670 passed. - `-Baseline` gate passed after each perf change. - The window fix has not been tried with a real display disconnect. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds a benchmark that compares DT's default engine with Ghostty on identical input, then uses it to speed the default engine up. The default engine now matches or beats the Ghostty library on 8 of 13 workloads. It started at 0.01–0.4× on most of them.
Full methodology, results and remaining gaps:
docs/performance-benchmarks.md.Benchmarks
vtmode intools/Devolutions.Terminal.Bench. It feeds identical bytes, in one process, to three targets:libghostty-vt;fasteronly when the builtin's slowest run beats Ghostty's fastest.ghostty-genand vtebench.--frame-kb Nalso takes a screen snapshot every N KiB, as the renderer would.scripts/Invoke-TerminalBenchmarks.ps1runs one process per workload.-Baselinefails on a builtin/Ghostty ratio drop of more than 15% on identical inputs.-GhosttyBenchalso replays the inputs through upstreamghostty-bench.Builtin engine
CellRow/PackedCell): 16 bytes per cell with no garbage-collected references, down from about 40. Combining marks and hyperlinks move to per-line side arrays. The publicCellandTermColortypes are unchanged.ESC [sequences;Ghostty engine
Results (one Windows x64 machine, builtin speed ÷ Ghostty speed in the same run)
y\n)Screens are identical on every workload except one
styledrow. There, the builtin engine drops a combining mark that follows a space, a pre-existing difference.Known gaps
Cell.Tests
AsciiFastPathTests,CsiFastPathTests,CellRowModelTests,PackedCellStorageTests(Core);GhosttyProjectionTests(Ghostty).Devolutions.Terminal.slnx: 0 warnings, 0 errors.🤖 Generated with Claude Code