• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayFrom,
5  ArrayIsArray,
6  ArrayPrototypeFilter,
7  ArrayPrototypeIncludes,
8  ArrayPrototypePush,
9  ArrayPrototypePushApply,
10  ArrayPrototypeSlice,
11  ArrayPrototypeSort,
12  Error,
13  MathMax,
14  MathMin,
15  ObjectDefineProperties,
16  ObjectFreeze,
17  ObjectKeys,
18  SafeMap,
19  SafeSet,
20  Symbol,
21} = primordials;
22
23const {
24  constants: {
25    NODE_PERFORMANCE_ENTRY_TYPE_GC,
26    NODE_PERFORMANCE_ENTRY_TYPE_HTTP2,
27    NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
28    NODE_PERFORMANCE_ENTRY_TYPE_NET,
29    NODE_PERFORMANCE_ENTRY_TYPE_DNS,
30  },
31  installGarbageCollectionTracking,
32  observerCounts,
33  removeGarbageCollectionTracking,
34  setupObservers,
35} = internalBinding('performance');
36
37const {
38  InternalPerformanceEntry,
39  isPerformanceEntry,
40} = require('internal/perf/performance_entry');
41
42const {
43  codes: {
44    ERR_INVALID_ARG_VALUE,
45    ERR_INVALID_ARG_TYPE,
46    ERR_MISSING_ARGS,
47  },
48} = require('internal/errors');
49
50const {
51  validateFunction,
52  validateObject,
53} = require('internal/validators');
54
55const {
56  customInspectSymbol: kInspect,
57  deprecate,
58  lazyDOMException,
59  kEmptyObject,
60} = require('internal/util');
61
62const {
63  setImmediate,
64} = require('timers');
65
66const { inspect } = require('util');
67
68const { now } = require('internal/perf/utils');
69const { convertToInt } = require('internal/webidl');
70
71const kDispatch = Symbol('kDispatch');
72const kMaybeBuffer = Symbol('kMaybeBuffer');
73const kDeprecatedFields = Symbol('kDeprecatedFields');
74
75const kDeprecationMessage =
76  'Custom PerformanceEntry accessors are deprecated. ' +
77  'Please use the detail property.';
78
79const kTypeSingle = 0;
80const kTypeMultiple = 1;
81
82let gcTrackingInstalled = false;
83
84const kSupportedEntryTypes = ObjectFreeze([
85  'dns',
86  'function',
87  'gc',
88  'http',
89  'http2',
90  'mark',
91  'measure',
92  'net',
93  'resource',
94]);
95
96// Performance timeline entry Buffers
97let markEntryBuffer = [];
98let measureEntryBuffer = [];
99let resourceTimingBuffer = [];
100let resourceTimingSecondaryBuffer = [];
101const kPerformanceEntryBufferWarnSize = 1e6;
102// https://www.w3.org/TR/timing-entrytypes-registry/#registry
103// Default buffer limit for resource timing entries.
104let resourceTimingBufferSizeLimit = 250;
105let dispatchBufferFull;
106let resourceTimingBufferFullPending = false;
107
108const kClearPerformanceEntryBuffers = ObjectFreeze({
109  'mark': 'performance.clearMarks',
110  'measure': 'performance.clearMeasures',
111});
112const kWarnedEntryTypes = new SafeMap();
113
114const kObservers = new SafeSet();
115const kPending = new SafeSet();
116let isPending = false;
117
118function queuePending() {
119  if (isPending) return;
120  isPending = true;
121  setImmediate(() => {
122    isPending = false;
123    const pendings = ArrayFrom(kPending.values());
124    kPending.clear();
125    for (const pending of pendings)
126      pending[kDispatch]();
127  });
128}
129
130function getObserverType(type) {
131  switch (type) {
132    case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC;
133    case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
134    case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP;
135    case 'net': return NODE_PERFORMANCE_ENTRY_TYPE_NET;
136    case 'dns': return NODE_PERFORMANCE_ENTRY_TYPE_DNS;
137  }
138}
139
140function maybeDecrementObserverCounts(entryTypes) {
141  for (const type of entryTypes) {
142    const observerType = getObserverType(type);
143
144    if (observerType !== undefined) {
145      observerCounts[observerType]--;
146
147      if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC &&
148          observerCounts[observerType] === 0) {
149        removeGarbageCollectionTracking();
150        gcTrackingInstalled = false;
151      }
152    }
153  }
154}
155
156function maybeIncrementObserverCount(type) {
157  const observerType = getObserverType(type);
158
159  if (observerType !== undefined) {
160    observerCounts[observerType]++;
161    if (!gcTrackingInstalled &&
162        observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC) {
163      installGarbageCollectionTracking();
164      gcTrackingInstalled = true;
165    }
166  }
167}
168
169class PerformanceObserverEntryList {
170  #buffer = [];
171
172  constructor(entries) {
173    this.#buffer = ArrayPrototypeSort(entries, (first, second) => {
174      return first.startTime - second.startTime;
175    });
176  }
177
178  getEntries() {
179    return ArrayPrototypeSlice(this.#buffer);
180  }
181
182  getEntriesByType(type) {
183    type = `${type}`;
184    return ArrayPrototypeFilter(
185      this.#buffer,
186      (entry) => entry.entryType === type);
187  }
188
189  getEntriesByName(name, type) {
190    name = `${name}`;
191    if (type != null /** not nullish */) {
192      return ArrayPrototypeFilter(
193        this.#buffer,
194        (entry) => entry.name === name && entry.entryType === type);
195    }
196    return ArrayPrototypeFilter(
197      this.#buffer,
198      (entry) => entry.name === name);
199  }
200
201  [kInspect](depth, options) {
202    if (depth < 0) return this;
203
204    const opts = {
205      ...options,
206      depth: options.depth == null ? null : options.depth - 1,
207    };
208
209    return `PerformanceObserverEntryList ${inspect(this.#buffer, opts)}`;
210  }
211}
212
213class PerformanceObserver {
214  #buffer = [];
215  #entryTypes = new SafeSet();
216  #type;
217  #callback;
218
219  constructor(callback) {
220    validateFunction(callback, 'callback');
221    this.#callback = callback;
222  }
223
224  observe(options = kEmptyObject) {
225    validateObject(options, 'options');
226    const {
227      entryTypes,
228      type,
229      buffered,
230    } = { ...options };
231    if (entryTypes === undefined && type === undefined)
232      throw new ERR_MISSING_ARGS('options.entryTypes', 'options.type');
233    if (entryTypes != null && type != null)
234      throw new ERR_INVALID_ARG_VALUE('options.entryTypes',
235                                      entryTypes,
236                                      'options.entryTypes can not set with ' +
237                                      'options.type together');
238
239    switch (this.#type) {
240      case undefined:
241        if (entryTypes !== undefined) this.#type = kTypeMultiple;
242        if (type !== undefined) this.#type = kTypeSingle;
243        break;
244      case kTypeSingle:
245        if (entryTypes !== undefined)
246          throw lazyDOMException(
247            'PerformanceObserver can not change to multiple observations',
248            'InvalidModificationError');
249        break;
250      case kTypeMultiple:
251        if (type !== undefined)
252          throw lazyDOMException(
253            'PerformanceObserver can not change to single observation',
254            'InvalidModificationError');
255        break;
256    }
257
258    if (this.#type === kTypeMultiple) {
259      if (!ArrayIsArray(entryTypes)) {
260        throw new ERR_INVALID_ARG_TYPE(
261          'options.entryTypes',
262          'string[]',
263          entryTypes);
264      }
265      maybeDecrementObserverCounts(this.#entryTypes);
266      this.#entryTypes.clear();
267      for (let n = 0; n < entryTypes.length; n++) {
268        if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) {
269          this.#entryTypes.add(entryTypes[n]);
270          maybeIncrementObserverCount(entryTypes[n]);
271        }
272      }
273    } else {
274      if (!ArrayPrototypeIncludes(kSupportedEntryTypes, type))
275        return;
276      this.#entryTypes.add(type);
277      maybeIncrementObserverCount(type);
278      if (buffered) {
279        const entries = filterBufferMapByNameAndType(undefined, type);
280        ArrayPrototypePushApply(this.#buffer, entries);
281        kPending.add(this);
282        if (kPending.size)
283          queuePending();
284      }
285    }
286
287    if (this.#entryTypes.size)
288      kObservers.add(this);
289    else
290      this.disconnect();
291  }
292
293  disconnect() {
294    maybeDecrementObserverCounts(this.#entryTypes);
295    kObservers.delete(this);
296    kPending.delete(this);
297    this.#buffer = [];
298    this.#entryTypes.clear();
299    this.#type = undefined;
300  }
301
302  takeRecords() {
303    const list = this.#buffer;
304    this.#buffer = [];
305    return list;
306  }
307
308  static get supportedEntryTypes() {
309    return kSupportedEntryTypes;
310  }
311
312  [kMaybeBuffer](entry) {
313    if (!this.#entryTypes.has(entry.entryType))
314      return;
315    ArrayPrototypePush(this.#buffer, entry);
316    kPending.add(this);
317    if (kPending.size)
318      queuePending();
319  }
320
321  [kDispatch]() {
322    this.#callback(new PerformanceObserverEntryList(this.takeRecords()),
323                   this);
324  }
325
326  [kInspect](depth, options) {
327    if (depth < 0) return this;
328
329    const opts = {
330      ...options,
331      depth: options.depth == null ? null : options.depth - 1,
332    };
333
334    return `PerformanceObserver ${inspect({
335      connected: kObservers.has(this),
336      pending: kPending.has(this),
337      entryTypes: ArrayFrom(this.#entryTypes),
338      buffer: this.#buffer,
339    }, opts)}`;
340  }
341}
342
343/**
344 * https://www.w3.org/TR/performance-timeline/#dfn-queue-a-performanceentry
345 *
346 * Add the performance entry to the interested performance observer's queue.
347 */
348function enqueue(entry) {
349  if (!isPerformanceEntry(entry))
350    throw new ERR_INVALID_ARG_TYPE('entry', 'PerformanceEntry', entry);
351
352  for (const obs of kObservers) {
353    obs[kMaybeBuffer](entry);
354  }
355}
356
357/**
358 * Add the user timing entry to the global buffer.
359 */
360function bufferUserTiming(entry) {
361  const entryType = entry.entryType;
362  let buffer;
363  if (entryType === 'mark') {
364    buffer = markEntryBuffer;
365  } else if (entryType === 'measure') {
366    buffer = measureEntryBuffer;
367  } else {
368    return;
369  }
370
371  ArrayPrototypePush(buffer, entry);
372  const count = buffer.length;
373
374  if (count > kPerformanceEntryBufferWarnSize &&
375    !kWarnedEntryTypes.has(entryType)) {
376    kWarnedEntryTypes.set(entryType, true);
377    // No error code for this since it is a Warning
378    // eslint-disable-next-line no-restricted-syntax
379    const w = new Error('Possible perf_hooks memory leak detected. ' +
380                        `${count} ${entryType} entries added to the global ` +
381                        'performance entry buffer. Use ' +
382                        `${kClearPerformanceEntryBuffers[entryType]} to ` +
383                        'clear the buffer.');
384    w.name = 'MaxPerformanceEntryBufferExceededWarning';
385    w.entryType = entryType;
386    w.count = count;
387    process.emitWarning(w);
388  }
389}
390
391/**
392 * Add the resource timing entry to the global buffer if the buffer size is not
393 * exceeding the buffer limit, or dispatch a buffer full event on the global
394 * performance object.
395 *
396 * See also https://www.w3.org/TR/resource-timing-2/#dfn-add-a-performanceresourcetiming-entry
397 */
398function bufferResourceTiming(entry) {
399  if (resourceTimingBuffer.length < resourceTimingBufferSizeLimit && !resourceTimingBufferFullPending) {
400    ArrayPrototypePush(resourceTimingBuffer, entry);
401    return;
402  }
403
404  if (!resourceTimingBufferFullPending) {
405    resourceTimingBufferFullPending = true;
406    setImmediate(() => {
407      while (resourceTimingSecondaryBuffer.length > 0) {
408        const excessNumberBefore = resourceTimingSecondaryBuffer.length;
409        dispatchBufferFull('resourcetimingbufferfull');
410
411        // Calculate the number of items to be pushed to the global buffer.
412        const numbersToPreserve = MathMax(
413          MathMin(resourceTimingBufferSizeLimit - resourceTimingBuffer.length, resourceTimingSecondaryBuffer.length),
414          0,
415        );
416        const excessNumberAfter = resourceTimingSecondaryBuffer.length - numbersToPreserve;
417        for (let idx = 0; idx < numbersToPreserve; idx++) {
418          ArrayPrototypePush(resourceTimingBuffer, resourceTimingSecondaryBuffer[idx]);
419        }
420
421        if (excessNumberBefore <= excessNumberAfter) {
422          resourceTimingSecondaryBuffer = [];
423        }
424      }
425      resourceTimingBufferFullPending = false;
426    });
427  }
428
429  ArrayPrototypePush(resourceTimingSecondaryBuffer, entry);
430}
431
432// https://w3c.github.io/resource-timing/#dom-performance-setresourcetimingbuffersize
433function setResourceTimingBufferSize(maxSize) {
434  // unsigned long
435  maxSize = convertToInt('maxSize', maxSize, 32);
436  // If the maxSize parameter is less than resource timing buffer current
437  // size, no PerformanceResourceTiming objects are to be removed from the
438  // performance entry buffer.
439  resourceTimingBufferSizeLimit = maxSize;
440}
441
442function setDispatchBufferFull(fn) {
443  dispatchBufferFull = fn;
444}
445
446function clearEntriesFromBuffer(type, name) {
447  if (type !== 'mark' && type !== 'measure' && type !== 'resource') {
448    return;
449  }
450
451  if (type === 'mark') {
452    markEntryBuffer = name === undefined ?
453      [] : ArrayPrototypeFilter(markEntryBuffer, (entry) => entry.name !== name);
454  } else if (type === 'measure') {
455    measureEntryBuffer = name === undefined ?
456      [] : ArrayPrototypeFilter(measureEntryBuffer, (entry) => entry.name !== name);
457  } else {
458    resourceTimingBuffer = name === undefined ?
459      [] : ArrayPrototypeFilter(resourceTimingBuffer, (entry) => entry.name !== name);
460  }
461}
462
463function filterBufferMapByNameAndType(name, type) {
464  let bufferList;
465  if (type === 'mark') {
466    bufferList = markEntryBuffer;
467  } else if (type === 'measure') {
468    bufferList = measureEntryBuffer;
469  } else if (type === 'resource') {
470    bufferList = resourceTimingBuffer;
471  } else if (type !== undefined) {
472    // Unrecognized type;
473    return [];
474  } else {
475    bufferList = [];
476    ArrayPrototypePushApply(bufferList, markEntryBuffer);
477    ArrayPrototypePushApply(bufferList, measureEntryBuffer);
478    ArrayPrototypePushApply(bufferList, resourceTimingBuffer);
479  }
480  if (name !== undefined) {
481    bufferList = ArrayPrototypeFilter(bufferList, (buffer) => buffer.name === name);
482  } else if (type !== undefined) {
483    bufferList = ArrayPrototypeSlice(bufferList);
484  }
485
486  return ArrayPrototypeSort(bufferList, (first, second) => {
487    return first.startTime - second.startTime;
488  });
489}
490
491function observerCallback(name, type, startTime, duration, details) {
492  const entry =
493    new InternalPerformanceEntry(
494      name,
495      type,
496      startTime,
497      duration,
498      details);
499
500  if (details !== undefined) {
501    // GC, HTTP2, and HTTP PerformanceEntry used additional
502    // properties directly off the entry. Those have been
503    // moved into the details property. The existing accessors
504    // are still included but are deprecated.
505    entry[kDeprecatedFields] = new SafeMap();
506
507    const detailKeys = ObjectKeys(details);
508    const props = {};
509    for (let n = 0; n < detailKeys.length; n++) {
510      const key = detailKeys[n];
511      entry[kDeprecatedFields].set(key, details[key]);
512      props[key] = {
513        configurable: true,
514        enumerable: true,
515        get: deprecate(() => {
516          return entry[kDeprecatedFields].get(key);
517        }, kDeprecationMessage, 'DEP0152'),
518        set: deprecate((value) => {
519          entry[kDeprecatedFields].set(key, value);
520        }, kDeprecationMessage, 'DEP0152'),
521      };
522    }
523    ObjectDefineProperties(entry, props);
524  }
525
526  enqueue(entry);
527}
528
529setupObservers(observerCallback);
530
531function hasObserver(type) {
532  const observerType = getObserverType(type);
533  return observerCounts[observerType] > 0;
534}
535
536
537function startPerf(target, key, context = {}) {
538  target[key] = {
539    ...context,
540    startTime: now(),
541  };
542}
543
544function stopPerf(target, key, context = {}) {
545  const ctx = target[key];
546  if (!ctx) {
547    return;
548  }
549  const startTime = ctx.startTime;
550  const entry = new InternalPerformanceEntry(
551    ctx.name,
552    ctx.type,
553    startTime,
554    now() - startTime,
555    { ...ctx.detail, ...context.detail },
556  );
557  enqueue(entry);
558}
559
560module.exports = {
561  PerformanceObserver,
562  PerformanceObserverEntryList,
563  enqueue,
564  hasObserver,
565  clearEntriesFromBuffer,
566  filterBufferMapByNameAndType,
567  startPerf,
568  stopPerf,
569
570  bufferUserTiming,
571  bufferResourceTiming,
572  setResourceTimingBufferSize,
573  setDispatchBufferFull,
574};
575