diff options
author | James M Snell <jasnell@gmail.com> | 2017-08-08 01:53:24 +0300 |
---|---|---|
committer | Myles Borins <mylesborins@google.com> | 2017-09-12 04:18:33 +0300 |
commit | f75faddb1f7ffb6b713f8fced7dc499eb6632df8 (patch) | |
tree | 4030ecd4f16f19304a31eee0ad0bb5d403fdde41 /test | |
parent | 8cee5d66bdadffd0a51cab70956da011937a9868 (diff) |
perf_hooks: implementation of the perf timing API
An initial implementation of the Performance Timing API for Node.js.
This is the same Performance Timing API implemented by modern browsers
with a number of Node.js specific properties. The User Timing mark()
and measure() APIs are implemented, garbage collection timing, and
node startup milestone timing.
```js
const { performance } = require('perf_hooks');
performance.mark('A');
setTimeout(() => {
performance.mark('B');
performance.measure('A to B', 'A', 'B');
const entry = performance.getEntriesByName('A to B', 'measure')[0];
console.log(entry.duration);
}, 10000);
```
The implementation is at the native layer and makes use of uv_hrtime().
This should enable *eventual* integration with things like Tracing
and Inspection.
The implementation is extensible and should allow us to add new
performance entry types as we go (e.g. for measuring i/o perf,
etc).
Documentation and a test are provided.
PR-URL: https://github.com/nodejs/node/pull/14680
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Diffstat (limited to 'test')
-rw-r--r-- | test/parallel/test-performance-function.js | 93 | ||||
-rwxr-xr-x | test/parallel/test-performance-gc.js | 51 | ||||
-rw-r--r-- | test/parallel/test-performance.js | 124 | ||||
-rw-r--r-- | test/parallel/test-performanceobserver.js | 139 |
4 files changed, 407 insertions, 0 deletions
diff --git a/test/parallel/test-performance-function.js b/test/parallel/test-performance-function.js new file mode 100644 index 00000000000..8dd4d3004ba --- /dev/null +++ b/test/parallel/test-performance-function.js @@ -0,0 +1,93 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const { + performance, + PerformanceObserver +} = require('perf_hooks'); + +{ + // Intentional non-op. Do not wrap in common.mustCall(); + const n = performance.timerify(() => {}); + n(); + const entries = performance.getEntriesByType('function'); + assert.strictEqual(entries.length, 0); + + const obs = new PerformanceObserver(common.mustCall((list) => { + const entries = list.getEntries(); + const entry = entries[0]; + assert(entry); + assert.strictEqual(entry.name, 'performance.timerify'); + assert.strictEqual(entry.entryType, 'function'); + assert.strictEqual(typeof entry.duration, 'number'); + assert.strictEqual(typeof entry.startTime, 'number'); + obs.disconnect(); + performance.clearFunctions(); + })); + obs.observe({ entryTypes: ['function'] }); + n(); +} + +{ + // If the error throws, the error should just be bubbled up and the + // performance timeline entry will not be reported. + const obs = new PerformanceObserver(common.mustNotCall()); + obs.observe({ entryTypes: ['function'] }); + const n = performance.timerify(() => { + throw new Error('test'); + }); + assert.throws(() => n(), /^Error: test$/); + const entries = performance.getEntriesByType('function'); + assert.strictEqual(entries.length, 0); + obs.disconnect(); +} + +{ + class N {} + const n = performance.timerify(N); + new n(); + const entries = performance.getEntriesByType('function'); + assert.strictEqual(entries.length, 0); + + const obs = new PerformanceObserver(common.mustCall((list) => { + const entries = list.getEntries(); + const entry = entries[0]; + assert.strictEqual(entry[0], 1); + assert.strictEqual(entry[1], 'abc'); + assert(entry); + assert.strictEqual(entry.name, 'N'); + assert.strictEqual(entry.entryType, 'function'); + assert.strictEqual(typeof entry.duration, 'number'); + assert.strictEqual(typeof entry.startTime, 'number'); + obs.disconnect(); + performance.clearFunctions(); + })); + obs.observe({ entryTypes: ['function'] }); + + new n(1, 'abc'); +} + +{ + [1, {}, [], null, undefined, Infinity].forEach((i) => { + assert.throws(() => performance.timerify(i), + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "fn" argument must be of type Function' + })); + }); +} + +// Function can only be wrapped once, also check length and name +{ + const m = (a, b = 1) => {}; + const n = performance.timerify(m); + const o = performance.timerify(m); + const p = performance.timerify(n); + assert.strictEqual(n, o); + assert.strictEqual(n, p); + assert.strictEqual(n.length, m.length); + assert.strictEqual(n.name, 'timerified m'); +} diff --git a/test/parallel/test-performance-gc.js b/test/parallel/test-performance-gc.js new file mode 100755 index 00000000000..1ff4c9ae629 --- /dev/null +++ b/test/parallel/test-performance-gc.js @@ -0,0 +1,51 @@ +// Flags: --expose-gc +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + performance, + PerformanceObserver +} = require('perf_hooks'); + +const { + NODE_PERFORMANCE_GC_MAJOR, + NODE_PERFORMANCE_GC_MINOR, + NODE_PERFORMANCE_GC_INCREMENTAL, + NODE_PERFORMANCE_GC_WEAKCB +} = process.binding('performance').constants; + +const kinds = [ + NODE_PERFORMANCE_GC_MAJOR, + NODE_PERFORMANCE_GC_MINOR, + NODE_PERFORMANCE_GC_INCREMENTAL, + NODE_PERFORMANCE_GC_WEAKCB +]; + +// No observers for GC events, no entries should appear +{ + global.gc(); + const entries = performance.getEntriesByType('gc'); + assert.strictEqual(entries.length, 0); +} + +// Adding an observer should force at least one gc to appear +{ + const obs = new PerformanceObserver(common.mustCallAtLeast((list) => { + const entry = list.getEntries()[0]; + assert(entry); + assert.strictEqual(entry.name, 'gc'); + assert.strictEqual(entry.entryType, 'gc'); + assert(kinds.includes(entry.kind)); + assert.strictEqual(typeof entry.startTime, 'number'); + assert.strictEqual(typeof entry.duration, 'number'); + + performance.clearGC(); + + const entries = performance.getEntriesByType('gc'); + assert.strictEqual(entries.length, 0); + obs.disconnect(); + })); + obs.observe({ entryTypes: ['gc'] }); + global.gc(); +} diff --git a/test/parallel/test-performance.js b/test/parallel/test-performance.js new file mode 100644 index 00000000000..62fb13ed289 --- /dev/null +++ b/test/parallel/test-performance.js @@ -0,0 +1,124 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { performance } = require('perf_hooks'); + +assert(performance); +assert(performance.nodeTiming); +assert.strictEqual(typeof performance.timeOrigin, 'number'); + +{ + const entries = performance.getEntries(); + assert(Array.isArray(entries)); + assert.strictEqual(entries.length, 1); + assert.strictEqual(entries[0], performance.nodeTiming); +} + +{ + const entries = performance.getEntriesByName('node'); + assert(Array.isArray(entries)); + assert.strictEqual(entries.length, 1); + assert.strictEqual(entries[0], performance.nodeTiming); +} + +{ + let n; + let entries = performance.getEntries(); + for (n = 0; n < entries.length; n++) { + const entry = entries[n]; + assert.notStrictEqual(entry.name, 'A'); + assert.notStrictEqual(entry.entryType, 'mark'); + } + performance.mark('A'); + entries = performance.getEntries(); + const markA = entries[1]; + assert.strictEqual(markA.name, 'A'); + assert.strictEqual(markA.entryType, 'mark'); + performance.clearMarks('A'); + entries = performance.getEntries(); + for (n = 0; n < entries.length; n++) { + const entry = entries[n]; + assert.notStrictEqual(entry.name, 'A'); + assert.notStrictEqual(entry.entryType, 'mark'); + } +} + +{ + let entries = performance.getEntries(); + for (let n = 0; n < entries.length; n++) { + const entry = entries[n]; + assert.notStrictEqual(entry.name, 'B'); + assert.notStrictEqual(entry.entryType, 'mark'); + } + performance.mark('B'); + entries = performance.getEntries(); + const markB = entries[1]; + assert.strictEqual(markB.name, 'B'); + assert.strictEqual(markB.entryType, 'mark'); + performance.clearMarks(); + entries = performance.getEntries(); + assert.strictEqual(entries.length, 1); +} + +{ + performance.mark('A'); + [undefined, null, 'foo', 'initialize', 1].forEach((i) => { + assert.doesNotThrow(() => performance.measure('test', i, 'A')); + }); + + [undefined, null, 'foo', 1].forEach((i) => { + assert.throws(() => performance.measure('test', 'A', i), + common.expectsError({ + code: 'ERR_INVALID_PERFORMANCE_MARK', + type: Error, + message: `The "${i}" performance mark has not been set` + })); + }); + + performance.clearMeasures(); + performance.clearMarks(); + + const entries = performance.getEntries(); + assert.strictEqual(entries.length, 1); +} + +{ + performance.mark('A'); + setImmediate(() => { + performance.mark('B'); + performance.measure('foo', 'A', 'B'); + const entry = performance.getEntriesByName('foo')[0]; + const markA = performance.getEntriesByName('A', 'mark')[0]; + const markB = performance.getEntriesByName('B', 'mark')[0]; + assert.strictEqual(entry.name, 'foo'); + assert.strictEqual(entry.entryType, 'measure'); + assert.strictEqual(entry.startTime, markA.startTime); + assert.strictEqual(entry.duration.toPrecision(3), + (markB.startTime - markA.startTime).toPrecision(3)); + }); +} + +assert.strictEqual(performance.nodeTiming.name, 'node'); +assert.strictEqual(performance.nodeTiming.entryType, 'node'); + +[ + 'startTime', + 'duration', + 'nodeStart', + 'v8Start', + 'bootstrapComplete', + 'environment', + 'loopStart', + 'loopExit', + 'thirdPartyMainStart', + 'thirdPartyMainEnd', + 'clusterSetupStart', + 'clusterSetupEnd', + 'moduleLoadStart', + 'moduleLoadEnd', + 'preloadModuleLoadStart', + 'preloadModuleLoadEnd' +].forEach((i) => { + assert.strictEqual(typeof performance.nodeTiming[i], 'number'); +}); diff --git a/test/parallel/test-performanceobserver.js b/test/parallel/test-performanceobserver.js new file mode 100644 index 00000000000..081362939ce --- /dev/null +++ b/test/parallel/test-performanceobserver.js @@ -0,0 +1,139 @@ +'use strict'; + +const common = require('../common'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { + observerCounts: counts +} = process.binding('performance'); +const { + performance, + PerformanceObserver, + constants +} = require('perf_hooks'); + +const { + NODE_PERFORMANCE_ENTRY_TYPE_NODE, + NODE_PERFORMANCE_ENTRY_TYPE_MARK, + NODE_PERFORMANCE_ENTRY_TYPE_MEASURE, + NODE_PERFORMANCE_ENTRY_TYPE_GC, + NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION, +} = constants; + +assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_NODE], 0); +assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 1); +assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MEASURE], 1); +assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_GC], 0); +assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION], 0); + +{ + [1, null, undefined, {}, [], Infinity].forEach((i) => { + assert.throws(() => new PerformanceObserver(i), + common.expectsError({ + code: 'ERR_INVALID_CALLBACK', + type: TypeError, + message: 'callback must be a function' + })); + }); + const observer = new PerformanceObserver(common.mustNotCall()); + + [1, null, undefined].forEach((i) => { + //observer.observe(i); + assert.throws(() => observer.observe(i), + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "options" argument must be of type Object' + })); + }); + + [1, undefined, null, {}, Infinity].forEach((i) => { + assert.throws(() => observer.observe({ entryTypes: i }), + common.expectsError({ + code: 'ERR_INVALID_OPT_VALUE', + type: TypeError, + message: 'The value "[object Object]" is invalid for ' + + 'option "entryTypes"' + })); + }); +} + +// Test Non-Buffered +{ + const observer = + new PerformanceObserver(common.mustCall(callback, 3)); + + const countdown = + new Countdown(3, common.mustCall(() => { + observer.disconnect(); + assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 1); + })); + + function callback(list, obs) { + assert.strictEqual(obs, observer); + const entries = list.getEntries(); + assert.strictEqual(entries.length, 1); + countdown.dec(); + } + assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 1); + assert.doesNotThrow(() => observer.observe({ entryTypes: ['mark'] })); + assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 2); + performance.mark('test1'); + performance.mark('test2'); + performance.mark('test3'); +} + + +// Test Buffered +{ + const observer = + new PerformanceObserver(common.mustCall(callback, 1)); + assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 1); + + function callback(list, obs) { + assert.strictEqual(obs, observer); + const entries = list.getEntries(); + assert.strictEqual(entries.length, 3); + observer.disconnect(); + assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 1); + + { + const entriesByName = list.getEntriesByName('test1'); + assert.strictEqual(entriesByName.length, 1); + assert.strictEqual(entriesByName[0].name, 'test1'); + assert.strictEqual(entriesByName[0].entryType, 'mark'); + } + + { + const entriesByName = list.getEntriesByName('test1', 'mark'); + assert.strictEqual(entriesByName.length, 1); + assert.strictEqual(entriesByName[0].name, 'test1'); + assert.strictEqual(entriesByName[0].entryType, 'mark'); + } + + { + const entriesByName = list.getEntriesByName('test1', 'measure'); + assert.strictEqual(entriesByName.length, 0); + } + + { + const entriesByType = list.getEntriesByType('measure'); + assert.strictEqual(entriesByType.length, 1); + assert.strictEqual(entriesByType[0].name, 'test3'); + assert.strictEqual(entriesByType[0].entryType, 'measure'); + } + } + + assert.doesNotThrow(() => { + observer.observe({ entryTypes: ['mark', 'measure'], buffered: true }); + }); + // Do this twice to make sure it doesn't throw + assert.doesNotThrow(() => { + observer.observe({ entryTypes: ['mark', 'measure'], buffered: true }); + }); + // Even tho we called twice, count should be 1 + assert.strictEqual(counts[NODE_PERFORMANCE_ENTRY_TYPE_MARK], 2); + performance.mark('test1'); + performance.mark('test2'); + performance.measure('test3', 'test1', 'test2'); +} |