Skip to content

Commit 68a2665

Browse files
committed
src: add locksCounters() to monitor Web Locks usage
1 parent fdcf4d9 commit 68a2665

File tree

9 files changed

+401
-2
lines changed

9 files changed

+401
-2
lines changed

doc/api/globals.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,9 @@ navigator.locks.request('shared_resource', { mode: 'shared' }, async (lock) => {
839839

840840
See [`worker_threads.locks`][] for detailed API documentation.
841841

842+
Use [`process.locksCounters()`][] to monitor lock acquisitions, contention, and
843+
queue sizes across the process.
844+
842845
## Class: `PerformanceEntry`
843846

844847
<!-- YAML
@@ -1376,6 +1379,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
13761379
[`localStorage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
13771380
[`module`]: modules.md#module
13781381
[`perf_hooks.performance`]: perf_hooks.md#perf_hooksperformance
1382+
[`process.locksCounters()`]: process.md#processlockscounters
13791383
[`process.nextTick()`]: process.md#processnexttickcallback-args
13801384
[`process` object]: process.md#process
13811385
[`require()`]: modules.md#requireid

doc/api/process.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3735,6 +3735,60 @@ console.log(resourceUsage());
37353735
*/
37363736
```
37373737
3738+
## `process.locksCounters()`
3739+
3740+
<!-- YAML
3741+
added: REPLACEME
3742+
-->
3743+
3744+
* Returns: {Object} Web Locks usage statistics for the current process.
3745+
* `totalAborts` {bigint} Lock requests aborted (either `ifAvailable` could not
3746+
be granted, or the callback rejected/threw).
3747+
* `totalSteals` {bigint} Lock requests granted with `{ steal: true }`.
3748+
* `totalExclusiveAcquired` {bigint} Total exclusive locks acquired.
3749+
* `totalSharedAcquired` {bigint} Total shared locks acquired.
3750+
* `holdersExclusive` {number} Exclusive locks currently held.
3751+
* `holdersShared` {number} Shared locks currently held.
3752+
* `pendingExclusive` {number} Exclusive lock requests currently queued.
3753+
* `pendingShared` {number} Shared lock requests currently queued.
3754+
3755+
Returns an object containing lock usage metrics for the current process to provide visibility into
3756+
[`navigator.locks`][] (Web Locks API).
3757+
3758+
```mjs
3759+
import process from 'node:process';
3760+
import { locks } from 'node:worker_threads';
3761+
3762+
const before = process.locksCounters();
3763+
3764+
await locks.request('my_resource', async () => {
3765+
const current = process.locksCounters();
3766+
console.log(current.holdersExclusive); // 1
3767+
console.log(current.totalExclusiveAcquired - before.totalExclusiveAcquired); // 1n
3768+
});
3769+
3770+
const after = process.locksCounters();
3771+
console.log(after.holdersExclusive); // 0 (released)
3772+
console.log(after.totalExclusiveAcquired - before.totalExclusiveAcquired); // 1n (cumulative)
3773+
```
3774+
3775+
```cjs
3776+
const process = require('node:process');
3777+
const { locks } = require('node:worker_threads');
3778+
3779+
const before = process.locksCounters();
3780+
3781+
locks.request('my_resource', async () => {
3782+
const current = process.locksCounters();
3783+
console.log(current.holdersExclusive); // 1
3784+
console.log(current.totalExclusiveAcquired - before.totalExclusiveAcquired); // 1n
3785+
}).then(() => {
3786+
const after = process.locksCounters();
3787+
console.log(after.holdersExclusive); // 0 (released)
3788+
console.log(after.totalExclusiveAcquired - before.totalExclusiveAcquired); // 1n (cumulative)
3789+
});
3790+
```
3791+
37383792
## `process.send(message[, sendHandle[, options]][, callback])`
37393793
37403794
<!-- YAML
@@ -4535,6 +4589,7 @@ cases:
45354589
[`module.getSourceMapsSupport()`]: module.md#modulegetsourcemapssupport
45364590
[`module.isBuiltin(id)`]: module.md#moduleisbuiltinmodulename
45374591
[`module.setSourceMapsSupport()`]: module.md#modulesetsourcemapssupportenabled-options
4592+
[`navigator.locks`]: globals.md#navigatorlocks
45384593
[`net.Server`]: net.md#class-netserver
45394594
[`net.Socket`]: net.md#class-netsocket
45404595
[`os.constants.dlopen`]: os.md#dlopen-constants

doc/api/worker_threads.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,10 @@ added: v24.5.0
781781
An instance of a [`LockManager`][LockManager] that can be used to coordinate
782782
access to resources that may be shared across multiple threads within the same
783783
process. The API mirrors the semantics of the
784-
[browser `LockManager`][]
784+
[browser `LockManager`][].
785+
786+
Use [`process.locksCounters()`][] to monitor lock acquisitions, contention, and
787+
queue sizes across the process.
785788
786789
### Class: `Lock`
787790
@@ -2249,6 +2252,7 @@ thread spawned will spawn another until the application crashes.
22492252
[`process.env`]: process.md#processenv
22502253
[`process.execArgv`]: process.md#processexecargv
22512254
[`process.exit()`]: process.md#processexitcode
2255+
[`process.locksCounters()`]: process.md#processlockscounters
22522256
[`process.stderr`]: process.md#processstderr
22532257
[`process.stdin`]: process.md#processstdin
22542258
[`process.stdout`]: process.md#processstdout

lib/internal/bootstrap/node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ const rawMethods = internalBinding('process_methods');
172172
process.cpuUsage = wrapped.cpuUsage;
173173
process.threadCpuUsage = wrapped.threadCpuUsage;
174174
process.resourceUsage = wrapped.resourceUsage;
175+
process.locksCounters = wrapped.locksCounters;
175176
process.memoryUsage = wrapped.memoryUsage;
176177
process.constrainedMemory = rawMethods.constrainedMemory;
177178
process.availableMemory = rawMethods.availableMemory;

lib/internal/process/per_thread.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ function wrapProcessMethods(binding) {
110110
threadCpuUsage: _threadCpuUsage,
111111
memoryUsage: _memoryUsage,
112112
rss,
113+
locksCounters: _locksCounters,
113114
resourceUsage: _resourceUsage,
114115
loadEnvFile: _loadEnvFile,
115116
execve: _execve,
@@ -349,6 +350,23 @@ function wrapProcessMethods(binding) {
349350
};
350351
}
351352

353+
const locksCountersBuffer = new Float64Array(8);
354+
const locksCountersBigIntView = new BigUint64Array(locksCountersBuffer.buffer, 0, 4);
355+
356+
function locksCounters() {
357+
_locksCounters(locksCountersBuffer);
358+
return {
359+
totalAborts: locksCountersBigIntView[0],
360+
totalSteals: locksCountersBigIntView[1],
361+
totalExclusiveAcquired: locksCountersBigIntView[2],
362+
totalSharedAcquired: locksCountersBigIntView[3],
363+
holdersExclusive: locksCountersBuffer[4],
364+
holdersShared: locksCountersBuffer[5],
365+
pendingExclusive: locksCountersBuffer[6],
366+
pendingShared: locksCountersBuffer[7],
367+
};
368+
}
369+
352370
/**
353371
* Loads the `.env` file to process.env.
354372
* @param {string | URL | Buffer | undefined} path
@@ -372,6 +390,7 @@ function wrapProcessMethods(binding) {
372390
kill,
373391
exit,
374392
execve,
393+
locksCounters,
375394
loadEnvFile,
376395
};
377396
}

src/node_locks.cc

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,10 @@ void LockManager::ProcessQueue(Environment* env) {
319319
* remove later
320320
*/
321321
if (if_available_request) {
322+
{
323+
Mutex::ScopedLock scoped_lock(mutex_);
324+
counters.total_aborts++;
325+
}
322326
Local<Value> null_arg = Null(isolate);
323327
Local<Value> callback_result;
324328
{
@@ -456,7 +460,17 @@ void LockManager::ProcessQueue(Environment* env) {
456460
grantable_request->released_promise());
457461
{
458462
Mutex::ScopedLock scoped_lock(mutex_);
459-
held_locks_[grantable_request->name()].push_back(granted_lock);
463+
auto& resource_locks = held_locks_[grantable_request->name()];
464+
resource_locks.push_back(granted_lock);
465+
if (grantable_request->steal()) {
466+
counters.total_steals++;
467+
}
468+
469+
if (grantable_request->mode() == Lock::Mode::Exclusive) {
470+
counters.total_exclusive_acquired++;
471+
} else {
472+
counters.total_shared_acquired++;
473+
}
460474
}
461475

462476
// Create and store the new granted lock
@@ -715,6 +729,10 @@ void LockManager::ReleaseLockAndProcessQueue(Environment* env,
715729
// stolen.
716730
if (!lock->is_stolen()) {
717731
if (was_rejected) {
732+
{
733+
Mutex::ScopedLock scoped_lock(mutex_);
734+
counters.total_aborts++;
735+
}
718736
// Propagate rejection from the user callback
719737
if (lock->released_promise()
720738
->Reject(context, callback_result)
@@ -800,6 +818,37 @@ void LockManager::CleanupEnvironment(Environment* env_to_cleanup) {
800818
registered_envs_.erase(env_to_cleanup);
801819
}
802820

821+
LockManager::LocksCountersSnapshot LockManager::GetCountersSnapshot() const {
822+
LocksCountersSnapshot snapshot;
823+
Mutex::ScopedLock scoped_lock(mutex_);
824+
825+
snapshot.total_steals = counters.total_steals;
826+
snapshot.total_aborts = counters.total_aborts;
827+
snapshot.total_exclusive_acquired = counters.total_exclusive_acquired;
828+
snapshot.total_shared_acquired = counters.total_shared_acquired;
829+
830+
for (const auto& pending_request : pending_queue_) {
831+
if (pending_request->mode() == Lock::Mode::Exclusive) {
832+
snapshot.pending_exclusive++;
833+
} else {
834+
snapshot.pending_shared++;
835+
}
836+
}
837+
838+
for (const auto& resource : held_locks_) {
839+
const auto& locks = resource.second;
840+
for (const auto& lock : locks) {
841+
if (lock->mode() == Lock::Mode::Exclusive) {
842+
snapshot.holders_exclusive++;
843+
} else {
844+
snapshot.holders_shared++;
845+
}
846+
}
847+
}
848+
849+
return snapshot;
850+
}
851+
803852
// Cleanup hook wrapper
804853
void LockManager::OnEnvironmentCleanup(void* arg) {
805854
Environment* env = static_cast<Environment*>(arg);

src/node_locks.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ class LockManager final {
155155
std::shared_ptr<Lock> lock,
156156
v8::Local<v8::Value> result,
157157
bool was_rejected = false);
158+
struct LocksCountersSnapshot {
159+
uint64_t total_steals = 0;
160+
uint64_t total_aborts = 0;
161+
uint64_t total_exclusive_acquired = 0;
162+
uint64_t total_shared_acquired = 0;
163+
size_t holders_exclusive = 0;
164+
size_t holders_shared = 0;
165+
size_t pending_exclusive = 0;
166+
size_t pending_shared = 0;
167+
};
168+
LocksCountersSnapshot GetCountersSnapshot() const;
158169

159170
private:
160171
LockManager() = default;
@@ -171,6 +182,13 @@ class LockManager final {
171182
static LockManager current_;
172183

173184
mutable Mutex mutex_;
185+
struct Counters {
186+
uint64_t total_steals = 0;
187+
uint64_t total_aborts = 0;
188+
uint64_t total_exclusive_acquired = 0;
189+
uint64_t total_shared_acquired = 0;
190+
};
191+
Counters counters;
174192
// All entries for a given Environment* are purged in CleanupEnvironment().
175193
std::unordered_map<std::u16string, std::deque<std::shared_ptr<Lock>>>
176194
held_locks_;

src/node_process_methods.cc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "node_errors.h"
99
#include "node_external_reference.h"
1010
#include "node_internals.h"
11+
#include "node_locks.h"
1112
#include "node_process-inl.h"
1213
#include "path.h"
1314
#include "util-inl.h"
@@ -39,6 +40,7 @@ typedef int mode_t;
3940

4041
namespace node {
4142

43+
using node::worker::locks::LockManager;
4244
using v8::Array;
4345
using v8::ArrayBuffer;
4446
using v8::CFunction;
@@ -156,6 +158,25 @@ static void ThreadCPUUsage(const FunctionCallbackInfo<Value>& args) {
156158
fields[1] = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec;
157159
}
158160

161+
static void LocksCounters(const FunctionCallbackInfo<Value>& args) {
162+
LockManager::LocksCountersSnapshot snapshot =
163+
LockManager::GetCurrent()->GetCountersSnapshot();
164+
165+
Local<ArrayBuffer> ab = get_fields_array_buffer(args, 0, 8);
166+
167+
uint64_t* bigint_fields = static_cast<uint64_t*>(ab->Data());
168+
bigint_fields[0] = snapshot.total_aborts;
169+
bigint_fields[1] = snapshot.total_steals;
170+
bigint_fields[2] = snapshot.total_exclusive_acquired;
171+
bigint_fields[3] = snapshot.total_shared_acquired;
172+
173+
double* fields = static_cast<double*>(ab->Data());
174+
fields[4] = static_cast<double>(snapshot.holders_exclusive);
175+
fields[5] = static_cast<double>(snapshot.holders_shared);
176+
fields[6] = static_cast<double>(snapshot.pending_exclusive);
177+
fields[7] = static_cast<double>(snapshot.pending_shared);
178+
}
179+
159180
static void Cwd(const FunctionCallbackInfo<Value>& args) {
160181
Environment* env = Environment::GetCurrent(args);
161182
CHECK(env->has_run_bootstrapping_code());
@@ -770,6 +791,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
770791
SetMethod(isolate, target, "rss", Rss);
771792
SetMethod(isolate, target, "cpuUsage", CPUUsage);
772793
SetMethod(isolate, target, "threadCpuUsage", ThreadCPUUsage);
794+
SetMethodNoSideEffect(isolate, target, "locksCounters", LocksCounters);
773795
SetMethod(isolate, target, "resourceUsage", ResourceUsage);
774796

775797
SetMethod(isolate, target, "_debugEnd", DebugEnd);
@@ -819,6 +841,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
819841
registry->Register(Rss);
820842
registry->Register(CPUUsage);
821843
registry->Register(ThreadCPUUsage);
844+
registry->Register(LocksCounters);
822845
registry->Register(ResourceUsage);
823846

824847
registry->Register(GetActiveRequests);

0 commit comments

Comments
 (0)