From 766d0ef0217b3a3b78541dc6ac8c2ffa90e12d76 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Thu, 24 Jul 2025 20:31:12 +0100 Subject: [PATCH 1/2] Add support for source maps Use the new getCallSites API, which is both a bit simpler to use than V8 one, but also integrates correctly with Node.js source maps support. For a demo, I renamed example.js to example.ts) and used [tsx](https://github.com/privatenumber/tsx) to run it. Before: ``` > npx tsx --enable-source-maps example.ts There are 4 handle(s) keeping the process running. # Timeout example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import # TCPSERVERWRAP example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import # Timeout example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import # TCPSERVERWRAP example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import example.ts:1 - import whyIsNodeRunning from 'why-is-node-running' // should be your first import ``` After: ``` > npx tsx --enable-source-maps example.ts There are 4 handle(s) keeping the process running. # Timeout example.ts:6 - setInterval(() => {}, 1000) example.ts:10 - startServer() # TCPSERVERWRAP example.ts:7 - server.listen(0) example.ts:10 - startServer() # Timeout example.ts:6 - setInterval(() => {}, 1000) example.ts:11 - startServer() # TCPSERVERWRAP example.ts:7 - server.listen(0) example.ts:11 - startServer() ``` --- index.js | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/index.js b/index.js index 9189e97..9080f63 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ import { createHook } from 'node:async_hooks' import { readFileSync } from 'node:fs' import { relative } from 'node:path' import { fileURLToPath } from 'node:url' +import { getCallSites } from 'node:util' const IGNORED_TYPES = [ 'TIMERWRAP', @@ -17,7 +18,7 @@ const hook = createHook({ return } - const stacks = captureStackTraces().slice(1) + const stacks = getCallSites().slice(1) asyncResources.set(asyncId, { type, @@ -42,7 +43,7 @@ export default function whyIsNodeRunning (logger = console) { if (resource === undefined) { return false } - + return resource.hasRef?.() ?? true }) @@ -55,7 +56,7 @@ export default function whyIsNodeRunning (logger = console) { function printStacks (asyncResource, logger) { const stacks = asyncResource.stacks.filter((stack) => { - const fileName = stack.fileName + const fileName = stack.scriptName return fileName !== null && !fileName.startsWith('node:') }) @@ -72,9 +73,9 @@ function printStacks (asyncResource, logger) { for (const stack of stacks) { const location = formatLocation(stack) const padding = ' '.repeat(maxLength - location.length) - + try { - const lines = readFileSync(normalizeFilePath(stack.fileName), 'utf-8').split(/\n|\r\n/) + const lines = readFileSync(normalizeFilePath(stack.scriptName), 'utf-8').split(/\n|\r\n/) const line = lines[stack.lineNumber - 1].trim() logger.error(`${location}${padding} - ${line}`) @@ -85,7 +86,7 @@ function printStacks (asyncResource, logger) { } function formatLocation (stack) { - const filePath = formatFilePath(stack.fileName) + const filePath = formatFilePath(stack.scriptName) return `${filePath}:${stack.lineNumber}` } @@ -99,24 +100,3 @@ function formatFilePath (filePath) { function normalizeFilePath (filePath) { return filePath.startsWith('file://') ? fileURLToPath(filePath) : filePath } - -function prepareStackTrace(error, stackTraces) { - return stackTraces.map(stack => ({ - lineNumber: stack.getLineNumber(), - fileName: stack.getFileName() - })) -} - -// See: https://v8.dev/docs/stack-trace-api -function captureStackTraces () { - const target = {} - const original = Error.prepareStackTrace - - Error.prepareStackTrace = prepareStackTrace - Error.captureStackTrace(target, captureStackTraces) - - const capturedTraces = target.stack - Error.prepareStackTrace = original - - return capturedTraces -} From c53d42633d4da08cf11ba1bdb3112c8570612319 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Sun, 8 Mar 2026 05:50:55 -0400 Subject: [PATCH 2/2] improve async type classification and labels --- index.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 9080f63..e60ea32 100644 --- a/index.js +++ b/index.js @@ -4,18 +4,61 @@ import { relative } from 'node:path' import { fileURLToPath } from 'node:url' import { getCallSites } from 'node:util' -const IGNORED_TYPES = [ - 'TIMERWRAP', - 'PROMISE', - 'PerformanceObserver', - 'RANDOMBYTESREQUEST' -] + + +const isStdio = resource => { + try { + return resource[Object.getOwnPropertySymbols(resource)[0]]._isStdio + } catch { + return false + } +} + +export const ASYNC_TYPE_INFO = new Map([ + ['TCPWRAP', { blocks: 'yes', label: 'TCP socket' }], + ['TCPSERVERWRAP', { blocks: 'yes', label: 'TCP server' }], + ['PIPEWRAP', { blocks: 'yes', label: 'Pipe' }], + ['PIPECONNECTWRAP', { blocks: 'yes', label: 'Pipe connection' }], + ['TTYWRAP', { blocks: isStdio, label: 'TTY' }], + ['UDPWRAP', { blocks: 'yes', label: 'UDP socket' }], + ['SIGNALWRAP', { blocks: 'yes', label: 'Signal listener' }], + ['PROCESSWRAP', { blocks: 'yes', label: 'Child process' }], + ['WORKER', { blocks: 'yes', label: 'Worker' }], + ['MESSAGEPORT', { blocks: 'yes', label: 'Message port' }], + ['Timeout', { blocks: 'yes', label: 'Timeout' }], + ['Immediate', { blocks: 'yes', label: 'Immediate' }], + ['FSREQCALLBACK', { blocks: 'yes', label: 'Filesystem request' }], + ['GETADDRINFOREQWRAP', { blocks: 'yes', label: 'DNS lookup request' }], + ['GETNAMEINFOREQWRAP', { blocks: 'yes', label: 'Reverse DNS lookup request' }], + ['CONNECTWRAP', { blocks: 'yes', label: 'Socket connection attempt' }], + ['PIPECONNECTWRAP', { blocks: 'yes', label: 'Pipe connection' }], + ['WRITEWRAP', { blocks: 'yes', label: 'Write request' }], + ['SHUTDOWNWRAP', { blocks: 'yes', label: 'Socket shutdown' }], + ['STATWATCHER', { blocks: 'yes', label: 'File stat watcher' }], + ['FSEVENTWRAP', { blocks: 'yes', label: 'Filesystem event watcher' }], + ['ZLIB', { blocks: 'yes', label: 'Zlib operation' }], + ['DNSCHANNEL', { blocks: 'yes', label: 'DNS channel' }], + ['PROMISE', { blocks: 'no', label: 'Promise' }], + ['TickObject', { blocks: 'no', label: 'Tick object' }], + ['Microtask', { blocks: 'no', label: 'Microtask' }], + ['PerformanceObserver', { blocks: 'no', label: 'Performance observer' }], + ['RANDOMBYTESREQUEST', { blocks: 'no', label: 'Random bytes request' }], + ['HTTPPARSER', { blocks: 'no', label: 'HTTP parser' }], +]) const asyncResources = new Map() const hook = createHook({ init (asyncId, type, triggerAsyncId, resource) { - if (IGNORED_TYPES.includes(type)) { - return + // process._getActiveHandles() and process._getActiveRequests() are + // undocumented APIs that return the internal lists of active handles and + // requests which are keeping the event loop alive. But they don't include + // timers and they don't include stack traces, so we use async hooks to track + // those ourselves. + + const info = ASYNC_TYPE_INFO.get(type) + if (info) { + if (info.blocks === 'no') return + if (typeof info.blocks === 'function' && !info.blocks(resource)) return } const stacks = getCallSites().slice(1) @@ -61,7 +104,8 @@ function printStacks (asyncResource, logger) { }) logger.error('') - logger.error(`# ${asyncResource.type}`) + const info = ASYNC_TYPE_INFO.get(asyncResource.type) + logger.error(`# ${info?.label ?? asyncResource.type}`) if (!stacks[0]) { logger.error('(unknown stack trace)')