mirror of https://github.com/nodejs/node.git
process: add threadCpuUsage
PR-URL: https://github.com/nodejs/node/pull/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>
This commit is contained in:
parent
ccf496cff9
commit
c6ddfa52fb
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -36,6 +36,7 @@ const {
|
|||
codes: {
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
ERR_OPERATION_FAILED,
|
||||
ERR_OUT_OF_RANGE,
|
||||
ERR_UNKNOWN_SIGNAL,
|
||||
},
|
||||
|
@ -97,6 +98,7 @@ function nop() {}
|
|||
function wrapProcessMethods(binding) {
|
||||
const {
|
||||
cpuUsage: _cpuUsage,
|
||||
threadCpuUsage: _threadCpuUsage,
|
||||
memoryUsage: _memoryUsage,
|
||||
rss,
|
||||
resourceUsage: _resourceUsage,
|
||||
|
@ -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) {
|
||||
|
@ -263,6 +309,7 @@ function wrapProcessMethods(binding) {
|
|||
return {
|
||||
_rawDebug,
|
||||
cpuUsage,
|
||||
threadCpuUsage,
|
||||
resourceUsage,
|
||||
memoryUsage,
|
||||
kill,
|
||||
|
|
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
|
|
@ -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')"
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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();
|
|
@ -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';
|
||||
|
@ -34,6 +35,7 @@ interface InternalBindingMap {
|
|||
modules: ModulesBinding;
|
||||
options: OptionsBinding;
|
||||
os: OSBinding;
|
||||
process: ProcessBinding;
|
||||
serdes: SerdesBinding;
|
||||
symbols: SymbolsBinding;
|
||||
timers: TimersBinding;
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue