Skip to content

perf(shared): skip parsing cached terminal styles - #8

Closed
UtkarshUsername wants to merge 1 commit into
mainfrom
perf/terminal-style-cache
Closed

UtkarshUsername wants to merge 1 commit into
mainfrom
perf/terminal-style-cache

Conversation

@UtkarshUsername

Copy link
Copy Markdown
Owner

Problem

The command-output terminal parser parses SGR numeric parameters before looking in its existing style-transition cache. Repeated colors and resets in build/test output therefore still allocate and parse arrays even when the resulting style is already cached. Both the desktop command-output client and its server use this parser.

Change

Move the existing SGR branch before numeric parsing, so only cache misses parse SGR parameters. Cursor/erase commands retain their existing parsing. Extend the existing chunk-boundary test with repeated bold/reset/RGB styles and both small and generous tail limits.

No new cache, API, output format, or terminal behavior. The existing 512-entry cache and eviction policy are unchanged.

Scope and approval

Owner-requested performance work on this fork. This is a focused removal of redundant work in an existing cache, not a new feature or workflow; no upstream maintainer approval is implied. No mobile-specific implementation or UI testing. The repository's affected-package typecheck includes shared-code dependents.

Verification

  • Shared terminal tests: 16 passed; client command-output reducer: 1 passed; server CommandOutputHub: 8 passed.
  • vp run check:changed: formatting, lint, both knip passes, and its 17 tests (shared terminal + agent-context budget) passed. Its cached typecheck could not launch because this sandbox lacks seccomp_unotify. The same affected-package scripts were rerun through vp run --no-cache; all 31 affected packages passed (18 passed in the first uncached run; the remaining 13 passed sequentially after four concurrent large checks exited 137 under memory pressure).
  • 89,600 full-state equivalence assertions matched the baseline across seeded chunk sequences, controls, partial/invalid escapes, Unicode, truncation, repeated styles, and cache eviction. Every measured workload's full-state hash matched.
  • git diff --check and forward git apply --check passed. Independent static review found no correctness issues.

Performance evidence

Baseline: 37fbefd147197fab9d88f9c120560c3f21b52cfc. Node 26.12.1, V8 14.6.202.34-node.40, Linux x64, Intel Xeon Platinum 8573C (9 visible CPUs). Project Vite+ 1.4.0 built the exact before/after module as isolated, minified ES2022 libraries. This is a pure-parser throughput benchmark, not a whole Electron/UI benchmark.

Synthetic representative workload: 3,237 npm-style colored result lines, 262,197 UTF-16 code units / 268,671 UTF-8 bytes, delivered in 65 chunks of at most 4,096 code units through the real appendTerminalOutput, including its default 65,536-code-unit tail handling.

Per complete colored replay Before After
Median wall time 21.159 ms 17.565 ms (17.0% lower)
Median process CPU time 21.162 ms 17.566 ms
Sampled allocated bytes 24,106,875 21,194,616 (12.08% lower)
Numeric parses, cold style cache 12,948 4
Numeric parses, warm style cache 12,948 0

Timing used five fresh-process A/A pairs, then five alternating A/B pairs, pinned to CPU 0. Each process ran 100 warm-up replays and 20 batches of 10 measured replays. Figures use conventional per-process medians of batch means, then the median of five processes. No outlier was removed. All five A/B pairs improved; the maximum paired A/A difference divided by the smaller observation was 12.3%. A/A lane medians differed 4.34% relative to the first lane. CPU governor and hardware instruction counters were unavailable; CPU time and deterministic work counts are reported alongside wall time.

All primary timing trials (ms per replay)
Trial A/A first A/A second Before A/B After A/B
1 19.731 19.923 21.159 17.567
2 23.091 21.693 27.567 18.242
3 22.803 22.090 22.264 17.565
4 23.808 23.332 19.897 16.539
5 24.226 27.208 20.378 17.022

An earlier unpinned artifact campaign had a 31.5% A/A outlier and was inconclusive in isolation. It was retained, not silently discarded; the longer affinity-pinned campaign above is a separately recorded confirmation. One campaign known to overlap aggregate checks was separately labeled contaminated and excluded on that interference condition. Raw scripts originally recorded an upper-middle median field; final results were recomputed from every raw batch array using conventional medians.

Allocation used three separate inspector profiles per arm, a 16 KiB sampling interval, both minor- and major-collected objects included, and 50 measured replays after 30 warm-ups. These are sampled allocation estimates, not exact byte counters. An independent 81-replay GC trace showed total pause 124.308 → 100.308 ms, maximum pause 5.510 → 4.823 ms, minor collections 71 → 68, and major collections 2 → 2. Promoted bytes increased from 33.66 to 35.17 MB; the trace is descriptive, not a statistical pause guarantee or retained-memory claim. Emitted csi and feed naturally reached TurboFan in both arms; optimization was not forced.

Uncolored controls showed no causal performance benefit. The isolated module grows 14 bytes (8 gzipped); no new cache or retained state is added. Full Electron/browser integration was unavailable because the checkout requires T3's Browser panel, whose tools were absent; no alternative UI automation or mobile UI testing was used.

Reproduce the primary isolated-module benchmark

Run from the checkout after vp i, with Node 26.12.1. This writes only scratch files outside the repository. CPU 0 must be available on Linux; choose an allowed core if needed and report the change. Keep other builds/tests stopped while measuring.

export REPO="$PWD"
export PROOF="$(mktemp -d)"
git show 37fbefd147197fab9d88f9c120560c3f21b52cfc:packages/shared/src/terminalOutput.ts > "$PROOF/terminal-before.ts"
cp packages/shared/src/terminalOutput.ts "$PROOF/terminal-after.ts"
cat > "$PROOF/build.mjs" <<'JS'
import { pathToFileURL } from 'node:url';
const { build } = await import(pathToFileURL(`${process.env.REPO}/node_modules/vite-plus/dist/index.js`));
for (const arm of ['before', 'after']) {
  await build({ configFile: false, root: process.env.PROOF, build: {
    target: 'es2022', minify: true, sourcemap: true,
    outDir: `${process.env.PROOF}/build-${arm}`, emptyOutDir: true,
    lib: { entry: `${process.env.PROOF}/terminal-${arm}.ts`, formats: ['es'], fileName: 'terminal' }
  }});
}
JS
node "$PROOF/build.mjs"
cat > "$PROOF/bench.mjs" <<'JS'
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import os from 'node:os';
const arm = process.argv[2];
const file = new URL(`./build-${arm}/terminal.mjs`, import.meta.url);
const { appendTerminalOutput, EMPTY_TERMINAL_OUTPUT } = await import(file.href);
const line = '\x1b[32m ✓ \x1b[0m packages/shared/src/terminalOutput.test.ts \x1b[2m(18 tests) 123ms\x1b[0m\n';
const payload = line.repeat(Math.ceil(262144 / line.length));
const chunks = [];
for (let i = 0; i < payload.length; i += 4096) chunks.push(payload.slice(i, i + 4096));
function run() {
  let state = EMPTY_TERMINAL_OUTPUT;
  for (const chunk of chunks) state = appendTerminalOutput(state, chunk);
  return state;
}
let sink = 0;
const result = run();
for (let i = 0; i < 100; i++) sink += run().text.length;
const samples = [], cpu = [];
for (let i = 0; i < 20; i++) {
  const start = performance.now(), usage = process.cpuUsage();
  for (let j = 0; j < 10; j++) sink += run().text.length;
  samples.push((performance.now() - start) / 10);
  const used = process.cpuUsage(usage);
  cpu.push((used.user + used.system) / 10000);
}
const median = values => { const sorted = values.toSorted((a, b) => a - b); return (sorted[9] + sorted[10]) / 2; };
console.log(JSON.stringify({ arm, samples, cpu, median: median(samples), cpuMedian: median(cpu), sink,
  hash: createHash('sha256').update(JSON.stringify(result)).digest('hex'),
  artifact: createHash('sha256').update(readFileSync(file)).digest('hex'),
  node: process.version, v8: process.versions.v8, load: os.loadavg(), rssKiB: process.resourceUsage().maxRSS }));
JS
python3 - <<'PY'
import json, os, pathlib, statistics, subprocess
root = pathlib.Path(os.environ['PROOF'])
rows = []
for phase in ['AA', 'AB']:
    for trial in range(5):
        arms = ['before', 'before'] if phase == 'AA' else (['before', 'after'] if trial % 2 == 0 else ['after', 'before'])
        for lane, arm in enumerate(arms):
            row = json.loads(subprocess.check_output(['taskset', '-c', '0', 'node', str(root / 'bench.mjs'), arm], text=True))
            row.update(phase=phase, trial=trial, lane=lane)
            rows.append(row)
(root / 'trials.ndjson').write_text(''.join(json.dumps(row) + '\n' for row in rows))
assert len({row['hash'] for row in rows}) == 1
for arm in ['before', 'after']:
    values = [row['median'] for row in rows if row['phase'] == 'AB' and row['arm'] == arm]
    print(arm, values, 'median', statistics.median(values))
PY

Measured emitted SHA-256: before 6007e13ab47033739362e69fb5d68fa88d325d12132f8c04ff16f9806ba3a391; after 7d92eb2c04194732d311430775ff1aa7cf4524a25a6dc6ec9dd3748821576e8e.

Model: OpenAI (exact model identifier not exposed by the execution environment). Harness: ChatGPT Work cloud agent.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S labels Oct 29, 2026
@UtkarshUsername

Copy link
Copy Markdown
Owner Author

Opened upstream PR pingdotgg#27041

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant