Skip to content

Commit 6386c1a

Browse files
committed
cli: add --trace-atomics-wait flag
Adds a flag that helps with debugging deadlocks due to incorrectly implemented `Atomics.wait()` calls.
1 parent d135b50 commit 6386c1a

File tree

7 files changed

+160
-0
lines changed

7 files changed

+160
-0
lines changed

benchmark/worker/atomics-wait.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const bench = common.createBenchmark(main, {
5+
n: [1e7]
6+
});
7+
8+
function main({ n }) {
9+
const i32arr = new Int32Array(new SharedArrayBuffer(4));
10+
bench.start();
11+
for (let i = 0; i < n; i++)
12+
Atomics.wait(i32arr, 0, 1); // Will return immediately.
13+
bench.end(n);
14+
}

doc/api/cli.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,25 @@ added: v12.0.0
816816
Set default [`tls.DEFAULT_MIN_VERSION`][] to 'TLSv1.3'. Use to disable support
817817
for TLSv1.2, which is not as secure as TLSv1.3.
818818

819+
### `--trace-atomics-wait`
820+
<!-- YAML
821+
added: REPLACEME
822+
-->
823+
824+
Print short summaries of calls to `Atomics.wait()` to stderr.
825+
The output could look like this:
826+
827+
```text
828+
[Thread 0] Atomics.wait(0x55d134fe4290 + 0, 1, inf) started
829+
[Thread 0] Atomics.wait(0x55d134fe4290 + 0, 1, inf) did not wait because the values mismatched
830+
[Thread 0] Atomics.wait(0x55d134fe4290 + 0, 0, 10) started
831+
[Thread 0] Atomics.wait(0x55d134fe4290 + 0, 0, 10) timed out
832+
[Thread 0] Atomics.wait(0x55d134fe4290 + 4, 0, inf) started
833+
[Thread 1] Atomics.wait(0x55d134fe4290 + 4, -1, inf) started
834+
[Thread 0] Atomics.wait(0x55d134fe4290 + 4, 0, inf) was woken up by another thread
835+
[Thread 1] Atomics.wait(0x55d134fe4290 + 4, -1, inf) was woken up by another thread
836+
```
837+
819838
### `--trace-deprecation`
820839
<!-- YAML
821840
added: v0.8.0
@@ -1205,6 +1224,7 @@ Node.js options that are allowed are:
12051224
* `--tls-min-v1.1`
12061225
* `--tls-min-v1.2`
12071226
* `--tls-min-v1.3`
1227+
* `--trace-atomics-wait`
12081228
* `--trace-deprecation`
12091229
* `--trace-event-categories`
12101230
* `--trace-event-file-pattern`

doc/node.1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ but the option is supported for compatibility with older Node.js versions.
363363
Set default minVersion to 'TLSv1.3'. Use to disable support for TLSv1.2 in
364364
favour of TLSv1.3, which is more secure.
365365
.
366+
.It Fl -trace-atomics-wait
367+
Print short summaries of calls to
368+
.Sy Atomics.wait() .
369+
.
366370
.It Fl -trace-deprecation
367371
Print stack traces for deprecations.
368372
.

src/node.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,54 @@ int Environment::InitializeInspector(
229229
}
230230
#endif // HAVE_INSPECTOR && NODE_USE_V8_PLATFORM
231231

232+
#define ATOMIC_WAIT_EVENTS(V) \
233+
V(kStartWait, "started") \
234+
V(kWokenUp, "was woken up by another thread") \
235+
V(kTimedOut, "timed out") \
236+
V(kTerminatedExecution, "was stopped by terminated execution") \
237+
V(kAPIStopped, "was stopped through the embedder API") \
238+
V(kNotEqual, "did not wait because the values mismatched") \
239+
240+
static void AtomicsWaitCallback(Isolate::AtomicsWaitEvent event,
241+
Local<v8::SharedArrayBuffer> array_buffer,
242+
size_t offset_in_bytes, int64_t value,
243+
double timeout_in_ms,
244+
Isolate::AtomicsWaitWakeHandle* stop_handle,
245+
void* data) {
246+
Environment* env = static_cast<Environment*>(data);
247+
248+
const char* message;
249+
switch (event) {
250+
#define V(key, msg) \
251+
case Isolate::AtomicsWaitEvent::key: \
252+
message = msg; \
253+
break;
254+
ATOMIC_WAIT_EVENTS(V)
255+
#undef V
256+
}
257+
258+
fprintf(stderr,
259+
"[Thread %" PRIu64 "] Atomics.wait(%p + %zx, %" PRId64 ", %.f) %s\n",
260+
env->thread_id(),
261+
array_buffer->GetBackingStore()->Data(),
262+
offset_in_bytes,
263+
value,
264+
timeout_in_ms,
265+
message);
266+
}
267+
232268
void Environment::InitializeDiagnostics() {
233269
isolate_->GetHeapProfiler()->AddBuildEmbedderGraphCallback(
234270
Environment::BuildEmbedderGraph, this);
235271
if (options_->trace_uncaught)
236272
isolate_->SetCaptureStackTraceForUncaughtExceptions(true);
273+
if (options_->trace_atomics_wait) {
274+
isolate_->SetAtomicsWaitCallback(AtomicsWaitCallback, this);
275+
AddCleanupHook([](void* data) {
276+
Environment* env = static_cast<Environment*>(data);
277+
env->isolate()->SetAtomicsWaitCallback(nullptr, nullptr);
278+
}, this);
279+
}
237280

238281
#if defined HAVE_DTRACE || defined HAVE_ETW
239282
InitDTrace(this);

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
435435
"throw an exception on deprecations",
436436
&EnvironmentOptions::throw_deprecation,
437437
kAllowedInEnvironment);
438+
AddOption("--trace-atomics-wait",
439+
"trace Atomics.wait() operations",
440+
&EnvironmentOptions::trace_atomics_wait,
441+
kAllowedInEnvironment);
438442
AddOption("--trace-deprecation",
439443
"show stack traces on deprecations",
440444
&EnvironmentOptions::trace_deprecation,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ class EnvironmentOptions : public Options {
140140
std::string redirect_warnings;
141141
bool test_udp_no_try_send = false;
142142
bool throw_deprecation = false;
143+
bool trace_atomics_wait = false;
143144
bool trace_deprecation = false;
144145
bool trace_exit = false;
145146
bool trace_sync_io = false;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert');
4+
const child_process = require('child_process');
5+
const { Worker } = require('worker_threads');
6+
7+
if (process.argv[2] === 'child') {
8+
const i32arr = new Int32Array(new SharedArrayBuffer(8));
9+
assert.strictEqual(Atomics.wait(i32arr, 0, 1), 'not-equal');
10+
assert.strictEqual(Atomics.wait(i32arr, 0, 0, 10), 'timed-out');
11+
12+
new Worker(`
13+
const i32arr = require('worker_threads').workerData;
14+
Atomics.store(i32arr, 1, -1);
15+
Atomics.notify(i32arr, 1);
16+
Atomics.wait(i32arr, 1, -1);
17+
`, { eval: true, workerData: i32arr });
18+
19+
Atomics.wait(i32arr, 1, 0);
20+
assert.strictEqual(Atomics.load(i32arr, 1), -1);
21+
Atomics.store(i32arr, 1, 0);
22+
Atomics.notify(i32arr, 1);
23+
return;
24+
}
25+
26+
const proc = child_process.spawnSync(
27+
process.execPath,
28+
[ '--trace-atomics-wait', __filename, 'child' ],
29+
{ encoding: 'utf8', stdio: [ 'inherit', 'inherit', 'pipe' ] });
30+
31+
if (proc.status !== 0) console.log(proc);
32+
assert.strictEqual(proc.status, 0);
33+
34+
const expectedLines = [
35+
{ threadId: 0, offset: 0, value: 1, timeout: 'inf',
36+
message: 'started' },
37+
{ threadId: 0, offset: 0, value: 1, timeout: 'inf',
38+
message: 'did not wait because the values mismatched' },
39+
{ threadId: 0, offset: 0, value: 0, timeout: '10',
40+
message: 'started' },
41+
{ threadId: 0, offset: 0, value: 0, timeout: '10',
42+
message: 'timed out' },
43+
{ threadId: 0, offset: 4, value: 0, timeout: 'inf',
44+
message: 'started' },
45+
{ threadId: 1, offset: 4, value: -1, timeout: 'inf',
46+
message: 'started' },
47+
{ threadId: 0, offset: 4, value: 0, timeout: 'inf',
48+
message: 'was woken up by another thread' },
49+
{ threadId: 1, offset: 4, value: -1, timeout: 'inf',
50+
message: 'was woken up by another thread' }
51+
];
52+
53+
let SABAddress;
54+
const re = /^\[Thread (?<threadId>\d+)\] Atomics\.wait\((?<SAB>(?:0x)?[0-9a-f]+) \+ (?<offset>\d+), (?<value>-?\d+), (?<timeout>inf|infinity|[0-9.]+)\) (?<message>.+)$/;
55+
for (const line of proc.stderr.split('\n').map((line) => line.trim())) {
56+
if (!line) continue;
57+
console.log('Matching', { line });
58+
const actual = line.match(re).groups;
59+
const expected = expectedLines.shift();
60+
61+
if (SABAddress === undefined)
62+
SABAddress = actual.SAB;
63+
else
64+
assert.strictEqual(actual.SAB, SABAddress);
65+
66+
assert.strictEqual(+actual.threadId, expected.threadId);
67+
assert.strictEqual(+actual.offset, expected.offset);
68+
assert.strictEqual(+actual.value, expected.value);
69+
assert.strictEqual(actual.message, expected.message);
70+
if (expected.timeout === 'inf')
71+
assert.match(actual.timeout, /inf(inity)?/);
72+
else
73+
assert.strictEqual(actual.timeout, expected.timeout);
74+
}

0 commit comments

Comments
 (0)