Skip to content

Commit 62e3447

Browse files
committed
Introduce files.maxMemoryForClosedFilesUndoStackMB
1 parent 2e44999 commit 62e3447

8 files changed

Lines changed: 107 additions & 83 deletions

File tree

src/vs/editor/common/services/modelServiceImpl.ts

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as nls from 'vs/nls';
76
import { Emitter, Event } from 'vs/base/common/event';
87
import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
98
import * as platform from 'vs/base/common/platform';
@@ -28,13 +27,9 @@ import { ILogService } from 'vs/platform/log/common/log';
2827
import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
2928
import { StringSHA1 } from 'vs/base/common/hash';
3029
import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack';
31-
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
3230
import { Schemas } from 'vs/base/common/network';
33-
import Severity from 'vs/base/common/severity';
3431
import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling';
3532

36-
export const MAINTAIN_UNDO_REDO_STACK = true;
37-
3833
export interface IEditorSemanticHighlightingOptions {
3934
enabled?: boolean;
4035
}
@@ -143,6 +138,8 @@ function isEditStackElements(elements: IUndoRedoElement[]): elements is EditStac
143138
class DisposedModelInfo {
144139
constructor(
145140
public readonly uri: URI,
141+
public readonly time: number,
142+
public readonly heapSize: number,
146143
public readonly sha1: string,
147144
public readonly versionId: number,
148145
public readonly alternativeVersionId: number,
@@ -151,8 +148,6 @@ class DisposedModelInfo {
151148

152149
export class ModelServiceImpl extends Disposable implements IModelService {
153150

154-
private static _PROMPT_UNDO_REDO_SIZE_LIMIT = 10 * 1024 * 1024; // 10MB
155-
156151
public _serviceBrand: undefined;
157152

158153
private readonly _onModelAdded: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());
@@ -171,6 +166,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
171166
*/
172167
private readonly _models: { [modelId: string]: ModelData; };
173168
private readonly _disposedModels: Map<string, DisposedModelInfo>;
169+
private _disposedModelsHeapSize: number;
174170
private readonly _semanticStyling: SemanticStyling;
175171

176172
constructor(
@@ -179,12 +175,12 @@ export class ModelServiceImpl extends Disposable implements IModelService {
179175
@IThemeService private readonly _themeService: IThemeService,
180176
@ILogService private readonly _logService: ILogService,
181177
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
182-
@IDialogService private readonly _dialogService: IDialogService,
183178
) {
184179
super();
185180
this._modelCreationOptionsByLanguageAndResource = Object.create(null);
186181
this._models = {};
187182
this._disposedModels = new Map<string, DisposedModelInfo>();
183+
this._disposedModelsHeapSize = 0;
188184
this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._logService));
189185

190186
this._register(this._configurationService.onDidChangeConfiguration(() => this._updateModelOptions()));
@@ -267,6 +263,14 @@ export class ModelServiceImpl extends Disposable implements IModelService {
267263
return platform.OS === platform.OperatingSystem.Linux || platform.OS === platform.OperatingSystem.Macintosh ? '\n' : '\r\n';
268264
}
269265

266+
private _getMaxMemoryForClosedFilesUndoStack(): number {
267+
const result = this._configurationService.getValue<number>('files.maxMemoryForClosedFilesUndoStackMB');
268+
if (typeof result === 'number') {
269+
return result * 1024 * 1024;
270+
}
271+
return 20 * 1024 * 1024;
272+
}
273+
270274
public getCreationOptions(language: string, resource: URI | undefined, isForSimpleWidget: boolean): ITextModelCreationOptions {
271275
let creationOptions = this._modelCreationOptionsByLanguageAndResource[language + resource];
272276
if (!creationOptions) {
@@ -328,13 +332,40 @@ export class ModelServiceImpl extends Disposable implements IModelService {
328332

329333
// --- begin IModelService
330334

335+
private _insertDisposedModel(disposedModelData: DisposedModelInfo): void {
336+
this._disposedModels.set(MODEL_ID(disposedModelData.uri), disposedModelData);
337+
this._disposedModelsHeapSize += disposedModelData.heapSize;
338+
}
339+
340+
private _removeDisposedModel(resource: URI): DisposedModelInfo | undefined {
341+
const disposedModelData = this._disposedModels.get(MODEL_ID(resource));
342+
if (disposedModelData) {
343+
this._disposedModelsHeapSize -= disposedModelData.heapSize;
344+
}
345+
this._disposedModels.delete(MODEL_ID(resource));
346+
return disposedModelData;
347+
}
348+
349+
private _ensureDisposedModelsHeapSize(maxModelsHeapSize: number): void {
350+
if (this._disposedModelsHeapSize > maxModelsHeapSize) {
351+
// we must remove some old undo stack elements to free up some memory
352+
const disposedModels: DisposedModelInfo[] = [];
353+
this._disposedModels.forEach(entry => disposedModels.push(entry));
354+
disposedModels.sort((a, b) => a.time - b.time);
355+
while (disposedModels.length > 0 && this._disposedModelsHeapSize > maxModelsHeapSize) {
356+
const disposedModel = disposedModels.shift()!;
357+
this._removeDisposedModel(disposedModel.uri);
358+
this._undoRedoService.removeElements(disposedModel.uri);
359+
}
360+
}
361+
}
362+
331363
private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {
332364
// create & save the model
333365
const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget);
334366
const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService);
335367
if (resource && this._disposedModels.has(MODEL_ID(resource))) {
336-
const disposedModelData = this._disposedModels.get(MODEL_ID(resource))!;
337-
this._disposedModels.delete(MODEL_ID(resource));
368+
const disposedModelData = this._removeDisposedModel(resource)!;
338369
const elements = this._undoRedoService.getElements(resource);
339370
if (computeModelSha1(model) === disposedModelData.sha1 && isEditStackPastFutureElements(elements)) {
340371
for (const element of elements.past) {
@@ -473,7 +504,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
473504
const model = modelData.model;
474505
let maintainUndoRedoStack = false;
475506
let heapSize = 0;
476-
if (MAINTAIN_UNDO_REDO_STACK && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData)) {
507+
if (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData) {
477508
const elements = this._undoRedoService.getElements(resource);
478509
if ((elements.past.length > 0 || elements.future.length > 0) && isEditStackPastFutureElements(elements)) {
479510
maintainUndoRedoStack = true;
@@ -490,37 +521,27 @@ export class ModelServiceImpl extends Disposable implements IModelService {
490521
}
491522
}
492523

493-
if (maintainUndoRedoStack) {
494-
// We only invalidate the elements, but they remain in the undo-redo service.
495-
this._undoRedoService.setElementsIsValid(resource, false);
496-
this._disposedModels.set(MODEL_ID(resource), new DisposedModelInfo(resource, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId()));
497-
} else {
524+
if (!maintainUndoRedoStack) {
498525
this._undoRedoService.removeElements(resource);
526+
modelData.model.dispose();
527+
return;
499528
}
500529

501-
modelData.model.dispose();
502-
503-
// After disposing the model, prompt and ask if we should keep the undo-redo stack
504-
if (maintainUndoRedoStack && heapSize > ModelServiceImpl._PROMPT_UNDO_REDO_SIZE_LIMIT) {
505-
const mbSize = (heapSize / 1024 / 1024).toFixed(1);
506-
this._dialogService.show(
507-
Severity.Info,
508-
nls.localize('undoRedoConfirm', "Keep the undo-redo stack for {0} in memory ({1} MB)?", (resource.scheme === Schemas.file ? resource.fsPath : resource.path), mbSize),
509-
[
510-
nls.localize('nok', "Discard"),
511-
nls.localize('ok', "Keep"),
512-
],
513-
{
514-
cancelId: 2
515-
}
516-
).then((result) => {
517-
const discard = (result.choice === 2 || result.choice === 0);
518-
if (discard) {
519-
this._disposedModels.delete(MODEL_ID(resource));
520-
this._undoRedoService.removeElements(resource);
521-
}
522-
});
530+
const maxMemory = this._getMaxMemoryForClosedFilesUndoStack();
531+
if (heapSize > maxMemory) {
532+
// the undo stack for this file would never fit in the configured memory, so don't bother with it.
533+
this._undoRedoService.removeElements(resource);
534+
modelData.model.dispose();
535+
return;
523536
}
537+
538+
this._ensureDisposedModelsHeapSize(maxMemory - heapSize);
539+
540+
// We only invalidate the elements, but they remain in the undo-redo service.
541+
this._undoRedoService.setElementsIsValid(resource, false);
542+
this._insertDisposedModel(new DisposedModelInfo(resource, Date.now(), heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId()));
543+
544+
modelData.model.dispose();
524545
}
525546

526547
public getModels(): ITextModel[] {

src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ suite('SmartSelect', () => {
5151
setup(() => {
5252
const configurationService = new TestConfigurationService();
5353
const dialogService = new TestDialogService();
54-
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), dialogService);
54+
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()));
5555
mode = new MockJSMode();
5656
});
5757

src/vs/editor/standalone/browser/standaloneServices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export module StaticServices {
157157

158158
export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o)));
159159

160-
export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o), dialogService.get(o)));
160+
export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o)));
161161

162162
export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o)));
163163

src/vs/editor/test/common/services/modelService.test.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection';
1313
import { createStringBuilder } from 'vs/editor/common/core/stringBuilder';
1414
import { DefaultEndOfLine } from 'vs/editor/common/model';
1515
import { createTextBuffer } from 'vs/editor/common/model/textModel';
16-
import { ModelServiceImpl, MAINTAIN_UNDO_REDO_STACK } from 'vs/editor/common/services/modelServiceImpl';
16+
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
1717
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
1818
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1919
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
@@ -35,7 +35,7 @@ suite('ModelService', () => {
3535
configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot'));
3636

3737
const dialogService = new TestDialogService();
38-
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), dialogService);
38+
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()));
3939
});
4040

4141
teardown(() => {
@@ -310,44 +310,42 @@ suite('ModelService', () => {
310310
assertComputeEdits(file1, file2);
311311
});
312312

313-
if (MAINTAIN_UNDO_REDO_STACK) {
314-
test('maintains undo for same resource and same content', () => {
315-
const resource = URI.parse('file://test.txt');
316-
317-
// create a model
318-
const model1 = modelService.createModel('text', null, resource);
319-
// make an edit
320-
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
321-
assert.equal(model1.getValue(), 'text1');
322-
// dispose it
323-
modelService.destroyModel(resource);
324-
325-
// create a new model with the same content
326-
const model2 = modelService.createModel('text1', null, resource);
327-
// undo
328-
model2.undo();
329-
assert.equal(model2.getValue(), 'text');
330-
});
331-
332-
test('maintains version id and alternative version id for same resource and same content', () => {
333-
const resource = URI.parse('file://test.txt');
334-
335-
// create a model
336-
const model1 = modelService.createModel('text', null, resource);
337-
// make an edit
338-
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
339-
assert.equal(model1.getValue(), 'text1');
340-
const versionId = model1.getVersionId();
341-
const alternativeVersionId = model1.getAlternativeVersionId();
342-
// dispose it
343-
modelService.destroyModel(resource);
344-
345-
// create a new model with the same content
346-
const model2 = modelService.createModel('text1', null, resource);
347-
assert.equal(model2.getVersionId(), versionId);
348-
assert.equal(model2.getAlternativeVersionId(), alternativeVersionId);
349-
});
350-
}
313+
test('maintains undo for same resource and same content', () => {
314+
const resource = URI.parse('file://test.txt');
315+
316+
// create a model
317+
const model1 = modelService.createModel('text', null, resource);
318+
// make an edit
319+
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
320+
assert.equal(model1.getValue(), 'text1');
321+
// dispose it
322+
modelService.destroyModel(resource);
323+
324+
// create a new model with the same content
325+
const model2 = modelService.createModel('text1', null, resource);
326+
// undo
327+
model2.undo();
328+
assert.equal(model2.getValue(), 'text');
329+
});
330+
331+
test('maintains version id and alternative version id for same resource and same content', () => {
332+
const resource = URI.parse('file://test.txt');
333+
334+
// create a model
335+
const model1 = modelService.createModel('text', null, resource);
336+
// make an edit
337+
model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]);
338+
assert.equal(model1.getValue(), 'text1');
339+
const versionId = model1.getVersionId();
340+
const alternativeVersionId = model1.getAlternativeVersionId();
341+
// dispose it
342+
modelService.destroyModel(resource);
343+
344+
// create a new model with the same content
345+
const model2 = modelService.createModel('text1', null, resource);
346+
assert.equal(model2.getVersionId(), versionId);
347+
assert.equal(model2.getAlternativeVersionId(), alternativeVersionId);
348+
});
351349

352350
test('does not maintain undo for same resource and different content', () => {
353351
const resource = URI.parse('file://test.txt');

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ configurationRegistry.registerConfiguration({
320320
'markdownDescription': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying `--max-memory=NEWSIZE` on the command line."),
321321
included: platform.isNative
322322
},
323+
'files.maxMemoryForClosedFilesUndoStackMB': {
324+
'type': 'number',
325+
'default': 20,
326+
'markdownDescription': nls.localize('maxMemoryForClosedFilesUndoStackMB', "Controls the maximum ammount of memory the undo stack should hold for files that have been closed.")
327+
},
323328
'files.saveConflictResolution': {
324329
'type': 'string',
325330
'enum': [

src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ suite('MainThreadDocumentsAndEditors', () => {
5151
const dialogService = new TestDialogService();
5252
const notificationService = new TestNotificationService();
5353
const undoRedoService = new UndoRedoService(dialogService, notificationService);
54-
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService, dialogService);
54+
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService);
5555
codeEditorService = new TestCodeEditorService();
5656
textFileService = new class extends mock<ITextFileService>() {
5757
isDirty() { return false; }

src/vs/workbench/test/browser/api/mainThreadEditors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ suite('MainThreadEditors', () => {
7373
const dialogService = new TestDialogService();
7474
const notificationService = new TestNotificationService();
7575
const undoRedoService = new UndoRedoService(dialogService, notificationService);
76-
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService, dialogService);
76+
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService);
7777

7878

7979
const services = new ServiceCollection();

src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ suite.skip('TextSearch performance (integration)', () => {
7979
[IDialogService, dialogService],
8080
[INotificationService, notificationService],
8181
[IUndoRedoService, undoRedoService],
82-
[IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService, dialogService)],
82+
[IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService)],
8383
[IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))],
8484
[IEditorService, new TestEditorService()],
8585
[IEditorGroupsService, new TestEditorGroupsService()],

0 commit comments

Comments
 (0)