Skip to content

Commit 035da74

Browse files
ShogunPandaaduh95
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 667ee82 commit 035da74

File tree

8 files changed

+287
-0
lines changed

8 files changed

+287
-0
lines changed

doc/api/process.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4247,6 +4247,25 @@ Thrown:
42474247
[DeprecationWarning: test] { name: 'DeprecationWarning' }
42484248
```
42494249
4250+
## `process.threadCpuUsage([previousValue])`
4251+
4252+
<!-- YAML
4253+
added: REPLACEME
4254+
-->
4255+
4256+
* `previousValue` {Object} A previous return value from calling
4257+
`process.cpuUsage()`
4258+
* Returns: {Object}
4259+
* `user` {integer}
4260+
* `system` {integer}
4261+
4262+
The `process.threadCpuUsage()` method returns the user and system CPU time usage of
4263+
the current worker thread, in an object with properties `user` and `system`, whose
4264+
values are microsecond values (millionth of a second).
4265+
4266+
The result of a previous call to `process.threadCpuUsage()` can be passed as the
4267+
argument to the function, to get a diff reading.
4268+
42504269
## `process.title`
42514270
42524271
<!-- YAML

lib/internal/bootstrap/node.js

Lines changed: 1 addition & 0 deletions
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;

lib/internal/process/per_thread.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const {
4040
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
4141
ERR_INVALID_ARG_TYPE,
4242
ERR_INVALID_ARG_VALUE,
43+
ERR_OPERATION_FAILED,
4344
ERR_OUT_OF_RANGE,
4445
ERR_UNKNOWN_SIGNAL,
4546
ERR_WORKER_UNSUPPORTED_OPERATION,
@@ -110,6 +111,7 @@ function nop() {}
110111
function wrapProcessMethods(binding) {
111112
const {
112113
cpuUsage: _cpuUsage,
114+
threadCpuUsage: _threadCpuUsage,
113115
memoryUsage: _memoryUsage,
114116
rss,
115117
resourceUsage: _resourceUsage,
@@ -162,6 +164,50 @@ function wrapProcessMethods(binding) {
162164
};
163165
}
164166

167+
const threadCpuValues = new Float64Array(2);
168+
169+
// Replace the native function with the JS version that calls the native
170+
// function.
171+
function threadCpuUsage(prevValue) {
172+
// If a previous value was passed in, ensure it has the correct shape.
173+
if (prevValue) {
174+
if (!previousValueIsValid(prevValue.user)) {
175+
validateObject(prevValue, 'prevValue');
176+
177+
validateNumber(prevValue.user, 'prevValue.user');
178+
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.user',
179+
prevValue.user);
180+
}
181+
182+
if (!previousValueIsValid(prevValue.system)) {
183+
validateNumber(prevValue.system, 'prevValue.system');
184+
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.system',
185+
prevValue.system);
186+
}
187+
}
188+
189+
if (process.platform === 'sunos') {
190+
throw new ERR_OPERATION_FAILED('threadCpuUsage is not available on SunOS');
191+
}
192+
193+
// Call the native function to get the current values.
194+
_threadCpuUsage(threadCpuValues);
195+
196+
// If a previous value was passed in, return diff of current from previous.
197+
if (prevValue) {
198+
return {
199+
user: threadCpuValues[0] - prevValue.user,
200+
system: threadCpuValues[1] - prevValue.system,
201+
};
202+
}
203+
204+
// If no previous value passed in, return current value.
205+
return {
206+
user: threadCpuValues[0],
207+
system: threadCpuValues[1],
208+
};
209+
}
210+
165211
// Ensure that a previously passed in value is valid. Currently, the native
166212
// implementation always returns numbers <= Number.MAX_SAFE_INTEGER.
167213
function previousValueIsValid(num) {
@@ -326,6 +372,7 @@ function wrapProcessMethods(binding) {
326372
return {
327373
_rawDebug,
328374
cpuUsage,
375+
threadCpuUsage,
329376
resourceUsage,
330377
memoryUsage,
331378
kill,

src/node_process_methods.cc

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

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

750774
SetMethod(isolate, target, "_debugEnd", DebugEnd);
@@ -793,6 +817,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
793817
registry->Register(GetAvailableMemory);
794818
registry->Register(Rss);
795819
registry->Register(CPUUsage);
820+
registry->Register(ThreadCPUUsage);
796821
registry->Register(ResourceUsage);
797822

798823
registry->Register(GetActiveRequests);
Lines changed: 87 additions & 0 deletions
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+
}
Lines changed: 91 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { FsDirBinding } from './internalBinding/fs_dir';
1010
import { MessagingBinding } from './internalBinding/messaging';
1111
import { OptionsBinding } from './internalBinding/options';
1212
import { OSBinding } from './internalBinding/os';
13+
import { ProcessBinding } from './internalBinding/process';
1314
import { SerdesBinding } from './internalBinding/serdes';
1415
import { SymbolsBinding } from './internalBinding/symbols';
1516
import { TimersBinding } from './internalBinding/timers';
@@ -35,6 +36,7 @@ interface InternalBindingMap {
3536
modules: ModulesBinding;
3637
options: OptionsBinding;
3738
os: OSBinding;
39+
process: ProcessBinding;
3840
serdes: SerdesBinding;
3941
symbols: SymbolsBinding;
4042
timers: TimersBinding;

typings/internalBinding/process.d.ts

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)