Skip to content

Commit 4df2003

Browse files
authored
storedStoredFileWorkingCopy leak (microsoft#158038) (microsoft#158342)
1 parent 8512ce3 commit 4df2003

File tree

2 files changed

+48
-22
lines changed

2 files changed

+48
-22
lines changed

src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,9 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
12331233
this.inConflictMode = false;
12341234
this.inErrorMode = false;
12351235

1236+
// Free up model for GC
1237+
this._model = undefined;
1238+
12361239
super.dispose();
12371240
}
12381241

src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@ export abstract class WorkingCopyBackupTracker extends Disposable {
102102
private readonly mapWorkingCopyToContentVersion = new Map<IWorkingCopy, number>();
103103

104104
// A map of scheduled pending backup operations for working copies
105-
protected readonly pendingBackupOperations = new Map<IWorkingCopy, IDisposable>();
105+
// Given https://github.com/microsoft/vscode/issues/158038, we explicitly
106+
// do not store `IWorkingCopy` but the identifier in the map, since it
107+
// looks like GC is not runnin for the working copy otherwise.
108+
protected readonly pendingBackupOperations = new Map<IWorkingCopyIdentifier, IDisposable>();
106109

107110
private suspended = false;
108111

@@ -174,6 +177,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable {
174177
this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString(), workingCopy.typeId);
175178

176179
// Schedule new backup
180+
const workingCopyIdentifier = { resource: workingCopy.resource, typeId: workingCopy.typeId };
177181
const cts = new CancellationTokenSource();
178182
const handle = setTimeout(async () => {
179183
if (cts.token.isCancellationRequested) {
@@ -203,12 +207,12 @@ export abstract class WorkingCopyBackupTracker extends Disposable {
203207
// Clear disposable unless we got canceled which would
204208
// indicate another operation has started meanwhile
205209
if (!cts.token.isCancellationRequested) {
206-
this.pendingBackupOperations.delete(workingCopy);
210+
this.pendingBackupOperations.delete(workingCopyIdentifier);
207211
}
208212
}, this.getBackupScheduleDelay(workingCopy));
209213

210214
// Keep in map for disposal as needed
211-
this.pendingBackupOperations.set(workingCopy, toDisposable(() => {
215+
this.pendingBackupOperations.set(workingCopyIdentifier, toDisposable(() => {
212216
this.logService.trace(`[backup tracker] clearing pending backup creation`, workingCopy.resource.toString(), workingCopy.typeId);
213217

214218
cts.dispose(true);
@@ -235,35 +239,54 @@ export abstract class WorkingCopyBackupTracker extends Disposable {
235239
this.cancelBackupOperation(workingCopy);
236240

237241
// Schedule backup discard asap
242+
const workingCopyIdentifier = { resource: workingCopy.resource, typeId: workingCopy.typeId };
238243
const cts = new CancellationTokenSource();
239-
(async () => {
240-
this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString(), workingCopy.typeId);
241-
242-
// Discard backup
243-
try {
244-
await this.workingCopyBackupService.discardBackup(workingCopy, cts.token);
245-
} catch (error) {
246-
this.logService.error(error);
247-
}
248-
249-
// Clear disposable unless we got canceled which would
250-
// indicate another operation has started meanwhile
251-
if (!cts.token.isCancellationRequested) {
252-
this.pendingBackupOperations.delete(workingCopy);
253-
}
254-
})();
244+
this.doDiscardBackup(workingCopyIdentifier, cts);
255245

256246
// Keep in map for disposal as needed
257-
this.pendingBackupOperations.set(workingCopy, toDisposable(() => {
247+
this.pendingBackupOperations.set(workingCopyIdentifier, toDisposable(() => {
258248
this.logService.trace(`[backup tracker] clearing pending backup discard`, workingCopy.resource.toString(), workingCopy.typeId);
259249

260250
cts.dispose(true);
261251
}));
262252
}
263253

254+
private async doDiscardBackup(workingCopyIdentifier: IWorkingCopyIdentifier, cts: CancellationTokenSource) {
255+
this.logService.trace(`[backup tracker] discarding backup`, workingCopyIdentifier.resource.toString(), workingCopyIdentifier.typeId);
256+
257+
// Discard backup
258+
try {
259+
await this.workingCopyBackupService.discardBackup(workingCopyIdentifier, cts.token);
260+
} catch (error) {
261+
this.logService.error(error);
262+
}
263+
264+
// Clear disposable unless we got canceled which would
265+
// indicate another operation has started meanwhile
266+
if (!cts.token.isCancellationRequested) {
267+
this.pendingBackupOperations.delete(workingCopyIdentifier);
268+
}
269+
}
270+
264271
private cancelBackupOperation(workingCopy: IWorkingCopy): void {
265-
dispose(this.pendingBackupOperations.get(workingCopy));
266-
this.pendingBackupOperations.delete(workingCopy);
272+
273+
// Given a working copy we want to find the matching
274+
// identifier in our pending operations map because
275+
// we cannot use the working copy directly, as the
276+
// identifier might have different object identity.
277+
278+
let workingCopyIdentifier: IWorkingCopyIdentifier | undefined = undefined;
279+
for (const [identifier] of this.pendingBackupOperations) {
280+
if (identifier.resource.toString() === workingCopy.resource.toString() && identifier.typeId === workingCopy.typeId) {
281+
workingCopyIdentifier = identifier;
282+
break;
283+
}
284+
}
285+
286+
if (workingCopyIdentifier) {
287+
dispose(this.pendingBackupOperations.get(workingCopyIdentifier));
288+
this.pendingBackupOperations.delete(workingCopyIdentifier);
289+
}
267290
}
268291

269292
protected cancelBackupOperations(): void {

0 commit comments

Comments
 (0)