• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      // Don't throw if we see invocations due to an assertion in a test
172      // failing since we want to list the assertion failure instead
173      if (/process\._fatalException/.test(err.stack)) return null;
174      throw err;
175    }
176    return h;
177  }
178
179  _init(uid, type, triggerAsyncId, handle) {
180    const activity = {
181      uid,
182      type,
183      triggerAsyncId,
184      // In some cases (e.g. Timeout) the handle is a function, thus the usual
185      // `typeof handle === 'object' && handle !== null` check can't be used.
186      handleIsObject: handle instanceof Object,
187      handle
188    };
189    this._stamp(activity, 'init');
190    this._activities.set(uid, activity);
191    this._maybeLog(uid, type, 'init');
192    this.oninit(uid, type, triggerAsyncId, handle);
193  }
194
195  _before(uid) {
196    const h = this._getActivity(uid, 'before');
197    this._stamp(h, 'before');
198    this._maybeLog(uid, h && h.type, 'before');
199    this.onbefore(uid);
200  }
201
202  _after(uid) {
203    const h = this._getActivity(uid, 'after');
204    this._stamp(h, 'after');
205    this._maybeLog(uid, h && h.type, 'after');
206    this.onafter(uid);
207  }
208
209  _destroy(uid) {
210    const h = this._getActivity(uid, 'destroy');
211    this._stamp(h, 'destroy');
212    this._maybeLog(uid, h && h.type, 'destroy');
213    this.ondestroy(uid);
214  }
215
216  _promiseResolve(uid) {
217    const h = this._getActivity(uid, 'promiseResolve');
218    this._stamp(h, 'promiseResolve');
219    this._maybeLog(uid, h && h.type, 'promiseResolve');
220    this.onpromiseResolve(uid);
221  }
222
223  _maybeLog(uid, type, name) {
224    if (this._logid &&
225      (type == null || this._logtype == null || this._logtype === type)) {
226      print(`${this._logid}.${name}.uid-${uid}`);
227    }
228  }
229}
230
231exports = module.exports = function initHooks({
232  oninit,
233  onbefore,
234  onafter,
235  ondestroy,
236  onpromiseResolve,
237  allowNoInit,
238  logid,
239  logtype
240} = {}) {
241  return new ActivityCollector(process.hrtime(), {
242    oninit,
243    onbefore,
244    onafter,
245    ondestroy,
246    onpromiseResolve,
247    allowNoInit,
248    logid,
249    logtype
250  });
251};
252