Skip to content

Commit a2bb647

Browse files
ShogunPandaRafaelGSS
authored andcommitted
process: add threadCpuUsage
PR-URL: #56467 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 78e4882 commit a2bb647

File tree

8 files changed

+287
-0
lines changed

8 files changed

+287
-0
lines changed

Diff for: doc/api/process.md

+19
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: REPLACEME
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

Diff for: lib/internal/bootstrap/node.js

+1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ const rawMethods = internalBinding('process_methods');
173173
process.loadEnvFile = wrapped.loadEnvFile;
174174
process._rawDebug = wrapped._rawDebug;
175175
process.cpuUsage = wrapped.cpuUsage;
176+
process.threadCpuUsage = wrapped.threadCpuUsage;
176177
process.resourceUsage = wrapped.resourceUsage;
177178
process.memoryUsage = wrapped.memoryUsage;
178179
process.constrainedMemory = rawMethods.constrainedMemory;

Diff for: lib/internal/process/per_thread.js

+47
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const {
3737
ERR_ASSERTION,
3838
ERR_INVALID_ARG_TYPE,
3939
ERR_INVALID_ARG_VALUE,
40+
ERR_OPERATION_FAILED,
4041
ERR_OUT_OF_RANGE,
4142
ERR_UNKNOWN_SIGNAL,
4243
},
@@ -101,6 +102,7 @@ function nop() {}
101102
function wrapProcessMethods(binding) {
102103
const {
103104
cpuUsage: _cpuUsage,
105+
threadCpuUsage: _threadCpuUsage,
104106
memoryUsage: _memoryUsage,
105107
rss,
106108
resourceUsage: _resourceUsage,
@@ -152,6 +154,50 @@ function wrapProcessMethods(binding) {
152154
};
153155
}
154156

157+
const threadCpuValues = new Float64Array(2);
158+
159+
// Replace the native function with the JS version that calls the native
160+
// function.
161+
function threadCpuUsage(prevValue) {
162+
// If a previous value was passed in, ensure it has the correct shape.
163+
if (prevValue) {
164+
if (!previousValueIsValid(prevValue.user)) {
165+
validateObject(prevValue, 'prevValue');
166+
167+
validateNumber(prevValue.user, 'prevValue.user');
168+
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.user',
169+
prevValue.user);
170+
}
171+
172+
if (!previousValueIsValid(prevValue.system)) {
173+
validateNumber(prevValue.system, 'prevValue.system');
174+
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.system',
175+
prevValue.system);
176+
}
177+
}
178+
179+
if (process.platform === 'sunos') {
180+
throw new ERR_OPERATION_FAILED('threadCpuUsage is not available on SunOS');
181+
}
182+
183+
// Call the native function to get the current values.
184+
_threadCpuUsage(threadCpuValues);
185+
186+
// If a previous value was passed in, return diff of current from previous.
187+
if (prevValue) {
188+
return {
189+
user: threadCpuValues[0] - prevValue.user,
190+
system: threadCpuValues[1] - prevValue.system,
191+
};
192+
}
193+
194+
// If no previous value passed in, return current value.
195+
return {
196+
user: threadCpuValues[0],
197+
system: threadCpuValues[1],
198+
};
199+
}
200+
155201
// Ensure that a previously passed in value is valid. Currently, the native
156202
// implementation always returns numbers <= Number.MAX_SAFE_INTEGER.
157203
function previousValueIsValid(num) {
@@ -267,6 +313,7 @@ function wrapProcessMethods(binding) {
267313
return {
268314
_rawDebug,
269315
cpuUsage,
316+
threadCpuUsage,
270317
resourceUsage,
271318
memoryUsage,
272319
kill,

Diff for: src/node_process_methods.cc

+25
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);
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
const { isSunOS } = 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+
// The if check and the else branch should be removed once SmartOS support is fixed in
19+
// https://github.com/libuv/libuv/issues/4706
20+
if (!isSunOS) {
21+
const result = process.threadCpuUsage();
22+
23+
// Validate the result of calling with no previous value argument.
24+
validateResult(process.threadCpuUsage());
25+
26+
// Validate the result of calling with a previous value argument.
27+
validateResult(process.threadCpuUsage(result));
28+
29+
// Ensure the results are >= the previous.
30+
let thisUsage;
31+
let lastUsage = process.threadCpuUsage();
32+
for (let i = 0; i < 10; i++) {
33+
thisUsage = process.threadCpuUsage();
34+
validateResult(thisUsage);
35+
ok(thisUsage.user >= lastUsage.user);
36+
ok(thisUsage.system >= lastUsage.system);
37+
lastUsage = thisUsage;
38+
}
39+
} else {
40+
throws(
41+
() => process.threadCpuUsage(),
42+
{
43+
code: 'ERR_OPERATION_FAILED',
44+
name: 'Error',
45+
message: 'Operation failed: threadCpuUsage is not available on SunOS'
46+
}
47+
);
48+
}
49+
50+
// Test argument validaton
51+
{
52+
throws(
53+
() => process.threadCpuUsage(123),
54+
{
55+
code: 'ERR_INVALID_ARG_TYPE',
56+
name: 'TypeError',
57+
message: 'The "prevValue" argument must be of type object. Received type number (123)'
58+
}
59+
);
60+
61+
throws(
62+
() => process.threadCpuUsage([]),
63+
{
64+
code: 'ERR_INVALID_ARG_TYPE',
65+
name: 'TypeError',
66+
message: 'The "prevValue" argument must be of type object. Received an instance of Array'
67+
}
68+
);
69+
70+
throws(
71+
() => process.threadCpuUsage({ user: -123 }),
72+
{
73+
code: 'ERR_INVALID_ARG_VALUE',
74+
name: 'RangeError',
75+
message: "The property 'prevValue.user' is invalid. Received -123"
76+
}
77+
);
78+
79+
throws(
80+
() => process.threadCpuUsage({ user: 0, system: 'bar' }),
81+
{
82+
code: 'ERR_INVALID_ARG_TYPE',
83+
name: 'TypeError',
84+
message: "The \"prevValue.system\" property must be of type number. Received type string ('bar')"
85+
}
86+
);
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const { mustCall, platformTimeout, hasCrypto, skip, isSunOS } = require('../common');
4+
5+
if (!hasCrypto) {
6+
skip('missing crypto');
7+
};
8+
9+
// This block can be removed once SmartOS support is fixed in
10+
// https://github.com/libuv/libuv/issues/4706
11+
// The behavior on SunOS is tested in
12+
// test/parallel/test-process-threadCpuUsage-main-thread.js
13+
if (isSunOS) {
14+
skip('Operation not supported yet on SmartOS');
15+
}
16+
17+
const { ok } = require('assert');
18+
const { randomBytes, createHash } = require('crypto');
19+
const { once } = require('events');
20+
const { Worker, parentPort, workerData } = require('worker_threads');
21+
22+
const FREQUENCIES = [100, 500, 1000];
23+
24+
function performLoad() {
25+
const buffer = randomBytes(1e8);
26+
27+
// Do some work
28+
return setInterval(() => {
29+
createHash('sha256').update(buffer).end(buffer);
30+
}, platformTimeout(workerData?.frequency ?? 100));
31+
}
32+
33+
function getUsages() {
34+
return { process: process.cpuUsage(), thread: process.threadCpuUsage() };
35+
}
36+
37+
function validateResults(results) {
38+
// This test should have checked that the CPU usage of each thread is greater
39+
// than the previous one, while the process one was not.
40+
// Unfortunately, the real values are not really predictable on the CI so we
41+
// just check that all the values are positive numbers.
42+
for (let i = 0; i < 3; i++) {
43+
ok(typeof results[i].process.user === 'number');
44+
ok(results[i].process.user >= 0);
45+
46+
ok(typeof results[i].process.system === 'number');
47+
ok(results[i].process.system >= 0);
48+
49+
ok(typeof results[i].thread.user === 'number');
50+
ok(results[i].thread.user >= 0);
51+
52+
ok(typeof results[i].thread.system === 'number');
53+
ok(results[i].thread.system >= 0);
54+
}
55+
}
56+
57+
// The main thread will spawn three more threads, then after a while it will ask all of them to
58+
// report the thread CPU usage and exit.
59+
if (!workerData?.frequency) { // Do not use isMainThread here otherwise test will not run in --worker mode
60+
const workers = [];
61+
for (const frequency of FREQUENCIES) {
62+
workers.push(new Worker(__filename, { workerData: { frequency } }));
63+
}
64+
65+
setTimeout(mustCall(async () => {
66+
clearInterval(interval);
67+
68+
const results = [getUsages()];
69+
70+
for (const worker of workers) {
71+
const statusPromise = once(worker, 'message');
72+
73+
worker.postMessage('done');
74+
const [status] = await statusPromise;
75+
results.push(status);
76+
worker.terminate();
77+
}
78+
79+
validateResults(results);
80+
}), platformTimeout(5000));
81+
82+
} else {
83+
parentPort.on('message', () => {
84+
clearInterval(interval);
85+
parentPort.postMessage(getUsages());
86+
process.exit(0);
87+
});
88+
}
89+
90+
// Perform load on each thread
91+
const interval = performLoad();

Diff for: typings/globals.d.ts

+2
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';
@@ -34,6 +35,7 @@ interface InternalBindingMap {
3435
modules: ModulesBinding;
3536
options: OptionsBinding;
3637
os: OSBinding;
38+
process: ProcessBinding;
3739
serdes: SerdesBinding;
3840
symbols: SymbolsBinding;
3941
timers: TimersBinding;

Diff for: typings/internalBinding/process.d.ts

+15
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)