From 62e3447324d4d97d9ba3fb497474d315cb505ae2 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 28 Apr 2020 02:20:14 +0200 Subject: [PATCH] Introduce `files.maxMemoryForClosedFilesUndoStackMB` --- .../common/services/modelServiceImpl.ts | 97 +++++++++++-------- .../smartSelect/test/smartSelect.test.ts | 2 +- .../standalone/browser/standaloneServices.ts | 2 +- .../test/common/services/modelService.test.ts | 78 ++++++++------- .../files/browser/files.contribution.ts | 5 + .../api/mainThreadDocumentsAndEditors.test.ts | 2 +- .../browser/api/mainThreadEditors.test.ts | 2 +- .../textsearch.perf.integrationTest.ts | 2 +- 8 files changed, 107 insertions(+), 83 deletions(-) diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 42d2754445e72..3e743520bcd13 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; @@ -28,13 +27,9 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; import { StringSHA1 } from 'vs/base/common/hash'; import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; -import Severity from 'vs/base/common/severity'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; -export const MAINTAIN_UNDO_REDO_STACK = true; - export interface IEditorSemanticHighlightingOptions { enabled?: boolean; } @@ -143,6 +138,8 @@ function isEditStackElements(elements: IUndoRedoElement[]): elements is EditStac class DisposedModelInfo { constructor( public readonly uri: URI, + public readonly time: number, + public readonly heapSize: number, public readonly sha1: string, public readonly versionId: number, public readonly alternativeVersionId: number, @@ -151,8 +148,6 @@ class DisposedModelInfo { export class ModelServiceImpl extends Disposable implements IModelService { - private static _PROMPT_UNDO_REDO_SIZE_LIMIT = 10 * 1024 * 1024; // 10MB - public _serviceBrand: undefined; private readonly _onModelAdded: Emitter = this._register(new Emitter()); @@ -171,6 +166,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { */ private readonly _models: { [modelId: string]: ModelData; }; private readonly _disposedModels: Map; + private _disposedModelsHeapSize: number; private readonly _semanticStyling: SemanticStyling; constructor( @@ -179,12 +175,12 @@ export class ModelServiceImpl extends Disposable implements IModelService { @IThemeService private readonly _themeService: IThemeService, @ILogService private readonly _logService: ILogService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, - @IDialogService private readonly _dialogService: IDialogService, ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); this._models = {}; this._disposedModels = new Map(); + this._disposedModelsHeapSize = 0; this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._logService)); this._register(this._configurationService.onDidChangeConfiguration(() => this._updateModelOptions())); @@ -267,6 +263,14 @@ export class ModelServiceImpl extends Disposable implements IModelService { return platform.OS === platform.OperatingSystem.Linux || platform.OS === platform.OperatingSystem.Macintosh ? '\n' : '\r\n'; } + private _getMaxMemoryForClosedFilesUndoStack(): number { + const result = this._configurationService.getValue('files.maxMemoryForClosedFilesUndoStackMB'); + if (typeof result === 'number') { + return result * 1024 * 1024; + } + return 20 * 1024 * 1024; + } + public getCreationOptions(language: string, resource: URI | undefined, isForSimpleWidget: boolean): ITextModelCreationOptions { let creationOptions = this._modelCreationOptionsByLanguageAndResource[language + resource]; if (!creationOptions) { @@ -328,13 +332,40 @@ export class ModelServiceImpl extends Disposable implements IModelService { // --- begin IModelService + private _insertDisposedModel(disposedModelData: DisposedModelInfo): void { + this._disposedModels.set(MODEL_ID(disposedModelData.uri), disposedModelData); + this._disposedModelsHeapSize += disposedModelData.heapSize; + } + + private _removeDisposedModel(resource: URI): DisposedModelInfo | undefined { + const disposedModelData = this._disposedModels.get(MODEL_ID(resource)); + if (disposedModelData) { + this._disposedModelsHeapSize -= disposedModelData.heapSize; + } + this._disposedModels.delete(MODEL_ID(resource)); + return disposedModelData; + } + + private _ensureDisposedModelsHeapSize(maxModelsHeapSize: number): void { + if (this._disposedModelsHeapSize > maxModelsHeapSize) { + // we must remove some old undo stack elements to free up some memory + const disposedModels: DisposedModelInfo[] = []; + this._disposedModels.forEach(entry => disposedModels.push(entry)); + disposedModels.sort((a, b) => a.time - b.time); + while (disposedModels.length > 0 && this._disposedModelsHeapSize > maxModelsHeapSize) { + const disposedModel = disposedModels.shift()!; + this._removeDisposedModel(disposedModel.uri); + this._undoRedoService.removeElements(disposedModel.uri); + } + } + } + private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData { // create & save the model const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget); const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService); if (resource && this._disposedModels.has(MODEL_ID(resource))) { - const disposedModelData = this._disposedModels.get(MODEL_ID(resource))!; - this._disposedModels.delete(MODEL_ID(resource)); + const disposedModelData = this._removeDisposedModel(resource)!; const elements = this._undoRedoService.getElements(resource); if (computeModelSha1(model) === disposedModelData.sha1 && isEditStackPastFutureElements(elements)) { for (const element of elements.past) { @@ -473,7 +504,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { const model = modelData.model; let maintainUndoRedoStack = false; let heapSize = 0; - if (MAINTAIN_UNDO_REDO_STACK && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData)) { + if (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData) { const elements = this._undoRedoService.getElements(resource); if ((elements.past.length > 0 || elements.future.length > 0) && isEditStackPastFutureElements(elements)) { maintainUndoRedoStack = true; @@ -490,37 +521,27 @@ export class ModelServiceImpl extends Disposable implements IModelService { } } - if (maintainUndoRedoStack) { - // We only invalidate the elements, but they remain in the undo-redo service. - this._undoRedoService.setElementsIsValid(resource, false); - this._disposedModels.set(MODEL_ID(resource), new DisposedModelInfo(resource, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); - } else { + if (!maintainUndoRedoStack) { this._undoRedoService.removeElements(resource); + modelData.model.dispose(); + return; } - modelData.model.dispose(); - - // After disposing the model, prompt and ask if we should keep the undo-redo stack - if (maintainUndoRedoStack && heapSize > ModelServiceImpl._PROMPT_UNDO_REDO_SIZE_LIMIT) { - const mbSize = (heapSize / 1024 / 1024).toFixed(1); - this._dialogService.show( - Severity.Info, - nls.localize('undoRedoConfirm', "Keep the undo-redo stack for {0} in memory ({1} MB)?", (resource.scheme === Schemas.file ? resource.fsPath : resource.path), mbSize), - [ - nls.localize('nok', "Discard"), - nls.localize('ok', "Keep"), - ], - { - cancelId: 2 - } - ).then((result) => { - const discard = (result.choice === 2 || result.choice === 0); - if (discard) { - this._disposedModels.delete(MODEL_ID(resource)); - this._undoRedoService.removeElements(resource); - } - }); + const maxMemory = this._getMaxMemoryForClosedFilesUndoStack(); + if (heapSize > maxMemory) { + // the undo stack for this file would never fit in the configured memory, so don't bother with it. + this._undoRedoService.removeElements(resource); + modelData.model.dispose(); + return; } + + this._ensureDisposedModelsHeapSize(maxMemory - heapSize); + + // We only invalidate the elements, but they remain in the undo-redo service. + this._undoRedoService.setElementsIsValid(resource, false); + this._insertDisposedModel(new DisposedModelInfo(resource, Date.now(), heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); + + modelData.model.dispose(); } public getModels(): ITextModel[] { diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 4c17969b9cdac..5ca1c5b592724 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -51,7 +51,7 @@ suite('SmartSelect', () => { setup(() => { const configurationService = new TestConfigurationService(); const dialogService = new TestDialogService(); - modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), dialogService); + modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService())); mode = new MockJSMode(); }); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 9864cf06a1405..b8b42dd9a6ce3 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -157,7 +157,7 @@ export module StaticServices { export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o))); - 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))); + export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o))); export const markerDecorationsService = define(IMarkerDecorationsService, (o) => new MarkerDecorationsService(modelService.get(o), markerService.get(o))); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index f9a53739bdb3b..6aef4d186a52a 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -13,7 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ModelServiceImpl, MAINTAIN_UNDO_REDO_STACK } from 'vs/editor/common/services/modelServiceImpl'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -35,7 +35,7 @@ suite('ModelService', () => { configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot')); const dialogService = new TestDialogService(); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService()), dialogService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(dialogService, new TestNotificationService())); }); teardown(() => { @@ -310,44 +310,42 @@ suite('ModelService', () => { assertComputeEdits(file1, file2); }); - if (MAINTAIN_UNDO_REDO_STACK) { - test('maintains undo for same resource and same content', () => { - const resource = URI.parse('file://test.txt'); - - // create a model - const model1 = modelService.createModel('text', null, resource); - // make an edit - model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); - // dispose it - modelService.destroyModel(resource); - - // create a new model with the same content - const model2 = modelService.createModel('text1', null, resource); - // undo - model2.undo(); - assert.equal(model2.getValue(), 'text'); - }); - - test('maintains version id and alternative version id for same resource and same content', () => { - const resource = URI.parse('file://test.txt'); - - // create a model - const model1 = modelService.createModel('text', null, resource); - // make an edit - model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); - const versionId = model1.getVersionId(); - const alternativeVersionId = model1.getAlternativeVersionId(); - // dispose it - modelService.destroyModel(resource); - - // create a new model with the same content - const model2 = modelService.createModel('text1', null, resource); - assert.equal(model2.getVersionId(), versionId); - assert.equal(model2.getAlternativeVersionId(), alternativeVersionId); - }); - } + test('maintains undo for same resource and same content', () => { + const resource = URI.parse('file://test.txt'); + + // create a model + const model1 = modelService.createModel('text', null, resource); + // make an edit + model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); + assert.equal(model1.getValue(), 'text1'); + // dispose it + modelService.destroyModel(resource); + + // create a new model with the same content + const model2 = modelService.createModel('text1', null, resource); + // undo + model2.undo(); + assert.equal(model2.getValue(), 'text'); + }); + + test('maintains version id and alternative version id for same resource and same content', () => { + const resource = URI.parse('file://test.txt'); + + // create a model + const model1 = modelService.createModel('text', null, resource); + // make an edit + model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); + assert.equal(model1.getValue(), 'text1'); + const versionId = model1.getVersionId(); + const alternativeVersionId = model1.getAlternativeVersionId(); + // dispose it + modelService.destroyModel(resource); + + // create a new model with the same content + const model2 = modelService.createModel('text1', null, resource); + assert.equal(model2.getVersionId(), versionId); + assert.equal(model2.getAlternativeVersionId(), alternativeVersionId); + }); test('does not maintain undo for same resource and different content', () => { const resource = URI.parse('file://test.txt'); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 605a43c8ba506..b26c4ecbaabd8 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -320,6 +320,11 @@ configurationRegistry.registerConfiguration({ '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."), included: platform.isNative }, + 'files.maxMemoryForClosedFilesUndoStackMB': { + 'type': 'number', + 'default': 20, + 'markdownDescription': nls.localize('maxMemoryForClosedFilesUndoStackMB', "Controls the maximum ammount of memory the undo stack should hold for files that have been closed.") + }, 'files.saveConflictResolution': { 'type': 'string', 'enum': [ diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 46e4713ca0853..09a1e6a56a9d7 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -51,7 +51,7 @@ suite('MainThreadDocumentsAndEditors', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService, dialogService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); codeEditorService = new TestCodeEditorService(); textFileService = new class extends mock() { isDirty() { return false; } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index 7e8645f2c40bc..2dc9e4ca630f9 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -73,7 +73,7 @@ suite('MainThreadEditors', () => { const dialogService = new TestDialogService(); const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService, dialogService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); const services = new ServiceCollection(); diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index bcf4ec6475fd3..a96d990cc89ab 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -79,7 +79,7 @@ suite.skip('TextSearch performance (integration)', () => { [IDialogService, dialogService], [INotificationService, notificationService], [IUndoRedoService, undoRedoService], - [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService, dialogService)], + [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService)], [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], [IEditorService, new TestEditorService()], [IEditorGroupsService, new TestEditorGroupsService()],