Comparing version 0.2.2 to 0.3.0
@@ -1,25 +0,28 @@ | ||
export interface CnysaOptions { | ||
export declare type Serializable<T> = { | ||
[P in keyof T]: T[P] extends string | number | boolean ? T[P] : T[P] extends Array<infer S> | IterableIterator<infer S> ? Array<Serializable<S>> : T[P] extends Function | RegExp ? string : T[P] extends {} ? Serializable<T[P]> : string; | ||
}; | ||
export declare type Flexible<T> = Partial<T | Serializable<T>>; | ||
export interface CnysaAsyncSnapshotOptions { | ||
width: number; | ||
ignoreTypes: RegExp | string; | ||
highlightTypes: RegExp | string; | ||
ignoreUnhighlighted: boolean; | ||
ignoreTypes: RegExp; | ||
roots: number[]; | ||
padding: number; | ||
colors: Array<string>; | ||
format: string; | ||
} | ||
export declare class Cnysa { | ||
private width; | ||
private ignoreTypes; | ||
private highlightTypes; | ||
private ignoreUnhighlighted; | ||
private getColor; | ||
private padding; | ||
private static activeInstances; | ||
private currentScopes; | ||
private resources; | ||
private events; | ||
private hook; | ||
private processOnExit; | ||
private globalContinuationEnded; | ||
static DEFAULTS: CnysaOptions; | ||
constructor(options: Partial<CnysaOptions>); | ||
static ASYNC_SNAPSHOT_DEFAULTS: CnysaAsyncSnapshotOptions; | ||
static get(): Cnysa; | ||
constructor(); | ||
enable(): void; | ||
disable(): void; | ||
private hasAncestor(resource, ancestor); | ||
private canonicalizeAsyncSnapshotOptions(options?); | ||
mark(name: string | number): void; | ||
getAsyncSnapshot(options?: Flexible<CnysaAsyncSnapshotOptions>): string; | ||
} |
303
index.js
@@ -62,24 +62,8 @@ "use strict"; | ||
class Cnysa { | ||
constructor(options) { | ||
constructor() { | ||
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.ignoreUnhighlighted = canonicalOpts.ignoreUnhighlighted; | ||
this.padding = canonicalOpts.padding; | ||
this.resources = { | ||
1: { uid: 1, type: '(initial)' } | ||
1: { uid: 1, type: '(initial)', parents: [], internal: false } | ||
}; | ||
this.currentScopes = [1]; | ||
this.events = [{ | ||
@@ -91,7 +75,13 @@ timestamp: Date.now(), | ||
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 }; | ||
init: (uid, type, triggerId) => { | ||
const eid = ah.executionAsyncId(); | ||
if (type.startsWith('cnysa')) { | ||
this.resources[uid] = { uid, type, parents: this.currentScopes.map(x => x), internal: true }; | ||
this.events.push({ timestamp: Date.now(), uid, type: 'internal' }); | ||
} | ||
else { | ||
this.resources[uid] = { uid, type, parents: this.currentScopes.map(x => x), internal: false }; | ||
if (triggerId !== eid) { | ||
this.resources[uid].tid = triggerId; | ||
} | ||
this.events.push({ timestamp: Date.now(), uid, type: 'init' }); | ||
@@ -101,3 +91,3 @@ } | ||
before: (uid) => { | ||
if (this.resources[uid] && !this.resources[uid].type.match(this.ignoreTypes)) { | ||
if (this.resources[uid] && !this.resources[uid].internal) { | ||
if (!this.globalContinuationEnded) { | ||
@@ -110,13 +100,16 @@ this.globalContinuationEnded = true; | ||
}); | ||
this.currentScopes.pop(); | ||
} | ||
this.events.push({ timestamp: Date.now(), uid, type: 'before' }); | ||
this.currentScopes.push(uid); | ||
} | ||
}, | ||
after: (uid) => { | ||
if (this.resources[uid] && !this.resources[uid].type.match(this.ignoreTypes)) { | ||
if (this.resources[uid] && !this.resources[uid].internal) { | ||
this.events.push({ timestamp: Date.now(), uid, type: 'after' }); | ||
this.currentScopes.pop(); | ||
} | ||
}, | ||
destroy: (uid) => { | ||
if (this.resources[uid] && !this.resources[uid].type.match(this.ignoreTypes)) { | ||
if (this.resources[uid] && !this.resources[uid].internal) { | ||
this.events.push({ timestamp: Date.now(), uid, type: 'destroy' }); | ||
@@ -129,65 +122,120 @@ } | ||
}); | ||
this.processOnExit = () => { | ||
this.hook.disable(); | ||
const uncoloredLength = Object.keys(this.resources).reduce((acc, key) => { | ||
Cnysa.activeInstances.push(this); | ||
} | ||
static get() { | ||
if (Cnysa.activeInstances.length === 0) { | ||
Cnysa.activeInstances.push(new Cnysa()); | ||
} | ||
return Cnysa.activeInstances[Cnysa.activeInstances.length - 1]; | ||
} | ||
enable() { | ||
this.hook.enable(); | ||
} | ||
disable() { | ||
this.hook.disable(); | ||
} | ||
hasAncestor(resource, ancestor) { | ||
if (Array.isArray(ancestor) && ancestor.length === 0) { | ||
return true; | ||
} | ||
let predicate; | ||
if (ancestor instanceof RegExp) { | ||
predicate = res => !!res.type.match(ancestor) || res.parents.some(parent => predicate(this.resources[parent])); | ||
} | ||
else { | ||
predicate = res => ancestor.indexOf(res.uid) !== -1 || res.parents.some(parent => predicate(this.resources[parent])); | ||
} | ||
return predicate(resource); | ||
} | ||
canonicalizeAsyncSnapshotOptions(options = {}) { | ||
const opts = Object.assign({}, Cnysa.ASYNC_SNAPSHOT_DEFAULTS, options); | ||
opts.ignoreTypes = typeof opts.ignoreTypes === 'string' ? | ||
new RegExp(opts.ignoreTypes) : opts.ignoreTypes; | ||
return opts; | ||
} | ||
mark(name) { | ||
new ah.AsyncResource(`cnysa(${name})`).emitDestroy(); | ||
} | ||
getAsyncSnapshot(options = {}) { | ||
// Initialize options. | ||
const config = this.canonicalizeAsyncSnapshotOptions(options); | ||
if (!this.globalContinuationEnded) { | ||
this.globalContinuationEnded = true; | ||
this.events.push({ | ||
timestamp: Date.now(), | ||
uid: 1, | ||
type: 'after' | ||
}); | ||
} | ||
const ignoredResources = Object.keys(this.resources).filter(key => { | ||
const k = Number(key); | ||
return this.resources[k].type.match(config.ignoreTypes) || !this.hasAncestor(this.resources[k], config.roots); | ||
}).reduce((acc, key) => acc.add(Number(key)), new Set()); | ||
const maxLength = Object.keys(this.resources).reduce((acc, key) => { | ||
const k = Number(key); | ||
if (ignoredResources.has(k)) { | ||
return acc; | ||
} | ||
const tidLength = this.resources[k].tid !== undefined ? (this.resources[k].uid.toString().length + 3) : 0; | ||
const uidLength = this.resources[k].uid.toString().length + 1; | ||
const typeLength = this.resources[k].type.length + 1; | ||
return Math.max(acc, tidLength + uidLength + typeLength); | ||
}, -Infinity); | ||
const stack = []; | ||
const paddedEvents = []; | ||
for (const event of this.events) { | ||
if (!ignoredResources.has(event.uid)) { | ||
paddedEvents.push(event); | ||
for (let i = 0; i < config.padding; i++) { | ||
paddedEvents.push({ uid: -1, timestamp: 0, type: 'pad' }); | ||
} | ||
} | ||
} | ||
const adjustedWidth = config.width - maxLength; | ||
const eventStrings = {}; | ||
for (const event of paddedEvents) { | ||
Object.keys(this.resources).forEach(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, `${chalk_1.default.magenta(this.resources[k].uid.toString())} ${chalk_1.default.yellow(this.resources[k].type)} `.length); | ||
}, -Infinity); | ||
const stack = []; | ||
const paddedEvents = []; | ||
for (const event of this.events) { | ||
if (event.uid === 1 || !this.ignoreUnhighlighted || this.resources[event.uid].color) { | ||
paddedEvents.push(event); | ||
for (let i = 0; i < this.padding; i++) { | ||
paddedEvents.push({ uid: -1, timestamp: 0, type: 'pad' }); | ||
} | ||
if (!eventStrings[k]) { | ||
eventStrings[k] = { alive: false, str: new StringRowsBuilder(adjustedWidth) }; | ||
} | ||
} | ||
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'); | ||
eventStrings[k].alive = true; | ||
} | ||
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 === 'before') { | ||
eventStrings[k].str.appendChar('{', 'blue'); | ||
stack.push(k); | ||
} | ||
else if (event.type === 'after') { | ||
eventStrings[k].str.appendChar('}', 'blue'); | ||
stack.pop(); | ||
} | ||
else if (event.type === 'destroy') { | ||
eventStrings[k].str.appendChar('*', 'red'); | ||
eventStrings[k].alive = false; | ||
} | ||
else if (event.type === 'promiseResolve') { | ||
eventStrings[k].str.appendChar('*', 'gray'); | ||
eventStrings[k].alive = false; | ||
} | ||
else if (event.type === 'internal') { | ||
eventStrings[k].str.appendChar('*', 'yellow'); | ||
} | ||
else { | ||
if (event.type === 'init' && stack.length > 0) { | ||
eventStrings[k].str.appendChar('?', 'gray'); | ||
} | ||
} | ||
else { | ||
const maybeVertical = (color) => { | ||
if (stack.length > 0) { | ||
if (event.uid > k) { | ||
if (stack[stack.length - 1] < k) { | ||
eventStrings[k].str.appendChar('|', 'green', this.resources[event.uid].color); | ||
return; | ||
eventStrings[k].str.appendChar('|', color); | ||
return true; | ||
} | ||
else if (stack[stack.length - 1] === k) { | ||
eventStrings[k].str.appendChar('.', 'green', this.resources[event.uid].color); | ||
return; | ||
eventStrings[k].str.appendChar('.', color); | ||
return true; | ||
} | ||
@@ -197,61 +245,72 @@ } | ||
if (stack[stack.length - 1] > k) { | ||
eventStrings[k].str.appendChar('|', 'green', this.resources[k].color); | ||
return; | ||
eventStrings[k].str.appendChar('|', color); | ||
return true; | ||
} | ||
else if (stack[stack.length - 1] === k) { | ||
eventStrings[k].str.appendChar('.', 'green', this.resources[k].color); | ||
return; | ||
eventStrings[k].str.appendChar('.', color); | ||
return true; | ||
} | ||
} | ||
} | ||
if (stack.indexOf(k) !== -1) { | ||
eventStrings[k].str.appendChar('.', 'blue', this.resources[k].color); | ||
return false; | ||
}; | ||
if (event.type === 'internal' && maybeVertical('yellow')) { | ||
return; | ||
} | ||
else if (event.type === 'init' && maybeVertical('green')) { | ||
return; | ||
} | ||
if (stack.indexOf(k) !== -1) { | ||
eventStrings[k].str.appendChar('.', 'blue'); | ||
} | ||
else { | ||
if (eventStrings[k].alive) { | ||
eventStrings[k].str.appendChar('-', 'gray'); | ||
} | ||
else { | ||
if (eventStrings[k].alive) { | ||
eventStrings[k].str.appendChar('-', 'gray', this.resources[k].color); | ||
} | ||
else { | ||
eventStrings[k].str.appendChar(' '); | ||
} | ||
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.dirty) { | ||
return ''; | ||
} | ||
}); | ||
} | ||
const separator = new Array(config.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.dirty) { | ||
return ''; | ||
} | ||
else { | ||
if (this.resources[k].tid !== undefined) { | ||
return leftPad(`${chalk_1.default.magenta(`${this.resources[k].uid}`)} (${chalk_1.default.green(`${this.resources[k].tid}`)}) ${chalk_1.default.yellow(this.resources[k].type)} `, maxLength + (chalk_1.default.magenta(' ').length - 1) * 3) + rhs.str; | ||
} | ||
else { | ||
return leftPad(`${chalk_1.default.magenta(this.resources[k].uid.toString())} ${chalk_1.default.yellow(this.resources[k].type)} `, maxLength) + rhs.str; | ||
return leftPad(`${chalk_1.default.magenta(`${this.resources[k].uid}`)} ${chalk_1.default.yellow(this.resources[k].type)} `, maxLength + (chalk_1.default.magenta(' ').length - 1) * 2) + rhs.str; | ||
} | ||
}); | ||
}), separator).filter(line => line.length > 0), | ||
separator | ||
].join('\n'); | ||
console.log(output); | ||
}; | ||
} | ||
}); | ||
}), separator).filter(line => line.length > 0), | ||
separator | ||
].join('\n'); | ||
const format = options && options.format ? options.format : config.format; | ||
if (format === 'svg') { | ||
const ansiToSvg = require('ansi-to-svg'); | ||
return ansiToSvg(output); | ||
} | ||
else { | ||
return output; | ||
} | ||
} | ||
enable() { | ||
this.hook.enable(); | ||
process.on('exit', this.processOnExit); | ||
} | ||
disable() { | ||
this.hook.disable(); | ||
process.removeListener('exit', this.processOnExit); | ||
} | ||
} | ||
Cnysa.DEFAULTS = { | ||
Cnysa.activeInstances = []; | ||
Cnysa.ASYNC_SNAPSHOT_DEFAULTS = { | ||
width: process.stdout.columns || 80, | ||
ignoreTypes: / /, | ||
highlightTypes: / /, | ||
ignoreUnhighlighted: false, | ||
roots: [], | ||
padding: 1, | ||
colors: ['bgMagenta', 'bgYellow', 'bgCyan'] | ||
format: 'default' | ||
}; | ||
exports.Cnysa = Cnysa; |
{ | ||
"name": "cnysa", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"description": "A tool for understanding async-hooks", | ||
@@ -8,2 +8,3 @@ "main": "index.js", | ||
"files": [ | ||
"doc", | ||
"*.js", | ||
@@ -24,2 +25,3 @@ "*.d.ts" | ||
"dependencies": { | ||
"ansi-to-svg": "^1.4.1", | ||
"chalk": "^2.3.2", | ||
@@ -29,4 +31,5 @@ "left-pad": "^1.2.0" | ||
"devDependencies": { | ||
"@types/node": "^9.6.5" | ||
"@types/node": "^9.6.5", | ||
"typescript": "^2.8.3" | ||
} | ||
} |
@@ -26,16 +26,24 @@ `cnysa` (unpronouncible) is a module that allows you to see information about what the `async_hooks` module is doing under the covers. | ||
* `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. | ||
* `options.ignoreTypes`: A string or RegExp to filter out `AsyncResource` types. | ||
* `options.highlightTypes`: A string or RegExp to highlight certain `AsyncResource` types and their descendants. | ||
* `options.ignoreUnhighlighted`: Boolean to determine whether unhighlighted types should be ignored. | ||
* `options.padding`: Number that represents the amount of space between each depicted event. | ||
* `options.colors`: An array of strings that represent colors to use when highlighting. | ||
* `options.format`: A string that represents how the output should be formatted. Currently, the available options are `'default'` and `'svg'` (which uses [`ansi-to-svg`](https://github.com/F1LT3R/ansi-to-svg)). | ||
## `Cnysa#enable()` | ||
Starts recording async events and registers a process exit hook. | ||
Starts recording async events. | ||
## `Cnysa#disable()` | ||
Stops recording async events and unregisters the process exit hook. | ||
Stops recording async events. | ||
## `Cnysa#getAsyncSnapshot()` | ||
Returns a formatted async ancestry tree. | ||
# Understanding output | ||
For each `AsyncResource`, a time line will be printed, with a number of colored symbols: | ||
For each `AsyncResource`, a timeline will be printed, with a number of colored symbols: | ||
@@ -52,1 +60,3 @@ * Green `*` represents the async resource creation. | ||
``` | ||
![example-readfile.svg](./doc/images/example-readfile.svg) |
@@ -14,3 +14,7 @@ "use strict"; | ||
finally { | ||
new index_1.Cnysa(config).enable(); | ||
const cnysa = new index_1.Cnysa(); | ||
cnysa.enable(); | ||
process.once('exit', () => { | ||
console.log(cnysa.getAsyncSnapshot(config)); | ||
}); | ||
} |
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
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
21640
7
358
61
3
2
+ Addedansi-to-svg@^1.4.1
+ Added@xmldom/xmldom@0.8.10(transitive)
+ Addedansi-regex@3.0.1(transitive)
+ Addedansi-to@1.5.1(transitive)
+ Addedansi-to-svg@1.4.3(transitive)
+ Addedarray-uniq@1.0.3(transitive)
+ Addedbase64-js@1.5.1(transitive)
+ Addeddeepmerge@2.2.1(transitive)
+ Addedhe@1.2.0(transitive)
+ Addeditermcolors-to-hex@1.0.1(transitive)
+ Addedparse-ansi@1.0.3(transitive)
+ Addedplist@3.1.0(transitive)
+ Addedrgb-hex@2.1.0(transitive)
+ Addedstrip-ansi@4.0.0(transitive)
+ Addedsuper-split@1.1.0(transitive)
+ Addedxmlbuilder@15.1.1(transitive)