mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-05 23:28:41 +00:00
This PR speeds up our formatting (plain text, html, and VT) by anywhere from ~1.5x to ~8x. The formatter is the hot path behind multiple features in Ghostty GUI: clipboard copy (plain/VT/HTML), `write_screen_file`, `selectionString`, and terminal search sliding window. It's also the hot path for libghostty users, namely people like [zmx](https://github.com/neurosnap/zmx) which utilize the VT formatter to restore a terminal. This PR also adds the benchmarking infrastructure for the formatter. ## How - **Fast cell-run optimization.** For simple cells (single codepoint, no style/hyperlink) we encode them as a single run rather than one at a time. - **Make some arguments comptime.** Generates more code but benchmarks show it improves things, specifically for per-format switches that we do a LOT. - **Interned style id fast path.** Styles are interned per page, so id equality implies style equality. We track the id of the active style and skip the per-cell `Style` copy + `eql` when it matches. - **Fast printing.** Avoid `std.fmt` where possible and assemble integers, RGB colors, codepoints in fixed-width buffers with a single memcpy. This was extracted partially to `fastprint.zig` so we can reuse it. - **Avoid double-formatting for tracked pins.** Previously we formatted twice (once through a `Discarding` writer to count bytes) for pin maps. Now I'm smarter about it and do a single pass. ## Performance All on my machine, 80x24 terminal, 10K lines of scrollback. | workload | main | this PR | speedup | throughput | | --------------------- | -------- | -------- | ------- | ---------- | | plain / plain | 5.74 ms | 1.67 ms | 3.4x | 364 MB/s | | plain / vt | 6.46 ms | 1.04 ms | 6.2x | 596 MB/s | | plain / html | 7.39 ms | 2.32 ms | 3.2x | 308 MB/s | | unicode / plain | 9.74 ms | 5.42 ms | 1.8x | 276 MB/s | | unicode / vt | 10.42 ms | 5.53 ms | 1.9x | 275 MB/s | | unicode / html | 12.53 ms | 7.35 ms | 1.7x | 509 MB/s | | styled / plain | 5.65 ms | 1.69 ms | 3.4x | 360 MB/s | | styled / vt | 9.07 ms | 4.20 ms | 2.2x | 409 MB/s | | styled / html | 10.78 ms | 6.64 ms | 1.6x | 740 MB/s | | mixed / plain | 8.59 ms | 4.81 ms | 1.8x | 226 MB/s | | mixed / vt | 11.25 ms | 6.65 ms | 1.7x | 250 MB/s | | mixed / html | 14.47 ms | 10.52 ms | 1.4x | 414 MB/s | | wrapped / plain | 7.30 ms | 1.11 ms | 6.6x | 733 MB/s | | wrapped / vt | 8.14 ms | 1.04 ms | 7.8x | 789 MB/s | | wrapped / html | 9.00 ms | 2.12 ms | 4.2x | 465 MB/s | | pin-map / plain | 12.51 ms | 3.80 ms | 3.3x | | | pin-map / vt | 13.12 ms | 2.99 ms | 4.4x | | | active screen / plain | 12.5 µs | 2.7 µs | 4.6x | | | active screen / vt | 18.4 µs | 6.8 µs | 2.7x | | Workloads: - `plain` is ASCII lines - `unicode` is 2/3/4-byte codepoints with 10% grapheme clusters - `styled` is heavy SGR churn - `mixed` is styles + Unicode + hyperlinks - `wrapped` is a continuous soft-wrapped stream - `pin-map`/`active screen` are the selectionString/search-style and visible-screen-only cases respectively.