Comparing version 0.1.0 to 0.2.0
337
index.js
@@ -1,122 +0,229 @@ | ||
const leftPad = require('left-pad'); | ||
const chalk = require('chalk').default; | ||
const fs = require('fs'); | ||
const asyncHooks = require('async_hooks'); | ||
class Cnysa { | ||
constructor(options = {}) { | ||
if (options.typeFilter) { | ||
this.typeFilter = new RegExp(options.typeFilter); | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const ah = require("async_hooks"); | ||
require("colors"); | ||
const leftPad = require("left-pad"); | ||
/** | ||
* Flatten an array of arrays, but in "column-major" order | ||
*/ | ||
const interleave = (input, separator) => { | ||
const result = []; | ||
const maxLength = input.reduce((acc, next) => Math.max(next.length, acc), -Infinity); | ||
for (let i = 0; i < maxLength; i++) { | ||
if (separator !== undefined && i > 0) { | ||
result.push(separator); | ||
} | ||
for (let j = 0; j < input.length; j++) { | ||
input[j][i] !== undefined && result.push(input[j][i]); | ||
} | ||
} | ||
this.start = Math.floor(Date.now() / 1000) % 1000; | ||
this.typesTable = {}; | ||
this.promiseTable = new Map(); | ||
this.indent = ''; | ||
} | ||
_init(id, type, trigger, resource) { | ||
if (this.typeFilter && !this.typeFilter.exec(type)) { | ||
return; | ||
return result; | ||
}; | ||
const until = (array, predicate) => { | ||
return array.slice(0, array.findIndex(e => !predicate(e))); | ||
}; | ||
class StringRowsBuilder { | ||
constructor(maxLength) { | ||
this.data = []; | ||
this.currentLength = 0; | ||
this.maxLength = maxLength; | ||
} | ||
this.typesTable[id] = type; | ||
if (type === 'PROMISE') { | ||
this.promiseTable.set(resource.promise, id); | ||
// treats as if one char | ||
appendChar(char, ...styles) { | ||
if (this.currentLength === 0) { | ||
this.data.push(''); | ||
} | ||
this.data[this.data.length - 1] += styles.reduce((acc, next) => next ? acc[next] : acc, char); | ||
if (++this.currentLength === this.maxLength) { | ||
this.currentLength = 0; | ||
} | ||
} | ||
this._write(this.indent); | ||
this._write('+ [', 'green'); | ||
this._write(leftPad(this._getTime(), 3)); | ||
this._write(`] ${this.typesTable[id]} `, 'green'); | ||
this._write(id, 'yellow'); | ||
if (asyncHooks.executionAsyncId() !== trigger) { | ||
this._write(':', 'green'); | ||
this._write(trigger, 'magenta'); | ||
getData() { | ||
return this.data; | ||
} | ||
this._write('\n'); | ||
} | ||
_before(id) { | ||
if (!this.typesTable[id]) return; | ||
this._write(this.indent); | ||
this._write('* [', 'blue'); | ||
this._write(leftPad(this._getTime(), 3)); | ||
this._write(`] ${this.typesTable[id]} `, 'blue'); | ||
this._write(`${id}`, 'yellow'); | ||
this._write(' {\n', 'blue'); | ||
this.indent += ' '; | ||
} | ||
_after(id) { | ||
if (!this.typesTable[id]) return; | ||
this.indent = this.indent.slice(2); | ||
this._write(this.indent); | ||
this._write('}', 'blue'); | ||
this._write('\n'); | ||
} | ||
_destroy(id) { | ||
if (!this.typesTable[id]) return; | ||
this._write(this.indent); | ||
this._write('- [', 'red'); | ||
this._write(leftPad(this._getTime(), 3)); | ||
this._write(`] ${this.typesTable[id]} `, 'red'); | ||
this._write(`${id}`, 'yellow'); | ||
this._write('\n'); | ||
} | ||
_promiseResolve(id) { | ||
if (!this.typesTable[id]) return; | ||
this._write(this.indent); | ||
this._write(': [', 'gray'); | ||
this._write(leftPad(this._getTime(), 3)); | ||
this._write(`] ${this.typesTable[id]} `, 'gray'); | ||
this._write(`${id}`, 'yellow'); | ||
this._write('\n'); | ||
} | ||
_getTime() { | ||
const s = (Math.floor(Date.now() / 1000) % 1000) - this.start; | ||
if (s < 0) s += 1000; | ||
return s; | ||
} | ||
_write(str, color) { | ||
if (color) { | ||
fs.writeSync(1, chalk[color]('' + str)); | ||
} else { | ||
fs.writeSync(1, '' + str); | ||
} | ||
class Cnysa { | ||
constructor(options) { | ||
this.globalContinuationEnded = false; | ||
// Initialize options. | ||
const canonicalOpts = Object.assign({}, Cnysa.DEFAULTS, options); | ||
this.width = canonicalOpts.width; | ||
this.ignoreTypes = typeof canonicalOpts.ignoreTypes === 'string' ? | ||
new RegExp(canonicalOpts.ignoreTypes) : canonicalOpts.ignoreTypes; | ||
this.highlightTypes = typeof canonicalOpts.highlightTypes === 'string' ? | ||
new RegExp(canonicalOpts.highlightTypes) : canonicalOpts.highlightTypes; | ||
this.getColor = (function* () { | ||
const colors = canonicalOpts.colors; | ||
while (true) { | ||
for (const color of colors) { | ||
yield color; | ||
} | ||
} | ||
})(); | ||
this.resources = { | ||
1: { uid: 1, type: '(initial)' } | ||
}; | ||
this.events = [{ | ||
timestamp: Date.now(), | ||
uid: 1, | ||
type: 'before' | ||
}]; | ||
this.hook = ah.createHook({ | ||
init: (uid, type) => { | ||
if (!type.match(this.ignoreTypes)) { | ||
const eid = ah.executionAsyncId(); | ||
const color = (this.resources[eid] && this.resources[eid].color) || (type.match(this.highlightTypes) ? this.getColor.next().value : undefined); | ||
this.resources[uid] = { uid, type, color }; | ||
this.events.push({ timestamp: Date.now(), uid, type: 'init' }); | ||
} | ||
}, | ||
before: (uid) => { | ||
if (this.resources[uid] && !this.resources[uid].type.match(this.ignoreTypes)) { | ||
if (!this.globalContinuationEnded) { | ||
this.globalContinuationEnded = true; | ||
this.events.push({ | ||
timestamp: Date.now(), | ||
uid: 1, | ||
type: 'after' | ||
}); | ||
} | ||
this.events.push({ timestamp: Date.now(), uid, type: 'before' }); | ||
} | ||
}, | ||
after: (uid) => { | ||
if (this.resources[uid] && !this.resources[uid].type.match(this.ignoreTypes)) { | ||
this.events.push({ timestamp: Date.now(), uid, type: 'after' }); | ||
} | ||
}, | ||
destroy: (uid) => { | ||
if (this.resources[uid] && !this.resources[uid].type.match(this.ignoreTypes)) { | ||
this.events.push({ timestamp: Date.now(), uid, type: 'destroy' }); | ||
} | ||
}, | ||
promiseResolve: (uid) => { | ||
this.events.push({ timestamp: Date.now(), uid, type: 'promiseResolve' }); | ||
} | ||
}); | ||
this.processOnExit = () => { | ||
this.hook.disable(); | ||
const uncoloredLength = Object.keys(this.resources).reduce((acc, key) => { | ||
const k = Number(key); | ||
return Math.max(acc, `${this.resources[k].uid.toString()} ${this.resources[k].type} `.length); | ||
}, -Infinity); | ||
const maxLength = Object.keys(this.resources).reduce((acc, key) => { | ||
const k = Number(key); | ||
return Math.max(acc, `${this.resources[k].uid.toString().magenta} ${this.resources[k].type.yellow} `.length); | ||
}, -Infinity); | ||
const stack = []; | ||
const paddedEvents = []; | ||
for (const event of this.events) { | ||
// if (resources[event.uid].color) { | ||
paddedEvents.push(event); | ||
paddedEvents.push({ uid: -1, timestamp: 0, type: 'pad' }); | ||
// } | ||
} | ||
const adjustedWidth = this.width - uncoloredLength; | ||
const eventStrings = {}; | ||
for (const event of paddedEvents) { | ||
Object.keys(this.resources).forEach(key => { | ||
const k = Number(key); | ||
if (!eventStrings[k]) { | ||
eventStrings[k] = { alive: false, str: new StringRowsBuilder(adjustedWidth) }; | ||
} | ||
if (event.uid === k) { | ||
if (event.type === 'init') { | ||
eventStrings[k].str.appendChar('*', 'green', this.resources[k].color); | ||
eventStrings[k].alive = true; | ||
} | ||
else if (event.type === 'before') { | ||
eventStrings[k].str.appendChar('{', 'blue', this.resources[k].color); | ||
stack.push(k); | ||
} | ||
else if (event.type === 'after') { | ||
eventStrings[k].str.appendChar('}', 'blue', this.resources[k].color); | ||
stack.pop(); | ||
} | ||
else if (event.type === 'destroy') { | ||
eventStrings[k].str.appendChar('*', 'red', this.resources[k].color); | ||
eventStrings[k].alive = false; | ||
} | ||
else if (event.type === 'promiseResolve') { | ||
eventStrings[k].str.appendChar('*', 'gray', this.resources[k].color); | ||
eventStrings[k].alive = false; | ||
} | ||
else { | ||
eventStrings[k].str.appendChar('?', 'gray', this.resources[k].color); | ||
} | ||
} | ||
else { | ||
if (event.type === 'init' && stack.length > 0) { | ||
if (event.uid > k) { | ||
if (stack[stack.length - 1] < k) { | ||
eventStrings[k].str.appendChar('|', 'green', this.resources[event.uid].color); | ||
return; | ||
} | ||
else if (stack[stack.length - 1] === k) { | ||
eventStrings[k].str.appendChar('.', 'green', this.resources[event.uid].color); | ||
return; | ||
} | ||
} | ||
else if (event.uid < k) { | ||
if (stack[stack.length - 1] > k) { | ||
eventStrings[k].str.appendChar('|', 'green', this.resources[k].color); | ||
return; | ||
} | ||
else if (stack[stack.length - 1] === k) { | ||
eventStrings[k].str.appendChar('.', 'green', this.resources[k].color); | ||
return; | ||
} | ||
} | ||
} | ||
if (stack.indexOf(k) !== -1) { | ||
eventStrings[k].str.appendChar('.', 'blue', this.resources[k].color); | ||
} | ||
else { | ||
if (eventStrings[k].alive) { | ||
eventStrings[k].str.appendChar('-', 'gray', this.resources[k].color); | ||
} | ||
else { | ||
eventStrings[k].str.appendChar(' '); | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
const separator = new Array(this.width).fill(':').join(''); | ||
const output = [ | ||
separator, | ||
...interleave(Object.keys(this.resources).map(key => { | ||
const k = Number(key); | ||
return eventStrings[k].str.getData().map(rhs => { | ||
if (rhs.strip.match(/^(\s|\|)+$/)) { | ||
return ''; | ||
} | ||
else { | ||
return leftPad(`${this.resources[k].uid.toString().magenta} ${this.resources[k].type.yellow} `, maxLength) + rhs; | ||
} | ||
}); | ||
}), separator).filter(line => line.length > 0), | ||
separator | ||
].join('\n'); | ||
console.log(output); | ||
}; | ||
} | ||
} | ||
enable() { | ||
this.hook = asyncHooks.createHook({ | ||
init: this._init.bind(this), | ||
before: this._before.bind(this), | ||
after: this._after.bind(this), | ||
destroy: this._destroy.bind(this), | ||
promiseResolve: this._promiseResolve.bind(this) | ||
}).enable(); | ||
return this; | ||
} | ||
disable() { | ||
this.hook.disable(); | ||
return this; | ||
} | ||
print(p, alias) { | ||
this._write(this.indent); | ||
this._write('> [', 'white'); | ||
this._write(leftPad(this._getTime(), 3)); | ||
this._write(`] PROMISE `, 'white'); | ||
this._write(`${this.promiseTable.get(p)||'?'}`, 'yellow'); | ||
if (alias) { | ||
this._write(' aka ', 'white'); | ||
this._write(alias, 'cyan'); | ||
enable() { | ||
this.hook.enable(); | ||
process.on('exit', this.processOnExit); | ||
} | ||
this._write('\n'); | ||
return p; | ||
} | ||
disable() { | ||
this.hook.disable(); | ||
process.removeListener('exit', this.processOnExit); | ||
} | ||
} | ||
module.exports.Cnysa = Cnysa; | ||
Cnysa.DEFAULTS = { | ||
width: process.stdout.columns || 80, | ||
ignoreTypes: / /, | ||
highlightTypes: / /, | ||
colors: ['bgMagenta', 'bgYellow', 'bgCyan'] | ||
}; | ||
exports.Cnysa = Cnysa; |
{ | ||
"name": "cnysa", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "A tool for understanding async-hooks", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"files": [ | ||
"*.js", | ||
"*.d.ts" | ||
], | ||
"keywords": [ | ||
"async", | ||
"async_hooks" | ||
"async_hooks", | ||
"visualization" | ||
], | ||
"scripts": { | ||
"prepack": "tsc" | ||
}, | ||
"author": "Kelvin Jin (kelvinjin@google.com)", | ||
@@ -14,5 +23,9 @@ "repository": "https://github.com/kjin/cnysa", | ||
"dependencies": { | ||
"chalk": "^2.3.0", | ||
"colors": "^1.2.1", | ||
"left-pad": "^1.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/colors": "^1.2.1", | ||
"@types/node": "^9.6.5" | ||
} | ||
} |
`cnysa` (unpronouncible) is a module that allows you to see information about what the `async_hooks` module is doing under the covers. | ||
__Note: This module currently uses the `colors` module.__ | ||
# Pre-Require Hook | ||
@@ -23,31 +25,23 @@ | ||
* `options.typeFilter`: String or RegExp to use as a filter for `AsyncResource` types. | ||
* `options.width`: Maximum number of characters to print on a single line before wrapping. Defaults to the current terminal width. | ||
* `options.ignoreTypes`: String or RegExp to filter out `AsyncResource` types. | ||
* `options.highlightTypes`: String or RegExp to highlight certain `AsyncResource` types and their descendants. | ||
## `Cnysa#enable()` | ||
Starts emitting output. | ||
Starts recording async events and registers a process exit hook. | ||
## `Cnysa#disable()` | ||
Stops emitting output. | ||
Stops recording async events and unregisters the process exit hook. | ||
# Understanding output | ||
For each `init`, `before`, `promiseResolve`, and `destroy` event, a line will be printed. | ||
For each `AsyncResource`, a time line will be printed, with a number of colored symbols: | ||
Each of those lines start with a symbol: | ||
* Green `*` represents the async resource creation. | ||
* Red `*` represents its destruction. | ||
* Blue `{...}` represent running in an async scope. | ||
* Gray `-` indicates the lifetime of the resource creation, and is bookended by `*` symbols. | ||
* `+` Init | ||
* `*` Before | ||
* `-` Destroy | ||
* `:` Promise Fulfillment | ||
The number that follows is the current time in seconds, mod 1000. This number might be useful in distinguishing "clusters" of async events. | ||
A string after represents the async resource type. | ||
The async resource's execution ID follows. For `init` events, if the trigger ID is different than the current execution ID, it is displayed as well. | ||
`before`-`after` pairs are visualized as indents. | ||
## Examples | ||
@@ -58,7 +52,1 @@ | ||
``` | ||
```bash | ||
(sleep 2 && curl localhost:8080) & node --require cnysa/register -e "http.createServer((req, res) => res.send('hi')).listen(8080)" | ||
``` | ||
https://gist.github.com/kjin/a499bfcb7c5e12a80f8c6ad66a30b740 |
@@ -1,12 +0,15 @@ | ||
const { Cnysa } = require('.'); | ||
const fs = require('fs'); | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const index_1 = require("./index"); | ||
const fs = require("fs"); | ||
const expectedConfigPath = `${process.cwd()}/cnysa.json`; | ||
let config = {}; | ||
try { | ||
fs.statSync(expectedConfigPath); | ||
const configJson = fs.readFileSync(expectedConfigPath, 'utf8'); | ||
config = JSON.parse(configJson); | ||
} catch (e) {} finally { | ||
new Cnysa(config).enable(); | ||
fs.statSync(expectedConfigPath); | ||
const configJson = fs.readFileSync(expectedConfigPath, 'utf8'); | ||
config = JSON.parse(configJson); | ||
} | ||
catch (e) { } | ||
finally { | ||
new index_1.Cnysa(config).enable(); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
13152
6
267
3
2
51
1
+ Addedcolors@^1.2.1
+ Addedcolors@1.4.0(transitive)
- Removedchalk@^2.3.0
- Removedansi-styles@3.2.1(transitive)
- Removedchalk@2.4.2(transitive)
- Removedcolor-convert@1.9.3(transitive)
- Removedcolor-name@1.1.3(transitive)
- Removedescape-string-regexp@1.0.5(transitive)
- Removedhas-flag@3.0.0(transitive)
- Removedsupports-color@5.5.0(transitive)