import * as asyncHooks from "async_hooks" ; import { inspect } from "util" ; let hook ; type AsyncState = "init" | "before" | "after" | "destroyed" | "resolved" ; interface AsyncObject { asyncId : number ; type : string ; triggerId : number ; resource : any ; state : AsyncState ; startedCount : number ; finishedCount : number ; stack ? : string ; } const asyncObjects = new Map ( ) ; const objectMapping = new Map ( ) ; export function startAsyncTracing ( stackTraces = false ) { hook = onAsyncHook ( stackTraces ) ; } interface Trace { obj : object ; trace : string ; } export function trace ( obj ) { let res = objectMapping . get ( obj ) ; if ( ! res ) { return ; } let asyncTrace = `== Tracing leaked object ${ res . asyncId } ==` ; while ( res ) { const { stack , ... rest } = res ; asyncTrace += `${ inspect ( rest ) }\n${ stack }` ; res = asyncObjects . get ( res . triggerId ) ; } console . log ( asyncTrace ) ; return { obj , trace : asyncTrace } ; } export function printAsyncStack ( ) { console . log ( `Async stack:` ) ; let res = asyncObjects . get ( asyncHooks . executionAsyncId ( ) ) ; while ( res ) { const { stack , ... rest } = res ; console . log ( `%O\n${ stack }` , rest ) ; res = asyncObjects . get ( res . triggerId ) ; } } export function detectAsyncLeaks ( ) { const leaks = [ ] ; ( process as any ) . _getActiveHandles ( ) . forEach ( ( h ) => { if ( h !== process . stdout && h !== process . stderr ) { const leak = trace ( h ) ; leak && leaks . push ( leak ) ; } } ) ; ( process as any ) . _getActiveRequests ( ) . forEach ( ( h ) => { const leak = trace ( h ) ; leak && leaks . push ( leak ) ; } ) ; return leaks ; } export function printHooks ( ) { for ( const obj of asyncObjects ) { console . log ( `%O` , obj ) ; } } export function stopAsyncTracing ( ) { hook ?. ( ) ; } export function onAsyncHook ( stackTraces ) { const hooks = { init , before , after , destroy , promiseResolve } ; const asyncHook = asyncHooks . createHook ( hooks ) ; asyncHook . enable ( ) ; return ( ) => { asyncHook . disable ( ) ; } ; function init ( asyncId , type , triggerId , resource ) { const obj = { asyncId , type , triggerId , resource , state : "init" , startedCount : 0 , finishedCount : 0 , stack : stackTraces ? new Error ( "stack:" ) . stack ! . replace ( /Error:/ , "" ) : undefined } ; asyncObjects . set ( asyncId , obj ) ; objectMapping . set ( resource , obj ) ; } function destroy ( asyncId ) { const obj = asyncObjects . get ( asyncId ) ; if ( obj ) { obj . state = "destroyed" ; } else { } } function before ( asyncId ) { const obj = asyncObjects . get ( asyncId ) ; if ( obj ) { obj . state = "before" ; obj . startedCount ++ ; } else { } } function after ( asyncId ) { const obj = asyncObjects . get ( asyncId ) ; if ( obj ) { obj . state = "after" ; obj . finishedCount ++ ; } else { } } function promiseResolve ( asyncId ) { const obj = asyncObjects . get ( asyncId ) ; if ( obj ) { obj . state = "resolved" ; } else { } } } export function clearLeakDetector ( ) { asyncObjects . clear ( ) ; objectMapping . clear ( ) ; }