Skip to content

Commit

Permalink
process: add threadCpuUsage
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
ShogunPanda authored and targos committed Feb 25, 2025
1 parent ccf496c commit c6ddfa5
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 0 deletions.
19 changes: 19 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -4204,6 +4204,25 @@ Thrown:
[DeprecationWarning: test] { name: 'DeprecationWarning' }
```
## `process.threadCpuUsage([previousValue])`
<!-- YAML
added: REPLACEME
-->
* `previousValue` {Object} A previous return value from calling
`process.cpuUsage()`
* Returns: {Object}
* `user` {integer}
* `system` {integer}
The `process.threadCpuUsage()` method returns the user and system CPU time usage of
the current worker thread, in an object with properties `user` and `system`, whose
values are microsecond values (millionth of a second).
The result of a previous call to `process.threadCpuUsage()` can be passed as the
argument to the function, to get a diff reading.
## `process.title`
<!-- YAML
Expand Down
1 change: 1 addition & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ const rawMethods = internalBinding('process_methods');
process.loadEnvFile = wrapped.loadEnvFile;
process._rawDebug = wrapped._rawDebug;
process.cpuUsage = wrapped.cpuUsage;
process.threadCpuUsage = wrapped.threadCpuUsage;
process.resourceUsage = wrapped.resourceUsage;
process.memoryUsage = wrapped.memoryUsage;
process.constrainedMemory = rawMethods.constrainedMemory;
Expand Down
47 changes: 47 additions & 0 deletions lib/internal/process/per_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OPERATION_FAILED,
ERR_OUT_OF_RANGE,
ERR_UNKNOWN_SIGNAL,
},
Expand Down Expand Up @@ -97,6 +98,7 @@ function nop() {}
function wrapProcessMethods(binding) {
const {
cpuUsage: _cpuUsage,
threadCpuUsage: _threadCpuUsage,
memoryUsage: _memoryUsage,
rss,
resourceUsage: _resourceUsage,
Expand Down Expand Up @@ -148,6 +150,50 @@ function wrapProcessMethods(binding) {
};
}

const threadCpuValues = new Float64Array(2);

// Replace the native function with the JS version that calls the native
// function.
function threadCpuUsage(prevValue) {
// If a previous value was passed in, ensure it has the correct shape.
if (prevValue) {
if (!previousValueIsValid(prevValue.user)) {
validateObject(prevValue, 'prevValue');

validateNumber(prevValue.user, 'prevValue.user');
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.user',
prevValue.user);
}

if (!previousValueIsValid(prevValue.system)) {
validateNumber(prevValue.system, 'prevValue.system');
throw new ERR_INVALID_ARG_VALUE.RangeError('prevValue.system',
prevValue.system);
}
}

if (process.platform === 'sunos') {
throw new ERR_OPERATION_FAILED('threadCpuUsage is not available on SunOS');
}

// Call the native function to get the current values.
_threadCpuUsage(threadCpuValues);

// If a previous value was passed in, return diff of current from previous.
if (prevValue) {
return {
user: threadCpuValues[0] - prevValue.user,
system: threadCpuValues[1] - prevValue.system,
};
}

// If no previous value passed in, return current value.
return {
user: threadCpuValues[0],
system: threadCpuValues[1],
};
}

// Ensure that a previously passed in value is valid. Currently, the native
// implementation always returns numbers <= Number.MAX_SAFE_INTEGER.
function previousValueIsValid(num) {
Expand Down Expand Up @@ -263,6 +309,7 @@ function wrapProcessMethods(binding) {
return {
_rawDebug,
cpuUsage,
threadCpuUsage,
resourceUsage,
memoryUsage,
kill,
Expand Down
25 changes: 25 additions & 0 deletions src/node_process_methods.cc
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,29 @@ static void CPUUsage(const FunctionCallbackInfo<Value>& args) {
fields[1] = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec;
}

// ThreadCPUUsage use libuv's uv_getrusage_thread() this-thread resource usage
// accessor, to access ru_utime (user CPU time used) and ru_stime
// (system CPU time used), which are uv_timeval_t structs
// (long tv_sec, long tv_usec).
// Returns those values as Float64 microseconds in the elements of the array
// passed to the function.
static void ThreadCPUUsage(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uv_rusage_t rusage;

// Call libuv to get the values we'll return.
int err = uv_getrusage_thread(&rusage);
if (err) return env->ThrowUVException(err, "uv_getrusage_thread");

// Get the double array pointer from the Float64Array argument.
Local<ArrayBuffer> ab = get_fields_array_buffer(args, 0, 2);
double* fields = static_cast<double*>(ab->Data());

// Set the Float64Array elements to be user / system values in microseconds.
fields[0] = MICROS_PER_SEC * rusage.ru_utime.tv_sec + rusage.ru_utime.tv_usec;
fields[1] = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec;
}

static void Cwd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(env->has_run_bootstrapping_code());
Expand Down Expand Up @@ -651,6 +674,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
SetMethod(isolate, target, "availableMemory", GetAvailableMemory);
SetMethod(isolate, target, "rss", Rss);
SetMethod(isolate, target, "cpuUsage", CPUUsage);
SetMethod(isolate, target, "threadCpuUsage", ThreadCPUUsage);
SetMethod(isolate, target, "resourceUsage", ResourceUsage);

SetMethod(isolate, target, "_debugEnd", DebugEnd);
Expand Down Expand Up @@ -695,6 +719,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetAvailableMemory);
registry->Register(Rss);
registry->Register(CPUUsage);
registry->Register(ThreadCPUUsage);
registry->Register(ResourceUsage);

registry->Register(GetActiveRequests);
Expand Down
87 changes: 87 additions & 0 deletions test/parallel/test-process-threadCpuUsage-main-thread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

const { isSunOS } = require('../common');

const { ok, throws, notStrictEqual } = require('assert');

function validateResult(result) {
notStrictEqual(result, null);

ok(Number.isFinite(result.user));
ok(Number.isFinite(result.system));

ok(result.user >= 0);
ok(result.system >= 0);
}

// Test that process.threadCpuUsage() works on the main thread
// The if check and the else branch should be removed once SmartOS support is fixed in
// https://github.com/libuv/libuv/issues/4706
if (!isSunOS) {
const result = process.threadCpuUsage();

// Validate the result of calling with no previous value argument.
validateResult(process.threadCpuUsage());

// Validate the result of calling with a previous value argument.
validateResult(process.threadCpuUsage(result));

// Ensure the results are >= the previous.
let thisUsage;
let lastUsage = process.threadCpuUsage();
for (let i = 0; i < 10; i++) {
thisUsage = process.threadCpuUsage();
validateResult(thisUsage);
ok(thisUsage.user >= lastUsage.user);
ok(thisUsage.system >= lastUsage.system);
lastUsage = thisUsage;
}
} else {
throws(
() => process.threadCpuUsage(),
{
code: 'ERR_OPERATION_FAILED',
name: 'Error',
message: 'Operation failed: threadCpuUsage is not available on SunOS'
}
);
}

// Test argument validaton
{
throws(
() => process.threadCpuUsage(123),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "prevValue" argument must be of type object. Received type number (123)'
}
);

throws(
() => process.threadCpuUsage([]),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "prevValue" argument must be of type object. Received an instance of Array'
}
);

throws(
() => process.threadCpuUsage({ user: -123 }),
{
code: 'ERR_INVALID_ARG_VALUE',
name: 'RangeError',
message: "The property 'prevValue.user' is invalid. Received -123"
}
);

throws(
() => process.threadCpuUsage({ user: 0, system: 'bar' }),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: "The \"prevValue.system\" property must be of type number. Received type string ('bar')"
}
);
}
91 changes: 91 additions & 0 deletions test/parallel/test-process-threadCpuUsage-worker-threads.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

const { mustCall, platformTimeout, hasCrypto, skip, isSunOS } = require('../common');

if (!hasCrypto) {
skip('missing crypto');
};

// This block can be removed once SmartOS support is fixed in
// https://github.com/libuv/libuv/issues/4706
// The behavior on SunOS is tested in
// test/parallel/test-process-threadCpuUsage-main-thread.js
if (isSunOS) {
skip('Operation not supported yet on SmartOS');
}

const { ok } = require('assert');
const { randomBytes, createHash } = require('crypto');
const { once } = require('events');
const { Worker, parentPort, workerData } = require('worker_threads');

const FREQUENCIES = [100, 500, 1000];

function performLoad() {
const buffer = randomBytes(1e8);

// Do some work
return setInterval(() => {
createHash('sha256').update(buffer).end(buffer);
}, platformTimeout(workerData?.frequency ?? 100));
}

function getUsages() {
return { process: process.cpuUsage(), thread: process.threadCpuUsage() };
}

function validateResults(results) {
// This test should have checked that the CPU usage of each thread is greater
// than the previous one, while the process one was not.
// Unfortunately, the real values are not really predictable on the CI so we
// just check that all the values are positive numbers.
for (let i = 0; i < 3; i++) {
ok(typeof results[i].process.user === 'number');
ok(results[i].process.user >= 0);

ok(typeof results[i].process.system === 'number');
ok(results[i].process.system >= 0);

ok(typeof results[i].thread.user === 'number');
ok(results[i].thread.user >= 0);

ok(typeof results[i].thread.system === 'number');
ok(results[i].thread.system >= 0);
}
}

// The main thread will spawn three more threads, then after a while it will ask all of them to
// report the thread CPU usage and exit.
if (!workerData?.frequency) { // Do not use isMainThread here otherwise test will not run in --worker mode
const workers = [];
for (const frequency of FREQUENCIES) {
workers.push(new Worker(__filename, { workerData: { frequency } }));
}

setTimeout(mustCall(async () => {
clearInterval(interval);

const results = [getUsages()];

for (const worker of workers) {
const statusPromise = once(worker, 'message');

worker.postMessage('done');
const [status] = await statusPromise;
results.push(status);
worker.terminate();
}

validateResults(results);
}), platformTimeout(5000));

} else {
parentPort.on('message', () => {
clearInterval(interval);
parentPort.postMessage(getUsages());
process.exit(0);
});
}

// Perform load on each thread
const interval = performLoad();
2 changes: 2 additions & 0 deletions typings/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FsDirBinding } from './internalBinding/fs_dir';
import { MessagingBinding } from './internalBinding/messaging';
import { OptionsBinding } from './internalBinding/options';
import { OSBinding } from './internalBinding/os';
import { ProcessBinding } from './internalBinding/process';
import { SerdesBinding } from './internalBinding/serdes';
import { SymbolsBinding } from './internalBinding/symbols';
import { TimersBinding } from './internalBinding/timers';
Expand All @@ -34,6 +35,7 @@ interface InternalBindingMap {
modules: ModulesBinding;
options: OptionsBinding;
os: OSBinding;
process: ProcessBinding;
serdes: SerdesBinding;
symbols: SymbolsBinding;
timers: TimersBinding;
Expand Down
15 changes: 15 additions & 0 deletions typings/internalBinding/process.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface CpuUsageValue {
user: number;
system: number;
}

declare namespace InternalProcessBinding {
interface Process {
cpuUsage(previousValue?: CpuUsageValue): CpuUsageValue;
threadCpuUsage(previousValue?: CpuUsageValue): CpuUsageValue;
}
}

export interface ProcessBinding {
process: InternalProcessBinding.Process;
}

0 comments on commit c6ddfa5

Please # to comment.