Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce files.maxMemoryForClosedFilesUndoStackMB #96317

Merged
merged 1 commit into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 59 additions & 38 deletions src/vs/editor/common/services/modelServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -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<ITextModel> = this._register(new Emitter<ITextModel>());
Expand All @@ -171,6 +166,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
*/
private readonly _models: { [modelId: string]: ModelData; };
private readonly _disposedModels: Map<string, DisposedModelInfo>;
private _disposedModelsHeapSize: number;
private readonly _semanticStyling: SemanticStyling;

constructor(
Expand All @@ -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<string, DisposedModelInfo>();
this._disposedModelsHeapSize = 0;
this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._logService));

this._register(this._configurationService.onDidChangeConfiguration(() => this._updateModelOptions()));
Expand Down Expand Up @@ -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<number>('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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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[] {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/standalone/browser/standaloneServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down
78 changes: 38 additions & 40 deletions src/vs/editor/test/common/services/modelService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/files/browser/files.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITextFileService>() {
isDirty() { return false; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
Expand Down