Skip to content

Commit f82c9eb

Browse files
authored
fix: improve protocol handling performance in all cases (#2002)
1 parent 774a6fb commit f82c9eb

File tree

10 files changed

+184
-65
lines changed

10 files changed

+184
-65
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he
55
## Nightly (only)
66

77
- fix: improve main-thread performance of source map rename ([vscode#210518](https://github.com/microsoft/vscode/issues/210518))
8+
- fix: improve protocol handling performance in all cases ([#2001](https://github.com/microsoft/vscode-js-debug/issues/2001))
89

910
## v1.89 (April 2024)
1011

package-lock.json

Lines changed: 0 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
"reflect-metadata": "^0.2.1",
7979
"signale": "^1.4.0",
8080
"source-map-support": "^0.5.21",
81-
"split2": "^4.2.0",
8281
"to-absolute-glob": "^3.0.0",
8382
"vscode-tas-client": "^0.1.84",
8483
"ws": "^8.16.0"
@@ -118,7 +117,6 @@
118117
"@types/prettier": "^2.7.3",
119118
"@types/signale": "^1.4.7",
120119
"@types/sinon": "^17.0.3",
121-
"@types/split2": "^4.2.3",
122120
"@types/stream-buffers": "^3.0.7",
123121
"@types/tmp": "^0.2.6",
124122
"@types/to-absolute-glob": "^2.0.3",

src/cdp/rawPipeTransport.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5-
import split from 'split2';
65
import { Duplex, Readable, Writable } from 'stream';
76
import { EventEmitter } from '../common/events';
87
import { HrTime } from '../common/hrnow';
98
import { ILogger, LogTag } from '../common/logging';
109
import { once } from '../common/objUtils';
10+
import { StreamSplitter } from '../common/streamSplitter';
1111
import { ITransport } from './transport';
1212

1313
export class RawPipeTransport implements ITransport {
@@ -57,8 +57,8 @@ export class RawPipeTransport implements ITransport {
5757
this.streams = {
5858
read: read
5959
.on('error', error => this.logger.error(LogTag.Internal, 'pipeRead error', { error }))
60-
.pipe(split('\0'))
61-
.on('data', json => this.messageEmitter.fire([json, new HrTime()]))
60+
.pipe(new StreamSplitter(0))
61+
.on('data', (json: Buffer) => this.messageEmitter.fire([json.toString(), new HrTime()]))
6262
.on('end', this.onceEnded),
6363
write: pipeWrite.on('end', this.onceEnded).on('error', this.onWriteError),
6464
};

src/common/streamSplitter.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import * as assert from 'assert';
6+
import { Writable } from 'stream';
7+
import { StreamSplitter } from './streamSplitter';
8+
9+
describe('StreamSplitter', () => {
10+
it('should split a stream', done => {
11+
const chunks: string[] = [];
12+
const splitter = new StreamSplitter('\n');
13+
const writable = new Writable({
14+
write(chunk, _encoding, callback) {
15+
chunks.push(chunk.toString());
16+
callback();
17+
},
18+
});
19+
20+
splitter.pipe(writable);
21+
splitter.write('hello\nwor');
22+
splitter.write('ld\n');
23+
splitter.write('foo\nbar\nz');
24+
splitter.end(() => {
25+
assert.deepStrictEqual(chunks, ['hello', 'world', 'foo', 'bar', 'z']);
26+
done();
27+
});
28+
});
29+
});

src/common/streamSplitter.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
// Based on VS Code's src/vs/base/node/nodeStreams.ts
6+
7+
import { Transform } from 'stream';
8+
9+
/**
10+
* A Transform stream that splits the input on the "splitter" substring.
11+
* The resulting chunks will contain (and trail with) the splitter match.
12+
* The last chunk when the stream ends will be emitted even if a splitter
13+
* is not encountered.
14+
*/
15+
export class StreamSplitter extends Transform {
16+
private prefix: Buffer[] = [];
17+
private readonly splitter: number;
18+
19+
/** Suffix added after each split chunk. */
20+
protected splitSuffix = Buffer.alloc(0);
21+
22+
constructor(splitter: string | number | Buffer) {
23+
super();
24+
if (typeof splitter === 'string' && splitter.length === 1) {
25+
this.splitter = splitter.charCodeAt(0);
26+
} else if (typeof splitter === 'number') {
27+
this.splitter = splitter;
28+
} else {
29+
throw new Error('not implemented here');
30+
}
31+
}
32+
33+
override _transform(
34+
chunk: Buffer,
35+
_encoding: string,
36+
callback: (error?: Error | null, data?: unknown) => void,
37+
): void {
38+
let offset = 0;
39+
while (offset < chunk.length) {
40+
const index = chunk.indexOf(this.splitter, offset);
41+
if (index === -1) {
42+
break;
43+
}
44+
45+
const thisChunk = chunk.subarray(offset, index);
46+
const toEmit =
47+
this.prefix.length || this.splitSuffix.length
48+
? Buffer.concat([...this.prefix, thisChunk, this.splitSuffix])
49+
: thisChunk;
50+
51+
this.push(toEmit);
52+
this.prefix.length = 0;
53+
offset = index + 1;
54+
}
55+
56+
if (offset < chunk.length) {
57+
this.prefix.push(chunk.subarray(offset));
58+
}
59+
60+
callback();
61+
}
62+
63+
override _flush(callback: (error?: Error | null, data?: unknown) => void): void {
64+
for (const buf of this.prefix) {
65+
this.push(buf);
66+
}
67+
68+
callback();
69+
}
70+
}

src/targets/node/subprocessProgramLauncher.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44

55
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
66
import { inject, injectable } from 'inversify';
7-
import split from 'split2';
8-
import { Transform } from 'stream';
97
import { EnvironmentVars } from '../../common/environmentVars';
108
import { ILogger } from '../../common/logging';
9+
import { StreamSplitter } from '../../common/streamSplitter';
1110
import * as urlUtils from '../../common/urlUtils';
1211
import { INodeLaunchConfiguration, OutputSource } from '../../configuration';
1312
import Dap from '../../dap/api';
1413
import { ILaunchContext } from '../targets';
15-
import { getNodeLaunchArgs, IProgramLauncher } from './processLauncher';
14+
import { IProgramLauncher, getNodeLaunchArgs } from './processLauncher';
1615
import { SubprocessProgram } from './program';
1716

1817
/**
@@ -64,12 +63,12 @@ export class SubprocessProgramLauncher implements IProgramLauncher {
6463
*/
6564
private captureStdio(dap: Dap.Api, child: ChildProcessWithoutNullStreams) {
6665
child.stdout
67-
.pipe(EtxSplitter.stream())
68-
.on('data', output => dap.output({ category: 'stdout', output }))
66+
.pipe(new EtxSplitter())
67+
.on('data', output => dap.output({ category: 'stdout', output: output.toString() }))
6968
.resume();
7069
child.stderr
71-
.pipe(EtxSplitter.stream())
72-
.on('data', output => dap.output({ category: 'stderr', output }))
70+
.pipe(new EtxSplitter())
71+
.on('data', output => dap.output({ category: 'stderr', output: output.toString() }))
7372
.resume();
7473
}
7574

@@ -158,7 +157,7 @@ const formatArguments = (executable: string, args: ReadonlyArray<string>, cwd: s
158157
};
159158

160159
const enum Char {
161-
ETX = '\u0003',
160+
ETX = 3,
162161
}
163162

164163
/**
@@ -168,22 +167,28 @@ const enum Char {
168167
* finds any, it will switch from splitting the stream on newlines to
169168
* splitting on ETX characters.
170169
*/
171-
export class EtxSplitter {
170+
export class EtxSplitter extends StreamSplitter {
172171
private etxSpotted = false;
173172

174-
public static stream(): Transform {
175-
return split(new EtxSplitter());
173+
constructor() {
174+
super(Char.ETX);
175+
this.splitSuffix = Buffer.from('\n');
176176
}
177177

178-
[Symbol.split](str: string) {
179-
this.etxSpotted ||= str.includes(Char.ETX);
180-
if (!this.etxSpotted) {
181-
return [str, ''];
178+
override _transform(
179+
chunk: Buffer,
180+
_encoding: string,
181+
callback: (error?: Error | null | undefined, data?: unknown) => void,
182+
): void {
183+
if (!this.etxSpotted && chunk.includes(Char.ETX)) {
184+
this.etxSpotted = true;
182185
}
183186

184-
const split = str.split(Char.ETX);
187+
if (!this.etxSpotted) {
188+
this.push(chunk);
189+
return callback();
190+
}
185191

186-
// restore or add new lines between each record for proper debug console display
187-
return split.length > 1 ? split.map((s, i) => (i < split.length - 1 ? `${s}\n` : s)) : split;
192+
return super._transform(chunk, _encoding, callback);
188193
}
189194
}

src/test/extension/pickAttach.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import { promises as fsPromises } from 'fs';
88
import { tmpdir } from 'os';
99
import * as path from 'path';
1010
import { SinonSandbox, createSandbox } from 'sinon';
11-
import split from 'split2';
1211
import * as vscode from 'vscode';
1312
import { Commands, DebugType } from '../../common/contributionUtils';
1413
import { findOpenPort } from '../../common/findOpenPort';
1514
import { LocalFsUtils } from '../../common/fsUtils';
1615
import { delay } from '../../common/promiseUtil';
16+
import { StreamSplitter } from '../../common/streamSplitter';
1717
import { nodeAttachConfigDefaults } from '../../configuration';
1818
import { resolveProcessId } from '../../ui/processPicker';
1919
import { createFileTree } from '../createFileTree';
@@ -89,8 +89,11 @@ describe('pick and attach', () => {
8989
child = spawn('node', ['--inspect-brk', `--inspect-port=${port}`], { stdio: 'pipe' });
9090
child.on('error', console.error);
9191
child
92-
.stderr!.pipe(split())
93-
.on('data', (line: string) => (attached = attached || line.includes('Debugger attached')));
92+
.stderr!.pipe(new StreamSplitter('\n'))
93+
.on(
94+
'data',
95+
(line: string) => (attached = attached || line.toString().includes('Debugger attached')),
96+
);
9497
});
9598

9699
it('end to end', async function () {

0 commit comments

Comments
 (0)