[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:
Benjamin Pasero
2026-03-16 15:25:56 +01:00
committed by GitHub
parent 576a66c339
commit a720ababad
2 changed files with 135 additions and 35 deletions

View File

@@ -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()}`);
}
}

View File

@@ -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();
});