fix: detach retained terminal mode scan tails

This commit is contained in:
m4air
2026-09-16 20:24:20 -07:00
parent 8d599520e4
commit 0d2efbc0d3
10 changed files with 1460 additions and 3 deletions
@@ -0,0 +1,122 @@
# Retained terminal mode scan tails
The kitty keyboard tracker and daemon mouse-mode mirror retain an incomplete
escape-sequence tail of at most 4,096 UTF-16 code units. A V8 sliced string can
keep the entire consumed input alive through that small tail. An ordinary
split grouped mode sequence, `ESC[?1049;2004;1000;`, is enough: its 18-character
tail retains each input backing string while its parser stays idle.
The correction copies only accepted incomplete tails through the existing
`ownRetainedString` helper. Empty/rejected tails and short ESC/CSI prefixes keep
their existing behavior; the helper leaves strings shorter than 13 code units
alone. Parser state, live/replay semantics, stack caps, mode flags, and wire
content are unchanged. These are additional boundaries in
[#20960](https://github.com/stablyai/orca/pull/20960), alongside the
[PTY detector carries](../pty-detector-retention/README.md).
## Reproduce
```sh
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/terminal-mode-tail-retention/reproduce.cjs
```
Run the same script with the installed Electron executable, setting
`ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, and passing the same
Node flags. This launches no app window or native PTY. Each run has a 30-second
deadline and writes either [Node results](./node-results.json) or
[Electron results](./electron-results.json).
The loader reads the actual five source modules, verifies their fixed hashes,
reverses only the three copy calls/imports in memory for the baseline, and
verifies the resulting baseline hashes. All evaluated module and bundle hashes
are recorded. It needs no Git history, absolute development paths, or ignored
notes. CRLF source text is normalized before hashing. The parsers and flag
parser match `v1.4.198`; all five modules match the pre-extension topic
`8d599520e44654a5c28e9930e3070c00d6499931`, except for these copy calls. This
tests current dependencies and the selected source modules, not a historical
application binary. See [source versions](./source-versions.json).
Each runtime checks 42 bounded heap cases: baseline, fixed Buffer copier, and
fixed Bufferless copier; kitty live/replay, mouse live; 32 distinct 64-Ki-character
inputs and eight 4-Mi-character inputs; short, complete, oversized, and C1-CSI
tail controls. Completion must reconstruct the correct modes and release the
large backing strings. Additional controls preserve replay push idempotence,
the 16-frame live stack cap, alternate-screen state, snapshot unknownness,
mouse encodings, and RIS with a trailing partial sequence.
Both runtimes pass all 42 cases. Representative live-path heap deltas in bytes:
| Runtime | Parser | Input × owners | Baseline | Buffer copy | Bufferless copy |
| --------------------- | ------ | -------------- | ---------: | ----------: | --------------: |
| Node 26 | Kitty | 64 Ki × 32 | 2,120,952 | 18,376 | 9,480 |
| Node 26 | Mouse | 64 Ki × 32 | 2,111,984 | 15,112 | 9,336 |
| Node 26 | Kitty | 4 Mi × 8 | 33,557,944 | 3,448 | 3,448 |
| Node 26 | Mouse | 4 Mi × 8 | 33,557,072 | 1,312 | 1,312 |
| Electron 43 / Node 24 | Kitty | 64 Ki × 32 | 2,111,864 | 12,244 | 5,192 |
| Electron 43 / Node 24 | Mouse | 64 Ki × 32 | 2,103,444 | 14,432 | 8,004 |
| Electron 43 / Node 24 | Kitty | 4 Mi × 8 | 33,556,316 | 1,884 | 2,604 |
| Electron 43 / Node 24 | Mouse | 4 Mi × 8 | 33,556,172 | 776 | 752 |
Heap readings include owner overhead and follow forced GC. The harness clears
V8's last successful regexp input identically in baseline and fixed cases to
isolate per-owner storage. That independent process-wide regexp reference can
keep a most-recent input alive until another successful match; this change does
not eliminate it. The Bufferless selection is memoized while Buffer is absent,
then Buffer is restored before measuring; it exercises the actual renderer
fallback without running a browser renderer.
Two permanent kitty heap regressions fail before the correction at 33,560,832
and 33,575,040 retained bytes against a 2-MiB limit. They also verify that the
retained prefix completes correctly and that replay/pop/snapshot state remains
valid. Existing parser and copier tests provide the wider protocol controls.
The two mouse regressions likewise fail before the correction at 33,559,240 and
33,573,200 bytes, and pass afterward with both CSI encodings. The five-suite
run passes 98 tests, including actual headless-emulator mode snapshots; Node
and renderer TypeScript checks pass.
## Callers and lifetime
- Kitty renderer panes create or reuse one tracker per pane in
`connect-pane-pty.ts:160`. `write-pty-output-to-xterm.ts:23` feeds application
output; `apply-reattach-payload.ts` and `hidden-output-seq-and-skip.ts` feed
replay. Fresh spawn and exit reset it, and
`terminal-pane-pane-closed.ts:69` deletes the map entry. Dashboard previews
own another tracker per effect (`AgentTerminalPreview.tsx:116`); cleanup
removes its listeners and disposes its terminal.
- Main's `orca-runtime-capture-provider-terminal-buffer.ts:23` registers
temporary live scanners during provider snapshot acquisition and removes
them in `finally`. It creates a persistent tracker only after observing an
alternate-screen transition (`:4857`). `orca-runtime-on-pty-data.ts:30`
feeds those trackers before later output processing. Exit, floating PTY
liveness cleanup, and provider generation reset delete the persistent entry.
- The daemon does not directly instantiate the kitty tracker, despite its
old class comment: its kitty flags come from xterm. No mobile bundle imports
this class. Mobile can exercise main-side snapshot acquisition; SSH output
can reach main and renderer trackers through the existing provider routes.
- Mouse mirrors are owned by `HeadlessEmulator` (`headless-emulator.ts:59`).
Async writes scan after xterm parses the data (`:190`); synchronous live and
cold-restore writes scan at `:224`. Both daemon sessions and main's headless
projections use this emulator. It therefore also covers local/remote host
emulators serving mobile clients. Emulator disposal stops future writes;
eventual owner release removes the mirror. Completing/replacing its tail
also releases the old backing string. No ownership or shutdown rule changes.
## Scope and limits
This is a per-owner last-input cost. It does not grow indefinitely with a fixed
set of parsers and bounded input chunks, and further output often completes or
replaces the tail. Multiple readers of the same input can share its backing
storage; do not sum their measurements as independent process memory.
Ordinary daemon bulk frames delivered to main are at most 64 Ki characters
(`daemon-stream-data-batcher.ts:35`), and ordinary relay output slices are
16 Ki characters (`relay/pty-handler.ts:343`). Mouse scanning inside the daemon
happens before outgoing stream framing. The 64-Ki cases demonstrate the issue
at a normal main-input bound; the 4-Mi cases amplify the mechanism, not a claim
that ordinary native reads or daemon frames have that size. Replay inputs and
transformed streams follow their own existing limits. No network, application
renderer, operating-system PTY, or incident heap was used in this proof.
This reduces retained output in local and SSH paths without changing published
terminal content. It neither establishes the trigger in #19831/#19768 nor
explains a reported sustained growth rate or multi-gigabyte incident by itself.
@@ -0,0 +1,467 @@
{
"node": "v24.21.0",
"electron": "43.7.0",
"v8": "15.0.245.31-electron.0",
"platform": "darwin",
"runnerSha256": "b78dc54a08345732235d4fcc1f72ca1534b00f89280981beda00667d22b8f291",
"loaderSha256": "730da9091abba3997432657aef4a2e6e7a3dd0e9218ea78f1838806b6c0fa789",
"pendingTailCodeUnits": 18,
"clearedRegexStatics": true,
"bundles": {
"baseline": {
"bundleSha256": "6023f5d6868f5db601f6883acda5d56990d204084a6a3c812799aff1e533a5b0",
"evaluatedSources": {
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403",
"src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1",
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
},
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
},
"fixed-buffer": {
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
"evaluatedSources": {
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
},
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
},
"fixed-fallback": {
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
"evaluatedSources": {
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
},
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
}
},
"reports": [
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 2111864,
"afterCompletionDelta": 29832
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 33556316,
"afterCompletionDelta": 9244
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 2102416,
"afterCompletionDelta": 4916
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 33556304,
"afterCompletionDelta": 1968
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 1756,
"afterCompletionDelta": 1712
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 1652,
"afterCompletionDelta": 2880
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 1652,
"afterCompletionDelta": 3024
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 33556316,
"afterCompletionDelta": 1692
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 2103444,
"afterCompletionDelta": 5584
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 33556172,
"afterCompletionDelta": 1496
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 672,
"afterCompletionDelta": 644
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 520,
"afterCompletionDelta": 532
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 520,
"afterCompletionDelta": 532
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 33555224,
"afterCompletionDelta": 560
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 12244,
"afterCompletionDelta": 11632
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 1884,
"afterCompletionDelta": 1640
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 5148,
"afterCompletionDelta": 4176
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 1884,
"afterCompletionDelta": 1640
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 1756,
"afterCompletionDelta": 1688
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 1652,
"afterCompletionDelta": 2808
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 1628,
"afterCompletionDelta": 1640
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 1884,
"afterCompletionDelta": 1652
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 14432,
"afterCompletionDelta": 13692
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 776,
"afterCompletionDelta": 532
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 672,
"afterCompletionDelta": 628
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 520,
"afterCompletionDelta": 532
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 520,
"afterCompletionDelta": 532
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 792,
"afterCompletionDelta": 560
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 5192,
"afterCompletionDelta": 4180
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 2604,
"afterCompletionDelta": 2360
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 5176,
"afterCompletionDelta": 7104
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 1884,
"afterCompletionDelta": 1640
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 2744,
"afterCompletionDelta": 2676
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 1652,
"afterCompletionDelta": 1664
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": -3344,
"afterCompletionDelta": -3332
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 1884,
"afterCompletionDelta": 1652
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 8004,
"afterCompletionDelta": 6992
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 752,
"afterCompletionDelta": 508
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 672,
"afterCompletionDelta": 1464
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 520,
"afterCompletionDelta": 532
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 1548,
"afterCompletionDelta": 1560
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 776,
"afterCompletionDelta": 544
}
]
}
@@ -0,0 +1,68 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const Module = require('node:module')
const { createHash } = require('node:crypto')
const { build } = require('esbuild')
const root = path.resolve(__dirname, '../../..')
const sha = (value) => createHash('sha256').update(value).digest('hex')
const read = (file) => fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
const versionsText = read(path.join(__dirname, 'source-versions.json'))
const versions = JSON.parse(versionsText)
async function loadSource(fixed) {
const evaluatedSources = {}
const built = await build({
stdin: {
contents: [
"export { TerminalKittyKeyboardModeTracker } from './src/shared/terminal-kitty-keyboard-mode-tracker'",
"export { TerminalMouseModeMirror } from './src/main/daemon/terminal-mouse-mode-mirror'",
"export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'"
].join('\n'),
resolveDir: root
},
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
plugins: [
{
name: 'hash-fenced-retained-mode-tails',
setup(builder) {
builder.onLoad({ filter: /\.ts$/ }, ({ path: filename }) => {
const relative = path.relative(root, filename).split(path.sep).join('/')
const version = versions.sources[relative]
assert.ok(version, `Unreviewed source: ${relative}`)
let contents = read(filename)
assert.equal(sha(contents), version.fixedSha256, `Fixed source changed: ${relative}`)
if (!fixed && version.reverse) {
for (const { from, to, count } of version.reverse) {
assert.equal(contents.split(from).length - 1, count)
contents = contents.replaceAll(from, to)
}
}
const expected = fixed ? version.fixedSha256 : version.baselineSha256
assert.equal(sha(contents), expected, `Evaluated source changed: ${relative}`)
evaluatedSources[relative] = sha(contents)
return { contents, loader: 'ts' }
})
}
}
]
})
assert.deepEqual(Object.keys(evaluatedSources).sort(), Object.keys(versions.sources).sort())
const filename = path.join(__dirname, fixed ? 'fixed-bundle.cjs' : 'baseline-bundle.cjs')
const loaded = new Module(filename, module)
loaded.filename = filename
loaded.paths = Module._nodeModulePaths(root)
loaded._compile(built.outputFiles[0].text, filename)
return {
...loaded.exports,
evaluatedSources,
bundleSha256: sha(built.outputFiles[0].text),
sourceVersionsSha256: sha(versionsText)
}
}
module.exports = { loadSource, sha, read }
@@ -0,0 +1,467 @@
{
"node": "v26.6.0",
"electron": null,
"v8": "14.6.202.34-node.26",
"platform": "darwin",
"runnerSha256": "b78dc54a08345732235d4fcc1f72ca1534b00f89280981beda00667d22b8f291",
"loaderSha256": "730da9091abba3997432657aef4a2e6e7a3dd0e9218ea78f1838806b6c0fa789",
"pendingTailCodeUnits": 18,
"clearedRegexStatics": true,
"bundles": {
"baseline": {
"bundleSha256": "6023f5d6868f5db601f6883acda5d56990d204084a6a3c812799aff1e533a5b0",
"evaluatedSources": {
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403",
"src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1",
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
},
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
},
"fixed-buffer": {
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
"evaluatedSources": {
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
},
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
},
"fixed-fallback": {
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
"evaluatedSources": {
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
},
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
}
},
"reports": [
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 2120952,
"afterCompletionDelta": 31112
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 33557944,
"afterCompletionDelta": 3840
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 2107000,
"afterCompletionDelta": 9336
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 33557928,
"afterCompletionDelta": 3752
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 3320,
"afterCompletionDelta": 3288
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 3176,
"afterCompletionDelta": 4792
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 3176,
"afterCompletionDelta": 4904
},
{
"variant": "baseline",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 33557944,
"afterCompletionDelta": 3232
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 2111984,
"afterCompletionDelta": 13904
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 33557072,
"afterCompletionDelta": 2272
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 1232,
"afterCompletionDelta": 1232
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 992,
"afterCompletionDelta": 1008
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 992,
"afterCompletionDelta": 1008
},
{
"variant": "baseline",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 33555840,
"afterCompletionDelta": 1048
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 18376,
"afterCompletionDelta": 25472
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 3448,
"afterCompletionDelta": 3144
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 9400,
"afterCompletionDelta": 8216
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 3696,
"afterCompletionDelta": 3392
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 3320,
"afterCompletionDelta": 3240
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 3176,
"afterCompletionDelta": 4648
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 3128,
"afterCompletionDelta": 3144
},
{
"variant": "fixed-buffer",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 3448,
"afterCompletionDelta": 3152
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 15112,
"afterCompletionDelta": 14376
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 1312,
"afterCompletionDelta": 1008
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 1232,
"afterCompletionDelta": 1200
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 992,
"afterCompletionDelta": 1008
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 992,
"afterCompletionDelta": 1008
},
{
"variant": "fixed-buffer",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 1344,
"afterCompletionDelta": 1048
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 9480,
"afterCompletionDelta": 8216
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 3448,
"afterCompletionDelta": 4048
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 13216,
"afterCompletionDelta": 11952
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scanReplay",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 3448,
"afterCompletionDelta": 3144
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 3320,
"afterCompletionDelta": 3240
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 3176,
"afterCompletionDelta": 3192
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 3912,
"afterCompletionDelta": 3928
},
{
"variant": "fixed-fallback",
"kind": "kitty",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 3448,
"afterCompletionDelta": 3152
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 65536,
"count": 32,
"expectedTailLength": 18,
"heapDelta": 9336,
"afterCompletionDelta": 8072
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 18,
"heapDelta": 1312,
"afterCompletionDelta": 1008
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 2,
"heapDelta": 1232,
"afterCompletionDelta": 8184
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 992,
"afterCompletionDelta": 1008
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 0,
"heapDelta": 2280,
"afterCompletionDelta": 2296
},
{
"variant": "fixed-fallback",
"kind": "mouse",
"method": "scan",
"inputChars": 4194304,
"count": 8,
"expectedTailLength": 17,
"heapDelta": 1312,
"afterCompletionDelta": 1016
}
]
}
@@ -0,0 +1,205 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const { loadSource, sha, read } = require('./load-source.cjs')
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
assert.equal(typeof global.gc, 'function', 'Run with --expose-gc')
const pending = '\x1b[?1049;2004;1000;'
const sizes = [
[64 * 1024, 32],
[4 * 1024 * 1024, 8]
]
async function heap() {
// Isolate owner storage from V8's process-wide last successful regexp input.
;/reset/.test('reset')
for (let round = 0; round < 4; round++) {
await new Promise((resolve) => setImmediate(resolve))
global.gc()
}
return process.memoryUsage().heapUsed
}
function createOwner(Owner, method, chars, index, suffix) {
const prefix = `${index}:`
const data = `${prefix}${'x'.repeat(chars - prefix.length - suffix.length)}${suffix}`
const owner = new Owner()
owner[method](data)
return owner
}
async function measure(Owner, variant, kind, method, inputChars, count, suffix = pending) {
const beforeHeap = await heap()
const owners = Array.from({ length: count }, (_, index) =>
createOwner(Owner, method, inputChars, index, suffix)
)
const heapDelta = (await heap()) - beforeHeap
const expectedTailLength = suffix.length > 4096 || suffix.endsWith('h') ? 0 : suffix.length
assert.ok(owners.every((owner) => owner.scanTail.length === expectedTailLength))
const retainsParent = variant === 'baseline' && expectedTailLength >= 13
assert.ok(
retainsParent ? heapDelta > inputChars * count * 0.75 : heapDelta < 768 * 1024,
JSON.stringify({ variant, kind, method, inputChars, count, expectedTailLength, heapDelta })
)
for (const owner of owners) {
owner[method]('1006h')
if (kind === 'kitty') {
if (suffix === pending) {
assert.equal(owner.isAlternateScreen, true)
}
owner[method]('\x1b[>3u')
assert.equal(owner.flags, 3)
owner.scan('\x1b[<u')
assert.equal(owner.flags, 0)
} else if (expectedTailLength >= 13) {
assert.equal(owner.mouseTrackingMode, 'vt200')
assert.equal(owner.sgrMouseMode, true)
}
assert.equal(owner.scanTail, '')
}
const afterCompletionDelta = (await heap()) - beforeHeap
assert.ok(
afterCompletionDelta < 768 * 1024,
JSON.stringify({ variant, kind, method, afterCompletionDelta })
)
for (const owner of owners) {
if (kind === 'kitty') {
owner.resetForSnapshot()
assert.equal(owner.snapshotFlags, undefined)
} else {
owner.scan('\x1bc')
assert.equal(owner.mouseTrackingMode, 'none')
assert.equal(owner.sgrMouseMode, false)
}
}
return {
variant,
kind,
method,
inputChars,
count,
expectedTailLength,
heapDelta,
afterCompletionDelta
}
}
function configureCopier(api, fallback) {
api.resetOwnRetainedStringCopier()
const originalBuffer = globalThis.Buffer
try {
if (fallback) {
globalThis.Buffer = undefined
}
assert.equal(api.ownRetainedString(pending), pending)
} finally {
globalThis.Buffer = originalBuffer
}
}
function behavior(Tracker, Mirror) {
const replay = new Tracker()
for (let index = 0; index < 70; index++) {
replay.scanReplay('\x1b[>3u')
}
assert.equal(replay.mainStack.length, 0)
assert.equal(replay.flags, 3)
replay.scan('\x1b[<u')
assert.equal(replay.flags, 0)
const live = new Tracker()
for (let index = 0; index < 70; index++) {
live.scan('\x1b[>3u')
}
assert.equal(live.mainStack.length, 16)
live.scan('\x1b[?1049h\x1b[>5u')
assert.equal(live.altStack.length, 1)
live.scan('\x1b[?1049l')
assert.equal(live.flags, 3)
live.scan(`\x1bc${pending}`)
assert.equal(live.flags, 0)
assert.equal(live.scanTail, pending)
live.scan('1006h')
assert.equal(live.isAlternateScreen, true)
live.reset()
assert.equal(live.scanTail, '')
assert.equal(live.snapshotFlags, 0)
const mouse = new Mirror()
mouse.scan('\x1b[?1003;1016h')
assert.equal(mouse.mouseTrackingMode, 'any')
assert.equal(mouse.sgrMousePixelsMode, true)
mouse.scan('\x9b?1002;1006h')
assert.equal(mouse.mouseTrackingMode, 'drag')
assert.equal(mouse.sgrMouseMode, true)
assert.equal(mouse.sgrMousePixelsMode, false)
mouse.scan(`\x1bc${pending}`)
assert.equal(mouse.mouseTrackingMode, 'none')
assert.equal(mouse.scanTail, pending)
mouse.scan('1006h')
assert.equal(mouse.mouseTrackingMode, 'vt200')
assert.equal(mouse.sgrMouseMode, true)
}
async function main() {
const reports = []
const bundles = {}
for (const variant of ['baseline', 'fixed-buffer', 'fixed-fallback']) {
const api = await loadSource(variant !== 'baseline')
const { TerminalKittyKeyboardModeTracker: Tracker, TerminalMouseModeMirror: Mirror } = api
bundles[variant] = {
bundleSha256: api.bundleSha256,
evaluatedSources: api.evaluatedSources,
sourceVersionsSha256: api.sourceVersionsSha256
}
configureCopier(api, variant === 'fixed-fallback')
behavior(Tracker, Mirror)
for (const [kind, Owner, methods] of [
['kitty', Tracker, ['scan', 'scanReplay']],
['mouse', Mirror, ['scan']]
]) {
for (const method of methods) {
for (const [chars, count] of sizes) {
reports.push(await measure(Owner, variant, kind, method, chars, count))
}
}
for (const suffix of [
'\x1b[',
'\x1b[?1049;2004;1000;1006h',
`\x1b[${'1'.repeat(4095)}`,
pending.replace('\x1b[', '\x9b')
]) {
reports.push(await measure(Owner, variant, kind, 'scan', 4 * 1024 * 1024, 8, suffix))
}
}
}
const report = {
node: process.version,
electron: process.versions.electron ?? null,
v8: process.versions.v8,
platform: process.platform,
runnerSha256: sha(read(__filename)),
loaderSha256: sha(read(path.join(__dirname, 'load-source.cjs'))),
pendingTailCodeUnits: pending.length,
clearedRegexStatics: true,
bundles,
reports
}
const output = path.join(
__dirname,
`${process.versions.electron ? 'electron' : 'node'}-results.json`
)
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`)
console.log(
JSON.stringify({ output, passed: reports.length, node: report.node, electron: report.electron })
)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
setTimeout(() => {
console.error('fixture deadline')
process.exit(2)
}, 30000).unref()
@@ -0,0 +1,44 @@
{
"publicationTopic": "np-oom-scan-retained-text-slices",
"publicationBaseCommit": "8d599520e44654a5c28e9930e3070c00d6499931",
"historicalParserRef": "v1.4.198",
"historicalScope": "Both parser modules and kitty flag parser match this ref; owned-string helpers come from the publication topic. This is not a historical app binary.",
"sources": {
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": {
"baselineSha256": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403",
"fixedSha256": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
"reverse": [
{
"from": "import { ownRetainedString } from './own-retained-string'\n",
"to": "",
"count": 1
},
{ "from": "? ownRetainedString(tail) : ''", "to": "? tail : ''", "count": 1 }
]
},
"src/main/daemon/terminal-mouse-mode-mirror.ts": {
"baselineSha256": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1",
"fixedSha256": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
"reverse": [
{
"from": "import { ownRetainedString } from '../../shared/own-retained-string'\n",
"to": "",
"count": 1
},
{ "from": "? ownRetainedString(tail) : ''", "to": "? tail : ''", "count": 2 }
]
},
"src/shared/own-retained-string.ts": {
"baselineSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
"fixedSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2"
},
"src/shared/owned-utf16-suffix.ts": {
"baselineSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
"fixedSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
},
"src/shared/terminal-kitty-keyboard-flags.ts": {
"baselineSha256": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225",
"fixedSha256": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
}
}
}
@@ -1,3 +1,4 @@
import { ownRetainedString } from '../../shared/own-retained-string'
import type { TerminalModes } from './types'
type MouseTrackingMode = NonNullable<TerminalModes['mouseTrackingMode']>
@@ -105,10 +106,10 @@ export class TerminalMouseModeMirror {
return tail
}
if (tail.startsWith('\x1b[?')) {
return this.isIncompleteParams(tail.slice(3)) ? tail : ''
return this.isIncompleteParams(tail.slice(3)) ? ownRetainedString(tail) : ''
}
if (tail.startsWith('\x9b?')) {
return this.isIncompleteParams(tail.slice(2)) ? tail : ''
return this.isIncompleteParams(tail.slice(2)) ? ownRetainedString(tail) : ''
}
return ''
}
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { TerminalMouseModeMirror } from './terminal-mouse-mode-mirror'
function heapAfterGc(): number {
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
throw new Error('The test runner must enable --expose-gc')
}
// Isolate mirror ownership from V8's process-wide last successful regexp input.
void /reset/.test('reset')
globalThis.gc()
globalThis.gc()
return process.memoryUsage().heapUsed
}
describe('mouse mode scan tail retention', () => {
it.each(['\x1b[', '\x9b'])(
'retains a split %j mode sequence without retaining consumed output',
(introducer) => {
const before = heapAfterGc()
const mirrors = Array.from({ length: 8 }, (_value, index) => {
const mirror = new TerminalMouseModeMirror()
mirror.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${introducer}?1049;2004;1000;`)
return mirror
})
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
for (const mirror of mirrors) {
expect(mirror.mouseTrackingMode).toBe('none')
mirror.scan('1006h')
expect(mirror.mouseTrackingMode).toBe('vt200')
expect(mirror.sgrMouseMode).toBe(true)
mirror.scan('\x1b[?1016h')
expect(mirror.sgrMouseMode).toBe(false)
expect(mirror.sgrMousePixelsMode).toBe(true)
mirror.scan('\x1bc')
expect(mirror.mouseTrackingMode).toBe('none')
expect(mirror.sgrMousePixelsMode).toBe(false)
}
}
)
})
@@ -1,3 +1,4 @@
import { ownRetainedString } from './own-retained-string'
import { parseTerminalKittyKeyboardFlags } from './terminal-kitty-keyboard-flags'
// Why: PTY/SSH chunks can split an escape sequence before its final byte.
@@ -308,7 +309,7 @@ export class TerminalKittyKeyboardModeTracker {
if (body === null) {
return ''
}
return this.isIncompleteSequenceBody(body) ? tail : ''
return this.isIncompleteSequenceBody(body) ? ownRetainedString(tail) : ''
}
private isIncompleteSequenceBody(body: string): boolean {
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { TerminalKittyKeyboardModeTracker } from './terminal-kitty-keyboard-mode-tracker'
const INCOMPLETE_MODE = '\x1b[?1049;2004;1000;'
function heapAfterGc(): number {
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
throw new Error('The test runner must enable --expose-gc')
}
// Isolate tracker ownership from V8's process-wide last successful regexp input.
void /reset/.test('reset')
globalThis.gc()
globalThis.gc()
return process.memoryUsage().heapUsed
}
describe('kitty keyboard scan tail retention', () => {
it.each(['scan', 'scanReplay'] as const)(
'%s retains a split mode sequence without retaining consumed output',
(method) => {
const before = heapAfterGc()
const trackers = Array.from({ length: 8 }, (_value, index) => {
const tracker = new TerminalKittyKeyboardModeTracker()
tracker[method](`${index}:${'x'.repeat(4 * 1024 * 1024)}${INCOMPLETE_MODE}`)
return tracker
})
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
for (const tracker of trackers) {
expect(tracker.isAlternateScreen).toBe(false)
tracker[method]('1006h\x1b[>3u')
expect(tracker.isAlternateScreen).toBe(true)
expect(tracker.flags).toBe(3)
tracker.scan('\x1b[<u')
expect(tracker.flags).toBe(0)
tracker.resetForSnapshot()
expect(tracker.snapshotFlags).toBeUndefined()
}
}
)
})