From 3b08b7dcef47fafd8d779ed3ca4ed67875021e44 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 6 Jun 2026 16:22:34 +0000 Subject: [PATCH] Remove CDP profiling script; document sentinel design in BufferLine Co-authored-by: Daniel Imms --- bin/profile_yes_cdp.mjs | 140 -------------------------------- src/common/buffer/BufferLine.ts | 7 +- 2 files changed, 6 insertions(+), 141 deletions(-) delete mode 100644 bin/profile_yes_cdp.mjs diff --git a/bin/profile_yes_cdp.mjs b/bin/profile_yes_cdp.mjs deleted file mode 100644 index e408ed8dc..000000000 --- a/bin/profile_yes_cdp.mjs +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2026 The xterm.js authors. All rights reserved. - * @license MIT - * - * CPU profile `timeout 1s yes` on the demo terminal via CDP (Chromium). - * - * Usage: - * node bin/profile_yes_cdp.mjs [--out path.cpuprofile] [--label baseline] - * - * Requires demo server on port 3000 (npm start). - */ - -import { chromium } from 'playwright'; -import { writeFileSync, mkdirSync, readFileSync } from 'fs'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const root = join(__dirname, '..'); - -function parseArgs() { - const args = process.argv.slice(2); - let out = join(root, 'out-test', 'yes.cpuprofile'); - let label = 'profile'; - for (let i = 0; i < args.length; i++) { - if (args[i] === '--out' && args[i + 1]) { - out = args[++i]; - label = out.replace(/\.cpuprofile$/, '').split('/').pop() ?? label; - } else if (args[i] === '--label' && args[i + 1]) { - label = args[++i]; - } - } - return { out, label }; -} - -const INTEREST = [ - 'copyFrom', - '_copySparseMapsFrom', - '_copyCellMapsFrom', - 'BufferService.scroll' -]; - -export function summarizeCpuProfileFromObject(profile, label = 'profile') { - const nodes = profile.nodes ?? []; - const samples = profile.samples ?? []; - const timeDeltas = profile.timeDeltas ?? []; - const idToNode = new Map(nodes.map(n => [n.id, n])); - const stats = new Map(INTEREST.map(n => [n, { hitCount: 0, selfTimeUs: 0, totalTimeUs: 0 }])); - let totalSampleUs = 0; - - for (let i = 0; i < samples.length; i++) { - const deltaUs = timeDeltas[i] ?? 0; - totalSampleUs += deltaUs; - const stack = []; - let nodeId = samples[i]; - while (nodeId !== undefined) { - const node = idToNode.get(nodeId); - if (!node) break; - stack.push(node); - nodeId = node.parent; - } - for (const s of INTEREST) { - if (stack.some(n => (n.callFrame?.functionName ?? '').includes(s))) { - const st = stats.get(s); - st.hitCount++; - st.totalTimeUs += deltaUs; - } - } - const leaf = stack[0]; - const fn = leaf?.callFrame?.functionName ?? ''; - for (const s of INTEREST) { - if (fn.includes(s)) { - stats.get(s).selfTimeUs += deltaUs; - break; - } - } - } - - const rows = [...stats.entries()] - .filter(([, v]) => v.hitCount > 0) - .map(([name, v]) => ({ - name, - hitCount: v.hitCount, - selfMs: +(v.selfTimeUs / 1000).toFixed(2), - totalMs: +(v.totalTimeUs / 1000).toFixed(2), - pctTotal: totalSampleUs ? +((100 * v.totalTimeUs) / totalSampleUs).toFixed(2) : 0 - })); - - return { label, totalSampleMs: +(totalSampleUs / 1000).toFixed(2), rows }; -} - -export function summarizeCpuProfile(profilePath) { - const profile = JSON.parse(readFileSync(profilePath, 'utf8')); - return summarizeCpuProfileFromObject(profile, profilePath); -} - -async function main() { - const { out, label } = parseArgs(); - mkdirSync(dirname(out), { recursive: true }); - - const browser = await chromium.launch({ headless: true }); - const page = await browser.newPage(); - - await page.goto('http://localhost:3000/', { waitUntil: 'networkidle' }); - await page.waitForFunction(() => window.term && window.term.element, undefined, { timeout: 60000 }); - - const termInfo = await page.evaluate(() => ({ - cols: window.term.cols, - rows: window.term.rows, - scrollback: window.term.options.scrollback - })); - console.log('Terminal:', JSON.stringify(termInfo)); - - const cdp = await page.context().newCDPSession(page); - await cdp.send('Profiler.enable'); - await cdp.send('Profiler.setSamplingInterval', { interval: 100 }); - await cdp.send('Profiler.start'); - - const textarea = page.locator('.xterm-helper-textarea'); - await textarea.click(); - await page.keyboard.type('timeout 1s yes', { delay: 5 }); - await page.keyboard.press('Enter'); - await page.waitForTimeout(2500); - - const { profile } = await cdp.send('Profiler.stop'); - writeFileSync(out, JSON.stringify(profile)); - - const summary = summarizeCpuProfileFromObject(profile, label); - const summaryPath = out.replace(/\.cpuprofile$/, '.summary.json'); - writeFileSync(summaryPath, JSON.stringify({ ...summary, termInfo }, null, 2)); - console.log('Wrote', out); - console.log('Summary:', JSON.stringify(summary, null, 2)); - - await browser.close(); -} - -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index daa99259b..a83ed8ff6 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -71,7 +71,12 @@ export interface IBufferLineStringCache { * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. */ -/** Shared empty sparse maps for lines with no combining/extended data. */ +/** + * Shared empty sparse maps for lines with no combining/extended data. + * Uses a sentinel + copy-on-write (not `undefined`) so `copyFrom` can detect empty maps + * with reference equality. `undefined` was explored but regressed sparse `copyFrom`; + * `Object.freeze` on these sentinels is safe with COW but did not improve perf. + */ const EMPTY_SPARSE_MAP: {[index: number]: string} = Object.create(null); const EMPTY_SPARSE_EXTENDED: {[index: number]: IExtendedAttrs | undefined} = Object.create(null);