From 53088e16de9728696df12b04670a0f2d5203592b Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Wed, 16 Feb 2022 19:53:17 +0100 Subject: feat(test): improved op sanitizer errors + traces (#13676) This commit improves the error messages for the `deno test` async op sanitizer. It does this in two ways: - it uses handwritten error messages for each op that could be leaking - it includes traces showing where each op was started This "async op tracing" functionality is a new feature in deno_core. It likely has a significant performance impact, which is why it is only enabled in tests. --- core/01_core.js | 23 ++++++++++++++++++++++- core/lib.deno_core.d.ts | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) (limited to 'core') diff --git a/core/01_core.js b/core/01_core.js index d9a110eea..3a05a0cff 100644 --- a/core/01_core.js +++ b/core/01_core.js @@ -22,6 +22,8 @@ MapPrototypeDelete, MapPrototypeSet, PromisePrototypeThen, + PromisePrototypeFinally, + StringPrototypeSlice, ObjectAssign, SymbolFor, } = window.__bootstrap.primordials; @@ -48,6 +50,13 @@ // to users. Currently missing bindings. const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); + let opCallTracingEnabled = false; + const opCallTraces = new Map(); + + function enableOpCallTracing() { + opCallTracingEnabled = true; + } + function setPromise(promiseId) { const idx = promiseId % RING_SIZE; // Move old promise from ring to map @@ -139,7 +148,17 @@ const maybeError = opcallAsync(opsCache[opName], promiseId, arg1, arg2); // Handle sync error (e.g: error parsing args) if (maybeError) return unwrapOpResult(maybeError); - const p = PromisePrototypeThen(setPromise(promiseId), unwrapOpResult); + let p = PromisePrototypeThen(setPromise(promiseId), unwrapOpResult); + if (opCallTracingEnabled) { + // Capture a stack trace by creating a new `Error` object. We remove the + // first 6 characters (the `Error\n` prefix) to get just the stack trace. + const stack = StringPrototypeSlice(new Error().stack, 6); + MapPrototypeSet(opCallTraces, promiseId, { opName, stack }); + p = PromisePrototypeFinally( + p, + () => MapPrototypeDelete(opCallTraces, promiseId), + ); + } // Save the id on the promise so it can later be ref'ed or unref'ed p[promiseIdSymbol] = promiseId; return p; @@ -226,6 +245,8 @@ BadResourcePrototype, Interrupted, InterruptedPrototype, + enableOpCallTracing, + opCallTraces, }); ObjectAssign(globalThis.__bootstrap, { core }); diff --git a/core/lib.deno_core.d.ts b/core/lib.deno_core.d.ts index 9f554f12b..37cd33190 100644 --- a/core/lib.deno_core.d.ts +++ b/core/lib.deno_core.d.ts @@ -141,5 +141,28 @@ declare namespace Deno { ): undefined | UncaughtExceptionCallback; export type UncaughtExceptionCallback = (err: any) => void; + + /** + * Enables collection of stack traces of all async ops. This allows for + * debugging of where a given async op was started. Deno CLI uses this for + * improving error message in op sanitizer errors for `deno test`. + * + * **NOTE:** enabling tracing has a significant negative performance impact. + * To get high level metrics on async ops with no added performance cost, + * use `Deno.core.metrics()`. + */ + function enableOpCallTracing(): void; + + export interface OpCallTrace { + opName: string; + stack: string; + } + + /** + * A map containing traces for all ongoing async ops. The key is the op id. + * Tracing only occurs when `Deno.core.enableOpCallTracing()` was previously + * enabled. + */ + const opCallTraces: Map; } } -- cgit v1.2.3