475 lines
15 KiB
JavaScript
475 lines
15 KiB
JavaScript
/* eslint-disable no-control-regex -- Benchmarks mirror PTY ANSI/OSC parsing regexes. */
|
|
import { performance } from 'node:perf_hooks'
|
|
import v8 from 'node:v8'
|
|
|
|
const PTY_COUNT = Number.parseInt(process.env.ORCA_PTY_BENCH_PTY_COUNT ?? '24', 10)
|
|
const PAYLOAD_CHARS = Number.parseInt(process.env.ORCA_PTY_BENCH_PAYLOAD_CHARS ?? '262144', 10)
|
|
const RUNS = Number.parseInt(process.env.ORCA_PTY_BENCH_RUNS ?? '30', 10)
|
|
const MEASURE_TIMER_DELAYS = process.env.ORCA_PTY_BENCH_MEASURE_TIMER_DELAYS !== '0'
|
|
const INGRESS_CHUNKS = Number.parseInt(process.env.ORCA_PTY_BENCH_INGRESS_CHUNKS ?? '96', 10)
|
|
const INGRESS_CHUNK_CHARS = Number.parseInt(process.env.ORCA_PTY_BENCH_INGRESS_CHARS ?? '65536', 10)
|
|
const CHUNK_CHARS = 16 * 1024
|
|
const MAX_WRITES_PER_SLICE = 2
|
|
const RECENT_PTY_OUTPUT_LIMIT = 4096
|
|
const MAX_TAIL_LINES = 2000
|
|
const MAX_TAIL_CHARS = 256 * 1024
|
|
const MAX_TAIL_PARTIAL_CHARS = 4000
|
|
const OSC_TITLE_RE = /\x1b\]([012]);([^\x07\x1b]*?)(?:\x07|\x1b\\)/g
|
|
const URL_CANDIDATE_PATTERN = /\bhttps?:\/\/[^\s<>"'`]+/gi
|
|
|
|
if (!Number.isInteger(PTY_COUNT) || PTY_COUNT <= 0) {
|
|
throw new Error(`ORCA_PTY_BENCH_PTY_COUNT must be positive, received ${PTY_COUNT}`)
|
|
}
|
|
if (!Number.isInteger(PAYLOAD_CHARS) || PAYLOAD_CHARS <= 0) {
|
|
throw new Error(`ORCA_PTY_BENCH_PAYLOAD_CHARS must be positive, received ${PAYLOAD_CHARS}`)
|
|
}
|
|
if (!Number.isInteger(RUNS) || RUNS <= 0) {
|
|
throw new Error(`ORCA_PTY_BENCH_RUNS must be positive, received ${RUNS}`)
|
|
}
|
|
if (!Number.isInteger(INGRESS_CHUNKS) || INGRESS_CHUNKS <= 0) {
|
|
throw new Error(`ORCA_PTY_BENCH_INGRESS_CHUNKS must be positive, received ${INGRESS_CHUNKS}`)
|
|
}
|
|
if (!Number.isInteger(INGRESS_CHUNK_CHARS) || INGRESS_CHUNK_CHARS <= 0) {
|
|
throw new Error(`ORCA_PTY_BENCH_INGRESS_CHARS must be positive, received ${INGRESS_CHUNK_CHARS}`)
|
|
}
|
|
|
|
function makePendingData() {
|
|
const pending = new Map()
|
|
for (let index = 0; index < PTY_COUNT; index++) {
|
|
pending.set(`pty-${index}`, `${index}:`.padEnd(PAYLOAD_CHARS, 'x'))
|
|
}
|
|
return pending
|
|
}
|
|
|
|
function simulateWebContentsSend(id, data) {
|
|
return v8.serialize({ channel: 'pty:data', payload: { id, data } }).byteLength
|
|
}
|
|
|
|
function flushLegacy(pending) {
|
|
let bytes = 0
|
|
const start = performance.now()
|
|
for (const [id, data] of pending) {
|
|
bytes += simulateWebContentsSend(id, data)
|
|
}
|
|
pending.clear()
|
|
return { bytes, durationMs: performance.now() - start }
|
|
}
|
|
|
|
function flushBoundedSlice(pending) {
|
|
let bytes = 0
|
|
let writes = 0
|
|
const start = performance.now()
|
|
while (pending.size > 0 && writes < MAX_WRITES_PER_SLICE) {
|
|
const next = pending.entries().next().value
|
|
if (!next) {
|
|
break
|
|
}
|
|
const [id, data] = next
|
|
pending.delete(id)
|
|
const chunk = data.slice(0, CHUNK_CHARS)
|
|
const remaining = data.slice(CHUNK_CHARS)
|
|
if (remaining) {
|
|
pending.set(id, remaining)
|
|
}
|
|
bytes += simulateWebContentsSend(id, chunk)
|
|
writes++
|
|
}
|
|
return { bytes, durationMs: performance.now() - start }
|
|
}
|
|
|
|
function drainBounded(pending) {
|
|
let bytes = 0
|
|
const sliceDurations = []
|
|
while (pending.size > 0) {
|
|
const result = flushBoundedSlice(pending)
|
|
bytes += result.bytes
|
|
sliceDurations.push(result.durationMs)
|
|
}
|
|
return { bytes, sliceDurations }
|
|
}
|
|
|
|
function makeIngressChunk() {
|
|
return `${'x'.repeat(INGRESS_CHUNK_CHARS - 1)}\n`
|
|
}
|
|
|
|
function extractLastOscTitleLegacy(data) {
|
|
let last = null
|
|
for (const m of data.matchAll(OSC_TITLE_RE)) {
|
|
last = m[2]
|
|
}
|
|
return last
|
|
}
|
|
|
|
function extractLastOscTitleCurrent(data) {
|
|
if (!data.includes('\x1b]')) {
|
|
return null
|
|
}
|
|
return extractLastOscTitleLegacy(data)
|
|
}
|
|
|
|
function hasMeaningfulContentLegacy(chunk) {
|
|
return (
|
|
chunk
|
|
.replace(/\r\n/g, '\n')
|
|
.replace(/\r/g, '\n')
|
|
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
.replace(/\x1b[@-_]/g, '')
|
|
.replace(/\u0008/g, '')
|
|
.replace(/[^\x09\x0a\x20-\x7e]/g, '')
|
|
.trim().length > 0
|
|
)
|
|
}
|
|
|
|
function hasMeaningfulContentCurrent(chunk) {
|
|
for (let index = 0; index < chunk.length; index++) {
|
|
const code = chunk.charCodeAt(index)
|
|
if (code === 0x1b || code < 0x09 || (code > 0x0d && code < 0x20) || code > 0x7e) {
|
|
break
|
|
}
|
|
if (code > 0x20) {
|
|
return true
|
|
}
|
|
}
|
|
return hasMeaningfulContentLegacy(chunk)
|
|
}
|
|
|
|
function normalizeTerminalChunkLegacy(chunk) {
|
|
return chunk
|
|
.replace(/\r\n/g, '\n')
|
|
.replace(/\r/g, '\n')
|
|
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
.replace(/\x1b[@-_]/g, '')
|
|
.replace(/\u0008/g, '')
|
|
.replace(/[^\x09\x0a\x20-\x7e]/g, '')
|
|
}
|
|
|
|
function terminalChunkNeedsNormalization(chunk) {
|
|
for (let index = 0; index < chunk.length; index++) {
|
|
const code = chunk.charCodeAt(index)
|
|
if (
|
|
code === 0x1b ||
|
|
code === 0x0d ||
|
|
code < 0x09 ||
|
|
(code > 0x0a && code < 0x20) ||
|
|
code > 0x7e
|
|
) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
function normalizeTerminalChunkCurrent(chunk) {
|
|
return terminalChunkNeedsNormalization(chunk) ? normalizeTerminalChunkLegacy(chunk) : chunk
|
|
}
|
|
|
|
function appendNormalizedToTailBuffer(previousLines, previousPartialLine, normalizedChunk) {
|
|
if (normalizedChunk.length === 0) {
|
|
return {
|
|
lines: previousLines,
|
|
partialLine: previousPartialLine
|
|
}
|
|
}
|
|
|
|
const boundedPreviousPartialLine = previousPartialLine.slice(-MAX_TAIL_PARTIAL_CHARS)
|
|
const pieces = `${boundedPreviousPartialLine}${normalizedChunk}`.split('\n')
|
|
const nextPartialLine = (pieces.pop() ?? '').replace(/[ \t]+$/g, '')
|
|
const retainedPartialLine = nextPartialLine.slice(-MAX_TAIL_PARTIAL_CHARS)
|
|
let nextLines =
|
|
pieces.length > 0
|
|
? [...previousLines, ...pieces.map((line) => line.replace(/[ \t]+$/g, ''))]
|
|
: previousLines
|
|
|
|
while (nextLines.length > MAX_TAIL_LINES) {
|
|
nextLines.shift()
|
|
}
|
|
|
|
if (pieces.length > 0 || retainedPartialLine.length > previousPartialLine.length) {
|
|
if (nextLines === previousLines) {
|
|
nextLines = [...previousLines]
|
|
}
|
|
let totalChars =
|
|
nextLines.reduce((sum, line) => sum + line.length, 0) + retainedPartialLine.length
|
|
while (nextLines.length > 0 && totalChars > MAX_TAIL_CHARS) {
|
|
totalChars -= nextLines.shift().length
|
|
}
|
|
}
|
|
|
|
return {
|
|
lines: nextLines,
|
|
partialLine: retainedPartialLine
|
|
}
|
|
}
|
|
|
|
function appendTailBufferLegacy(previousLines, previousPartialLine, chunk) {
|
|
return appendNormalizedToTailBuffer(
|
|
previousLines,
|
|
previousPartialLine,
|
|
normalizeTerminalChunkLegacy(chunk)
|
|
)
|
|
}
|
|
|
|
function tailStateMatches(lines, partialLine, snapshot) {
|
|
if (
|
|
partialLine !== snapshot.partialLine ||
|
|
lines.length !== snapshot.lines.length ||
|
|
lines.length !== snapshot.linesTotal
|
|
) {
|
|
return false
|
|
}
|
|
if (lines === snapshot.lines) {
|
|
return true
|
|
}
|
|
for (let index = 0; index < lines.length; index++) {
|
|
if (lines[index] !== snapshot.lines[index]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
class AdvertisedUrlPtyBuffer {
|
|
raw = ''
|
|
|
|
ingest(chunk) {
|
|
this.raw += chunk
|
|
if (this.raw.length > 4096) {
|
|
this.raw = this.raw.slice(-4096)
|
|
}
|
|
const lastLineBreak = Math.max(this.raw.lastIndexOf('\n'), this.raw.lastIndexOf('\r'))
|
|
if (lastLineBreak === -1) {
|
|
return ''
|
|
}
|
|
const finalized = this.raw.slice(0, lastLineBreak + 1)
|
|
this.raw = this.raw.slice(lastLineBreak + 1)
|
|
return finalized
|
|
}
|
|
}
|
|
|
|
function scanAdvertisedUrls(buffer, chunk) {
|
|
const finalized = buffer.ingest(chunk)
|
|
if (!finalized) {
|
|
return
|
|
}
|
|
for (const _match of finalized.matchAll(URL_CANDIDATE_PATTERN)) {
|
|
// Candidate extraction is enough for this benchmark; URL validation is rare
|
|
// and only happens after a URL-shaped match.
|
|
}
|
|
}
|
|
|
|
function measureRuntimeIngressLegacy() {
|
|
const chunk = makeIngressChunk()
|
|
const advertisedUrlBuffer = new AdvertisedUrlPtyBuffer()
|
|
let recentOutput = ''
|
|
let ptyTail = { lines: [], partialLine: '' }
|
|
let leafTail = { lines: [], partialLine: '' }
|
|
const start = performance.now()
|
|
|
|
for (let index = 0; index < INGRESS_CHUNKS; index++) {
|
|
recentOutput = `${recentOutput}${chunk}`.slice(-RECENT_PTY_OUTPUT_LIMIT)
|
|
hasMeaningfulContentLegacy(chunk)
|
|
extractLastOscTitleLegacy(chunk)
|
|
scanAdvertisedUrls(advertisedUrlBuffer, chunk)
|
|
extractLastOscTitleLegacy(chunk)
|
|
ptyTail = appendTailBufferLegacy(ptyTail.lines, ptyTail.partialLine, chunk)
|
|
leafTail = appendTailBufferLegacy(leafTail.lines, leafTail.partialLine, chunk)
|
|
}
|
|
|
|
return performance.now() - start
|
|
}
|
|
|
|
function measureRuntimeIngressCurrent() {
|
|
const chunk = makeIngressChunk()
|
|
const advertisedUrlBuffer = new AdvertisedUrlPtyBuffer()
|
|
let recentOutput = ''
|
|
let ptyTail = { lines: [], partialLine: '' }
|
|
let leafTail = { lines: [], partialLine: '' }
|
|
const start = performance.now()
|
|
|
|
for (let index = 0; index < INGRESS_CHUNKS; index++) {
|
|
recentOutput = `${recentOutput}${chunk}`.slice(-RECENT_PTY_OUTPUT_LIMIT)
|
|
hasMeaningfulContentCurrent(chunk)
|
|
extractLastOscTitleCurrent(chunk)
|
|
scanAdvertisedUrls(advertisedUrlBuffer, chunk)
|
|
extractLastOscTitleCurrent(chunk)
|
|
const normalizedChunk = normalizeTerminalChunkCurrent(chunk)
|
|
const ptyTailBefore = {
|
|
lines: ptyTail.lines,
|
|
partialLine: ptyTail.partialLine,
|
|
linesTotal: ptyTail.lines.length
|
|
}
|
|
ptyTail = appendNormalizedToTailBuffer(ptyTail.lines, ptyTail.partialLine, normalizedChunk)
|
|
leafTail = tailStateMatches(leafTail.lines, leafTail.partialLine, ptyTailBefore)
|
|
? { lines: ptyTail.lines, partialLine: ptyTail.partialLine }
|
|
: appendNormalizedToTailBuffer(leafTail.lines, leafTail.partialLine, normalizedChunk)
|
|
}
|
|
|
|
return performance.now() - start
|
|
}
|
|
|
|
function scheduleLegacyFlush(pending) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
flushLegacy(pending)
|
|
resolve()
|
|
}, 0)
|
|
})
|
|
}
|
|
|
|
function scheduleBoundedFlush(pending) {
|
|
return new Promise((resolve) => {
|
|
const drain = () => {
|
|
flushBoundedSlice(pending)
|
|
if (pending.size > 0) {
|
|
setTimeout(drain, 1)
|
|
return
|
|
}
|
|
resolve()
|
|
}
|
|
setTimeout(drain, 0)
|
|
})
|
|
}
|
|
|
|
async function measureInputTimerDelay(flushKind) {
|
|
const pending = makePendingData()
|
|
const scheduledAt = performance.now()
|
|
const drainPromise =
|
|
flushKind === 'legacy' ? scheduleLegacyFlush(pending) : scheduleBoundedFlush(pending)
|
|
const inputDelay = await new Promise((resolve) => {
|
|
setTimeout(() => resolve(performance.now() - scheduledAt), 0)
|
|
})
|
|
await drainPromise
|
|
return inputDelay
|
|
}
|
|
|
|
function percentile(values, p) {
|
|
const sorted = [...values].sort((a, b) => a - b)
|
|
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))
|
|
return sorted[index] ?? 0
|
|
}
|
|
|
|
function summarize(values) {
|
|
return {
|
|
median: percentile(values, 50),
|
|
p95: percentile(values, 95),
|
|
max: Math.max(...values)
|
|
}
|
|
}
|
|
|
|
function formatMs(value) {
|
|
return `${value.toFixed(3)}ms`
|
|
}
|
|
|
|
async function runBenchmark() {
|
|
const legacyDurations = []
|
|
const boundedFirstSliceDurations = []
|
|
const boundedMaxSliceDurations = []
|
|
const boundedTotalDurations = []
|
|
const legacyInputTimerDelays = []
|
|
const boundedInputTimerDelays = []
|
|
const legacyRuntimeIngressDurations = []
|
|
const currentRuntimeIngressDurations = []
|
|
let legacyBytes = 0
|
|
let boundedBytes = 0
|
|
|
|
for (let run = 0; run < RUNS; run++) {
|
|
const legacy = flushLegacy(makePendingData())
|
|
legacyDurations.push(legacy.durationMs)
|
|
legacyBytes = legacy.bytes
|
|
|
|
const boundedStart = performance.now()
|
|
const bounded = drainBounded(makePendingData())
|
|
boundedBytes = bounded.bytes
|
|
boundedFirstSliceDurations.push(bounded.sliceDurations[0] ?? 0)
|
|
boundedMaxSliceDurations.push(Math.max(...bounded.sliceDurations))
|
|
boundedTotalDurations.push(performance.now() - boundedStart)
|
|
|
|
if (MEASURE_TIMER_DELAYS) {
|
|
legacyInputTimerDelays.push(await measureInputTimerDelay('legacy'))
|
|
boundedInputTimerDelays.push(await measureInputTimerDelay('bounded'))
|
|
}
|
|
|
|
legacyRuntimeIngressDurations.push(measureRuntimeIngressLegacy())
|
|
currentRuntimeIngressDurations.push(measureRuntimeIngressCurrent())
|
|
}
|
|
|
|
const legacySummary = summarize(legacyDurations)
|
|
const boundedFirstSliceSummary = summarize(boundedFirstSliceDurations)
|
|
const boundedMaxSliceSummary = summarize(boundedMaxSliceDurations)
|
|
const boundedTotalSummary = summarize(boundedTotalDurations)
|
|
const legacyInputTimerSummary = MEASURE_TIMER_DELAYS ? summarize(legacyInputTimerDelays) : null
|
|
const boundedInputTimerSummary = MEASURE_TIMER_DELAYS ? summarize(boundedInputTimerDelays) : null
|
|
const legacyRuntimeIngressSummary = summarize(legacyRuntimeIngressDurations)
|
|
const currentRuntimeIngressSummary = summarize(currentRuntimeIngressDurations)
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
scenario: {
|
|
ptyCount: PTY_COUNT,
|
|
payloadChars: PAYLOAD_CHARS,
|
|
runs: RUNS,
|
|
totalPayloadMiB: (PTY_COUNT * PAYLOAD_CHARS) / 1024 / 1024,
|
|
ingressChunks: INGRESS_CHUNKS,
|
|
ingressChunkChars: INGRESS_CHUNK_CHARS,
|
|
ingressPayloadMiB: (INGRESS_CHUNKS * INGRESS_CHUNK_CHARS) / 1024 / 1024
|
|
},
|
|
legacy: {
|
|
bytesPerRun: legacyBytes,
|
|
singleCallback: legacySummary,
|
|
inputTimerDelay: legacyInputTimerSummary
|
|
},
|
|
bounded: {
|
|
bytesPerRun: boundedBytes,
|
|
firstSlice: boundedFirstSliceSummary,
|
|
maxSlice: boundedMaxSliceSummary,
|
|
totalDrain: boundedTotalSummary,
|
|
inputTimerDelay: boundedInputTimerSummary
|
|
},
|
|
runtimeIngress: {
|
|
legacyRepeatedScans: legacyRuntimeIngressSummary,
|
|
currentFastPath: currentRuntimeIngressSummary,
|
|
estimatedReduction:
|
|
legacyRuntimeIngressSummary.max /
|
|
Math.max(currentRuntimeIngressSummary.max, Number.EPSILON)
|
|
},
|
|
estimatedPtyWriteDelay:
|
|
legacyInputTimerSummary && boundedInputTimerSummary
|
|
? {
|
|
before: legacyInputTimerSummary.max,
|
|
after: boundedInputTimerSummary.max,
|
|
reduction:
|
|
legacyInputTimerSummary.max /
|
|
Math.max(boundedInputTimerSummary.max, Number.EPSILON)
|
|
}
|
|
: null
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
|
|
console.error(
|
|
[
|
|
`legacy max single callback: ${formatMs(legacySummary.max)}`,
|
|
`bounded max first slice: ${formatMs(boundedFirstSliceSummary.max)}`,
|
|
`bounded max any slice: ${formatMs(boundedMaxSliceSummary.max)}`,
|
|
`bounded max total drain: ${formatMs(boundedTotalSummary.max)}`,
|
|
`runtime ingress legacy max: ${formatMs(legacyRuntimeIngressSummary.max)}`,
|
|
`runtime ingress current max: ${formatMs(currentRuntimeIngressSummary.max)}`,
|
|
legacyInputTimerSummary
|
|
? `legacy max input timer delay: ${formatMs(legacyInputTimerSummary.max)}`
|
|
: null,
|
|
boundedInputTimerSummary
|
|
? `bounded max input timer delay: ${formatMs(boundedInputTimerSummary.max)}`
|
|
: null
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
)
|
|
}
|
|
|
|
await runBenchmark()
|