mirror of
https://github.com/Microsoft/vscode
synced 2024-10-01 08:50:48 +00:00
add ListenerRefusalError
and ListenerLeakError
which get logged when listener thresholds are exceeded. (#212900)
* add `ListenerRefusalError` and `ListenerLeakError` which get logged when listener thresholds are exceeded. The `stack` property of these errors will point towards the most frequent listener and how often it is used. If that's a high number there is a leak (same listener is added over and over again), if that's a low number there might be a conceptual flaw that an emitter is simply too prominent. * rightfully don't use Error.captureStackTrace (v8/nodejs only)
This commit is contained in:
parent
e65febca09
commit
2d97803568
|
@ -835,6 +835,7 @@ class LeakageMonitor {
|
|||
private _warnCountdown: number = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _errorHandler: (err: Error) => void,
|
||||
readonly threshold: number,
|
||||
readonly name: string = Math.random().toString(18).slice(2, 5),
|
||||
) { }
|
||||
|
@ -862,18 +863,13 @@ class LeakageMonitor {
|
|||
// is exceeded by 50% again
|
||||
this._warnCountdown = threshold * 0.5;
|
||||
|
||||
// find most frequent listener and print warning
|
||||
let topStack: string | undefined;
|
||||
let topCount: number = 0;
|
||||
for (const [stack, count] of this._stacks) {
|
||||
if (!topStack || topCount < count) {
|
||||
topStack = stack;
|
||||
topCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`);
|
||||
const [topStack, topCount] = this.getMostFrequentStack()!;
|
||||
const message = `[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`;
|
||||
console.warn(message);
|
||||
console.warn(topStack!);
|
||||
|
||||
const error = new ListenerLeakError(message, topStack);
|
||||
this._errorHandler(error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -881,12 +877,28 @@ class LeakageMonitor {
|
|||
this._stacks!.set(stack.value, count - 1);
|
||||
};
|
||||
}
|
||||
|
||||
getMostFrequentStack(): [string, number] | undefined {
|
||||
if (!this._stacks) {
|
||||
return undefined;
|
||||
}
|
||||
let topStack: [string, number] | undefined;
|
||||
let topCount: number = 0;
|
||||
for (const [stack, count] of this._stacks) {
|
||||
if (!topStack || topCount < count) {
|
||||
topStack = [stack, count];
|
||||
topCount = count;
|
||||
}
|
||||
}
|
||||
return topStack;
|
||||
}
|
||||
}
|
||||
|
||||
class Stacktrace {
|
||||
|
||||
static create() {
|
||||
return new Stacktrace(new Error().stack ?? '');
|
||||
const err = new Error();
|
||||
return new Stacktrace(err.stack ?? '');
|
||||
}
|
||||
|
||||
private constructor(readonly value: string) { }
|
||||
|
@ -896,6 +908,25 @@ class Stacktrace {
|
|||
}
|
||||
}
|
||||
|
||||
// error that is logged when going over the configured listener threshold
|
||||
export class ListenerLeakError extends Error {
|
||||
constructor(message: string, stack: string) {
|
||||
super(message);
|
||||
this.name = 'ListenerLeakError';
|
||||
this.stack = stack;
|
||||
}
|
||||
}
|
||||
|
||||
// SEVERE error that is logged when having gone way over the configured listener
|
||||
// threshold so that the emitter refuses to accept more listeners
|
||||
export class ListenerRefusalError extends Error {
|
||||
constructor(message: string, stack: string) {
|
||||
super(message);
|
||||
this.name = 'ListenerRefusalError';
|
||||
this.stack = stack;
|
||||
}
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
class UniqueContainer<T> {
|
||||
stack?: Stacktrace;
|
||||
|
@ -988,7 +1019,9 @@ export class Emitter<T> {
|
|||
|
||||
constructor(options?: EmitterOptions) {
|
||||
this._options = options;
|
||||
this._leakageMon = _globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold ? new LeakageMonitor(this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) : undefined;
|
||||
this._leakageMon = (_globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold)
|
||||
? new LeakageMonitor(options?.onListenerError ?? onUnexpectedError, this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) :
|
||||
undefined;
|
||||
this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
|
||||
this._deliveryQueue = this._options?.deliveryQueue as EventDeliveryQueuePrivate | undefined;
|
||||
}
|
||||
|
@ -1033,7 +1066,14 @@ export class Emitter<T> {
|
|||
get event(): Event<T> {
|
||||
this._event ??= (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
|
||||
if (this._leakageMon && this._size > this._leakageMon.threshold * 3) {
|
||||
console.warn(`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far`);
|
||||
const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size}/${this._leakageMon.threshold})`;
|
||||
console.warn(message);
|
||||
|
||||
const tuple = this._leakageMon.getMostFrequentStack() ?? ['UNKNOWN stack', -1];
|
||||
const error = new ListenerRefusalError(`${message}. HINT: Stack shows most frequent listener (${tuple[1]}-times)`, tuple[0]);
|
||||
const errorHandler = this._options?.onListenerError || onUnexpectedError;
|
||||
errorHandler(error);
|
||||
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { stub } from 'sinon';
|
||||
import { tail2 } from 'vs/base/common/arrays';
|
||||
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 { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, ListenerLeakError, ListenerRefusalError, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event';
|
||||
import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, DisposableTracker } from 'vs/base/common/lifecycle';
|
||||
import { observableValue, transaction } from 'vs/base/common/observable';
|
||||
import { MicrotaskDelay } from 'vs/base/common/symbols';
|
||||
|
@ -368,6 +369,31 @@ suite('Event', function () {
|
|||
|
||||
});
|
||||
|
||||
test('throw ListenerLeakError', () => {
|
||||
|
||||
const store = new DisposableStore();
|
||||
const allError: any[] = [];
|
||||
|
||||
const a = ds.add(new Emitter<undefined>({
|
||||
onListenerError(e) { allError.push(e); },
|
||||
leakWarningThreshold: 1,
|
||||
}));
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
a.event(() => { }, undefined, store);
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(allError.length, 4);
|
||||
const [start, tail] = tail2(allError);
|
||||
assert.ok(tail instanceof ListenerRefusalError);
|
||||
|
||||
for (const item of start) {
|
||||
assert.ok(item instanceof ListenerLeakError);
|
||||
}
|
||||
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test('reusing event function and context', function () {
|
||||
let counter = 0;
|
||||
function listener() {
|
||||
|
|
|
@ -199,7 +199,8 @@ async function loadTests(opts) {
|
|||
'issue #149130: vscode freezes because of Bracket Pair Colorization', // https://github.com/microsoft/vscode/issues/192440
|
||||
'property limits', // https://github.com/microsoft/vscode/issues/192443
|
||||
'Error events', // https://github.com/microsoft/vscode/issues/192443
|
||||
'fetch returns keybinding with user first if title and id matches' //
|
||||
'fetch returns keybinding with user first if title and id matches', //
|
||||
'throw ListenerLeakError'
|
||||
]);
|
||||
|
||||
let _testsWithUnexpectedOutput = false;
|
||||
|
|
Loading…
Reference in a new issue