Skip to content

Commit e0d4983

Browse files
authored
Memory Leak when using MessageService#showProgress on Backend #13253 (#13254)
* dispose cancellation event listeners in RpcProtocol * introduce common DisposableWrapper
1 parent 59ea819 commit e0d4983

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed

packages/core/src/common/disposable.ts

+25
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,28 @@ export function disposableTimeout(...args: Parameters<typeof setTimeout>): Dispo
133133
const handle = setTimeout(...args);
134134
return { dispose: () => clearTimeout(handle) };
135135
}
136+
137+
/**
138+
* Wrapper for a {@link Disposable} that is not available immediately.
139+
*/
140+
export class DisposableWrapper implements Disposable {
141+
142+
private disposed = false;
143+
private disposable: Disposable | undefined = undefined;
144+
145+
set(disposable: Disposable): void {
146+
if (this.disposed) {
147+
disposable.dispose();
148+
} else {
149+
this.disposable = disposable;
150+
}
151+
}
152+
153+
dispose(): void {
154+
this.disposed = true;
155+
if (this.disposable) {
156+
this.disposable.dispose();
157+
this.disposable = undefined;
158+
}
159+
}
160+
}

packages/core/src/common/message-rpc/rpc-protocol.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
/* eslint-disable @typescript-eslint/no-explicit-any */
1717

1818
import { CancellationToken, CancellationTokenSource } from '../cancellation';
19-
import { Disposable, DisposableCollection } from '../disposable';
19+
import { DisposableWrapper, Disposable, DisposableCollection } from '../disposable';
2020
import { Emitter, Event } from '../event';
2121
import { Deferred } from '../promise-util';
2222
import { Channel } from './channel';
@@ -57,6 +57,7 @@ export class RpcProtocol {
5757
static readonly CANCELLATION_TOKEN_KEY = 'add.cancellation.token';
5858

5959
protected readonly pendingRequests: Map<number, Deferred<any>> = new Map();
60+
protected readonly pendingRequestCancellationEventListeners: Map<number, DisposableWrapper> = new Map();
6061

6162
protected nextMessageId: number = 0;
6263

@@ -80,6 +81,8 @@ export class RpcProtocol {
8081
channel.onClose(event => {
8182
this.pendingRequests.forEach(pending => pending.reject(new Error(event.reason)));
8283
this.pendingRequests.clear();
84+
this.pendingRequestCancellationEventListeners.forEach(disposable => disposable.dispose());
85+
this.pendingRequestCancellationEventListeners.clear();
8386
this.toDispose.dispose();
8487
});
8588
this.toDispose.push(channel.onMessage(readBuffer => this.handleMessage(this.decoder.parse(readBuffer()))));
@@ -131,6 +134,7 @@ export class RpcProtocol {
131134
} else {
132135
throw new Error(`No reply handler for reply with id: ${id}`);
133136
}
137+
this.disposeCancellationEventListener(id);
134138
}
135139

136140
protected handleReplyErr(id: number, error: any): void {
@@ -141,6 +145,15 @@ export class RpcProtocol {
141145
} else {
142146
throw new Error(`No reply handler for error reply with id: ${id}`);
143147
}
148+
this.disposeCancellationEventListener(id);
149+
}
150+
151+
protected disposeCancellationEventListener(id: number): void {
152+
const toDispose = this.pendingRequestCancellationEventListeners.get(id);
153+
if (toDispose) {
154+
this.pendingRequestCancellationEventListeners.delete(id);
155+
toDispose.dispose();
156+
}
144157
}
145158

146159
sendRequest<T>(method: string, args: any[]): Promise<T> {
@@ -157,14 +170,21 @@ export class RpcProtocol {
157170

158171
this.pendingRequests.set(id, reply);
159172

173+
// register disposable before output.commit() even when not available yet
174+
const disposableWrapper = new DisposableWrapper();
175+
this.pendingRequestCancellationEventListeners.set(id, disposableWrapper);
176+
160177
const output = this.channel.getWriteBuffer();
161178
this.encoder.request(output, id, method, args);
162179
output.commit();
163180

164181
if (cancellationToken?.isCancellationRequested) {
165182
this.sendCancel(id);
166183
} else {
167-
cancellationToken?.onCancellationRequested(() => this.sendCancel(id));
184+
const disposable = cancellationToken?.onCancellationRequested(() => this.sendCancel(id));
185+
if (disposable) {
186+
disposableWrapper.set(disposable);
187+
}
168188
}
169189

170190
return reply.promise;

0 commit comments

Comments
 (0)