mono/packages/core/dist/lifecycle.js
2025-01-28 13:42:22 +01:00

658 lines
20 KiB
JavaScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareBy, numberComparator } from './arrays.js';
import { groupBy } from './collections.js';
import { SetMap } from './map.js';
import { createSingleCallFunction } from './functional.js';
import { Iterable } from './iterator.js';
// #region Disposable Tracking
/**
* Enables logging of potentially leaked disposables.
*
* A disposable is considered leaked if it is not disposed or not registered as the child of
* another disposable. This tracking is very simple an only works for classes that either
* extend Disposable or use a DisposableStore. This means there are a lot of false positives.
*/
const TRACK_DISPOSABLES = false;
let disposableTracker = null;
export class GCBasedDisposableTracker {
_registry = new FinalizationRegistry(heldValue => {
console.warn(`[LEAKED DISPOSABLE] ${heldValue}`);
});
trackDisposable(disposable) {
const stack = new Error('CREATED via:').stack;
this._registry.register(disposable, stack, disposable);
}
setParent(child, parent) {
if (parent) {
this._registry.unregister(child);
}
else {
this.trackDisposable(child);
}
}
markAsDisposed(disposable) {
this._registry.unregister(disposable);
}
markAsSingleton(disposable) {
this._registry.unregister(disposable);
}
}
export class DisposableTracker {
static idx = 0;
livingDisposables = new Map();
getDisposableData(d) {
let val = this.livingDisposables.get(d);
if (!val) {
val = { parent: null, source: null, isSingleton: false, value: d, idx: DisposableTracker.idx++ };
this.livingDisposables.set(d, val);
}
return val;
}
trackDisposable(d) {
const data = this.getDisposableData(d);
if (!data.source) {
data.source =
new Error().stack;
}
}
setParent(child, parent) {
const data = this.getDisposableData(child);
data.parent = parent;
}
markAsDisposed(x) {
this.livingDisposables.delete(x);
}
markAsSingleton(disposable) {
this.getDisposableData(disposable).isSingleton = true;
}
getRootParent(data, cache) {
const cacheValue = cache.get(data);
if (cacheValue) {
return cacheValue;
}
const result = data.parent ? this.getRootParent(this.getDisposableData(data.parent), cache) : data;
cache.set(data, result);
return result;
}
getTrackedDisposables() {
const rootParentCache = new Map();
const leaking = [...this.livingDisposables.entries()]
.filter(([, v]) => v.source !== null && !this.getRootParent(v, rootParentCache).isSingleton)
.flatMap(([k]) => k);
return leaking;
}
computeLeakingDisposables(maxReported = 10, preComputedLeaks) {
let uncoveredLeakingObjs;
if (preComputedLeaks) {
uncoveredLeakingObjs = preComputedLeaks;
}
else {
const rootParentCache = new Map();
const leakingObjects = [...this.livingDisposables.values()]
.filter((info) => info.source !== null && !this.getRootParent(info, rootParentCache).isSingleton);
if (leakingObjects.length === 0) {
return;
}
const leakingObjsSet = new Set(leakingObjects.map(o => o.value));
// Remove all objects that are a child of other leaking objects. Assumes there are no cycles.
uncoveredLeakingObjs = leakingObjects.filter(l => {
return !(l.parent && leakingObjsSet.has(l.parent));
});
if (uncoveredLeakingObjs.length === 0) {
throw new Error('There are cyclic diposable chains!');
}
}
if (!uncoveredLeakingObjs) {
return undefined;
}
function getStackTracePath(leaking) {
function removePrefix(array, linesToRemove) {
while (array.length > 0 && linesToRemove.some(regexp => typeof regexp === 'string' ? regexp === array[0] : array[0].match(regexp))) {
array.shift();
}
}
const lines = leaking.source.split('\n').map(p => p.trim().replace('at ', '')).filter(l => l !== '');
removePrefix(lines, ['Error', /^trackDisposable \(.*\)$/, /^DisposableTracker.trackDisposable \(.*\)$/]);
return lines.reverse();
}
const stackTraceStarts = new SetMap();
for (const leaking of uncoveredLeakingObjs) {
const stackTracePath = getStackTracePath(leaking);
for (let i = 0; i <= stackTracePath.length; i++) {
stackTraceStarts.add(stackTracePath.slice(0, i).join('\n'), leaking);
}
}
// Put earlier leaks first
uncoveredLeakingObjs.sort(compareBy(l => l.idx, numberComparator));
let message = '';
let i = 0;
for (const leaking of uncoveredLeakingObjs.slice(0, maxReported)) {
i++;
const stackTracePath = getStackTracePath(leaking);
const stackTraceFormattedLines = [];
for (let i = 0; i < stackTracePath.length; i++) {
let line = stackTracePath[i];
const starts = stackTraceStarts.get(stackTracePath.slice(0, i + 1).join('\n'));
line = `(shared with ${starts.size}/${uncoveredLeakingObjs.length} leaks) at ${line}`;
const prevStarts = stackTraceStarts.get(stackTracePath.slice(0, i).join('\n'));
const continuations = groupBy([...prevStarts].map(d => getStackTracePath(d)[i]), v => v);
delete continuations[stackTracePath[i]];
for (const [cont, set] of Object.entries(continuations)) {
stackTraceFormattedLines.unshift(` - stacktraces of ${set.length} other leaks continue with ${cont}`);
}
stackTraceFormattedLines.unshift(line);
}
message += `\n\n\n==================== Leaking disposable ${i}/${uncoveredLeakingObjs.length}: ${leaking.value.constructor.name} ====================\n${stackTraceFormattedLines.join('\n')}\n============================================================\n\n`;
}
if (uncoveredLeakingObjs.length > maxReported) {
message += `\n\n\n... and ${uncoveredLeakingObjs.length - maxReported} more leaking disposables\n\n`;
}
return { leaks: uncoveredLeakingObjs, details: message };
}
}
export function setDisposableTracker(tracker) {
disposableTracker = tracker;
}
if (TRACK_DISPOSABLES) {
const __is_disposable_tracked__ = '__is_disposable_tracked__';
setDisposableTracker(new class {
trackDisposable(x) {
const stack = new Error('Potentially leaked disposable').stack;
setTimeout(() => {
if (!x[__is_disposable_tracked__]) {
console.log(stack);
}
}, 3000);
}
setParent(child, parent) {
if (child && child !== Disposable.None) {
try {
child[__is_disposable_tracked__] = true;
}
catch {
// noop
}
}
}
markAsDisposed(disposable) {
if (disposable && disposable !== Disposable.None) {
try {
disposable[__is_disposable_tracked__] = true;
}
catch {
// noop
}
}
}
markAsSingleton(disposable) { }
});
}
export function trackDisposable(x) {
disposableTracker?.trackDisposable(x);
return x;
}
export function markAsDisposed(disposable) {
disposableTracker?.markAsDisposed(disposable);
}
function setParentOfDisposable(child, parent) {
disposableTracker?.setParent(child, parent);
}
function setParentOfDisposables(children, parent) {
if (!disposableTracker) {
return;
}
for (const child of children) {
disposableTracker.setParent(child, parent);
}
}
/**
* Indicates that the given object is a singleton which does not need to be disposed.
*/
export function markAsSingleton(singleton) {
disposableTracker?.markAsSingleton(singleton);
return singleton;
}
/**
* Check if `thing` is {@link IDisposable disposable}.
*/
export function isDisposable(thing) {
return typeof thing === 'object' && thing !== null && typeof thing.dispose === 'function' && thing.dispose.length === 0;
}
export function dispose(arg) {
if (Iterable.is(arg)) {
const errors = [];
for (const d of arg) {
if (d) {
try {
d.dispose();
}
catch (e) {
errors.push(e);
}
}
}
if (errors.length === 1) {
throw errors[0];
}
else if (errors.length > 1) {
throw new AggregateError(errors, 'Encountered errors while disposing of store');
}
return Array.isArray(arg) ? [] : arg;
}
else if (arg) {
arg.dispose();
return arg;
}
}
export function disposeIfDisposable(disposables) {
for (const d of disposables) {
if (isDisposable(d)) {
d.dispose();
}
}
return [];
}
/**
* Combine multiple disposable values into a single {@link IDisposable}.
*/
export function combinedDisposable(...disposables) {
const parent = toDisposable(() => dispose(disposables));
setParentOfDisposables(disposables, parent);
return parent;
}
/**
* Turn a function that implements dispose into an {@link IDisposable}.
*
* @param fn Clean up function, guaranteed to be called only **once**.
*/
export function toDisposable(fn) {
const self = trackDisposable({
dispose: createSingleCallFunction(() => {
markAsDisposed(self);
fn();
})
});
return self;
}
/**
* Manages a collection of disposable values.
*
* This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an
* `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a
* store that has already been disposed of.
*/
export class DisposableStore {
static DISABLE_DISPOSED_WARNING = false;
_toDispose = new Set();
_isDisposed = false;
constructor() {
trackDisposable(this);
}
/**
* Dispose of all registered disposables and mark this object as disposed.
*
* Any future disposables added to this object will be disposed of on `add`.
*/
dispose() {
if (this._isDisposed) {
return;
}
markAsDisposed(this);
this._isDisposed = true;
this.clear();
}
/**
* @return `true` if this object has been disposed of.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Dispose of all registered disposables but do not mark this object as disposed.
*/
clear() {
if (this._toDispose.size === 0) {
return;
}
try {
dispose(this._toDispose);
}
finally {
this._toDispose.clear();
}
}
/**
* Add a new {@link IDisposable disposable} to the collection.
*/
add(o) {
if (!o) {
return o;
}
if (o === this) {
throw new Error('Cannot register a disposable on itself!');
}
setParentOfDisposable(o, this);
if (this._isDisposed) {
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
}
}
else {
this._toDispose.add(o);
}
return o;
}
/**
* Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the
* disposable even when the disposable is not part in the store.
*/
delete(o) {
if (!o) {
return;
}
if (o === this) {
throw new Error('Cannot dispose a disposable on itself!');
}
this._toDispose.delete(o);
o.dispose();
}
/**
* Deletes the value from the store, but does not dispose it.
*/
deleteAndLeak(o) {
if (!o) {
return;
}
if (this._toDispose.has(o)) {
this._toDispose.delete(o);
setParentOfDisposable(o, null);
}
}
}
/**
* Abstract base class for a {@link IDisposable disposable} object.
*
* Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of.
*/
export class Disposable {
/**
* A disposable that does nothing when it is disposed of.
*
* TODO: This should not be a static property.
*/
static None = Object.freeze({ dispose() { } });
_store = new DisposableStore();
constructor() {
trackDisposable(this);
setParentOfDisposable(this._store, this);
}
dispose() {
markAsDisposed(this);
this._store.dispose();
}
/**
* Adds `o` to the collection of disposables managed by this object.
*/
_register(o) {
if (o === this) {
throw new Error('Cannot register a disposable on itself!');
}
return this._store.add(o);
}
}
/**
* Manages the lifecycle of a disposable value that may be changed.
*
* This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can
* also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up.
*/
export class MutableDisposable {
_value;
_isDisposed = false;
constructor() {
trackDisposable(this);
}
get value() {
return this._isDisposed ? undefined : this._value;
}
set value(value) {
if (this._isDisposed || value === this._value) {
return;
}
this._value?.dispose();
if (value) {
setParentOfDisposable(value, this);
}
this._value = value;
}
/**
* Resets the stored value and disposed of the previously stored value.
*/
clear() {
this.value = undefined;
}
dispose() {
this._isDisposed = true;
markAsDisposed(this);
this._value?.dispose();
this._value = undefined;
}
/**
* Clears the value, but does not dispose it.
* The old value is returned.
*/
clearAndLeak() {
const oldValue = this._value;
this._value = undefined;
if (oldValue) {
setParentOfDisposable(oldValue, null);
}
return oldValue;
}
}
/**
* Manages the lifecycle of a disposable value that may be changed like {@link MutableDisposable}, but the value must
* exist and cannot be undefined.
*/
export class MandatoryMutableDisposable {
_disposable = new MutableDisposable();
_isDisposed = false;
constructor(initialValue) {
this._disposable.value = initialValue;
}
get value() {
return this._disposable.value;
}
set value(value) {
if (this._isDisposed || value === this._disposable.value) {
return;
}
this._disposable.value = value;
}
dispose() {
this._isDisposed = true;
this._disposable.dispose();
}
}
export class RefCountedDisposable {
_disposable;
_counter = 1;
constructor(_disposable) {
this._disposable = _disposable;
}
acquire() {
this._counter++;
return this;
}
release() {
if (--this._counter === 0) {
this._disposable.dispose();
}
return this;
}
}
/**
* A safe disposable can be `unset` so that a leaked reference (listener)
* can be cut-off.
*/
export class SafeDisposable {
dispose = () => { };
unset = () => { };
isset = () => false;
constructor() {
trackDisposable(this);
}
set(fn) {
let callback = fn;
this.unset = () => callback = undefined;
this.isset = () => callback !== undefined;
this.dispose = () => {
if (callback) {
callback();
callback = undefined;
markAsDisposed(this);
}
};
return this;
}
}
export class ReferenceCollection {
references = new Map();
acquire(key, ...args) {
let reference = this.references.get(key);
if (!reference) {
reference = { counter: 0, object: this.createReferencedObject(key, ...args) };
this.references.set(key, reference);
}
const { object } = reference;
const dispose = createSingleCallFunction(() => {
if (--reference.counter === 0) {
this.destroyReferencedObject(key, reference.object);
this.references.delete(key);
}
});
reference.counter++;
return { object, dispose };
}
}
/**
* Unwraps a reference collection of promised values. Makes sure
* references are disposed whenever promises get rejected.
*/
export class AsyncReferenceCollection {
referenceCollection;
constructor(referenceCollection) {
this.referenceCollection = referenceCollection;
}
async acquire(key, ...args) {
const ref = this.referenceCollection.acquire(key, ...args);
try {
const object = await ref.object;
return {
object,
dispose: () => ref.dispose()
};
}
catch (error) {
ref.dispose();
throw error;
}
}
}
export class ImmortalReference {
object;
constructor(object) {
this.object = object;
}
dispose() { }
}
export function disposeOnReturn(fn) {
const store = new DisposableStore();
try {
fn(store);
}
finally {
store.dispose();
}
}
/**
* A map the manages the lifecycle of the values that it stores.
*/
export class DisposableMap {
_store = new Map();
_isDisposed = false;
constructor() {
trackDisposable(this);
}
/**
* Disposes of all stored values and mark this object as disposed.
*
* Trying to use this object after it has been disposed of is an error.
*/
dispose() {
markAsDisposed(this);
this._isDisposed = true;
this.clearAndDisposeAll();
}
/**
* Disposes of all stored values and clear the map, but DO NOT mark this object as disposed.
*/
clearAndDisposeAll() {
if (!this._store.size) {
return;
}
try {
dispose(this._store.values());
}
finally {
this._store.clear();
}
}
has(key) {
return this._store.has(key);
}
get size() {
return this._store.size;
}
get(key) {
return this._store.get(key);
}
set(key, value, skipDisposeOnOverwrite = false) {
if (this._isDisposed) {
console.warn(new Error('Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!').stack);
}
if (!skipDisposeOnOverwrite) {
this._store.get(key)?.dispose();
}
this._store.set(key, value);
}
/**
* Delete the value stored for `key` from this map and also dispose of it.
*/
deleteAndDispose(key) {
this._store.get(key)?.dispose();
this._store.delete(key);
}
/**
* Delete the value stored for `key` from this map but return it. The caller is
* responsible for disposing of the value.
*/
deleteAndLeak(key) {
const value = this._store.get(key);
this._store.delete(key);
return value;
}
keys() {
return this._store.keys();
}
values() {
return this._store.values();
}
[Symbol.iterator]() {
return this._store[Symbol.iterator]();
}
}
//# sourceMappingURL=lifecycle.js.map