Skip to content

Commit e3f691d

Browse files
amungerbpasero
andauthored
Disable notebook backups when file size is too large (microsoft#210493)
* configurable limit for notebook output size for backups * disable backups entirely for notebooks with large outputs * working copy - make the snapshot context more explicit * test that snapshot will throw * fix casing --------- Co-authored-by: Benjamin Pasero <[email protected]>
1 parent 7964420 commit e3f691d

File tree

11 files changed

+91
-20
lines changed

11 files changed

+91
-20
lines changed

src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,5 +1096,10 @@ configurationRegistry.registerConfiguration({
10961096
type: 'boolean',
10971097
default: true
10981098
},
1099+
[NotebookSetting.outputBackupSizeLimit]: {
1100+
markdownDescription: nls.localize('notebook.backup.sizeLimit', "The limit of notebook output size in kilobytes (KB) where notebook files will no longer be backed up for hot reload. Use 0 for unlimited."),
1101+
type: 'number',
1102+
default: 10000
1103+
}
10991104
}
11001105
});

src/vs/workbench/contrib/notebook/common/notebookCommon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,7 @@ export const NotebookSetting = {
962962
notebookVariablesView: 'notebook.experimental.variablesView',
963963
InteractiveWindowPromptToSave: 'interactiveWindow.promptToSaveOnClose',
964964
cellFailureDiagnostics: 'notebook.cellFailureDiagnostics',
965+
outputBackupSizeLimit: 'notebook.backup.sizeLimit',
965966
} as const;
966967

967968
export const enum CellStatusbarAlignment {

src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no
2121
import { ICellDto2, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookData, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
2222
import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService';
2323
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
24-
import { IFileWorkingCopyModelConfiguration } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
24+
import { IFileWorkingCopyModelConfiguration, SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
2525
import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager';
2626
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent, StoredFileWorkingCopyState } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
2727
import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy';
@@ -252,14 +252,15 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF
252252
return this._notebookModel;
253253
}
254254

255-
async snapshot(token: CancellationToken): Promise<VSBufferReadableStream> {
255+
async snapshot(context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream> {
256256
const serializer = await this.getNotebookSerializer();
257257

258258
const data: NotebookData = {
259259
metadata: filter(this._notebookModel.metadata, key => !serializer.options.transientDocumentMetadata[key]),
260260
cells: [],
261261
};
262262

263+
let outputSize = 0;
263264
for (const cell of this._notebookModel.cells) {
264265
const cellData: ICellDto2 = {
265266
cellKind: cell.cellKind,
@@ -270,6 +271,18 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF
270271
internalMetadata: cell.internalMetadata
271272
};
272273

274+
const outputSizeLimit = this._configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
275+
if (context === SnapshotContext.Backup && outputSizeLimit > 0) {
276+
cell.outputs.forEach(output => {
277+
output.outputs.forEach(item => {
278+
outputSize += item.data.byteLength;
279+
});
280+
});
281+
if (outputSize > outputSizeLimit) {
282+
throw new Error('Notebook too large to backup');
283+
}
284+
}
285+
273286
cellData.outputs = !serializer.options.transientOutputs ? cell.outputs : [];
274287
cellData.metadata = filter(cell.metadata, key => !serializer.options.transientCellMetadata[key]);
275288

src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/
1515
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
1616
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
1717
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
18-
import { CellKind, NotebookData, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
18+
import { CellKind, IOutputDto, NotebookData, NotebookSetting, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
1919
import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
2020
import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService';
2121
import { setupInstantiationService } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor';
22+
import { SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
2223

2324
suite('NotebookFileWorkingCopyModel', function () {
2425

@@ -63,7 +64,7 @@ suite('NotebookFileWorkingCopyModel', function () {
6364
configurationService
6465
));
6566

66-
await model.snapshot(CancellationToken.None);
67+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
6768
assert.strictEqual(callCount, 1);
6869
}
6970

@@ -84,7 +85,7 @@ suite('NotebookFileWorkingCopyModel', function () {
8485
),
8586
configurationService
8687
));
87-
await model.snapshot(CancellationToken.None);
88+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
8889
assert.strictEqual(callCount, 1);
8990
}
9091
});
@@ -119,7 +120,7 @@ suite('NotebookFileWorkingCopyModel', function () {
119120
configurationService
120121
));
121122

122-
await model.snapshot(CancellationToken.None);
123+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
123124
assert.strictEqual(callCount, 1);
124125
}
125126

@@ -140,7 +141,7 @@ suite('NotebookFileWorkingCopyModel', function () {
140141
),
141142
configurationService
142143
));
143-
await model.snapshot(CancellationToken.None);
144+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
144145
assert.strictEqual(callCount, 1);
145146
}
146147
});
@@ -174,7 +175,7 @@ suite('NotebookFileWorkingCopyModel', function () {
174175
configurationService
175176
));
176177

177-
await model.snapshot(CancellationToken.None);
178+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
178179
assert.strictEqual(callCount, 1);
179180
}
180181

@@ -195,10 +196,52 @@ suite('NotebookFileWorkingCopyModel', function () {
195196
),
196197
configurationService
197198
));
198-
await model.snapshot(CancellationToken.None);
199+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
199200
assert.strictEqual(callCount, 1);
200201
}
201202
});
203+
204+
test('Notebooks with outputs beyond the size threshold will throw for backup snapshots', async function () {
205+
const outputLimit = 100;
206+
await configurationService.setUserConfiguration(NotebookSetting.outputBackupSizeLimit, outputLimit * 1.0 / 1024);
207+
const largeOutput: IOutputDto = { outputId: '123', outputs: [{ mime: Mimes.text, data: VSBuffer.fromString('a'.repeat(outputLimit + 1)) }] };
208+
const notebook = instantiationService.createInstance(NotebookTextModel,
209+
'notebook',
210+
URI.file('test'),
211+
[{ cellKind: CellKind.Code, language: 'foo', mime: 'foo', source: 'foo', outputs: [largeOutput], metadata: { foo: 123, bar: 456 } }],
212+
{},
213+
{ transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false, }
214+
);
215+
disposables.add(notebook);
216+
217+
let callCount = 0;
218+
const model = disposables.add(new NotebookFileWorkingCopyModel(
219+
notebook,
220+
mockNotebookService(notebook,
221+
new class extends mock<INotebookSerializer>() {
222+
override options: TransientOptions = { transientOutputs: true, transientDocumentMetadata: {}, transientCellMetadata: { bar: true }, cellContentMetadata: {} };
223+
override async notebookToData(notebook: NotebookData) {
224+
callCount += 1;
225+
assert.strictEqual(notebook.cells[0].metadata!.foo, 123);
226+
assert.strictEqual(notebook.cells[0].metadata!.bar, undefined);
227+
return VSBuffer.fromString('');
228+
}
229+
}
230+
),
231+
configurationService
232+
));
233+
234+
try {
235+
await model.snapshot(SnapshotContext.Backup, CancellationToken.None);
236+
assert.fail('Expected snapshot to throw an error for large output');
237+
} catch (e) {
238+
assert.notEqual(e.code, 'ERR_ASSERTION', e.message);
239+
}
240+
241+
await model.snapshot(SnapshotContext.Save, CancellationToken.None);
242+
assert.strictEqual(callCount, 1);
243+
244+
});
202245
});
203246

204247
function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: INotebookSerializer) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export interface IFileWorkingCopyModelConfiguration {
3535
readonly backupDelay?: number;
3636
}
3737

38+
export const enum SnapshotContext {
39+
Save = 1,
40+
Backup = 2
41+
}
42+
3843
/**
3944
* A generic file working copy model to be reused by untitled
4045
* and stored file working copies.
@@ -72,9 +77,10 @@ export interface IFileWorkingCopyModel extends IDisposable {
7277
* Snapshots the model's current content for writing. This must include
7378
* any changes that were made to the model that are in memory.
7479
*
80+
* @param context indicates in what context the snapshot is used
7581
* @param token support for cancellation
7682
*/
77-
snapshot(token: CancellationToken): Promise<VSBufferReadableStream>;
83+
snapshot(context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream>;
7884

7985
/**
8086
* Updates the model with the provided contents. The implementation should

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileW
2323
import { INewOrExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager';
2424
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
2525
import { IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager';
26-
import { IFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
26+
import { IFileWorkingCopy, SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
2727
import { ILabelService } from 'vs/platform/label/common/label';
2828
import { ILogService } from 'vs/platform/log/common/log';
2929
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -390,7 +390,7 @@ export class FileWorkingCopyManager<S extends IStoredFileWorkingCopyModel, U ext
390390
// use that to copy the contents to the target destination
391391
const sourceWorkingCopy = this.get(source);
392392
if (sourceWorkingCopy?.isResolved()) {
393-
sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None);
393+
sourceContents = await sourceWorkingCopy.model.snapshot(SnapshotContext.Save, CancellationToken.None);
394394
}
395395

396396
// Otherwise we resolve the contents from the underlying file

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/com
2727
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2828
import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService';
2929
import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy';
30-
import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
30+
import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
3131
import { IMarkdownString } from 'vs/base/common/htmlContent';
3232

3333
/**
@@ -813,7 +813,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
813813
// Fill in content if we are resolved
814814
let content: VSBufferReadableStream | undefined = undefined;
815815
if (this.isResolved()) {
816-
content = await raceCancellation(this.model.snapshot(token), token);
816+
content = await raceCancellation(this.model.snapshot(SnapshotContext.Backup, token), token);
817817
}
818818

819819
return { meta, content };
@@ -1026,7 +1026,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
10261026
else {
10271027

10281028
// Snapshot working copy model contents
1029-
const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token);
1029+
const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(SnapshotContext.Save, saveCancellation.token), saveCancellation.token);
10301030

10311031
// It is possible that a subsequent save is cancelling this
10321032
// running save. As such we return early when we detect that

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/com
2929
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
3030
import { isWeb } from 'vs/base/common/platform';
3131
import { onUnexpectedError } from 'vs/base/common/errors';
32+
import { SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
3233

3334
/**
3435
* The only one that should be dealing with `IStoredFileWorkingCopy` and handle all
@@ -352,7 +353,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel>
352353
workingCopiesToRestore.push({
353354
source: sourceResource,
354355
target: targetResource,
355-
snapshot: sourceWorkingCopy.isDirty() ? await sourceWorkingCopy.model?.snapshot(CancellationToken.None) : undefined
356+
snapshot: sourceWorkingCopy.isDirty() ? await sourceWorkingCopy.model?.snapshot(SnapshotContext.Save, CancellationToken.None) : undefined
356357
});
357358
}
358359
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Event, Emitter } from 'vs/base/common/event';
77
import { VSBufferReadableStream } from 'vs/base/common/buffer';
88
import { IWorkingCopyBackup, IWorkingCopySaveEvent, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy';
9-
import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
9+
import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
1010
import { Disposable } from 'vs/base/common/lifecycle';
1111
import { URI } from 'vs/base/common/uri';
1212
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
@@ -264,7 +264,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex
264264
// if any - to prevent backing up an unresolved working
265265
// copy and loosing the initial value.
266266
if (this.isResolved()) {
267-
content = await raceCancellation(this.model.snapshot(token), token);
267+
content = await raceCancellation(this.model.snapshot(SnapshotContext.Backup, token), token);
268268
} else if (this.initialContents) {
269269
content = this.initialContents.value;
270270
}

src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Promises, timeout } from 'vs/base/common/async';
1919
import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream';
2020
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
2121
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
22+
import { SnapshotContext } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
2223

2324
export class TestStoredFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel {
2425

@@ -45,7 +46,7 @@ export class TestStoredFileWorkingCopyModel extends Disposable implements IStore
4546
this.throwOnSnapshot = true;
4647
}
4748

48-
async snapshot(token: CancellationToken): Promise<VSBufferReadableStream> {
49+
async snapshot(context: SnapshotContext, token: CancellationToken): Promise<VSBufferReadableStream> {
4950
if (this.throwOnSnapshot) {
5051
throw new Error('Fail');
5152
}

0 commit comments

Comments
 (0)