Skip to content

Commit 6e8c2db

Browse files
Lifeng LuAArnott
authored andcommitted
This PR is to fix problems that getBufferFrom keeps spinning and consumes lots of CPU, also leads product to hang.
This happens when the stream has partial data ready to read, because read(size) returns null (unless the stream is closed), but because there are unread data in the stream, the code will not block waiting on anything but immediately tries to read again. This would consume lots of CPU. On the other hand, because the length of pipeline can be limited. when the reader wants a larger block over the size of the pipeline buffer, it would never get the data, because the writer cannot write any more data until reader takes some bytes out of the stream. This PR is intended to use readableLength to read partial data out, and joins them on the reader side when it is necessary. However, the PR turns out to be more complicated due to this state is not defined in the ReadableStream interface, but in the implementation (Readable). Not sure why an important property like this is not included in the contract. So the code lands to keep the old behavior unless readableLength is available. Also, i kept running into memory issues in unit tests. There were some event handler which can leak memory, and was fixed. But it turned out that the real reason is that reableEnded can be false when streamEnded event is fired. Because the earlier changed code depends on the state, it ends up spinning in the function. Interestingly, it leads out of memory.
1 parent d5ffbe7 commit 6e8c2db

File tree

1 file changed

+63
-15
lines changed

1 file changed

+63
-15
lines changed

src/nerdbank-streams/src/Utilities.ts

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,32 +72,80 @@ export async function getBufferFrom(
7272
cancellationToken?: CancellationToken): Promise<Buffer | null> {
7373

7474
const streamEnded = new Deferred<void>();
75-
while (size > 0) {
76-
cancellationToken?.throwIfCancelled();
7775

78-
const readBuffer = readable.read(size) as Buffer;
79-
if (readBuffer === null) {
80-
const bytesAvailable = new Deferred<void>();
81-
readable.once("readable", bytesAvailable.resolve.bind(bytesAvailable));
82-
readable.once("end", streamEnded.resolve.bind(streamEnded));
83-
const endPromise = Promise.race([bytesAvailable.promise, streamEnded.promise]);
84-
await (cancellationToken ? cancellationToken.racePromise(endPromise) : endPromise);
76+
if (size === 0) {
77+
return Buffer.from([]);
78+
}
8579

86-
if (bytesAvailable.isCompleted) {
87-
continue;
80+
let readBuffer: Buffer | null = null;
81+
let index: number = 0;
82+
while (size > 0) {
83+
cancellationToken?.throwIfCancelled();
84+
let availableSize = (readable as Readable).readableLength;
85+
if (!availableSize) {
86+
// Check the end of stream
87+
if ((readable as Readable).readableEnded || streamEnded.isCompleted) {
88+
// stream is closed
89+
if (!allowEndOfStream) {
90+
throw new Error("Stream terminated before required bytes were read.");
91+
}
92+
93+
// Returns what has been read so far
94+
if (readBuffer === null) {
95+
return null;
96+
}
97+
98+
// we need trim extra spaces
99+
return readBuffer.subarray(0, index)
88100
}
101+
102+
// we retain this behavior when availableSize === false
103+
// to make existing unit tests happy (which assumes we will try to read stream when no data is ready.)
104+
availableSize = size;
105+
} else if (availableSize > size) {
106+
availableSize = size;
89107
}
90108

91-
if (!allowEndOfStream) {
92-
if (!readBuffer || readBuffer.length < size) {
109+
const newBuffer = readable.read(availableSize) as Buffer;
110+
if (newBuffer) {
111+
if (newBuffer.length < availableSize && !allowEndOfStream) {
93112
throw new Error("Stream terminated before required bytes were read.");
94113
}
114+
115+
if (readBuffer === null) {
116+
if (availableSize === size || newBuffer.length < availableSize) {
117+
// in the fast pass, we read the entire data once, and donot allocate an extra array.
118+
return newBuffer;
119+
}
120+
121+
// if we read partial data, we need allocate a buffer to join all data together.
122+
readBuffer = Buffer.alloc(size);
123+
}
124+
125+
// now append new data to the buffer
126+
newBuffer.copy(readBuffer, index);
127+
128+
size -= newBuffer.length;
129+
index += newBuffer.length;
95130
}
96131

97-
return readBuffer;
132+
if (size > 0) {
133+
const bytesAvailable = new Deferred<void>();
134+
const bytesAvailableCallback = bytesAvailable.resolve.bind(bytesAvailable);
135+
const streamEndedCallback = streamEnded.resolve.bind(streamEnded);
136+
readable.once("readable", bytesAvailableCallback);
137+
readable.once("end", streamEndedCallback);
138+
try {
139+
const endPromise = Promise.race([bytesAvailable.promise, streamEnded.promise]);
140+
await (cancellationToken ? cancellationToken.racePromise(endPromise) : endPromise);
141+
} finally {
142+
readable.removeListener("readable", bytesAvailableCallback);
143+
readable.removeListener("end", streamEndedCallback);
144+
}
145+
}
98146
}
99147

100-
return Buffer.from([]);
148+
return readBuffer;
101149
}
102150

103151
export function throwIfDisposed(value: IDisposableObservable) {

0 commit comments

Comments
 (0)