mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
[Unhandled Error] [935a] potential listener LEAK detected, having 7391 listeners already. MOST frequent listener (7217... (fix #302016) (#302088)
* [Unhandled Error] [935a] potential listener LEAK detected, having 7391 listeners already. MOST frequent listener (7217... (fix #302016) * cover it up * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IDisposable, toDisposable, IReference, ReferenceCollection, Disposable, AsyncReferenceCollection } from '../../../../base/common/lifecycle.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { TextResourceEditorModel } from '../../../common/editor/textResourceEditorModel.js';
|
||||
@@ -23,7 +22,7 @@ import { UntitledTextEditorModel } from '../../untitled/common/untitledTextEdito
|
||||
class ResourceModelCollection extends ReferenceCollection<Promise<IResolvedTextEditorModel>> {
|
||||
|
||||
private readonly providers = new Map<string, ITextModelContentProvider[]>();
|
||||
private readonly modelsToDispose = new Set<string>();
|
||||
private readonly modelsToDispose = new Map<string, Promise<ITextEditorModel>>();
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@@ -39,24 +38,12 @@ class ResourceModelCollection extends ReferenceCollection<Promise<IResolvedTextE
|
||||
}
|
||||
|
||||
private async doCreateReferencedObject(key: string, skipActivateProvider?: boolean): Promise<IResolvedTextEditorModel> {
|
||||
const resource = URI.parse(key);
|
||||
|
||||
// Untrack as being disposed
|
||||
const pendingModel = this.modelsToDispose.get(key);
|
||||
this.modelsToDispose.delete(key);
|
||||
|
||||
// inMemory Schema: go through model service cache
|
||||
const resource = URI.parse(key);
|
||||
if (resource.scheme === Schemas.inMemory) {
|
||||
const cachedModel = this.modelService.getModel(resource);
|
||||
if (!cachedModel) {
|
||||
throw new Error(`Unable to resolve inMemory resource ${key}`);
|
||||
}
|
||||
|
||||
const model = this.instantiationService.createInstance(TextResourceEditorModel, resource);
|
||||
if (this.ensureResolvedModel(model, key)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// Untitled Schema: go through untitled text service
|
||||
if (resource.scheme === Schemas.untitled) {
|
||||
const model = await this.textFileService.untitled.resolve({ untitledResource: resource });
|
||||
@@ -73,11 +60,27 @@ class ResourceModelCollection extends ReferenceCollection<Promise<IResolvedTextE
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual documents
|
||||
if (this.providers.has(resource.scheme)) {
|
||||
await this.resolveTextModelContent(key);
|
||||
// In-Memory / Virtual documents
|
||||
if (resource.scheme === Schemas.inMemory || this.providers.has(resource.scheme)) {
|
||||
await this.ensureResolvedTextModelContent(resource); // throws if failing to resolve content
|
||||
|
||||
let model: ITextEditorModel | undefined = undefined;
|
||||
if (pendingModel) {
|
||||
try {
|
||||
// if we have a pending model for this key, we try to await that and prevent
|
||||
// creating a new model so that we are not leaking models. we only do this for
|
||||
// in-memory or virtual documents where we create models here, the others are
|
||||
// already shared by their respective services.
|
||||
model = await pendingModel;
|
||||
} catch {
|
||||
// ignore and re-create below
|
||||
}
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
model = this.instantiationService.createInstance(TextResourceEditorModel, resource);
|
||||
}
|
||||
|
||||
const model = this.instantiationService.createInstance(TextResourceEditorModel, resource);
|
||||
if (this.ensureResolvedModel(model, key)) {
|
||||
return model;
|
||||
}
|
||||
@@ -103,15 +106,9 @@ class ResourceModelCollection extends ReferenceCollection<Promise<IResolvedTextE
|
||||
|
||||
protected destroyReferencedObject(key: string, modelPromise: Promise<ITextEditorModel>): void {
|
||||
|
||||
// inMemory is bound to a different lifecycle
|
||||
const resource = URI.parse(key);
|
||||
if (resource.scheme === Schemas.inMemory) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track as being disposed before waiting for model to load
|
||||
// to handle the case that the reference is acquired again
|
||||
this.modelsToDispose.add(key);
|
||||
this.modelsToDispose.set(key, modelPromise);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -179,18 +176,25 @@ class ResourceModelCollection extends ReferenceCollection<Promise<IResolvedTextE
|
||||
return this.providers.get(scheme) !== undefined;
|
||||
}
|
||||
|
||||
private async resolveTextModelContent(key: string): Promise<ITextModel> {
|
||||
const resource = URI.parse(key);
|
||||
const providersForScheme = this.providers.get(resource.scheme) || [];
|
||||
private async ensureResolvedTextModelContent(resource: URI): Promise<void> {
|
||||
|
||||
for (const provider of providersForScheme) {
|
||||
const value = await provider.provideTextContent(resource);
|
||||
if (value) {
|
||||
return value;
|
||||
// in-memory based
|
||||
if (resource.scheme === Schemas.inMemory) {
|
||||
if (this.modelService.getModel(resource)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to resolve text model content for resource ${key}`);
|
||||
// provider based
|
||||
const providersForScheme = this.providers.get(resource.scheme) || [];
|
||||
|
||||
for (const provider of providersForScheme) {
|
||||
if (await provider.provideTextContent(resource)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to resolve text model content for resource ${resource.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { timeout } from '../../../../../base/common/async.js';
|
||||
import { UntitledTextEditorInput } from '../../../untitled/common/untitledTextEditorInput.js';
|
||||
import { createTextBufferFactory } from '../../../../../editor/common/model/textModel.js';
|
||||
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { Schemas } from '../../../../../base/common/network.js';
|
||||
|
||||
suite('Workbench - TextModelResolverService', () => {
|
||||
|
||||
@@ -206,5 +207,100 @@ suite('Workbench - TextModelResolverService', () => {
|
||||
assert(textModel.isDisposed(), 'the text model should finally be disposed');
|
||||
});
|
||||
|
||||
test('resolve inMemory', async () => {
|
||||
const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryDoc' });
|
||||
const languageSelection = accessor.languageService.createById('json');
|
||||
disposables.add(accessor.modelService.createModel('Hello InMemory', languageSelection, resource));
|
||||
|
||||
const ref = await accessor.textModelResolverService.createModelReference(resource);
|
||||
const model = ref.object;
|
||||
assert.ok(model);
|
||||
const textModel = model.textEditorModel;
|
||||
assert.ok(textModel);
|
||||
assert.strictEqual(textModel.getValue(), 'Hello InMemory');
|
||||
assert(!textModel.isDisposed(), 'the inMemory text model should not be disposed before releasing the reference');
|
||||
|
||||
const p = new Promise<void>(resolve => disposables.add(textModel.onWillDispose(resolve)));
|
||||
ref.dispose();
|
||||
|
||||
await p;
|
||||
assert(textModel.isDisposed(), 'the inMemory text model should be disposed after the reference is released');
|
||||
});
|
||||
|
||||
test('resolve inMemory throws when model not found', async () => {
|
||||
const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/nonExistent' });
|
||||
|
||||
await assert.rejects(
|
||||
() => accessor.textModelResolverService.createModelReference(resource),
|
||||
/Unable to resolve text model content for resource/
|
||||
);
|
||||
});
|
||||
|
||||
test('resolve inMemory disposes when last reference released', async () => {
|
||||
const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryDispose' });
|
||||
const languageSelection = accessor.languageService.createById('json');
|
||||
accessor.modelService.createModel('Hello InMemory', languageSelection, resource);
|
||||
|
||||
const ref = await accessor.textModelResolverService.createModelReference(resource);
|
||||
const textModel = ref.object.textEditorModel;
|
||||
assert.ok(textModel);
|
||||
assert(!textModel.isDisposed());
|
||||
|
||||
const p = new Promise<void>(resolve => disposables.add(textModel.onWillDispose(resolve)));
|
||||
ref.dispose();
|
||||
|
||||
await p;
|
||||
assert(textModel.isDisposed(), 'the inMemory text model should be disposed after last reference is released');
|
||||
});
|
||||
|
||||
test('resolve inMemory is refcounted', async () => {
|
||||
const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryRefcount' });
|
||||
const languageSelection = accessor.languageService.createById('json');
|
||||
accessor.modelService.createModel('Hello InMemory', languageSelection, resource);
|
||||
|
||||
const ref1 = await accessor.textModelResolverService.createModelReference(resource);
|
||||
const ref2 = await accessor.textModelResolverService.createModelReference(resource);
|
||||
const textModel = ref1.object.textEditorModel;
|
||||
|
||||
assert.strictEqual(ref1.object, ref2.object, 'they are the same model');
|
||||
assert(!textModel.isDisposed());
|
||||
|
||||
ref1.dispose();
|
||||
assert(!textModel.isDisposed(), 'should not dispose while ref2 is still alive');
|
||||
|
||||
const p = new Promise<void>(resolve => disposables.add(textModel.onWillDispose(resolve)));
|
||||
ref2.dispose();
|
||||
|
||||
await p;
|
||||
assert(textModel.isDisposed(), 'should dispose after last reference released');
|
||||
});
|
||||
|
||||
test('resolve inMemory reuses model when re-acquired during dispose', async () => {
|
||||
const resource = URI.from({ scheme: Schemas.inMemory, path: '/test/inMemoryReuse' });
|
||||
const languageSelection = accessor.languageService.createById('json');
|
||||
accessor.modelService.createModel('Hello Reuse', languageSelection, resource);
|
||||
|
||||
const ref1 = await accessor.textModelResolverService.createModelReference(resource);
|
||||
const model1 = ref1.object;
|
||||
|
||||
// Release last reference, starts async dispose
|
||||
ref1.dispose();
|
||||
|
||||
// Immediately re-acquire before the async dispose completes
|
||||
const ref2 = await accessor.textModelResolverService.createModelReference(resource);
|
||||
const model2 = ref2.object;
|
||||
|
||||
assert.ok(model2);
|
||||
const textModel = model2.textEditorModel;
|
||||
assert.strictEqual(textModel.getValue(), 'Hello Reuse');
|
||||
assert.strictEqual(model1, model2, 'should reuse the same model instance');
|
||||
|
||||
const p = new Promise<void>(resolve => disposables.add(textModel.onWillDispose(resolve)));
|
||||
ref2.dispose();
|
||||
|
||||
await p;
|
||||
assert(textModel.isDisposed());
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user