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