mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
272 lines
9.5 KiB
JavaScript
272 lines
9.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { performance } from 'node:perf_hooks'
|
|
import ts from 'typescript-api'
|
|
import { build } from 'esbuild'
|
|
|
|
// Pipe the baseline capture module on stdin. No Electron or visible terminal is launched.
|
|
const renderer = path.resolve('src/renderer/src')
|
|
const entry = path.join(renderer, 'components/terminal-pane/terminal-shutdown-layout-capture.ts')
|
|
const sources = [readFileSync(0, 'utf8'), readFileSync(entry, 'utf8')]
|
|
assert(sources.every((source) => source.includes('export function captureTerminalShutdownLayout')))
|
|
const layoutFile = path.join(renderer, 'components/terminal-pane/layout-serialization.ts')
|
|
const layoutSource = readFileSync(layoutFile, 'utf8')
|
|
const layoutAst = ts.createSourceFile(layoutFile, layoutSource, ts.ScriptTarget.Latest, true)
|
|
const layoutNames = ['getLayoutChildNodes', 'serializePaneTree', 'serializeTerminalLayout']
|
|
const layoutFunctions = layoutAst.statements.filter(
|
|
(node) => ts.isFunctionDeclaration(node) && layoutNames.includes(node.name?.text)
|
|
)
|
|
assert.equal(layoutFunctions.length, layoutNames.length)
|
|
const layoutSubset = `import { isTerminalLeafId } from '../../../../shared/stable-pane-id';
|
|
${layoutFunctions.map((node) => node.getText(layoutAst)).join('\n')}`
|
|
|
|
async function load(source) {
|
|
const result = await build({
|
|
entryPoints: [entry],
|
|
bundle: true,
|
|
platform: 'node',
|
|
format: 'esm',
|
|
write: false,
|
|
alias: { '@': renderer },
|
|
plugins: [
|
|
{
|
|
name: 'capture-benchmark',
|
|
setup(builder) {
|
|
builder.onLoad({ filter: /terminal-shutdown-layout-capture\.ts$/ }, () => ({
|
|
contents: `${source}\nexport { fitsSessionScrollbackByteLimit, MAX_BUFFER_BYTES };`,
|
|
loader: 'ts',
|
|
resolveDir: path.dirname(entry)
|
|
}))
|
|
// Select unchanged layout declarations to exclude unrelated renderer module side effects.
|
|
builder.onLoad({ filter: /[/\\]layout-serialization\.ts$/ }, () => ({
|
|
contents: layoutSubset,
|
|
loader: 'ts',
|
|
resolveDir: path.dirname(layoutFile)
|
|
}))
|
|
builder.onResolve({ filter: /pane-terminal-output-scheduler$/ }, () => ({
|
|
path: 'capture-scheduler',
|
|
namespace: 'fixture'
|
|
}))
|
|
builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({
|
|
contents: 'export function flushTerminalOutput(terminal) { terminal.recordFlush?.(); }',
|
|
loader: 'js'
|
|
}))
|
|
}
|
|
}
|
|
]
|
|
})
|
|
const bundled = `${result.outputFiles[0].text}\n//# sourceURL=shutdown-byte-limit-benchmark-bundle.js`
|
|
return import(`data:text/javascript;base64,${Buffer.from(bundled).toString('base64')}`)
|
|
}
|
|
const modules = await Promise.all(sources.map(load))
|
|
const captures = modules.map((module) => module.captureTerminalShutdownLayout)
|
|
const predicates = modules.map((module) => module.fitsSessionScrollbackByteLimit)
|
|
|
|
class FixtureElement {
|
|
constructor({ classes = [], dataset = {}, children = [], firstElementChild = null } = {}) {
|
|
this.classList = { contains: (name) => classes.includes(name) }
|
|
this.dataset = dataset
|
|
this.children = children
|
|
this.firstElementChild = firstElementChild
|
|
this.style = { flex: '1' }
|
|
}
|
|
}
|
|
globalThis.HTMLElement = FixtureElement
|
|
|
|
function leafId(index) {
|
|
return `${String(index + 1).padStart(8, '0')}-1111-4111-8111-111111111111`
|
|
}
|
|
|
|
function captureArgs(panes, captureBuffers = true, cleared = false) {
|
|
const leaves = panes.map(
|
|
(pane) =>
|
|
new FixtureElement({
|
|
classes: ['pane'],
|
|
dataset: { leafId: pane.leafId }
|
|
})
|
|
)
|
|
let root = leaves[0] ?? null
|
|
for (const leaf of leaves.slice(1)) {
|
|
root = new FixtureElement({ classes: ['pane-split', 'is-horizontal'], children: [root, leaf] })
|
|
}
|
|
const prior = Object.fromEntries(panes.map((pane) => [pane.leafId, 'prior-buffer']))
|
|
return {
|
|
manager: { getPanes: () => panes, getActivePane: () => panes[0] ?? null },
|
|
container: new FixtureElement({ firstElementChild: root }),
|
|
expandedPaneId: panes[1]?.id ?? null,
|
|
paneTransports: new Map(
|
|
panes.map((pane, index) => [
|
|
pane.id,
|
|
{
|
|
getPtyId: () => (index % 2 ? null : `ssh:target@@pty-${index}`)
|
|
}
|
|
])
|
|
),
|
|
paneTitlesByPaneId: Object.fromEntries(panes.map((pane) => [pane.id, `title-${pane.id}`])),
|
|
existingLayout: Object.freeze({
|
|
root: null,
|
|
activeLeafId: null,
|
|
expandedLeafId: null,
|
|
buffersByLeafId: Object.freeze(prior),
|
|
scrollbackRefsByLeafId: Object.freeze({ [leafId(0)]: 'v1-prior' }),
|
|
ptyIdsByLeafId: Object.freeze({ [leafId(1)]: 'local-prior' })
|
|
}),
|
|
captureBuffers,
|
|
clearedScrollbackLeafIds: new Set(cleared ? [leafId(0)] : [])
|
|
}
|
|
}
|
|
|
|
function syntheticFixture(config) {
|
|
const events = []
|
|
const panes = config.units.map((unit, index) =>
|
|
Object.freeze({
|
|
id: index + 1,
|
|
leafId: leafId(index),
|
|
terminal: Object.freeze({
|
|
options: { scrollback: config.rows },
|
|
cols: 120,
|
|
rows: 40,
|
|
buffer: { active: { cursorX: index, cursorY: index } },
|
|
recordFlush: () => events.push(['flush', index])
|
|
}),
|
|
serializeAddon: {
|
|
serialize(options) {
|
|
const rows = options?.scrollback ?? 0
|
|
events.push(['serialize', index, rows])
|
|
if (config.fail && index === 0) {
|
|
throw new Error('fixture serializer failure')
|
|
}
|
|
return unit.repeat(Math.max(0, rows))
|
|
}
|
|
}
|
|
})
|
|
)
|
|
return { args: captureArgs(panes, config.capture, config.cleared), events }
|
|
}
|
|
|
|
let seed = 0x17c0ffee
|
|
function random(max) {
|
|
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
|
return (seed >>> 8) % max
|
|
}
|
|
const limit = modules[0].MAX_BUFFER_BYTES
|
|
assert.equal(modules[1].MAX_BUFFER_BYTES, limit)
|
|
const units = ['x', 'é', '界', '😀', '\ud83d', '\udc00', '\x1b[31mtext\x1b[0m\r\n', '']
|
|
let comparisons = 0
|
|
function compareFixture(config) {
|
|
const results = captures.map((capture) => {
|
|
const fixture = syntheticFixture(config)
|
|
const layout = capture(fixture.args)
|
|
return { layout, events: fixture.events }
|
|
})
|
|
assert.deepEqual(results[1], results[0])
|
|
comparisons++
|
|
}
|
|
for (const unit of units.filter(Boolean)) {
|
|
for (let delta = -2; delta <= 2; delta++) {
|
|
const rows = Math.floor((limit - 7) / Buffer.byteLength(unit)) + delta
|
|
compareFixture({ units: [unit], rows, capture: true, cleared: false, fail: false })
|
|
}
|
|
}
|
|
for (let iteration = 0; iteration < 1500; iteration++) {
|
|
compareFixture({
|
|
units: Array.from({ length: random(4) }, () => units[random(units.length)].repeat(random(50))),
|
|
rows: random(2000),
|
|
capture: random(5) !== 0,
|
|
cleared: random(3) === 0,
|
|
fail: random(9) === 0
|
|
})
|
|
}
|
|
for (const unit of units) {
|
|
for (const length of [0, 1, 64, limit - 1, limit, limit + 1]) {
|
|
const input = unit.repeat(length)
|
|
assert.equal(predicates[1](input), predicates[0](input))
|
|
}
|
|
}
|
|
console.log(`${comparisons} full capture layouts/event traces and 48 byte-fit cases match`)
|
|
|
|
function sample(arm, input, repeats) {
|
|
const start = performance.now()
|
|
let output
|
|
for (let i = 0; i < repeats; i++) {
|
|
output = arm(input)
|
|
}
|
|
return { elapsed: (performance.now() - start) / repeats, output }
|
|
}
|
|
|
|
function benchmark(name, input, arms, minimumRepeats = 1) {
|
|
const expected = arms[0](input)
|
|
assert.deepEqual(arms[1](input), expected)
|
|
for (const arm of arms) {
|
|
const until = performance.now() + 250
|
|
while (performance.now() < until) {
|
|
sample(arm, input, 1)
|
|
}
|
|
}
|
|
const repeats = Math.max(
|
|
minimumRepeats,
|
|
Math.min(100000, Math.ceil(50 / sample(arms[0], input, 1).elapsed))
|
|
)
|
|
/** @type {number[][]} */
|
|
const times = [[], []]
|
|
for (let pair = 0; pair < 8; pair++) {
|
|
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
|
const result = sample(arms[index], input, repeats)
|
|
times[index].push(result.elapsed)
|
|
assert.deepEqual(result.output, expected)
|
|
}
|
|
}
|
|
const median = times.map((values) => {
|
|
values.sort((a, b) => a - b)
|
|
return (values[3] + values[4]) / 2
|
|
})
|
|
console.log(JSON.stringify({ name, repeats, median, times }))
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify({
|
|
node: process.version,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
pairs: 8,
|
|
unit: 'ms'
|
|
})
|
|
)
|
|
for (const size of [64, 32768, limit, 2 * 1024 * 1024]) {
|
|
benchmark(`fit ASCII ${size} code units`, 'x'.repeat(size), predicates)
|
|
benchmark(`fit Unicode ${size} code units`, '界'.repeat(size), predicates)
|
|
}
|
|
|
|
const xterm = await import('@xterm/headless')
|
|
const addon = await import('@xterm/addon-serialize')
|
|
const Terminal = xterm.Terminal ?? xterm.default.Terminal
|
|
const SerializeAddon = addon.SerializeAddon ?? addon.default.SerializeAddon
|
|
for (const [rows, unicode, paneCount] of [
|
|
[50, false, 1],
|
|
[500, false, 1],
|
|
[5000, false, 1],
|
|
[5000, true, 1],
|
|
[500, false, 8]
|
|
]) {
|
|
const panes = []
|
|
for (let index = 0; index < paneCount; index++) {
|
|
const terminal = new Terminal({ cols: 120, rows: 40, scrollback: 5000, allowProposedApi: true })
|
|
const serializeAddon = new SerializeAddon()
|
|
terminal.loadAddon(serializeAddon)
|
|
const row = unicode ? '界é😀'.repeat(15) : 'line x'.repeat(18)
|
|
await new Promise((resolve) => terminal.write(`${row}\r\n`.repeat(rows), resolve))
|
|
panes.push({ id: index + 1, leafId: leafId(index), terminal, serializeAddon })
|
|
}
|
|
try {
|
|
benchmark(
|
|
`full capture ${paneCount} panes / ${rows} ${unicode ? 'Unicode' : 'ASCII'} lines`,
|
|
captureArgs(panes),
|
|
captures,
|
|
4
|
|
)
|
|
} finally {
|
|
panes.forEach((pane) => pane.terminal.dispose())
|
|
}
|
|
}
|