Skip to content

Commit edc25c5

Browse files
committed
Migrate to use RenderState
From Mitchell's comment on HN: https://news.ycombinator.com/item?id=46111944This should improve performance dramatically.This also adds a basic set of benchmarks which have not yet been optimized.
1 parent 1d03ec8 commit edc25c5

File tree

14 files changed

+2091
-1674
lines changed

14 files changed

+2091
-1674
lines changed

bench/versus.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Terminal as XTerm } from '@xterm/xterm';
2+
import { bench, group, run } from 'mitata';
3+
import { Ghostty, Terminal as GhosttyTerminal } from '../lib';
4+
import '../happydom';
5+
6+
function generateColorText(lines: number): string {
7+
const colors = [31, 32, 33, 34, 35, 36];
8+
let output = '';
9+
for (let i = 0; i < lines; i++) {
10+
const color = colors[i % colors.length];
11+
output += `\x1b[${color}mLine ${i}: This is some colored text with ANSI escape sequences\x1b[0m\r\n`;
12+
}
13+
return output;
14+
}
15+
16+
function generateComplexVT(lines: number): string {
17+
let output = '';
18+
for (let i = 0; i < lines; i++) {
19+
output += `\x1b[1;4;38;2;255;128;0mBold underline RGB\x1b[0m `;
20+
output += `\x1b[48;5;236mBG 256\x1b[0m `;
21+
output += `\x1b[7mInverse\x1b[0m\r\n`;
22+
}
23+
return output;
24+
}
25+
26+
function generateRawBytes(size: number): Uint8Array {
27+
const data = new Uint8Array(size);
28+
for (let i = 0; i < size; i++) {
29+
const mod = i % 85;
30+
if (mod < 80) {
31+
data[i] = 32 + (i % 95); // Printable ASCII
32+
} else if (mod === 80) {
33+
data[i] = 13; // \r
34+
} else {
35+
data[i] = 10; // \n
36+
}
37+
}
38+
return data;
39+
}
40+
41+
function generateCursorMovement(ops: number): string {
42+
let output = '';
43+
for (let i = 0; i < ops; i++) {
44+
output += `\x1b[${(i % 24) + 1};${(i % 80) + 1}H`; // Cursor position
45+
output += `\x1b[K`; // Clear to end of line
46+
output += `Text at position ${i}`;
47+
output += `\x1b[A\x1b[B\x1b[C\x1b[D`; // Up, Down, Right, Left
48+
}
49+
return output;
50+
}
51+
52+
const withTerminals = async (fn: (term: GhosttyTerminal | XTerm) => Promise<void>) => {
53+
const ghostty = await Ghostty.load();
54+
bench('ghostty-web', async () => {
55+
const container = document.createElement('div');
56+
document.body.appendChild(container);
57+
const term = new GhosttyTerminal({ ghostty });
58+
await term.open(container);
59+
await fn(term);
60+
await term.dispose();
61+
});
62+
bench('xterm.js', async () => {
63+
const xterm = new XTerm();
64+
const container = document.createElement('div');
65+
document.body.appendChild(container);
66+
await xterm.open(container);
67+
await fn(xterm);
68+
await xterm.dispose();
69+
});
70+
};
71+
72+
const throughput = async (prefix: string, data: Record<string, Uint8Array | string>) => {
73+
await Promise.all(
74+
Object.entries(data).map(async ([name, data]) => {
75+
await group(`${prefix}: ${name}`, async () => {
76+
await withTerminals(async (term) => {
77+
await new Promise<void>((resolve) => {
78+
term.write(data, resolve);
79+
});
80+
});
81+
});
82+
})
83+
);
84+
};
85+
86+
await throughput('raw bytes', {
87+
'1KB': generateRawBytes(1024),
88+
'10KB': generateRawBytes(10 * 1024),
89+
'100KB': generateRawBytes(100 * 1024),
90+
'1MB': generateRawBytes(1024 * 1024),
91+
});
92+
93+
await throughput('color text', {
94+
'100 lines': generateColorText(100),
95+
'1000 lines': generateColorText(1000),
96+
'10000 lines': generateColorText(10000),
97+
});
98+
99+
await throughput('complex VT', {
100+
'100 lines': generateComplexVT(100),
101+
'1000 lines': generateComplexVT(1000),
102+
'10000 lines': generateComplexVT(10000),
103+
});
104+
105+
await throughput('cursor movement', {
106+
'1000 operations': generateCursorMovement(1000),
107+
'10000 operations': generateCursorMovement(10000),
108+
'100000 operations': generateCursorMovement(100000),
109+
});
110+
111+
await group('read full viewport', async () => {
112+
await withTerminals(async (term) => {
113+
const lines = term.rows;
114+
for (let i = 0; i < lines; i++) {
115+
const line = term.buffer.active.getLine(i);
116+
if (!line) {
117+
continue;
118+
}
119+
line.translateToString();
120+
}
121+
});
122+
});
123+
124+
await run();

bun.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"@biomejs/biome": "^1.9.4",
99
"@happy-dom/global-registrator": "^15.11.0",
1010
"@types/bun": "^1.3.2",
11+
"@xterm/headless": "^5.5.0",
12+
"@xterm/xterm": "^5.5.0",
13+
"mitata": "^1.0.34",
1114
"prettier": "^3.6.2",
1215
"typescript": "^5.9.3",
1316
"vite": "^4.5.0",
@@ -140,6 +143,10 @@
140143

141144
"@vue/shared": ["@vue/[email protected]", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
142145

146+
"@xterm/headless": ["@xterm/[email protected]", "", {}, "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g=="],
147+
148+
"@xterm/xterm": ["@xterm/[email protected]", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
149+
143150
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
144151

145152
"ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="],
@@ -218,6 +225,8 @@
218225

219226
"minimatch": ["[email protected]", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
220227

228+
"mitata": ["[email protected]", "", {}, "sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA=="],
229+
221230
"mlly": ["[email protected]", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
222231

223232
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

demo/bin/demo.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -446,23 +446,20 @@ wss.on('connection', (ws, req) => {
446446
});
447447

448448
// Send welcome message
449-
setTimeout(() => {
450-
if (ws.readyState !== ws.OPEN) return;
451-
const C = '\x1b[1;36m'; // Cyan
452-
const G = '\x1b[1;32m'; // Green
453-
const Y = '\x1b[1;33m'; // Yellow
454-
const R = '\x1b[0m'; // Reset
455-
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
456-
ws.send(
457-
`${C}${R} ${G}Welcome to ghostty-web!${R} ${C}${R}\r\n`
458-
);
459-
ws.send(`${C}${R} ${C}${R}\r\n`);
460-
ws.send(`${C}${R} You have a real shell session with full PTY support. ${C}${R}\r\n`);
461-
ws.send(
462-
`${C}${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}${R}\r\n`
463-
);
464-
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
465-
}, 100);
449+
const C = '\x1b[1;36m'; // Cyan
450+
const G = '\x1b[1;32m'; // Green
451+
const Y = '\x1b[1;33m'; // Yellow
452+
const R = '\x1b[0m'; // Reset
453+
ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`);
454+
ws.send(
455+
`${C}${R} ${G}Welcome to ghostty-web!${R} ${C}${R}\r\n`
456+
);
457+
ws.send(`${C}${R} ${C}${R}\r\n`);
458+
ws.send(`${C}${R} You have a real shell session with full PTY support. ${C}${R}\r\n`);
459+
ws.send(
460+
`${C}${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}${R}\r\n`
461+
);
462+
ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`);
466463
});
467464

468465
// ============================================================================

flake.lock

Lines changed: 147 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
description = "ghostty-web - Web terminal using Ghostty's VT100 parser via WASM";
3+
4+
inputs = {
5+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6+
flake-utils.url = "github:numtide/flake-utils";
7+
zig-overlay.url = "github:mitchellh/zig-overlay";
8+
};
9+
10+
outputs = { self, nixpkgs, flake-utils, zig-overlay }:
11+
flake-utils.lib.eachDefaultSystem (system:
12+
let
13+
pkgs = import nixpkgs {
14+
inherit system;
15+
overlays = [ zig-overlay.overlays.default ];
16+
};
17+
zig = pkgs.zigpkgs."0.15.2";
18+
in {
19+
devShells.default = pkgs.mkShell {
20+
buildInputs = [
21+
pkgs.bun
22+
pkgs.nodejs_22
23+
zig
24+
];
25+
};
26+
27+
packages.default = pkgs.stdenv.mkDerivation {
28+
pname = "ghostty-web";
29+
version = "0.3.0";
30+
31+
src = ./.;
32+
33+
nativeBuildInputs = [ pkgs.bun pkgs.nodejs_22 ];
34+
35+
buildPhase = ''
36+
export HOME=$TMPDIR
37+
bun install --frozen-lockfile
38+
bun run build
39+
'';
40+
41+
installPhase = ''
42+
mkdir -p $out
43+
cp -r dist/* $out/
44+
'';
45+
};
46+
}
47+
);
48+
}

ghostty

Submodule ghostty updated 294 files

0 commit comments

Comments
 (0)