diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index de2f0e00e14092..0f239cd91e3dd5 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -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; diff --git a/lib/internal/process/per_thread.js b/lib/internal/process/per_thread.js index 0921f583183d71..f49a168c6fd450 100644 --- a/lib/internal/process/per_thread.js +++ b/lib/internal/process/per_thread.js @@ -97,6 +97,7 @@ function nop() {} function wrapProcessMethods(binding) { const { cpuUsage: _cpuUsage, + threadCpuUsage: _threadCpuUsage, memoryUsage: _memoryUsage, rss, resourceUsage: _resourceUsage, @@ -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) { @@ -263,6 +304,7 @@ function wrapProcessMethods(binding) { return { _rawDebug, cpuUsage, + threadCpuUsage, resourceUsage, memoryUsage, kill, diff --git a/src/node_errors.h b/src/node_errors.h index a33177a5d8e7e6..9e6e6ccb84ab28 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -111,7 +111,8 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_WASI_NOT_STARTED, Error) \ V(ERR_ZLIB_INITIALIZATION_FAILED, Error) \ V(ERR_WORKER_INIT_FAILED, Error) \ - V(ERR_PROTO_ACCESS, Error) + V(ERR_PROTO_ACCESS, Error) \ + V(ERR_THREAD_CPU_USAGE_FAILED, Error) #define V(code, type) \ template \ @@ -214,7 +215,8 @@ ERRORS_WITH_CODE(V) V(ERR_WORKER_INIT_FAILED, "Worker initialization failure") \ V(ERR_PROTO_ACCESS, \ "Accessing Object.prototype.__proto__ has been " \ - "disallowed with --disable-proto=throw") + "disallowed with --disable-proto=throw") \ + V(ERR_THREAD_CPU_USAGE_FAILED, "Failed to get thread CPU usage") #define V(code, message) \ inline v8::Local code(v8::Isolate* isolate) { \ diff --git a/src/node_process_methods.cc b/src/node_process_methods.cc index 127ef63210b962..ea0fe3c0e851cc 100644 --- a/src/node_process_methods.cc +++ b/src/node_process_methods.cc @@ -130,6 +130,82 @@ static void CPUUsage(const FunctionCallbackInfo& args) { fields[1] = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec; } +// ThreadCPUUsage use system dependent implementation to get the current thread +// cpu times: +// +// - On Unix, it uses getrusage(2) with RUSAGE_THREAD. +// - On macOS, it uses thread_info(2) with THREAD_BASIC_INFO. +// - On Windows, it uses GetThreadTimes(2). +// +// Returns those values as Float64 microseconds in the elements of the array +// passed to the function. +static void ThreadCPUUsage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + // Get the double array pointer from the Float64Array argument. + Local ab = get_fields_array_buffer(args, 0, 2); + double* fields = static_cast(ab->Data()); + +#if defined(_WIN32) + // On Windows, GetCurrentThread will return the current thread pseudo-handler, + // which is then used in GetThreadTimes to retrieve the current thread timing + // information. + // The documentation is accessible here: + // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentthread + // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes + + HANDLE thread_ = GetCurrentThread(); + FILETIME _creation_time, _exit_time, kernel_time, user_time; + + if (!GetThreadTimes( + thread_, &_creation_time, &_exit_time, &kernel_time, &user_time)) { + return THROW_ERR_THREAD_CPU_USAGE_FAILED(env); + } + + // GetThreadTimes returns the time in 100-nanosecond intervals, so we need to + // convert it to microseconds. + fields[0] = (((static_cast(user_time.dwHighDateTime) << 32) | + static_cast(user_time.dwLowDateTime)) * + 100) / + 1000; + fields[1] = (((static_cast(system_ns.dwHighDateTime) << 32) | + static_cast(system_ns.dwLowDateTime)) * + 100) / + 1000; +#elif defined(__APPLE__) + // On MacOS, mach_thread_self returns the thread port for the current thread, + // then thread_info is used with THREAD_BASIC_INFO to get the current thread + // resource usage. The documentation is accessible here: + // https://www.gnu.org/software/hurd/gnumach-doc/Thread-Information.html. + mach_port_t thread = mach_thread_self(); + mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT; + thread_basic_info_data_t usage; + kern_return_t kr = + thread_info(thread, THREAD_BASIC_INFO, (thread_info_t)&usage, &count); + + if (kr != KERN_SUCCESS) { + return THROW_ERR_THREAD_CPU_USAGE_FAILED(env); + } + + fields[0] = + MICROS_PER_SEC * usage.user_time.seconds + usage.user_time.microseconds; + fields[1] = MICROS_PER_SEC * usage.system_time.seconds + + usage.system_time.microseconds; +#else + // On other Unix / Linux, getrusage(2) with RUSAGE_THREAD is used to get + // the current thread CPU usage. + // The documentation is accessible by running man getrusage. + struct rusage usage; + + if (getrusage(RUSAGE_THREAD, &usage) == -1) { + return THROW_ERR_THREAD_CPU_USAGE_FAILED(env); + } + + fields[0] = MICROS_PER_SEC * usage.ru_utime.tv_sec + usage.ru_utime.tv_usec; + fields[1] = MICROS_PER_SEC * usage.ru_stime.tv_sec + usage.ru_stime.tv_usec; +#endif +} + static void Cwd(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(env->has_run_bootstrapping_code()); @@ -650,6 +726,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); @@ -694,6 +771,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(GetAvailableMemory); registry->Register(Rss); registry->Register(CPUUsage); + registry->Register(ThreadCPUUsage); registry->Register(ResourceUsage); registry->Register(GetActiveRequests); diff --git a/test/parallel/test-process-threadCpuUsage-main-thread.js b/test/parallel/test-process-threadCpuUsage-main-thread.js new file mode 100644 index 00000000000000..80b7c9b16dccf6 --- /dev/null +++ b/test/parallel/test-process-threadCpuUsage-main-thread.js @@ -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')" + } + ); +} diff --git a/test/parallel/test-process-threadCpuUsage-worker-threads.js b/test/parallel/test-process-threadCpuUsage-worker-threads.js new file mode 100644 index 00000000000000..ceb1ff6405a234 --- /dev/null +++ b/test/parallel/test-process-threadCpuUsage-worker-threads.js @@ -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); + } +} + + +// 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(); diff --git a/typings/globals.d.ts b/typings/globals.d.ts index b37ce9428fd8f2..aef8b3f73b03a4 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -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'; @@ -33,6 +34,7 @@ interface InternalBindingMap { modules: ModulesBinding; options: OptionsBinding; os: OSBinding; + process: ProcessBinding; serdes: SerdesBinding; symbols: SymbolsBinding; timers: TimersBinding; diff --git a/typings/internalBinding/process.d.ts b/typings/internalBinding/process.d.ts new file mode 100644 index 00000000000000..99280fd43ba4d4 --- /dev/null +++ b/typings/internalBinding/process.d.ts @@ -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; +}