mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-15 16:34:50 +01:00
debt - allow to track disposables in running app (#192871)
* debt - allow to track disposables in running app * add snapshot support * add precondition * update * input
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { compareBy, numberComparator } from 'vs/base/common/arrays';
|
||||
import { SetMap, groupBy } from 'vs/base/common/collections';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
|
||||
@@ -41,6 +43,157 @@ export interface IDisposableTracker {
|
||||
markAsSingleton(disposable: IDisposable): void;
|
||||
}
|
||||
|
||||
export interface DisposableInfo {
|
||||
value: IDisposable;
|
||||
source: string | null;
|
||||
parent: IDisposable | null;
|
||||
isSingleton: boolean;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export class DisposableTracker implements IDisposableTracker {
|
||||
private static idx = 0;
|
||||
|
||||
private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
|
||||
|
||||
private getDisposableData(d: IDisposable): DisposableInfo {
|
||||
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: IDisposable): void {
|
||||
const data = this.getDisposableData(d);
|
||||
if (!data.source) {
|
||||
data.source =
|
||||
new Error().stack!;
|
||||
}
|
||||
}
|
||||
|
||||
setParent(child: IDisposable, parent: IDisposable | null): void {
|
||||
const data = this.getDisposableData(child);
|
||||
data.parent = parent;
|
||||
}
|
||||
|
||||
markAsDisposed(x: IDisposable): void {
|
||||
this.livingDisposables.delete(x);
|
||||
}
|
||||
|
||||
markAsSingleton(disposable: IDisposable): void {
|
||||
this.getDisposableData(disposable).isSingleton = true;
|
||||
}
|
||||
|
||||
private getRootParent(data: DisposableInfo, cache: Map<DisposableInfo, DisposableInfo>): DisposableInfo {
|
||||
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(): IDisposable[] {
|
||||
const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
|
||||
|
||||
const leaking = [...this.livingDisposables.entries()]
|
||||
.filter(([, v]) => v.source !== null && !this.getRootParent(v, rootParentCache).isSingleton)
|
||||
.map(([k]) => k)
|
||||
.flat();
|
||||
|
||||
return leaking;
|
||||
}
|
||||
|
||||
computeLeakingDisposables(maxReported = 10, preComputedLeaks?: DisposableInfo[]): { leaks: DisposableInfo[]; details: string } | undefined {
|
||||
let uncoveredLeakingObjs: DisposableInfo[] | undefined;
|
||||
if (preComputedLeaks) {
|
||||
uncoveredLeakingObjs = preComputedLeaks;
|
||||
} else {
|
||||
const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
|
||||
|
||||
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: DisposableInfo): string[] {
|
||||
function removePrefix(array: string[], linesToRemove: (string | RegExp)[]) {
|
||||
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<string, DisposableInfo>();
|
||||
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: IDisposableTracker | null): void {
|
||||
disposableTracker = tracker;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import { DeferredPromise, timeout } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
||||
import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event';
|
||||
import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, toDisposable, DisposableTracker } from 'vs/base/common/lifecycle';
|
||||
import { observableValue, transaction } from 'vs/base/common/observable';
|
||||
import { MicrotaskDelay } from 'vs/base/common/symbols';
|
||||
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
|
||||
import { DisposableTracker, ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
|
||||
namespace Samples {
|
||||
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { compareBy, numberComparator } from 'vs/base/common/arrays';
|
||||
import { SetMap, groupBy } from 'vs/base/common/collections';
|
||||
import { DisposableStore, IDisposable, IDisposableTracker, setDisposableTracker } from 'vs/base/common/lifecycle';
|
||||
import { DisposableStore, DisposableTracker, IDisposable, setDisposableTracker } from 'vs/base/common/lifecycle';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { trim } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export type ValueCallback<T = any> = (value: T | Promise<T>) => void;
|
||||
@@ -44,154 +41,6 @@ export async function assertThrowsAsync(block: () => any, message: string | Erro
|
||||
throw err;
|
||||
}
|
||||
|
||||
interface DisposableInfo {
|
||||
value: IDisposable;
|
||||
source: string | null;
|
||||
parent: IDisposable | null;
|
||||
isSingleton: boolean;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export class DisposableTracker implements IDisposableTracker {
|
||||
private static idx = 0;
|
||||
|
||||
private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
|
||||
|
||||
private getDisposableData(d: IDisposable) {
|
||||
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: IDisposable): void {
|
||||
const data = this.getDisposableData(d);
|
||||
if (!data.source) {
|
||||
data.source =
|
||||
new Error().stack!;
|
||||
}
|
||||
}
|
||||
|
||||
setParent(child: IDisposable, parent: IDisposable | null): void {
|
||||
const data = this.getDisposableData(child);
|
||||
data.parent = parent;
|
||||
}
|
||||
|
||||
markAsDisposed(x: IDisposable): void {
|
||||
this.livingDisposables.delete(x);
|
||||
}
|
||||
|
||||
markAsSingleton(disposable: IDisposable): void {
|
||||
this.getDisposableData(disposable).isSingleton = true;
|
||||
}
|
||||
|
||||
private getRootParent(data: DisposableInfo, cache: Map<DisposableInfo, DisposableInfo>): DisposableInfo {
|
||||
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<DisposableInfo, DisposableInfo>();
|
||||
|
||||
const leaking = [...this.livingDisposables.entries()]
|
||||
.filter(([, v]) => v.source !== null && !this.getRootParent(v, rootParentCache).isSingleton)
|
||||
.map(([k]) => k)
|
||||
.flat();
|
||||
|
||||
return leaking;
|
||||
}
|
||||
|
||||
ensureNoLeakingDisposables(logToConsole = true) {
|
||||
const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
|
||||
|
||||
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.
|
||||
const uncoveredLeakingObjs = leakingObjects.filter(l => {
|
||||
return !(l.parent && leakingObjsSet.has(l.parent));
|
||||
});
|
||||
|
||||
if (uncoveredLeakingObjs.length === 0) {
|
||||
throw new Error('There are cyclic diposable chains!');
|
||||
}
|
||||
|
||||
function getStackTracePath(leaking: DisposableInfo): string[] {
|
||||
function removePrefix(array: string[], linesToRemove: (string | RegExp)[]) {
|
||||
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 => trim(p.trim(), 'at ')).filter(l => l !== '');
|
||||
removePrefix(lines, ['Error', /^trackDisposable \(.*\)$/, /^DisposableTracker.trackDisposable \(.*\)$/]);
|
||||
return lines.reverse();
|
||||
}
|
||||
|
||||
const stackTraceStarts = new SetMap<string, DisposableInfo>();
|
||||
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));
|
||||
|
||||
const maxReported = 10;
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
if (logToConsole) {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
throw new Error(`There are ${uncoveredLeakingObjs.length} undisposed disposables!${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function to ensure that all disposables are cleaned up at the end of each test in the current suite.
|
||||
*
|
||||
@@ -214,7 +63,11 @@ export function ensureNoDisposablesAreLeakedInTestSuite(): Pick<DisposableStore,
|
||||
store.dispose();
|
||||
setDisposableTracker(null);
|
||||
if (this.currentTest?.state !== 'failed') {
|
||||
tracker!.ensureNoLeakingDisposables();
|
||||
const result = tracker!.computeLeakingDisposables();
|
||||
if (result) {
|
||||
console.error(result.details);
|
||||
throw new Error(`There are ${result.leaks.length} undisposed disposables!${result.details}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -232,7 +85,7 @@ export function throwIfDisposablesAreLeaked(body: () => void, logToConsole = tru
|
||||
setDisposableTracker(tracker);
|
||||
body();
|
||||
setDisposableTracker(null);
|
||||
tracker.ensureNoLeakingDisposables(logToConsole);
|
||||
computeLeakingDisposables(tracker, logToConsole);
|
||||
}
|
||||
|
||||
export async function throwIfDisposablesAreLeakedAsync(body: () => Promise<void>): Promise<void> {
|
||||
@@ -240,5 +93,15 @@ export async function throwIfDisposablesAreLeakedAsync(body: () => Promise<void>
|
||||
setDisposableTracker(tracker);
|
||||
await body();
|
||||
setDisposableTracker(null);
|
||||
tracker.ensureNoLeakingDisposables();
|
||||
computeLeakingDisposables(tracker);
|
||||
}
|
||||
|
||||
function computeLeakingDisposables(tracker: DisposableTracker, logToConsole = true) {
|
||||
const result = tracker.computeLeakingDisposables();
|
||||
if (result) {
|
||||
if (logToConsole) {
|
||||
console.error(result.details);
|
||||
}
|
||||
throw new Error(`There are ${result.leaks.length} undisposed disposables!${result.details}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { DomEmitter } from 'vs/base/browser/event';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, toDisposable, dispose, DisposableStore, setDisposableTracker, DisposableTracker, DisposableInfo } from 'vs/base/common/lifecycle';
|
||||
import { getDomNodePagePosition, createStyleSheet, createCSSRule, append, $ } from 'vs/base/browser/dom';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Context } from 'vs/platform/contextkey/browser/contextKeyService';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
@@ -36,6 +36,8 @@ import { windowLogId } from 'vs/workbench/services/log/common/logConstants';
|
||||
import { ByteSize } from 'vs/platform/files/common/files';
|
||||
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
class InspectContextKeysAction extends Action2 {
|
||||
|
||||
@@ -506,12 +508,104 @@ class RemoveLargeStorageEntriesAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
let tracker: DisposableTracker | undefined = undefined;
|
||||
let trackedDisposables = new Set<IDisposable>();
|
||||
|
||||
const DisposablesSnapshotStateContext = new RawContextKey<'started' | 'pending' | 'stopped'>('dirtyWorkingCopies', 'stopped');
|
||||
|
||||
class StartTrackDisposables extends Action2 {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.startTrackDisposables',
|
||||
title: { value: localize('startTrackDisposables', "Start Tracking Disposables"), original: 'Start Tracking Disposables' },
|
||||
category: Categories.Developer,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(DisposablesSnapshotStateContext.isEqualTo('pending').negate(), DisposablesSnapshotStateContext.isEqualTo('started').negate())
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const disposablesSnapshotStateContext = DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService));
|
||||
disposablesSnapshotStateContext.set('started');
|
||||
|
||||
trackedDisposables.clear();
|
||||
|
||||
tracker = new DisposableTracker();
|
||||
setDisposableTracker(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
class SnapshotTrackedDisposables extends Action2 {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.snapshotTrackedDisposables',
|
||||
title: { value: localize('snapshotTrackedDisposables', "Snapshot Tracked Disposables"), original: 'Snapshot Tracked Disposables' },
|
||||
category: Categories.Developer,
|
||||
f1: true,
|
||||
precondition: DisposablesSnapshotStateContext.isEqualTo('started')
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const disposablesSnapshotStateContext = DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService));
|
||||
disposablesSnapshotStateContext.set('pending');
|
||||
|
||||
trackedDisposables = new Set(tracker?.computeLeakingDisposables(1000)?.leaks.map(disposable => disposable.value));
|
||||
}
|
||||
}
|
||||
|
||||
class StopTrackDisposables extends Action2 {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.stopTrackDisposables',
|
||||
title: { value: localize('stopTrackDisposables', "Stop Tracking Disposables"), original: 'Stop Tracking Disposables' },
|
||||
category: Categories.Developer,
|
||||
f1: true,
|
||||
precondition: DisposablesSnapshotStateContext.isEqualTo('pending')
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
|
||||
const disposablesSnapshotStateContext = DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService));
|
||||
disposablesSnapshotStateContext.set('stopped');
|
||||
|
||||
if (tracker) {
|
||||
const disposableLeaks = new Set<DisposableInfo>();
|
||||
|
||||
for (const disposable of new Set(tracker.computeLeakingDisposables(1000)?.leaks) ?? []) {
|
||||
if (trackedDisposables.has(disposable.value)) {
|
||||
disposableLeaks.add(disposable);
|
||||
}
|
||||
}
|
||||
|
||||
const leaks = tracker.computeLeakingDisposables(1000, Array.from(disposableLeaks));
|
||||
if (leaks) {
|
||||
editorService.openEditor({ resource: undefined, contents: leaks.details });
|
||||
}
|
||||
}
|
||||
|
||||
setDisposableTracker(null);
|
||||
tracker = undefined;
|
||||
trackedDisposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Actions Registration
|
||||
registerAction2(InspectContextKeysAction);
|
||||
registerAction2(ToggleScreencastModeAction);
|
||||
registerAction2(LogStorageAction);
|
||||
registerAction2(LogWorkingCopiesAction);
|
||||
registerAction2(RemoveLargeStorageEntriesAction);
|
||||
if (!product.commit) {
|
||||
registerAction2(StartTrackDisposables);
|
||||
registerAction2(SnapshotTrackedDisposables);
|
||||
registerAction2(StopTrackDisposables);
|
||||
}
|
||||
|
||||
// --- Configuration
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { localize } from 'vs/nls';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { DeferredPromise, timeout } from 'vs/base/common/async';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
export class ExtensionActivationProgress implements IWorkbenchContribution {
|
||||
|
||||
@@ -39,7 +40,7 @@ export class ExtensionActivationProgress implements IWorkbenchContribution {
|
||||
|
||||
count++;
|
||||
|
||||
Promise.race([e.activation, timeout(5000)]).finally(() => {
|
||||
Promise.race([e.activation, timeout(5000, CancellationToken.None)]).finally(() => {
|
||||
if (--count === 0) {
|
||||
deferred!.complete(undefined);
|
||||
deferred = undefined;
|
||||
|
||||
Reference in New Issue
Block a user