Created
November 16, 2022 19:33
-
-
Save trevnorris/20a5241bd9b232fd7726170e1f43f400 to your computer and use it in GitHub Desktop.
Use async_hooks to automatically create stack traces of resources
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 TickObject | |
at AsyncStackTrace.initFn (index.js:89:18) | |
6 TCPWRAP | |
at AsyncStackTrace.initFn (index.js:89:18) | |
3 TCPSERVERWRAP | |
at AsyncStackTrace.initFn (index.js:89:18) | |
at Object.<anonymous> (test/http-noop.js:11:4) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const { createHook } = require('async_hooks'); | |
const { createHash } = require('crypto'); | |
const { openSync, writeSync } = require('fs'); | |
const { EOL } = require('os'); | |
const { now } = performance; | |
// The constructed AsyncHook | |
const _hook = Symbol('hook'); | |
// Track which stack have already been printed. | |
const _sha_map = Symbol('sha_map'); | |
/** | |
* sha of each stack so the stacks can be reconstructed. Printed, but does not | |
* need to be stored: | |
* { | |
* type: 'sha' | |
* sha: <sha of stack> | |
* stack: <full stack> | |
* } | |
* Object that contains the list of sha's that have been generated to quickly | |
* check if the if the previous object needs to be printed again. This is stored | |
* in memory and not printed. | |
* sha_map { | |
* [sha]: true | |
* } | |
* Information about each init, with a reference to the sha of the stack. This | |
* is also printed and not kept in memory | |
* { | |
* eid, tid, start, stop, sha, type: 'info' | |
* } | |
*/ | |
let output = process.stdout.fd; | |
// Calculate where to print log. | |
for (let i = 0; i < process.argv.length; i++) { | |
const o = process.argv[i]; | |
let file = null; | |
if (o.indexOf('--async-stack-output') != 0) | |
continue; | |
if (o.indexOf('--async-stack-output=') === 0) { | |
if (o.length <= 21) | |
throw new Error('Provide location for output'); | |
file = o.substr(21); | |
} else { | |
if (process.argv.length <= i + 1) | |
throw new Error('Provide location for output'); | |
file = process.argv[i + 1]; | |
} | |
if (!file) | |
throw new Error(`Invalid file: ${file}`); | |
output = openSync(file, 'w+'); | |
} | |
class AsyncStackTrace { | |
constructor(opts) { | |
this[_hook] = createAsyncHook(this); | |
this[_sha_map] = {}; | |
if (opts.enable === true) { | |
this[_hook].enable(); | |
} | |
} | |
enable() { | |
this[_hook].enable(); | |
} | |
disable() { | |
this[_hook].disable(); | |
} | |
} | |
function createAsyncHook(self) { | |
return createHook({ | |
init: initFn.bind(self), | |
destroy(eid) { | |
writeSync(output, JSON.stringify({ | |
ot: 'destroy', eid, timestamp: now() }) + EOL); | |
}, | |
}); | |
} | |
function initFn(eid, type, tid, resource) { | |
Error.stackTraceLimit = Infinity; | |
// TODO | |
//const stack = {}; | |
//Error.captureStackTrace(stack, null); | |
const stack = (new Error).stack.substr(5 + EOL.length); | |
const sha = createHash('sha1').update(stack).digest('hex'); | |
writeSync(output, JSON.stringify({ | |
ot: 'init', eid, tid, sha, type, timestamp: now() }) + EOL); | |
if (this[_sha_map][sha]) | |
return; | |
this[_sha_map][sha] = true; | |
writeSync(output, JSON.stringify({ ot: 'sha', sha, stack }) + EOL); | |
} | |
// new AsyncStackTrace({ enable: true }); | |
module.exports = AsyncStackTrace; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
module.exports = { parse, filter }; | |
const builtins = require('module').builtinModules; | |
const { EOL } = require('os'); | |
const { isBuffer } = Buffer; | |
const mv = +process.version.split('.')[0].substr(1); | |
const coreModuleRegExp = mv < 16 ? | |
/^ at (?:[^/\\(]+ \(|)((?<![/\\]).+)\.js:\d+:\d+\)?$/ : | |
/^ at (?:[^/\\(]+ \(|)node:(.+):\d+:\d+\)?$/; | |
function parse(file) { | |
if (isBuffer(file)) | |
file = file.toString(); | |
const objs = file.trim().split(/\r?\n/).map(e => JSON.parse(e)); | |
const eids = {}; | |
const shas = {}; | |
while (objs.length > 0) { | |
const e = objs.pop(); | |
if (e.ot === 'sha') { | |
shas[e.sha] = e; | |
continue; | |
} | |
if (!eids[e.eid]) { | |
eids[e.eid] = { }; | |
} | |
if (e.ot === 'init') { | |
const o = eids[e.eid] = { }; | |
o.eid = e.eid; | |
o.tid = e.tid; | |
o.sha = e.sha; | |
o.type = e.type; | |
o.init = e.timestamp; | |
o.destroy = -1; | |
} else if (e.ot === 'destroy') { | |
eids[e.eid].destroy = e.timestamp; | |
} else { | |
throw new Error(`invalid data ${e}`); | |
} | |
} | |
for (let o in eids) { | |
const tid = eids[o].tid; | |
if (!eids[tid]) | |
continue; | |
if (!Array.isArray(eids[tid].children)) { | |
eids[tid].children = []; | |
} | |
//eids[tid].children.push(+o); | |
eids[tid].children.push(eids[o]); | |
} | |
return [ eids, shas ]; | |
} | |
function filter(stack) { | |
const lines = stack.split(/\r?\n/); | |
const newl = []; | |
for (let e of lines) { | |
const core = e.match(coreModuleRegExp); | |
if (core === null) { | |
newl.push(e); | |
continue; | |
} | |
if (builtins.includes(core[1]) || core[1].indexOf('internal') === 0) { | |
continue; | |
} | |
newl.push(e); | |
} | |
return newl.join(EOL); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment