Skip to content

Commit

Permalink
process: add threadCpuUsage
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Jan 18, 2025
1 parent 6f946c9 commit 5b09f78
Show file tree
Hide file tree
Showing 8 changed files with 269 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 @@ -4211,6 +4211,25 @@ Thrown:
[DeprecationWarning: test] { name: 'DeprecationWarning' }
```
## `process.threadCpuUsage([previousValue])`
<!-- YAML
added: v6.1.0
-->
* `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
42 changes: 42 additions & 0 deletions lib/internal/process/per_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function nop() {}
function wrapProcessMethods(binding) {
const {
cpuUsage: _cpuUsage,
threadCpuUsage: _threadCpuUsage,
memoryUsage: _memoryUsage,
rss,
resourceUsage: _resourceUsage,
Expand Down Expand Up @@ -148,6 +149,46 @@ 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);
}
}

// 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 +304,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 @@ -650,6 +673,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 @@ -694,6 +718,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
76 changes: 76 additions & 0 deletions test/parallel/test-process-threadCpuUsage-main-thread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

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
{
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;
}
}

// 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')"
}
);
}
89 changes: 89 additions & 0 deletions test/parallel/test-process-threadCpuUsage-worker-threads.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';

const { mustCall, platformTimeout, hasCrypto, skip } = require('../common');
const { ok, deepStrictEqual } = require('assert');
const { randomBytes, createHash } = require('crypto');
const { once } = require('events');
const { Worker, isMainThread, parentPort, threadId } = require('worker_threads');

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

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

// Do some work
return setInterval(() => {
createHash('sha256').update(buffer).end(buffer);
}, platformTimeout(index ** 2 * 100));
}

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

function validateResults(results) {
for (let i = 0; i < 4; i++) {
deepStrictEqual(results[i].threadId, i);
}

for (let i = 0; i < 3; i++) {
const processDifference = results[i].process.user / results[i + 1].process.user;
const threadDifference = results[i].thread.user / results[i + 1].thread.user;

//
// All process CPU usages should be the same. Technically they should have returned the same
// value but since we measure it at different times they vary a little bit.
// Let's allow a tolerance of 20%
//
ok(processDifference > 0.8);
ok(processDifference < 1.2);

//
// Each thread is configured so that the performLoad schedules a new hash with an interval two times bigger of the
// previous thread. In theory this should give each thread a load about half of the previous one.
// But since we can't really predict CPU scheduling, we just check a monotonic increasing sequence.
//
ok(threadDifference > 1.2);

Check failure on line 49 in test/parallel/test-process-threadCpuUsage-worker-threads.js

View workflow job for this annotation

GitHub Actions / test-macOS (macos-13)

--- stderr --- node:internal/process/promises:394 triggerUncaughtException(err, true /* fromPromise */); ^ AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: ok(threadDifference > 1.2) at validateResults (/Users/runner/work/node/node/node/test/parallel/test-process-threadCpuUsage-worker-threads.js:49:5) at Timeout.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-process-threadCpuUsage-worker-threads.js:77:5) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) { generatedMessage: true, code: 'ERR_ASSERTION', actual: false, expected: true, operator: '==' } Node.js v24.0.0-pre Command: out/Release/node --test-reporter=./test/common/test-error-reporter.js --test-reporter-destination=stdout --test-reporter=./test/common/test-error-reporter.js --test-reporter-destination=stdout /Users/runner/work/node/node/node/test/parallel/test-process-threadCpuUsage-worker-threads.js
}
}


// 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 (isMainThread) {
const workers = [];
for (let i = 0; i < 3; i++) {
workers.push(new Worker(__filename));
}

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

const results = [getUsages()];

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

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

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 @@ -33,6 +34,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 5b09f78

Please sign in to comment.