chore - add IInstantiationService#dispose (#209421)

This commit is contained in:
Johannes Rieken 2024-04-03 12:57:30 +02:00 committed by GitHub
parent e773c56fe7
commit 03ef5ffe3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 226 additions and 13 deletions

View file

@ -281,8 +281,8 @@ export interface IDisposable {
/**
* Check if `thing` is {@link IDisposable disposable}.
*/
export function isDisposable<E extends object>(thing: E): thing is E & IDisposable {
return typeof (<IDisposable>thing).dispose === 'function' && (<IDisposable>thing).dispose.length === 0;
export function isDisposable<E extends any>(thing: E): thing is E & IDisposable {
return typeof thing === 'object' && thing !== null && typeof (<IDisposable><any>thing).dispose === 'function' && (<IDisposable><any>thing).dispose.length === 0;
}
/**

View file

@ -63,6 +63,16 @@ export interface IInstantiationService {
* and adds/overwrites the given services.
*/
createChild(services: ServiceCollection): IInstantiationService;
/**
* Disposes this instantiation service.
*
* - Will dispose all services that this instantiation service has created.
* - Will dispose all its children but not its parent.
* - Will NOT dispose services-instances that this service has been created with
* - Will NOT dispose consumer-instances this service has created
*/
dispose(): void;
}

View file

@ -6,7 +6,7 @@
import { GlobalIdleValue } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { illegalState } from 'vs/base/common/errors';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { dispose, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { SyncDescriptor, SyncDescriptor0 } from 'vs/platform/instantiation/common/descriptors';
import { Graph } from 'vs/platform/instantiation/common/graph';
import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from 'vs/platform/instantiation/common/instantiation';
@ -32,6 +32,10 @@ export class InstantiationService implements IInstantiationService {
readonly _globalGraph?: Graph<string>;
private _globalGraphImplicitDependency?: string;
private _isDisposed = false;
private readonly _servicesToMaybeDispose = new Set<any>();
private readonly _children = new Set<InstantiationService>();
constructor(
private readonly _services: ServiceCollection = new ServiceCollection(),
private readonly _strict: boolean = false,
@ -43,11 +47,45 @@ export class InstantiationService implements IInstantiationService {
this._globalGraph = _enableTracing ? _parent?._globalGraph ?? new Graph(e => e) : undefined;
}
dispose(): void {
if (!this._isDisposed) {
this._isDisposed = true;
// dispose all child services
dispose(this._children);
this._children.clear();
// dispose all services created by this service
for (const candidate of this._servicesToMaybeDispose) {
if (isDisposable(candidate)) {
candidate.dispose();
}
}
this._servicesToMaybeDispose.clear();
}
}
private _throwIfDisposed(): void {
if (this._isDisposed) {
throw new Error('InstantiationService has been disposed');
}
}
createChild(services: ServiceCollection): IInstantiationService {
return new InstantiationService(services, this._strict, this, this._enableTracing);
this._throwIfDisposed();
const result = new class extends InstantiationService {
override dispose(): void {
this._children.delete(result);
super.dispose();
}
}(services, this._strict, this, this._enableTracing);
this._children.add(result);
return result;
}
invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
this._throwIfDisposed();
const _trace = Trace.traceInvocation(this._enableTracing, fn);
let _done = false;
try {
@ -75,6 +113,8 @@ export class InstantiationService implements IInstantiationService {
createInstance<T>(descriptor: SyncDescriptor0<T>): T;
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): any {
this._throwIfDisposed();
let _trace: Trace;
let result: any;
if (ctorOrDescriptor instanceof SyncDescriptor) {
@ -119,11 +159,11 @@ export class InstantiationService implements IInstantiationService {
return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));
}
private _setServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
private _setCreatedServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
if (this._services.get(id) instanceof SyncDescriptor) {
this._services.set(id, instance);
} else if (this._parent) {
this._parent._setServiceInstance(id, instance);
this._parent._setCreatedServiceInstance(id, instance);
} else {
throw new Error('illegalState - setting UNKNOWN service instance');
}
@ -221,7 +261,7 @@ export class InstantiationService implements IInstantiationService {
if (instanceOrDesc instanceof SyncDescriptor) {
// create instance and overwrite the service collections
const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);
this._setServiceInstance(data.id, instance);
this._setCreatedServiceInstance(data.id, instance);
}
graph.removeNode(data);
}
@ -231,7 +271,7 @@ export class InstantiationService implements IInstantiationService {
private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T {
if (this._services.get(id) instanceof SyncDescriptor) {
return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace);
return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation, _trace, this._servicesToMaybeDispose);
} else if (this._parent) {
return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace);
} else {
@ -239,10 +279,12 @@ export class InstantiationService implements IInstantiationService {
}
}
private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace): T {
private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean, _trace: Trace, disposeBucket: Set<any>): T {
if (!supportsDelayedInstantiation) {
// eager instantiation
return this._createInstance(ctor, args, _trace);
const result = this._createInstance<T>(ctor, args, _trace);
disposeBucket.add(result);
return result;
} else {
const child = new InstantiationService(undefined, this._strict, this, this._enableTracing);
@ -274,7 +316,7 @@ export class InstantiationService implements IInstantiationService {
}
}
earlyListeners.clear();
disposeBucket.add(result);
return result;
});
return <T>new Proxy(Object.create(null), {

View file

@ -655,5 +655,163 @@ suite('Instantiation Service', () => {
assert.strictEqual(eventCount, 1);
});
test('Dispose services it created', function () {
let disposedA = false;
let disposedB = false;
const A = createDecorator<A>('A');
interface A {
_serviceBrand: undefined;
value: 1;
}
class AImpl implements A {
_serviceBrand: undefined;
value: 1 = 1;
dispose() {
disposedA = true;
}
}
const B = createDecorator<B>('B');
interface B {
_serviceBrand: undefined;
value: 1;
}
class BImpl implements B {
_serviceBrand: undefined;
value: 1 = 1;
dispose() {
disposedB = true;
}
}
const insta = new InstantiationService(new ServiceCollection(
[A, new SyncDescriptor(AImpl, undefined, true)],
[B, new BImpl()],
), true, undefined, true);
class Consumer {
constructor(
@A public readonly a: A,
@B public readonly b: B
) {
assert.strictEqual(a.value, b.value);
}
}
const c: Consumer = insta.createInstance(Consumer);
insta.dispose();
assert.ok(c);
assert.strictEqual(disposedA, true);
assert.strictEqual(disposedB, false);
});
test('Disposed service cannot be used anymore', function () {
const B = createDecorator<B>('B');
interface B {
_serviceBrand: undefined;
value: 1;
}
class BImpl implements B {
_serviceBrand: undefined;
value: 1 = 1;
}
const insta = new InstantiationService(new ServiceCollection(
[B, new BImpl()],
), true, undefined, true);
class Consumer {
constructor(
@B public readonly b: B
) {
assert.strictEqual(b.value, 1);
}
}
const c: Consumer = insta.createInstance(Consumer);
assert.ok(c);
insta.dispose();
assert.throws(() => insta.createInstance(Consumer));
assert.throws(() => insta.invokeFunction(accessor => { }));
assert.throws(() => insta.createChild(new ServiceCollection()));
});
test('Child does not dispose parent', function () {
const B = createDecorator<B>('B');
interface B {
_serviceBrand: undefined;
value: 1;
}
class BImpl implements B {
_serviceBrand: undefined;
value: 1 = 1;
}
const insta1 = new InstantiationService(new ServiceCollection(
[B, new BImpl()],
), true, undefined, true);
const insta2 = insta1.createChild(new ServiceCollection());
class Consumer {
constructor(
@B public readonly b: B
) {
assert.strictEqual(b.value, 1);
}
}
assert.ok(insta1.createInstance(Consumer));
assert.ok(insta2.createInstance(Consumer));
insta2.dispose();
assert.ok(insta1.createInstance(Consumer)); // parent NOT disposed by child
assert.throws(() => insta2.createInstance(Consumer));
});
test('Parent does dispose children', function () {
const B = createDecorator<B>('B');
interface B {
_serviceBrand: undefined;
value: 1;
}
class BImpl implements B {
_serviceBrand: undefined;
value: 1 = 1;
}
const insta1 = new InstantiationService(new ServiceCollection(
[B, new BImpl()],
), true, undefined, true);
const insta2 = insta1.createChild(new ServiceCollection());
class Consumer {
constructor(
@B public readonly b: B
) {
assert.strictEqual(b.value, 1);
}
}
assert.ok(insta1.createInstance(Consumer));
assert.ok(insta2.createInstance(Consumer));
insta1.dispose();
assert.throws(() => insta2.createInstance(Consumer)); // child is disposed by parent
assert.throws(() => insta1.createInstance(Consumer));
});
ensureNoDisposablesAreLeakedInTestSuite();
});

View file

@ -21,7 +21,7 @@ export class TestInstantiationService extends InstantiationService implements ID
private _servciesMap: Map<ServiceIdentifier<any>, any>;
constructor(private _serviceCollection: ServiceCollection = new ServiceCollection(), strict: boolean = false, parent?: TestInstantiationService) {
constructor(private _serviceCollection: ServiceCollection = new ServiceCollection(), strict: boolean = false, parent?: TestInstantiationService, private _properDispose?: boolean) {
super(_serviceCollection, strict, parent);
this._servciesMap = new Map<ServiceIdentifier<any>, any>();
@ -130,8 +130,11 @@ export class TestInstantiationService extends InstantiationService implements ID
return new TestInstantiationService(services, false, this);
}
dispose() {
override dispose() {
sinon.restore();
if (this._properDispose) {
super.dispose();
}
}
}