mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
Merge pull request #96317 from microsoft/alex/undo-redo-max-memory
Introduce `files.maxMemoryForClosedFilesUndoStackMB`
This commit is contained in:
@@ -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<ITextModel> = this._register(new Emitter<ITextModel>());
|
||||
@@ -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(
|
||||
@@ -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()));
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getModels(): ITextModel[] {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
|
||||
@@ -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');
|
||||
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 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');
|
||||
});
|
||||
// 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');
|
||||
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 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);
|
||||
});
|
||||
}
|
||||
// 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');
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()],
|
||||
|
||||
Reference in New Issue
Block a user