Skip to content

Commit 5b09f78

Browse files
committed
process: add threadCpuUsage
1 parent 6f946c9 commit 5b09f78

File tree

8 files changed

+269
-0
lines changed

8 files changed

+269
-0
lines changed

doc/api/process.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4211,6 +4211,25 @@ Thrown:
42114211
[DeprecationWarning: test] { name: 'DeprecationWarning' }
42124212
```
42134213
4214+
## `process.threadCpuUsage([previousValue])`
4215+
4216+
<!-- YAML
4217+
added: v6.1.0
4218+
-->
4219+
4220+
* `previousValue` {Object} A previous return value from calling
4221+
`process.cpuUsage()`
4222+
* Returns: {Object}
4223+
* `user` {integer}
4224+
* `system` {integer}
4225+
4226+
The `process.threadCpuUsage()` method returns the user and system CPU time usage of
4227+
the current worker thread, in an object with properties `user` and `system`, whose
4228+
values are microsecond values (millionth of a second).
4229+
4230+
The result of a previous call to `process.threadCpuUsage()` can be passed as the
4231+
argument to the function, to get a diff reading.
4232+
42144233
## `process.title`
42154234
42164235
<!-- YAML

lib/internal/bootstrap/node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ const rawMethods = internalBinding('process_methods');
172172
process.loadEnvFile = wrapped.loadEnvFile;
173173
process._rawDebug = wrapped._rawDebug;
174174
process.cpuUsage = wrapped.cpuUsage;
175+
process.threadCpuUsage = wrapped.threadCpuUsage;
175176
process.resourceUsage = wrapped.resourceUsage;
176177
process.memoryUsage = wrapped.memoryUsage;
177178
process.constrainedMemory = rawMethods.constrainedMemory;

lib/internal/process/per_thread.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function nop() {}
9797
function wrapProcessMethods(binding) {
9898
const {
9999
cpuUsage: _cpuUsage,
100+
threadCpuUsage: _threadCpuUsage,
100101
memoryUsage: _memoryUsage,
101102
rss,
102103
resourceUsage: _resourceUsage,
@@ -148,6 +149,46 @@ function wrapProcessMethods(binding) {
148149
};
149150
}
150151

152+
const threadCpuValues = new Float64Array(2);
153+
154+
// Replace the native function with the JS version that calls the native
155+
// function.
156+
function threadCpuUsage(prevValue) {
157+
// If a previous value was passed in, ensure it has the correct shape.
158+
if (prevValue) {
159+
if (!previousValueIsValid(prevValue.user)) {
160+
validateObject(prevValue, 'prevValue');
161+
162+
validateNumber(prevValue.user, 'prevValue.user');
163+
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.user',
164+
prevValue.user);
165+
}
166+
167+
if (!previousValueIsValid(prevValue.system)) {
168+
validateNumber(prevValue.system, 'prevValue.system');
169+
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.system',
170+
prevValue.system);
171+
}
172+
}
173+
174+
// Call the native function to get the current values.
175+
_threadCpuUsage(threadCpuValues);
176+
177+
// If a previous value was passed in, return diff of current from previous.
178+
if (prevValue) {
179+
return {
180+
user: threadCpuValues[0] - prevValue.user,
181+
system: threadCpuValues[1] - prevValue.system,
182+
};
183+
}
184+
185+
// If no previous value passed in, return current value.
186+
return {
187+
user: threadCpuValues[0],
188+
system: threadCpuValues[1],
189+
};
190+
}
191+
151192
// Ensure that a previously passed in value is valid. Currently, the native
152193
// implementation always returns numbers <= Number.MAX_SAFE_INTEGER.
153194
function previousValueIsValid(num) {
@@ -263,6 +304,7 @@ function wrapProcessMethods(binding) {
263304
return {
264305
_rawDebug,
265306
cpuUsage,
307+
threadCpuUsage,
266308
resourceUsage,
267309
memoryUsage,
268310
kill,

src/node_process_methods.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,29 @@ static void CPUUsage(const FunctionCallbackInfo<Value>& args) {
130130
fields[1] = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec;
131131
}
132132

133+
// ThreadCPUUsage use libuv's uv_getrusage_thread() this-thread resource usage
134+
// accessor, to access ru_utime (user CPU time used) and ru_stime
135+
// (system CPU time used), which are uv_timeval_t structs
136+
// (long tv_sec, long tv_usec).
137+
// Returns those values as Float64 microseconds in the elements of the array
138+
// passed to the function.
139+
static void ThreadCPUUsage(const FunctionCallbackInfo<Value>& args) {
140+
Environment* env = Environment::GetCurrent(args);
141+
uv_rusage_t rusage;
142+
143+
// Call libuv to get the values we'll return.
144+
int err = uv_getrusage_thread(&rusage);
145+
if (err) return env->ThrowUVException(err, "uv_getrusage_thread");
146+
147+
// Get the double array pointer from the Float64Array argument.
148+
Local<ArrayBuffer> ab = get_fields_array_buffer(args, 0, 2);
149+
double* fields = static_cast<double*>(ab->Data());
150+
151+
// Set the Float64Array elements to be user / system values in microseconds.
152+
fields[0] = MICROS_PER_SEC * rusage.ru_utime.tv_sec + rusage.ru_utime.tv_usec;
153+
fields[1] = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec;
154+
}
155+
133156
static void Cwd(const FunctionCallbackInfo<Value>& args) {
134157
Environment* env = Environment::GetCurrent(args);
135158
CHECK(env->has_run_bootstrapping_code());
@@ -650,6 +673,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
650673
SetMethod(isolate, target, "availableMemory", GetAvailableMemory);
651674
SetMethod(isolate, target, "rss", Rss);
652675
SetMethod(isolate, target, "cpuUsage", CPUUsage);
676+
SetMethod(isolate, target, "threadCpuUsage", ThreadCPUUsage);
653677
SetMethod(isolate, target, "resourceUsage", ResourceUsage);
654678

655679
SetMethod(isolate, target, "_debugEnd", DebugEnd);
@@ -694,6 +718,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
694718
registry->Register(GetAvailableMemory);
695719
registry->Register(Rss);
696720
registry->Register(CPUUsage);
721+
registry->Register(ThreadCPUUsage);
697722
registry->Register(ResourceUsage);
698723

699724
registry->Register(GetActiveRequests);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const { ok, throws, notStrictEqual } = require('assert');
6+
7+
function validateResult(result) {
8+
notStrictEqual(result, null);
9+
10+
ok(Number.isFinite(result.user));
11+
ok(Number.isFinite(result.system));
12+
13+
ok(result.user >= 0);
14+
ok(result.system >= 0);
15+
}
16+
17+
// Test that process.threadCpuUsage() works on the main thread
18+
{
19+
const result = process.threadCpuUsage();
20+
21+
// Validate the result of calling with no previous value argument.
22+
validateResult(process.threadCpuUsage());
23+
24+
// Validate the result of calling with a previous value argument.
25+
validateResult(process.threadCpuUsage(result));
26+
27+
// Ensure the results are >= the previous.
28+
let thisUsage;
29+
let lastUsage = process.threadCpuUsage();
30+
for (let i = 0; i < 10; i++) {
31+
thisUsage = process.threadCpuUsage();
32+
validateResult(thisUsage);
33+
ok(thisUsage.user >= lastUsage.user);
34+
ok(thisUsage.system >= lastUsage.system);
35+
lastUsage = thisUsage;
36+
}
37+
}
38+
39+
// Test argument validaton
40+
{
41+
throws(
42+
() => process.threadCpuUsage(123),
43+
{
44+
code: 'ERR_INVALID_ARG_TYPE',
45+
name: 'TypeError',
46+
message: 'The "prevValue" argument must be of type object. Received type number (123)'
47+
}
48+
);
49+
50+
throws(
51+
() => process.threadCpuUsage([]),
52+
{
53+
code: 'ERR_INVALID_ARG_TYPE',
54+
name: 'TypeError',
55+
message: 'The "prevValue" argument must be of type object. Received an instance of Array'
56+
}
57+
);
58+
59+
throws(
60+
() => process.threadCpuUsage({ user: -123 }),
61+
{
62+
code: 'ERR_INVALID_ARG_VALUE',
63+
name: 'RangeError',
64+
message: "The property 'prevValue.user' is invalid. Received -123"
65+
}
66+
);
67+
68+
throws(
69+
() => process.threadCpuUsage({ user: 0, system: 'bar' }),
70+
{
71+
code: 'ERR_INVALID_ARG_TYPE',
72+
name: 'TypeError',
73+
message: "The \"prevValue.system\" property must be of type number. Received type string ('bar')"
74+
}
75+
);
76+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict';
2+
3+
const { mustCall, platformTimeout, hasCrypto, skip } = require('../common');
4+
const { ok, deepStrictEqual } = require('assert');
5+
const { randomBytes, createHash } = require('crypto');
6+
const { once } = require('events');
7+
const { Worker, isMainThread, parentPort, threadId } = require('worker_threads');
8+
9+
if (!hasCrypto) {
10+
skip('missing crypto');
11+
};
12+
13+
function performLoad() {
14+
const buffer = randomBytes(1e8);
15+
const index = threadId + 1;
16+
17+
// Do some work
18+
return setInterval(() => {
19+
createHash('sha256').update(buffer).end(buffer);
20+
}, platformTimeout(index ** 2 * 100));
21+
}
22+
23+
function getUsages() {
24+
return { threadId, process: process.cpuUsage(), thread: process.threadCpuUsage() };
25+
}
26+
27+
function validateResults(results) {
28+
for (let i = 0; i < 4; i++) {
29+
deepStrictEqual(results[i].threadId, i);
30+
}
31+
32+
for (let i = 0; i < 3; i++) {
33+
const processDifference = results[i].process.user / results[i + 1].process.user;
34+
const threadDifference = results[i].thread.user / results[i + 1].thread.user;
35+
36+
//
37+
// All process CPU usages should be the same. Technically they should have returned the same
38+
// value but since we measure it at different times they vary a little bit.
39+
// Let's allow a tolerance of 20%
40+
//
41+
ok(processDifference > 0.8);
42+
ok(processDifference < 1.2);
43+
44+
//
45+
// Each thread is configured so that the performLoad schedules a new hash with an interval two times bigger of the
46+
// previous thread. In theory this should give each thread a load about half of the previous one.
47+
// But since we can't really predict CPU scheduling, we just check a monotonic increasing sequence.
48+
//
49+
ok(threadDifference > 1.2);
50+
}
51+
}
52+
53+
54+
// The main thread will spawn three more threads, then after a while it will ask all of them to
55+
// report the thread CPU usage and exit.
56+
if (isMainThread) {
57+
const workers = [];
58+
for (let i = 0; i < 3; i++) {
59+
workers.push(new Worker(__filename));
60+
}
61+
62+
setTimeout(mustCall(async () => {
63+
clearInterval(interval);
64+
65+
const results = [getUsages()];
66+
67+
for (const worker of workers) {
68+
const statusPromise = once(worker, 'message');
69+
const exitPromise = once(worker, 'exit');
70+
71+
worker.postMessage('done');
72+
const [status] = await statusPromise;
73+
results.push(status);
74+
await exitPromise;
75+
}
76+
77+
validateResults(results);
78+
}), platformTimeout(5000));
79+
80+
} else {
81+
parentPort.on('message', () => {
82+
clearInterval(interval);
83+
parentPort.postMessage(getUsages());
84+
process.exit(0);
85+
});
86+
}
87+
88+
// Perform load on each thread
89+
const interval = performLoad();

typings/globals.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { FsDirBinding } from './internalBinding/fs_dir';
99
import { MessagingBinding } from './internalBinding/messaging';
1010
import { OptionsBinding } from './internalBinding/options';
1111
import { OSBinding } from './internalBinding/os';
12+
import { ProcessBinding } from './internalBinding/process';
1213
import { SerdesBinding } from './internalBinding/serdes';
1314
import { SymbolsBinding } from './internalBinding/symbols';
1415
import { TimersBinding } from './internalBinding/timers';
@@ -33,6 +34,7 @@ interface InternalBindingMap {
3334
modules: ModulesBinding;
3435
options: OptionsBinding;
3536
os: OSBinding;
37+
process: ProcessBinding;
3638
serdes: SerdesBinding;
3739
symbols: SymbolsBinding;
3840
timers: TimersBinding;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface CpuUsageValue {
2+
user: number;
3+
system: number;
4+
}
5+
6+
declare namespace InternalProcessBinding {
7+
interface Process {
8+
cpuUsage(previousValue?: CpuUsageValue): CpuUsageValue;
9+
threadCpuUsage(previousValue?: CpuUsageValue): CpuUsageValue;
10+
}
11+
}
12+
13+
export interface ProcessBinding {
14+
process: InternalProcessBinding.Process;
15+
}

0 commit comments

Comments
 (0)