mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 00:02:24 +00:00
fix: detach retained terminal mode scan tails
This commit is contained in:
@@ -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 (`:48–57`). `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()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user