1'use strict'; 2// Flags: --expose-gc 3 4const common = require('../common'); 5const assert = require('assert'); 6const async_hooks = require('async_hooks'); 7const util = require('util'); 8const print = process._rawDebug; 9 10if (typeof global.gc === 'function') { 11 (function exity(cntr) { 12 process.once('beforeExit', () => { 13 global.gc(); 14 if (cntr < 4) setImmediate(() => exity(cntr + 1)); 15 }); 16 })(0); 17} 18 19function noop() {} 20 21class ActivityCollector { 22 constructor(start, { 23 allowNoInit = false, 24 oninit, 25 onbefore, 26 onafter, 27 ondestroy, 28 onpromiseResolve, 29 logid = null, 30 logtype = null, 31 } = {}) { 32 this._start = start; 33 this._allowNoInit = allowNoInit; 34 this._activities = new Map(); 35 this._logid = logid; 36 this._logtype = logtype; 37 38 // Register event handlers if provided 39 this.oninit = typeof oninit === 'function' ? oninit : noop; 40 this.onbefore = typeof onbefore === 'function' ? onbefore : noop; 41 this.onafter = typeof onafter === 'function' ? onafter : noop; 42 this.ondestroy = typeof ondestroy === 'function' ? ondestroy : noop; 43 this.onpromiseResolve = typeof onpromiseResolve === 'function' ? 44 onpromiseResolve : noop; 45 46 // Create the hook with which we'll collect activity data 47 this._asyncHook = async_hooks.createHook({ 48 init: this._init.bind(this), 49 before: this._before.bind(this), 50 after: this._after.bind(this), 51 destroy: this._destroy.bind(this), 52 promiseResolve: this._promiseResolve.bind(this), 53 }); 54 } 55 56 enable() { 57 this._asyncHook.enable(); 58 } 59 60 disable() { 61 this._asyncHook.disable(); 62 } 63 64 sanityCheck(types) { 65 if (types != null && !Array.isArray(types)) types = [ types ]; 66 67 function activityString(a) { 68 return util.inspect(a, false, 5, true); 69 } 70 71 const violations = []; 72 let tempActivityString; 73 74 function v(msg) { violations.push(msg); } 75 for (const a of this._activities.values()) { 76 tempActivityString = activityString(a); 77 if (types != null && !types.includes(a.type)) continue; 78 79 if (a.init && a.init.length > 1) { 80 v(`Activity inited twice\n${tempActivityString}` + 81 '\nExpected "init" to be called at most once'); 82 } 83 if (a.destroy && a.destroy.length > 1) { 84 v(`Activity destroyed twice\n${tempActivityString}` + 85 '\nExpected "destroy" to be called at most once'); 86 } 87 if (a.before && a.after) { 88 if (a.before.length < a.after.length) { 89 v('Activity called "after" without calling "before"\n' + 90 `${tempActivityString}` + 91 '\nExpected no "after" call without a "before"'); 92 } 93 if (a.before.some((x, idx) => x > a.after[idx])) { 94 v('Activity had an instance where "after" ' + 95 'was invoked before "before"\n' + 96 `${tempActivityString}` + 97 '\nExpected "after" to be called after "before"'); 98 } 99 } 100 if (a.before && a.destroy) { 101 if (a.before.some((x, idx) => x > a.destroy[idx])) { 102 v('Activity had an instance where "destroy" ' + 103 'was invoked before "before"\n' + 104 `${tempActivityString}` + 105 '\nExpected "destroy" to be called after "before"'); 106 } 107 } 108 if (a.after && a.destroy) { 109 if (a.after.some((x, idx) => x > a.destroy[idx])) { 110 v('Activity had an instance where "destroy" ' + 111 'was invoked before "after"\n' + 112 `${tempActivityString}` + 113 '\nExpected "destroy" to be called after "after"'); 114 } 115 } 116 if (!a.handleIsObject) { 117 v(`No resource object\n${tempActivityString}` + 118 '\nExpected "init" to be called with a resource object'); 119 } 120 } 121 if (violations.length) { 122 console.error(violations.join('\n\n') + '\n'); 123 assert.fail(`${violations.length} failed sanity checks`); 124 } 125 } 126 127 inspect(opts = {}) { 128 if (typeof opts === 'string') opts = { types: opts }; 129 const { types = null, depth = 5, stage = null } = opts; 130 const activities = types == null ? 131 Array.from(this._activities.values()) : 132 this.activitiesOfTypes(types); 133 134 if (stage != null) console.log(`\n${stage}`); 135 console.log(util.inspect(activities, false, depth, true)); 136 } 137 138 activitiesOfTypes(types) { 139 if (!Array.isArray(types)) types = [ types ]; 140 return this.activities.filter((x) => types.includes(x.type)); 141 } 142 143 get activities() { 144 return Array.from(this._activities.values()); 145 } 146 147 _stamp(h, hook) { 148 if (h == null) return; 149 if (h[hook] == null) h[hook] = []; 150 const time = process.hrtime(this._start); 151 h[hook].push((time[0] * 1e9) + time[1]); 152 } 153 154 _getActivity(uid, hook) { 155 const h = this._activities.get(uid); 156 if (!h) { 157 // If we allowed handles without init we ignore any further life time 158 // events this makes sense for a few tests in which we enable some hooks 159 // later 160 if (this._allowNoInit) { 161 const stub = { uid, type: 'Unknown', handleIsObject: true, handle: {} }; 162 this._activities.set(uid, stub); 163 return stub; 164 } else if (!common.isMainThread) { 165 // Worker threads start main script execution inside of an AsyncWrap 166 // callback, so we don't yield errors for these. 167 return null; 168 } 169 const err = new Error(`Found a handle whose ${hook}` + 170 ' hook was invoked but not its init hook'); 171 throw err; 172 } 173 return h; 174 } 175 176 _init(uid, type, triggerAsyncId, handle) { 177 const activity = { 178 uid, 179 type, 180 triggerAsyncId, 181 // In some cases (e.g. Timeout) the handle is a function, thus the usual 182 // `typeof handle === 'object' && handle !== null` check can't be used. 183 handleIsObject: handle instanceof Object, 184 handle, 185 }; 186 this._stamp(activity, 'init'); 187 this._activities.set(uid, activity); 188 this._maybeLog(uid, type, 'init'); 189 this.oninit(uid, type, triggerAsyncId, handle); 190 } 191 192 _before(uid) { 193 const h = this._getActivity(uid, 'before'); 194 this._stamp(h, 'before'); 195 this._maybeLog(uid, h && h.type, 'before'); 196 this.onbefore(uid); 197 } 198 199 _after(uid) { 200 const h = this._getActivity(uid, 'after'); 201 this._stamp(h, 'after'); 202 this._maybeLog(uid, h && h.type, 'after'); 203 this.onafter(uid); 204 } 205 206 _destroy(uid) { 207 const h = this._getActivity(uid, 'destroy'); 208 this._stamp(h, 'destroy'); 209 this._maybeLog(uid, h && h.type, 'destroy'); 210 this.ondestroy(uid); 211 } 212 213 _promiseResolve(uid) { 214 const h = this._getActivity(uid, 'promiseResolve'); 215 this._stamp(h, 'promiseResolve'); 216 this._maybeLog(uid, h && h.type, 'promiseResolve'); 217 this.onpromiseResolve(uid); 218 } 219 220 _maybeLog(uid, type, name) { 221 if (this._logid && 222 (type == null || this._logtype == null || this._logtype === type)) { 223 print(`${this._logid}.${name}.uid-${uid}`); 224 } 225 } 226} 227 228exports = module.exports = function initHooks({ 229 oninit, 230 onbefore, 231 onafter, 232 ondestroy, 233 onpromiseResolve, 234 allowNoInit, 235 logid, 236 logtype, 237} = {}) { 238 return new ActivityCollector(process.hrtime(), { 239 oninit, 240 onbefore, 241 onafter, 242 ondestroy, 243 onpromiseResolve, 244 allowNoInit, 245 logid, 246 logtype, 247 }); 248}; 249