From fcd9306a439a010d9113eedc0f52ccab9a6c8328 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 18 Sep 2026 20:48:55 -0700 Subject: [PATCH] test(terminal): prove empty glyph eviction preserves visible caches --- .gitignore | 3 + .../xterm-webgl-runtime-contract.test.mjs | 18 + .../webgl-empty-glyph-retention/README.md | 11 +- .../webgl-empty-glyph-retention/reproduce.mjs | 24 +- .../review-validation.json | 414 ++++++++++++++++++ 5 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 docs/audits/webgl-empty-glyph-retention/review-validation.json diff --git a/.gitignore b/.gitignore index 56ecc5854b6..9cbe7ed3ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,9 @@ design-docs/ # and the tracked reference docs linked from AGENTS.md / README.md). docs/** !docs/ +!docs/audits/ +!docs/audits/webgl-empty-glyph-retention/ +!docs/audits/webgl-empty-glyph-retention/** # The deployable docs app is source, not local engineering notes. !docs/site/ !docs/site/** diff --git a/config/scripts/xterm-webgl-runtime-contract.test.mjs b/config/scripts/xterm-webgl-runtime-contract.test.mjs index 5b234c805ea..c3695232571 100644 --- a/config/scripts/xterm-webgl-runtime-contract.test.mjs +++ b/config/scripts/xterm-webgl-runtime-contract.test.mjs @@ -37,6 +37,24 @@ describe('vendored xterm WebGL runtime contract', () => { } }) + it('keeps invisible glyph eviction separate from the visible glyph cache', () => { + const webgl = xtermManifest.packages.find((entry) => entry.name === '@xterm/addon-webgl') + for (const source of [ + readProject(webgl.sourcePatch), + readInstalled('@xterm/addon-webgl', 'src/TextureAtlas.ts') + ]) { + expect(source).toContain('emptyCacheMap.get(key, bg, fg, ext)') + expect(source).toContain('this._emptyGlyphCount >= Constants.EMPTY_GLYPH_CACHE_LIMIT') + expect(source).toContain('this._clearEmptyGlyphCache()') + } + for (const bundle of ['lib/addon-webgl.js', 'lib/addon-webgl.mjs']) { + const contents = readInstalled('@xterm/addon-webgl', bundle) + expect(contents, bundle).toContain('_emptyCacheMapCombined') + expect(contents, bundle).toContain('_clearEmptyGlyphCache') + expect(contents, bundle).toMatch(/_emptyGlyphCount>=4096/) + } + }) + it('keeps the Orca-only WebGL hunks in the generated patch', () => { const webgl = xtermManifest.packages.find((entry) => entry.name === '@xterm/addon-webgl') const patch = readProject(webgl.patch) diff --git a/docs/audits/webgl-empty-glyph-retention/README.md b/docs/audits/webgl-empty-glyph-retention/README.md index 7737cb3920a..5a0f8c441c7 100644 --- a/docs/audits/webgl-empty-glyph-retention/README.md +++ b/docs/audits/webgl-empty-glyph-retention/README.md @@ -38,7 +38,8 @@ two modes. It samples V8 heap after CDP collection and records bundle hashes in [results.json](./results.json). Two terminals share one atlas. Assertions cover: - Invisible entries stay at or below 4,096; the baseline accumulates 100,000. -- Visible glyph caches and the one texture page stay intact across overflow. +- After ASCII warmup finishes, visible cache and glyph counts stay exactly unchanged + across overflow, with no visible glyph rerasterized; the one texture page stays intact. - Rendered pixels for the unaffected first cell of both terminals stay identical. - The last 64 repeated variants cause no new rasterization. - Explicit clear drops invisible metadata even when the atlas has no drawn glyphs. @@ -48,7 +49,13 @@ run ends with 1,789 entries (93 visible and 1,696 invisible), with roughly 0.3 M of heap growth. Exact heap samples vary; the bounded entry count is the invariant. No Orca window was launched, and no browser window was shown. -Validation also passed 86 tests across the patch generator/runtime contract and +A follow-up [review-validation.json](./review-validation.json) reruns both installed +bundles and both output modes with the stronger warmup/count/rasterization checks +on Node 24.20.0. The original before/after measurements above remain in `results.json`. +The runtime contract test also guards the empty-glyph routing and cap in the source +patch, installed source, and both shipped bundles. Its three checks pass. + +Original validation also passed 86 tests across the patch generator/runtime contract and WebGL lifecycle/context/recovery suites, full typecheck, lint, and changed-code quality. Test commands used `ORCA_BACKGROUND_LAUNCH=1`. diff --git a/docs/audits/webgl-empty-glyph-retention/reproduce.mjs b/docs/audits/webgl-empty-glyph-retention/reproduce.mjs index d2c65f75246..ee2acc0a4d6 100644 --- a/docs/audits/webgl-empty-glyph-retention/reproduce.mjs +++ b/docs/audits/webgl-empty-glyph-retention/reproduce.mjs @@ -65,6 +65,21 @@ try { await new Promise(requestAnimationFrame) await new Promise(requestIdleCallback) }, mode) + // The atlas warms ASCII in idle tasks; sample only after the final queued glyph exists. + await page.waitForFunction(() => + Boolean(window.glyphAudit.first.addon._renderer._charAtlas._cacheMap.get(125, 0, 0, 0)) + ) + await page.evaluate(() => { + const atlas = window.glyphAudit.first.addon._renderer._charAtlas + const draw = atlas._drawToCache + window.glyphAudit.visibleRasterizations = 0 + atlas._drawToCache = function (...args) { + if (args[0] !== 32 && args[0] !== ' \u200d') { + window.glyphAudit.visibleRasterizations++ + } + return draw.apply(this, args) + } + }) const cdp = await page.context().newCDPSession(page) const samples = [] for (const count of [0, 1000, 5000, 10000, 50000, 100000]) { @@ -81,6 +96,7 @@ try { second.addon._renderer.renderRows(0, 0) window.glyphAudit.index = count const atlas = first.addon._renderer._charAtlas + // This audit is tied to the pinned addon's private FourKeyMap storage. const entries = (map) => { let result = 0 for (const second of Object.values(map?._data._data ?? {})) { @@ -106,6 +122,7 @@ try { } return { updates: count, + visibleRasterizations: window.glyphAudit.visibleRasterizations, firstCellPixels: pixels(first), secondCellPixels: pixels(second), regular: entries(atlas._cacheMap), @@ -160,7 +177,12 @@ try { if (phase === 'after') { assert.ok( samples.every( - (sample) => sample.empty <= 4096 && sample.regular + sample.combined < 200 + (sample) => + sample.empty <= 4096 && + sample.regular === samples[0].regular && + sample.combined === samples[0].combined && + sample.glyphs === samples[0].glyphs && + sample.visibleRasterizations === 0 ) ) assert.equal(checks.emptyAfterClear, 0) diff --git a/docs/audits/webgl-empty-glyph-retention/review-validation.json b/docs/audits/webgl-empty-glyph-retention/review-validation.json new file mode 100644 index 00000000000..e84fa14123c --- /dev/null +++ b/docs/audits/webgl-empty-glyph-retention/review-validation.json @@ -0,0 +1,414 @@ +{ + "node": "v24.20.0", + "browser": "146.0.7680.0", + "results": [ + { + "phase": "after", + "format": "cjs", + "mode": "space", + "sha256": "0667d4850345c52271515c6ffe434c40687c59db007ddd32aabc02f0118a1cad", + "samples": [ + { + "updates": 0, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 0, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3336616 + }, + { + "updates": 1000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1000, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3808452 + }, + { + "updates": 5000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 904, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3799644 + }, + { + "updates": 10000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1808, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3918852 + }, + { + "updates": 50000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 848, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3519764 + }, + { + "updates": 100000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1696, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3632676 + } + ], + "checks": { + "rasterizations": 0, + "emptyAfterClear": 0 + } + }, + { + "phase": "after", + "format": "cjs", + "mode": "space-joiner", + "sha256": "0667d4850345c52271515c6ffe434c40687c59db007ddd32aabc02f0118a1cad", + "samples": [ + { + "updates": 0, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 0, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3336388 + }, + { + "updates": 1000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1000, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3788324 + }, + { + "updates": 5000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 904, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3817620 + }, + { + "updates": 10000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1808, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3937304 + }, + { + "updates": 50000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 848, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3855688 + }, + { + "updates": 100000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1696, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3653104 + } + ], + "checks": { + "rasterizations": 0, + "emptyAfterClear": 0 + } + }, + { + "phase": "after", + "format": "esm", + "mode": "space", + "sha256": "1ff2a59921ed5d53c0958110dac290c8d9f8d4502ea631fac7fd8575227b30f0", + "samples": [ + { + "updates": 0, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 0, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3295588 + }, + { + "updates": 1000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1000, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3768188 + }, + { + "updates": 5000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 904, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3759512 + }, + { + "updates": 10000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1808, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3878776 + }, + { + "updates": 50000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 848, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3798292 + }, + { + "updates": 100000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1696, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3668712 + } + ], + "checks": { + "rasterizations": 0, + "emptyAfterClear": 0 + } + }, + { + "phase": "after", + "format": "esm", + "mode": "space-joiner", + "sha256": "1ff2a59921ed5d53c0958110dac290c8d9f8d4502ea631fac7fd8575227b30f0", + "samples": [ + { + "updates": 0, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 0, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3286928 + }, + { + "updates": 1000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1000, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3783224 + }, + { + "updates": 5000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 904, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3777172 + }, + { + "updates": 10000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1808, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3896896 + }, + { + "updates": 50000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 848, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3814948 + }, + { + "updates": 100000, + "visibleRasterizations": 0, + "firstCellPixels": 638013437, + "secondCellPixels": 395161943, + "regular": 93, + "combined": 0, + "empty": 1696, + "pages": 1, + "glyphs": 93, + "layoutVersion": 0, + "shared": true, + "preserved": true, + "heap": 3678164 + } + ], + "checks": { + "rasterizations": 0, + "emptyAfterClear": 0 + } + } + ] +}