Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Benchmark

on:
push:
branches: [main]
pull_request:
branches: [main]
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:

permissions:
contents: read
id-token: write

jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Build WASM
run: make

- name: Install dependencies
run: deno install

- name: Run benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
# IMPORTANT! deno task bench fails in CI due to incompatible V8 bindings
run: node bench/mod.ts
Comment on lines +47 to +48
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmark CI workflow failed with deno task bench because @codspeed/core@5.4.0 ships a native Node.js addon (node.napi.node) that uses V8's C++ API (v8::Isolate::GetCurrent()). Deno uses V8 internally but doesn't export that symbol, so we use node@22 directly here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bummer 🫠

41 changes: 22 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,25 +149,28 @@ Pass pointer state to `render()` to have clayterm do hit detection and return
pointer events in addition to the byte sequence.

```typescript
let { output, events } = term.render([
open("root", {
layout: { width: grow(), height: grow(), direction: "ltr" },
}),
open("sidebar", {
layout: { width: fixed(20), height: grow() },
bg: rgba(30, 30, 40),
}),
text("Sidebar"),
close(),
open("main", {
layout: { width: grow(), height: grow() },
}),
text("Main content"),
close(),
close(),
], {
pointer: { x: mouseX, y: mouseY, down: mouseDown },
});
let { output, events } = term.render(
[
open("root", {
layout: { width: grow(), height: grow(), direction: "ltr" },
}),
open("sidebar", {
layout: { width: fixed(20), height: grow() },
bg: rgba(30, 30, 40),
}),
text("Sidebar"),
close(),
open("main", {
layout: { width: grow(), height: grow() },
}),
text("Main content"),
close(),
close(),
],
{
pointer: { x: mouseX, y: mouseY, down: mouseDown },
},
);

for (let event of events) {
// { type: "pointerenter", id: "sidebar" }
Expand Down
55 changes: 55 additions & 0 deletions bench/input.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Bench } from "tinybench";
import { withCodSpeed } from "@codspeed/tinybench-plugin";
import { createInput } from "../input.ts";

function bytes(...values: number[]): Uint8Array {
return new Uint8Array(values);
}

function str(s: string): Uint8Array {
return new TextEncoder().encode(s);
}

let input = await createInput({ escLatency: 25 });

let longBurst = new Uint8Array(200);
for (let i = 0; i < 200; i++) {
longBurst[i] = 0x61 + (i % 26);
}

let bench = withCodSpeed(new Bench());

bench
.add("printable ASCII (single char)", () => {
input.scan(bytes(0x61));
})
.add("printable ASCII (short string)", () => {
input.scan(str("hello world"));
})
.add("arrow key (CSI sequence)", () => {
input.scan(bytes(0x1b, 0x5b, 0x41));
})
.add("modifier combo (Ctrl+Shift+Arrow)", () => {
input.scan(bytes(0x1b, 0x5b, 0x31, 0x3b, 0x38, 0x41));
})
.add("SGR mouse press", () => {
input.scan(str("\x1b[<0;35;12M"));
})
.add("multi-event burst (arrows + text)", () => {
input.scan(bytes(0x1b, 0x5b, 0x41, 0x1b, 0x5b, 0x42, 0x68, 0x69));
})
.add("UTF-8 3-byte character", () => {
input.scan(bytes(0xe4, 0xb8, 0xad));
})
.add("UTF-8 4-byte emoji", () => {
input.scan(bytes(0xf0, 0x9f, 0x8e, 0x89));
})
.add("Kitty protocol (CSI u with modifiers)", () => {
input.scan(str("\x1b[97;3u"));
})
.add("long input burst (200 bytes)", () => {
input.scan(longBurst);
});

await bench.run();
console.table(bench.table());
3 changes: 3 additions & 0 deletions bench/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import "./input.bench.ts";
import "./render.bench.ts";
import "./ops.bench.ts";
124 changes: 124 additions & 0 deletions bench/ops.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Bench } from "tinybench";
import { withCodSpeed } from "@codspeed/tinybench-plugin";
import { close, fixed, grow, open, pack, rgba, text } from "../ops.ts";
import type { Op } from "../ops.ts";

function makeBuf(size: number): ArrayBuffer {
return new ArrayBuffer(size);
}

let simpleOps: Op[] = [
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
}),
text("Hello, World!"),
close(),
];

let complexOps: Op[] = [
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
}),
open("header", {
layout: {
width: grow(),
height: fixed(3),
padding: { left: 1, right: 1 },
direction: "ltr",
},
bg: rgba(30, 30, 40),
border: {
color: rgba(100, 100, 120),
bottom: 1,
},
}),
text("Title", { color: rgba(255, 255, 255), fontSize: 1 }),
close(),
open("body", {
layout: {
width: grow(),
height: grow(),
direction: "ltr",
gap: 1,
},
}),
open("sidebar", {
layout: {
width: fixed(20),
height: grow(),
direction: "ttb",
padding: { left: 1, right: 1, top: 1 },
},
bg: rgba(25, 25, 35),
border: {
color: rgba(60, 60, 80),
right: 1,
},
}),
text("Menu Item 1"),
text("Menu Item 2"),
text("Menu Item 3"),
close(),
open("main", {
layout: {
width: grow(),
height: grow(),
direction: "ttb",
padding: { left: 2, top: 1 },
},
}),
text("Main content area with longer text to exercise the encoder"),
close(),
close(),
open("footer", {
layout: {
width: grow(),
height: fixed(1),
padding: { left: 1 },
direction: "ltr",
},
bg: rgba(30, 30, 40),
}),
text("Status: OK"),
close(),
close(),
];

let listOps: Op[] = [
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
}),
...Array.from({ length: 50 }, (_, i) => [
open(`item-${i}`, {
layout: {
width: grow(),
height: fixed(1),
padding: { left: 2 },
direction: "ltr",
},
bg: i % 2 === 0 ? rgba(30, 30, 40) : rgba(35, 35, 45),
}),
text(`List item ${i}: some description text`),
close(),
]).flat(),
close(),
];

let bench = withCodSpeed(new Bench());

bench
.add("simple tree (root + text)", () => {
let buf = makeBuf(4096);
pack(simpleOps, buf, 0);
})
.add("complex layout (header + sidebar + main + footer)", () => {
let buf = makeBuf(8192);
pack(complexOps, buf, 0);
})
.add("large list (50 items)", () => {
let buf = makeBuf(32768);
pack(listOps, buf, 0);
});

await bench.run();
console.table(bench.table());
Loading
Loading