• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayIsArray,
5  ArrayPrototypeFilter,
6  ArrayPrototypeForEach,
7  ArrayPrototypeIncludes,
8  ArrayPrototypeMap,
9  ArrayPrototypePush,
10  ArrayPrototypeSplice,
11  ArrayPrototypeUnshift,
12  Boolean,
13  NumberIsSafeInteger,
14  ObjectDefineProperties,
15  ObjectDefineProperty,
16  ObjectKeys,
17  SafeSet,
18  Symbol,
19  TypeError,
20} = primordials;
21
22const {
23  ELDHistogram: _ELDHistogram,
24  PerformanceEntry,
25  mark: _mark,
26  clearMark: _clearMark,
27  measure: _measure,
28  milestones,
29  observerCounts,
30  setupObservers,
31  timeOrigin,
32  timeOriginTimestamp,
33  timerify,
34  constants,
35  installGarbageCollectionTracking,
36  removeGarbageCollectionTracking,
37  loopIdleTime,
38} = internalBinding('performance');
39
40const {
41  NODE_PERFORMANCE_ENTRY_TYPE_NODE,
42  NODE_PERFORMANCE_ENTRY_TYPE_MARK,
43  NODE_PERFORMANCE_ENTRY_TYPE_MEASURE,
44  NODE_PERFORMANCE_ENTRY_TYPE_GC,
45  NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION,
46  NODE_PERFORMANCE_ENTRY_TYPE_HTTP2,
47  NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
48
49  NODE_PERFORMANCE_MILESTONE_NODE_START,
50  NODE_PERFORMANCE_MILESTONE_V8_START,
51  NODE_PERFORMANCE_MILESTONE_LOOP_START,
52  NODE_PERFORMANCE_MILESTONE_LOOP_EXIT,
53  NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE,
54  NODE_PERFORMANCE_MILESTONE_ENVIRONMENT
55} = constants;
56
57const { AsyncResource } = require('async_hooks');
58const L = require('internal/linkedlist');
59const kInspect = require('internal/util').customInspectSymbol;
60
61const {
62  ERR_INVALID_CALLBACK,
63  ERR_INVALID_ARG_TYPE,
64  ERR_INVALID_OPT_VALUE,
65  ERR_VALID_PERFORMANCE_ENTRY_TYPE,
66  ERR_INVALID_PERFORMANCE_MARK
67} = require('internal/errors').codes;
68
69const {
70  Histogram,
71  createHistogram,
72  kHandle,
73} = require('internal/histogram');
74
75const { setImmediate } = require('timers');
76const kCallback = Symbol('callback');
77const kTypes = Symbol('types');
78const kEntries = Symbol('entries');
79const kBuffer = Symbol('buffer');
80const kBuffering = Symbol('buffering');
81const kQueued = Symbol('queued');
82const kTimerified = Symbol('timerified');
83const kInsertEntry = Symbol('insert-entry');
84const kGetEntries = Symbol('get-entries');
85const kIndex = Symbol('index');
86const kMarks = Symbol('marks');
87const kEnabled = Symbol('kEnabled');
88
89const observers = {};
90const observerableTypes = [
91  'node',
92  'mark',
93  'measure',
94  'gc',
95  'function',
96  'http2',
97  'http',
98];
99
100const IDX_STREAM_STATS_ID = 0;
101const IDX_STREAM_STATS_TIMETOFIRSTBYTE = 1;
102const IDX_STREAM_STATS_TIMETOFIRSTHEADER = 2;
103const IDX_STREAM_STATS_TIMETOFIRSTBYTESENT = 3;
104const IDX_STREAM_STATS_SENTBYTES = 4;
105const IDX_STREAM_STATS_RECEIVEDBYTES = 5;
106
107const IDX_SESSION_STATS_TYPE = 0;
108const IDX_SESSION_STATS_PINGRTT = 1;
109const IDX_SESSION_STATS_FRAMESRECEIVED = 2;
110const IDX_SESSION_STATS_FRAMESSENT = 3;
111const IDX_SESSION_STATS_STREAMCOUNT = 4;
112const IDX_SESSION_STATS_STREAMAVERAGEDURATION = 5;
113const IDX_SESSION_STATS_DATA_SENT = 6;
114const IDX_SESSION_STATS_DATA_RECEIVED = 7;
115const IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS = 8;
116
117let http2;
118let sessionStats;
119let streamStats;
120
121function collectHttp2Stats(entry) {
122  if (http2 === undefined) http2 = internalBinding('http2');
123  switch (entry.name) {
124    case 'Http2Stream':
125      if (streamStats === undefined)
126        streamStats = http2.streamStats;
127      entry.id =
128        streamStats[IDX_STREAM_STATS_ID] >>> 0;
129      entry.timeToFirstByte =
130        streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTE];
131      entry.timeToFirstHeader =
132        streamStats[IDX_STREAM_STATS_TIMETOFIRSTHEADER];
133      entry.timeToFirstByteSent =
134        streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT];
135      entry.bytesWritten =
136        streamStats[IDX_STREAM_STATS_SENTBYTES];
137      entry.bytesRead =
138        streamStats[IDX_STREAM_STATS_RECEIVEDBYTES];
139      break;
140    case 'Http2Session':
141      if (sessionStats === undefined)
142        sessionStats = http2.sessionStats;
143      entry.type =
144        sessionStats[IDX_SESSION_STATS_TYPE] >>> 0 === 0 ? 'server' : 'client';
145      entry.pingRTT =
146        sessionStats[IDX_SESSION_STATS_PINGRTT];
147      entry.framesReceived =
148        sessionStats[IDX_SESSION_STATS_FRAMESRECEIVED];
149      entry.framesSent =
150        sessionStats[IDX_SESSION_STATS_FRAMESSENT];
151      entry.streamCount =
152        sessionStats[IDX_SESSION_STATS_STREAMCOUNT];
153      entry.streamAverageDuration =
154        sessionStats[IDX_SESSION_STATS_STREAMAVERAGEDURATION];
155      entry.bytesWritten =
156        sessionStats[IDX_SESSION_STATS_DATA_SENT];
157      entry.bytesRead =
158        sessionStats[IDX_SESSION_STATS_DATA_RECEIVED];
159      entry.maxConcurrentStreams =
160        sessionStats[IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS];
161      break;
162  }
163}
164
165function now() {
166  const hr = process.hrtime();
167  return hr[0] * 1000 + hr[1] / 1e6;
168}
169
170function getMilestoneTimestamp(milestoneIdx) {
171  const ns = milestones[milestoneIdx];
172  if (ns === -1)
173    return ns;
174  return ns / 1e6 - timeOrigin;
175}
176
177class PerformanceNodeTiming extends PerformanceEntry {
178  constructor() {
179    super();
180
181    ObjectDefineProperties(this, {
182      name: {
183        enumerable: true,
184        configurable: true,
185        value: 'node'
186      },
187
188      entryType: {
189        enumerable: true,
190        configurable: true,
191        value: 'node'
192      },
193
194      startTime: {
195        enumerable: true,
196        configurable: true,
197        value: 0
198      },
199
200      duration: {
201        enumerable: true,
202        configurable: true,
203        get() {
204          return now() - timeOrigin;
205        }
206      },
207
208      nodeStart: {
209        enumerable: true,
210        configurable: true,
211        get() {
212          return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_NODE_START);
213        }
214      },
215
216      v8Start: {
217        enumerable: true,
218        configurable: true,
219        get() {
220          return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_V8_START);
221        }
222      },
223
224      environment: {
225        enumerable: true,
226        configurable: true,
227        get() {
228          return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_ENVIRONMENT);
229        }
230      },
231
232      loopStart: {
233        enumerable: true,
234        configurable: true,
235        get() {
236          return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_LOOP_START);
237        }
238      },
239
240      loopExit: {
241        enumerable: true,
242        configurable: true,
243        get() {
244          return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
245        }
246      },
247
248      bootstrapComplete: {
249        enumerable: true,
250        configurable: true,
251        get() {
252          return getMilestoneTimestamp(
253            NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE);
254        }
255      },
256
257      idleTime: {
258        enumerable: true,
259        configurable: true,
260        get() {
261          return loopIdleTime();
262        }
263      }
264    });
265  }
266  [kInspect]() {
267    return {
268      name: 'node',
269      entryType: 'node',
270      startTime: this.startTime,
271      duration: this.duration,
272      nodeStart: this.nodeStart,
273      v8Start: this.v8Start,
274      bootstrapComplete: this.bootstrapComplete,
275      environment: this.environment,
276      loopStart: this.loopStart,
277      loopExit: this.loopExit
278    };
279  }
280}
281
282const nodeTiming = new PerformanceNodeTiming();
283
284// Maintains a list of entries as a linked list stored in insertion order.
285class PerformanceObserverEntryList {
286  constructor() {
287    ObjectDefineProperties(this, {
288      [kEntries]: {
289        writable: true,
290        enumerable: false,
291        value: {}
292      }
293    });
294    L.init(this[kEntries]);
295  }
296
297  [kInsertEntry](entry) {
298    const item = { entry };
299    L.append(this[kEntries], item);
300  }
301
302  [kGetEntries](name, type) {
303    const ret = [];
304    const list = this[kEntries];
305    if (!L.isEmpty(list)) {
306      let item = L.peek(list);
307      while (item && item !== list) {
308        const entry = item.entry;
309        if ((name && entry.name !== name) ||
310            (type && entry.entryType !== type)) {
311          item = item._idlePrev;
312          continue;
313        }
314        sortedInsert(ret, entry);
315        item = item._idlePrev;
316      }
317    }
318    return ret;
319  }
320
321  // While the items are stored in insertion order, getEntries() is
322  // required to return items sorted by startTime.
323  getEntries() {
324    return this[kGetEntries]();
325  }
326
327  getEntriesByType(type) {
328    return this[kGetEntries](undefined, `${type}`);
329  }
330
331  getEntriesByName(name, type) {
332    return this[kGetEntries](`${name}`, type !== undefined ? `${type}` : type);
333  }
334}
335
336class PerformanceObserver extends AsyncResource {
337  constructor(callback) {
338    if (typeof callback !== 'function') {
339      throw new ERR_INVALID_CALLBACK(callback);
340    }
341    super('PerformanceObserver');
342    ObjectDefineProperties(this, {
343      [kTypes]: {
344        enumerable: false,
345        writable: true,
346        value: {}
347      },
348      [kCallback]: {
349        enumerable: false,
350        writable: true,
351        value: callback
352      },
353      [kBuffer]: {
354        enumerable: false,
355        writable: true,
356        value: new PerformanceObserverEntryList()
357      },
358      [kBuffering]: {
359        enumerable: false,
360        writable: true,
361        value: false
362      },
363      [kQueued]: {
364        enumerable: false,
365        writable: true,
366        value: false
367      }
368    });
369  }
370
371  disconnect() {
372    const observerCountsGC = observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC];
373    const types = this[kTypes];
374    ArrayPrototypeForEach(ObjectKeys(types), (key) => {
375      const item = types[key];
376      if (item) {
377        L.remove(item);
378        observerCounts[key]--;
379      }
380    });
381    this[kTypes] = {};
382    if (observerCountsGC === 1 &&
383      observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC] === 0) {
384      removeGarbageCollectionTracking();
385    }
386  }
387
388  observe(options) {
389    if (typeof options !== 'object' || options === null) {
390      throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
391    }
392    const { entryTypes } = options;
393    if (!ArrayIsArray(entryTypes)) {
394      throw new ERR_INVALID_OPT_VALUE('entryTypes', entryTypes);
395    }
396    const filteredEntryTypes =
397      ArrayPrototypeMap(ArrayPrototypeFilter(entryTypes, filterTypes),
398                        mapTypes);
399    if (filteredEntryTypes.length === 0) {
400      throw new ERR_VALID_PERFORMANCE_ENTRY_TYPE();
401    }
402    this.disconnect();
403    const observerCountsGC = observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC];
404    this[kBuffer][kEntries] = [];
405    L.init(this[kBuffer][kEntries]);
406    this[kBuffering] = Boolean(options.buffered);
407    ArrayPrototypeForEach(filteredEntryTypes, (entryType) => {
408      const list = getObserversList(entryType);
409      if (this[kTypes][entryType]) return;
410      const item = { obs: this };
411      this[kTypes][entryType] = item;
412      L.append(list, item);
413      observerCounts[entryType]++;
414    });
415    if (observerCountsGC === 0 &&
416      observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC] === 1) {
417      installGarbageCollectionTracking();
418    }
419  }
420}
421
422class Performance {
423  constructor() {
424    this[kIndex] = {
425      [kMarks]: new SafeSet()
426    };
427  }
428
429  get nodeTiming() {
430    return nodeTiming;
431  }
432
433  get timeOrigin() {
434    return timeOriginTimestamp;
435  }
436
437  now() {
438    return now() - timeOrigin;
439  }
440
441  mark(name) {
442    name = `${name}`;
443    _mark(name);
444    this[kIndex][kMarks].add(name);
445  }
446
447  measure(name, startMark, endMark) {
448    name = `${name}`;
449    const marks = this[kIndex][kMarks];
450    if (arguments.length >= 3) {
451      if (!marks.has(endMark) && !(endMark in nodeTiming))
452        throw new ERR_INVALID_PERFORMANCE_MARK(endMark);
453      else
454        endMark = `${endMark}`;
455    }
456    startMark = startMark !== undefined ? `${startMark}` : '';
457    _measure(name, startMark, endMark);
458  }
459
460  clearMarks(name) {
461    if (name !== undefined) {
462      name = `${name}`;
463      this[kIndex][kMarks].delete(name);
464      _clearMark(name);
465    } else {
466      this[kIndex][kMarks].clear();
467      _clearMark();
468    }
469  }
470
471  timerify(fn) {
472    if (typeof fn !== 'function') {
473      throw new ERR_INVALID_ARG_TYPE('fn', 'Function', fn);
474    }
475    if (fn[kTimerified])
476      return fn[kTimerified];
477    const ret = timerify(fn, fn.length);
478    ObjectDefineProperty(fn, kTimerified, {
479      enumerable: false,
480      configurable: true,
481      writable: false,
482      value: ret
483    });
484    ObjectDefineProperties(ret, {
485      [kTimerified]: {
486        enumerable: false,
487        configurable: true,
488        writable: false,
489        value: ret
490      },
491      name: {
492        enumerable: false,
493        configurable: true,
494        writable: false,
495        value: `timerified ${fn.name}`
496      }
497    });
498    return ret;
499  }
500
501  eventLoopUtilization(util1, util2) {
502    const ls = nodeTiming.loopStart;
503
504    if (ls <= 0) {
505      return { idle: 0, active: 0, utilization: 0 };
506    }
507
508    if (util2) {
509      const idle = util1.idle - util2.idle;
510      const active = util1.active - util2.active;
511      return { idle, active, utilization: active / (idle + active) };
512    }
513
514    const idle = nodeTiming.idleTime;
515    const active = performance.now() - ls - idle;
516
517    if (!util1) {
518      return { idle, active, utilization: active / (idle + active) };
519    }
520
521    const idle_delta = idle - util1.idle;
522    const active_delta = active - util1.active;
523    const utilization = active_delta / (idle_delta + active_delta);
524    return { idle: idle_delta, active: active_delta, utilization };
525  }
526
527  [kInspect]() {
528    return {
529      nodeTiming: this.nodeTiming,
530      timeOrigin: this.timeOrigin,
531      idleTime: this.idleTime,
532    };
533  }
534}
535
536const performance = new Performance();
537
538function getObserversList(type) {
539  let list = observers[type];
540  if (list === undefined) {
541    list = observers[type] = {};
542    L.init(list);
543  }
544  return list;
545}
546
547function doNotify(observer) {
548  observer[kQueued] = false;
549  observer.runInAsyncScope(observer[kCallback],
550                           observer,
551                           observer[kBuffer],
552                           observer);
553  observer[kBuffer][kEntries] = [];
554  L.init(observer[kBuffer][kEntries]);
555}
556
557// Set up the callback used to receive PerformanceObserver notifications
558function observersCallback(entry) {
559  const type = mapTypes(entry.entryType);
560
561  if (type === NODE_PERFORMANCE_ENTRY_TYPE_HTTP2)
562    collectHttp2Stats(entry);
563
564  const list = getObserversList(type);
565
566  let current = L.peek(list);
567
568  while (current && current.obs) {
569    const observer = current.obs;
570    // First, add the item to the observers buffer
571    const buffer = observer[kBuffer];
572    buffer[kInsertEntry](entry);
573    // Second, check to see if we're buffering
574    if (observer[kBuffering]) {
575      // If we are, schedule a setImmediate call if one hasn't already
576      if (!observer[kQueued]) {
577        observer[kQueued] = true;
578        // Use setImmediate instead of nextTick to give more time
579        // for multiple entries to collect.
580        setImmediate(doNotify, observer);
581      }
582    } else {
583      // If not buffering, notify immediately
584      doNotify(observer);
585    }
586    current = current._idlePrev;
587  }
588}
589setupObservers(observersCallback);
590
591function filterTypes(i) {
592  return ArrayPrototypeIncludes(observerableTypes, `${i}`);
593}
594
595function mapTypes(i) {
596  switch (i) {
597    case 'node': return NODE_PERFORMANCE_ENTRY_TYPE_NODE;
598    case 'mark': return NODE_PERFORMANCE_ENTRY_TYPE_MARK;
599    case 'measure': return NODE_PERFORMANCE_ENTRY_TYPE_MEASURE;
600    case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC;
601    case 'function': return NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION;
602    case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
603    case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP;
604  }
605}
606
607// The specification requires that PerformanceEntry instances are sorted
608// according to startTime. Unfortunately, they are not necessarily created
609// in that same order, and can be reported to the JS layer in any order,
610// which means we need to keep the list sorted as we insert.
611function getInsertLocation(list, entryStartTime) {
612  let start = 0;
613  let end = list.length;
614  while (start < end) {
615    const pivot = (end + start) >>> 1;
616    if (list[pivot].startTime === entryStartTime)
617      return pivot;
618    if (list[pivot].startTime < entryStartTime)
619      start = pivot + 1;
620    else
621      end = pivot;
622  }
623  return start;
624}
625
626function sortedInsert(list, entry) {
627  const entryStartTime = entry.startTime;
628  if (list.length === 0 ||
629      (list[list.length - 1].startTime < entryStartTime)) {
630    ArrayPrototypePush(list, entry);
631    return;
632  }
633  if (list[0] && (list[0].startTime > entryStartTime)) {
634    ArrayPrototypeUnshift(list, entry);
635    return;
636  }
637  const location = getInsertLocation(list, entryStartTime);
638  ArrayPrototypeSplice(list, location, 0, entry);
639}
640
641class ELDHistogram extends Histogram {
642  constructor(i) {
643    if (!(i instanceof _ELDHistogram)) {
644      // eslint-disable-next-line no-restricted-syntax
645      throw new TypeError('illegal constructor');
646    }
647    super(i);
648    this[kEnabled] = false;
649  }
650  enable() {
651    if (this[kEnabled]) return false;
652    this[kEnabled] = true;
653    this[kHandle].start();
654    return true;
655  }
656  disable() {
657    if (!this[kEnabled]) return false;
658    this[kEnabled] = false;
659    this[kHandle].stop();
660    return true;
661  }
662}
663
664function monitorEventLoopDelay(options = {}) {
665  if (typeof options !== 'object' || options === null) {
666    throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
667  }
668  const { resolution = 10 } = options;
669  if (typeof resolution !== 'number') {
670    throw new ERR_INVALID_ARG_TYPE('options.resolution',
671                                   'number', resolution);
672  }
673  if (resolution <= 0 || !NumberIsSafeInteger(resolution)) {
674    throw new ERR_INVALID_OPT_VALUE.RangeError('resolution', resolution);
675  }
676  return new ELDHistogram(new _ELDHistogram(resolution));
677}
678
679module.exports = {
680  performance,
681  PerformanceObserver,
682  monitorEventLoopDelay,
683  createHistogram,
684};
685
686ObjectDefineProperty(module.exports, 'constants', {
687  configurable: false,
688  enumerable: true,
689  value: constants
690});
691