Skip to content

Commit 589b2a1

Browse files
trevnorrisTrott
authored andcommitted
perf_hooks: add idleTime and event loop util
Use uv_metrics_idle_time() to return a high resolution millisecond timer of the amount of time the event loop has been idle since it was initialized. Include performance.eventLoopUtilization() API to handle the math of calculating the idle and active times. This has been added to prevent accidental miscalculations of the event loop utilization. Such as not taking into consideration offsetting nodeTiming.loopStart or timing differences when being called from a Worker thread. PR-URL: #34938 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Adrian Estrada <[email protected]> Reviewed-By: Juan José Arboleda <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Stephen Belanger <[email protected]> Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: Gerhard Stöbich <[email protected]>
1 parent 47f4080 commit 589b2a1

File tree

8 files changed

+279
-2
lines changed

8 files changed

+279
-2
lines changed

benchmark/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ directory, see [the guide on benchmarks](../doc/guides/writing-and-running-bench
3232
| module | Benchmarks for the `module` subsystem. |
3333
| net | Benchmarks for the `net` subsystem. |
3434
| path | Benchmarks for the `path` subsystem. |
35+
| perf_hooks | Benchmarks for the `perf_hooks` subsystem. |
3536
| process | Benchmarks for the `process` subsystem. |
3637
| querystring | Benchmarks for the `querystring` subsystem. |
3738
| streams | Benchmarks for the `streams` subsystem. |
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const assert = require('assert').ok;
5+
const { performance } = require('perf_hooks');
6+
const { nodeTiming, eventLoopUtilization } = performance;
7+
8+
const bench = common.createBenchmark(main, {
9+
n: [1e6],
10+
method: [
11+
'idleTime',
12+
'ELU_simple',
13+
'ELU_passed',
14+
],
15+
});
16+
17+
function main({ method, n }) {
18+
switch (method) {
19+
case 'idleTime':
20+
benchIdleTime(n);
21+
break;
22+
case 'ELU_simple':
23+
benchELUSimple(n);
24+
break;
25+
case 'ELU_passed':
26+
benchELUPassed(n);
27+
break;
28+
default:
29+
throw new Error(`Unsupported method ${method}`);
30+
}
31+
}
32+
33+
function benchIdleTime(n) {
34+
bench.start();
35+
for (let i = 0; i < n; i++)
36+
nodeTiming.idleTime;
37+
bench.end(n);
38+
}
39+
40+
function benchELUSimple(n) {
41+
// Need to put this in setImmediate or will always return 0.
42+
setImmediate(() => {
43+
const elu = eventLoopUtilization();
44+
assert(elu.active + elu.idle > 0);
45+
46+
bench.start();
47+
for (let i = 0; i < n; i++)
48+
eventLoopUtilization();
49+
bench.end(n);
50+
});
51+
}
52+
53+
function benchELUPassed(n) {
54+
// Need to put this in setImmediate or will always return 0.
55+
setImmediate(() => {
56+
let elu = eventLoopUtilization();
57+
assert(elu.active + elu.idle > 0);
58+
59+
bench.start();
60+
for (let i = 0; i < n; i++)
61+
elu = eventLoopUtilization(elu);
62+
bench.end(n);
63+
});
64+
}

doc/api/perf_hooks.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,62 @@ obs.observe({ entryTypes: ['function'] });
165165
wrapped();
166166
```
167167

168+
### `performance.eventLoopUtilization([util1][,util2])`
169+
<!-- YAML
170+
added: REPLACEME
171+
-->
172+
173+
* `util1` {Object} The result of a previous call to `eventLoopUtilization()`
174+
* `util2` {Object} The result of a previous call to `eventLoopUtilization()`
175+
prior to `util1`
176+
* Returns {Object}
177+
* `idle` {number}
178+
* `active` {number}
179+
* `utilization` {number}
180+
181+
The `eventLoopUtilization()` method returns an object that contains the
182+
cumulative duration of time the event loop has been both idle and active as a
183+
high resolution milliseconds timer. The `utilization` value is the calculated
184+
Event Loop Utilization (ELU). If bootstrapping has not yet finished, the
185+
properties have the value of 0.
186+
187+
`util1` and `util2` are optional parameters.
188+
189+
If `util1` is passed then the delta between the current call's `active` and
190+
`idle` times are calculated and returned (similar to [`process.hrtime()`][]).
191+
Likewise the adjusted `utilization` value is calculated.
192+
193+
If `util1` and `util2` are both passed then the calculation adjustments are
194+
done between the two arguments. This is a convenience option because unlike
195+
[`process.hrtime()`][] additional work is done to calculate the ELU.
196+
197+
ELU is similar to CPU utilization except that it is calculated using high
198+
precision wall-clock time. It represents the percentage of time the event loop
199+
has spent outside the event loop's event provider (e.g. `epoll_wait`). No other
200+
CPU idle time is taken into consideration. The following is an example of how
201+
a mostly idle process will have a high ELU.
202+
203+
<!-- eslint-skip -->
204+
```js
205+
'use strict';
206+
const { eventLoopUtilization } = require('perf_hooks').performance;
207+
const { spawnSync } = require('child_process');
208+
209+
setImmediate(() => {
210+
const elu = eventLoopUtilization();
211+
spawnSync('sleep', ['5']);
212+
console.log(eventLoopUtilization(elu).utilization);
213+
});
214+
```
215+
216+
While the CPU is mostly idle while running this script the value of
217+
`utilization` is 1. This is because the call to [`child_process.spawnSync()`][]
218+
blocks the event loop from proceeding.
219+
220+
Passing in a user-defined object instead of the result of a previous call to
221+
`eventLoopUtilization()` will lead to undefined behavior. The return values
222+
are not guaranteed to reflect any correct state of the event loop.
223+
168224
## Class: `PerformanceEntry`
169225
<!-- YAML
170226
added: v8.5.0
@@ -332,6 +388,19 @@ added: v8.5.0
332388
The high resolution millisecond timestamp at which the V8 platform was
333389
initialized.
334390

391+
### `performanceNodeTiming.idleTime`
392+
<!-- YAML
393+
added: REPLACEME
394+
-->
395+
396+
* {number}
397+
398+
The high resolution millisecond timestamp of the amount of time the event loop
399+
has been idle within the event loop's event provider (e.g. `epoll_wait`). This
400+
does not take CPU usage into consideration. If the event loop has not yet
401+
started (e.g., in the first tick of the main script), the property has the
402+
value of 0.
403+
335404
## Class: `perf_hooks.PerformanceObserver`
336405

337406
### `new PerformanceObserver(callback)`
@@ -688,6 +757,8 @@ require('some-module');
688757
```
689758

690759
[`'exit'`]: process.html#process_event_exit
760+
[`process.hrtime()`]: process.html#process_process_hrtime_time
761+
[`child_process.spawnSync()`]: #child_process_child_process_spawnsync_command_args_options
691762
[`timeOrigin`]: https://w3c.github.io/hr-time/#dom-performance-timeorigin
692763
[`window.performance`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/performance
693764
[Async Hooks]: async_hooks.html

lib/perf_hooks.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const {
2525
timerify,
2626
constants,
2727
installGarbageCollectionTracking,
28-
removeGarbageCollectionTracking
28+
removeGarbageCollectionTracking,
29+
loopIdleTime,
2930
} = internalBinding('performance');
3031

3132
const {
@@ -205,6 +206,10 @@ class PerformanceNodeTiming extends PerformanceEntry {
205206
return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE);
206207
}
207208

209+
get idleTime() {
210+
return loopIdleTime();
211+
}
212+
208213
[kInspect]() {
209214
return {
210215
name: 'node',
@@ -448,10 +453,37 @@ class Performance {
448453
return ret;
449454
}
450455

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+
451482
[kInspect]() {
452483
return {
453484
nodeTiming: this.nodeTiming,
454-
timeOrigin: this.timeOrigin
485+
timeOrigin: this.timeOrigin,
486+
idleTime: this.idleTime,
455487
};
456488
}
457489
}

src/node.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ int Start(int argc, char** argv) {
10791079
env_info = NodeMainInstance::GetEnvSerializeInfo();
10801080
}
10811081
}
1082+
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
10821083

10831084
NodeMainInstance main_instance(&params,
10841085
uv_default_loop(),

src/node_perf.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,13 @@ void Notify(const FunctionCallbackInfo<Value>& args) {
440440
}
441441
}
442442

443+
// Return idle time of the event loop
444+
void LoopIdleTime(const FunctionCallbackInfo<Value>& args) {
445+
Environment* env = Environment::GetCurrent(args);
446+
uint64_t idle_time = uv_metrics_idle_time(env->event_loop());
447+
args.GetReturnValue().Set(1.0 * idle_time / 1e6);
448+
}
449+
443450

444451
// Event Loop Timing Histogram
445452
namespace {
@@ -629,6 +636,7 @@ void Initialize(Local<Object> target,
629636
"removeGarbageCollectionTracking",
630637
RemoveGarbageCollectionTracking);
631638
env->SetMethod(target, "notify", Notify);
639+
env->SetMethod(target, "loopIdleTime", LoopIdleTime);
632640

633641
Local<Object> constants = Object::New(isolate);
634642

src/node_worker.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ class WorkerThreadData {
135135
return;
136136
}
137137
loop_init_failed_ = false;
138+
uv_loop_configure(&loop_, UV_METRICS_IDLE_TIME);
138139

139140
std::shared_ptr<ArrayBufferAllocator> allocator =
140141
ArrayBufferAllocator::Create();
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const TIMEOUT = 50;
6+
7+
const assert = require('assert');
8+
const { performance } = require('perf_hooks');
9+
const { Worker, parentPort } = require('worker_threads');
10+
11+
const { nodeTiming, eventLoopUtilization } = performance;
12+
const elu = eventLoopUtilization();
13+
14+
// Take into account whether this test was started as a Worker.
15+
if (nodeTiming.loopStart === -1) {
16+
assert.strictEqual(nodeTiming.idleTime, 0);
17+
assert.deepStrictEqual(elu, { idle: 0, active: 0, utilization: 0 });
18+
assert.deepStrictEqual(eventLoopUtilization(elu),
19+
{ idle: 0, active: 0, utilization: 0 });
20+
assert.deepStrictEqual(eventLoopUtilization(elu, eventLoopUtilization()),
21+
{ idle: 0, active: 0, utilization: 0 });
22+
}
23+
24+
// Place in setTimeout() to make sure there is some idle time, but not going to
25+
// assert this since it could make the test flaky.
26+
setTimeout(() => {
27+
const t = Date.now();
28+
const elu1 = eventLoopUtilization();
29+
30+
while (Date.now() - t < 50) { }
31+
32+
const elu2 = eventLoopUtilization();
33+
const elu3 = eventLoopUtilization(elu1);
34+
const elu4 = eventLoopUtilization(elu2, elu1);
35+
36+
assert.strictEqual(elu3.idle, 0);
37+
assert.strictEqual(elu4.idle, 0);
38+
assert.strictEqual(elu3.utilization, 1);
39+
assert.strictEqual(elu4.utilization, 1);
40+
assert.strictEqual(elu2.active - elu1.active, elu4.active);
41+
assert.ok(elu2.active > elu3.active);
42+
assert.ok(elu2.active > elu4.active);
43+
assert.ok(elu3.active > elu4.active);
44+
45+
setTimeout(runIdleTimeTest, TIMEOUT);
46+
}, 5);
47+
48+
function runIdleTimeTest() {
49+
const idleTime = nodeTiming.idleTime;
50+
const elu1 = eventLoopUtilization();
51+
const sum = elu1.idle + elu1.active;
52+
53+
assert.ok(sum >= elu1.idle && sum >= elu1.active,
54+
`idle: ${elu1.idle} active: ${elu1.active} sum: ${sum}`);
55+
assert.strictEqual(elu1.idle, idleTime);
56+
assert.strictEqual(elu1.utilization, elu1.active / sum);
57+
58+
setTimeout(runCalcTest, TIMEOUT, elu1);
59+
}
60+
61+
function runCalcTest(elu1) {
62+
const now = performance.now();
63+
const elu2 = eventLoopUtilization();
64+
const elu3 = eventLoopUtilization(elu2, elu1);
65+
const active_delta = elu2.active - elu1.active;
66+
const idle_delta = elu2.idle - elu1.idle;
67+
68+
assert.ok(elu2.idle >= 0);
69+
assert.ok(elu2.active >= 0);
70+
assert.ok(elu3.idle >= 0);
71+
assert.ok(elu3.active >= 0);
72+
assert.ok(elu2.idle + elu2.active > elu1.idle + elu2.active);
73+
assert.ok(elu2.idle + elu2.active >= now - nodeTiming.loopStart);
74+
assert.strictEqual(elu3.active, elu2.active - elu1.active);
75+
assert.strictEqual(elu3.idle, elu2.idle - elu1.idle);
76+
assert.strictEqual(elu3.utilization,
77+
active_delta / (idle_delta + active_delta));
78+
79+
setImmediate(runWorkerTest);
80+
}
81+
82+
function runWorkerTest() {
83+
// Use argv to detect whether we're running as a Worker called by this test
84+
// vs. this test also being called as a Worker.
85+
if (process.argv[2] === 'iamalive') {
86+
parentPort.postMessage(JSON.stringify(eventLoopUtilization()));
87+
return;
88+
}
89+
90+
const elu1 = eventLoopUtilization();
91+
const worker = new Worker(__filename, { argv: [ 'iamalive' ] });
92+
93+
worker.on('message', (msg) => {
94+
const elu2 = eventLoopUtilization(elu1);
95+
const data = JSON.parse(msg);
96+
97+
assert.ok(elu2.active + elu2.idle > data.active + data.idle);
98+
});
99+
}

0 commit comments

Comments
 (0)