Skip to content

Commit 0236fbf

Browse files
ShogunPandatargos
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 8e3de98 commit 0236fbf

File tree

8 files changed

+287
-0
lines changed

8 files changed

+287
-0
lines changed

doc/api/process.md

+19
Original file line numberDiff line numberDiff line change
@@ -4204,6 +4204,25 @@ Thrown:
42044204
[DeprecationWarning: test] { name: 'DeprecationWarning' }
42054205
```
42064206
4207+
## `process.threadCpuUsage([previousValue])`
4208+
4209+
<!-- YAML
4210+
added: REPLACEME
4211+
-->
4212+
4213+
* `previousValue` {Object} A previous return value from calling
4214+
`process.cpuUsage()`
4215+
* Returns: {Object}
4216+
* `user` {integer}
4217+
* `system` {integer}
4218+
4219+
The `process.threadCpuUsage()` method returns the user and system CPU time usage of
4220+
the current worker thread, in an object with properties `user` and `system`, whose
4221+
values are microsecond values (millionth of a second).
4222+
4223+
The result of a previous call to `process.threadCpuUsage()` can be passed as the
4224+
argument to the function, to get a diff reading.
4225+
42074226
## `process.title`
42084227
42094228
<!-- YAML

lib/internal/bootstrap/node.js

+1
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

+47
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const {
3636
codes: {
3737
ERR_INVALID_ARG_TYPE,
3838
ERR_INVALID_ARG_VALUE,
39+
ERR_OPERATION_FAILED,
3940
ERR_OUT_OF_RANGE,
4041
ERR_UNKNOWN_SIGNAL,
4142
},
@@ -97,6 +98,7 @@ function nop() {}
9798
function wrapProcessMethods(binding) {
9899
const {
99100
cpuUsage: _cpuUsage,
101+
threadCpuUsage: _threadCpuUsage,
100102
memoryUsage: _memoryUsage,
101103
rss,
102104
resourceUsage: _resourceUsage,
@@ -148,6 +150,50 @@ function wrapProcessMethods(binding) {
148150
};
149151
}
150152

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

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());
@@ -651,6 +674,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
651674
SetMethod(isolate, target, "availableMemory", GetAvailableMemory);
652675
SetMethod(isolate, target, "rss", Rss);
653676
SetMethod(isolate, target, "cpuUsage", CPUUsage);
677+
SetMethod(isolate, target, "threadCpuUsage", ThreadCPUUsage);
654678
SetMethod(isolate, target, "resourceUsage", ResourceUsage);
655679

656680
SetMethod(isolate, target, "_debugEnd", DebugEnd);
@@ -695,6 +719,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
695719
registry->Register(GetAvailableMemory);
696720
registry->Register(Rss);
697721
registry->Register(CPUUsage);
722+
registry->Register(ThreadCPUUsage);
698723
registry->Register(ResourceUsage);
699724

700725
registry->Register(GetActiveRequests);
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();

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;

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)