Skip to content

Commit ffee1b2

Browse files
authored
feat: Allow disabling of watchdog tracking per thread (#11)
1 parent 9d955f7 commit ffee1b2

File tree

6 files changed

+120
-60
lines changed

6 files changed

+120
-60
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.2.0
4+
5+
- feat: Allow disabling of watchdog tracking per thread (#11)
6+
37
## 0.1.1
48

59
- meta: Improve `README.md`, `package.json` metadata and add `LICENSE` (#10)

README.md

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ a threshold, JavaScript stack traces can be captured. The heartbeats can
99
optionally include state information which is included with the corresponding
1010
stack trace.
1111

12+
This native module is used for Sentry's
13+
[Event Loop Blocked Detection](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/event-loop-block/)
14+
feature.
15+
1216
## Basic Usage
1317

1418
### 1. Register threads you want to monitor
@@ -38,58 +42,62 @@ Stack traces show where each thread is currently executing:
3842

3943
```js
4044
{
41-
'0': [
42-
{
43-
function: 'from',
44-
filename: 'node:buffer',
45-
lineno: 298,
46-
colno: 28
47-
},
48-
{
49-
function: 'pbkdf2Sync',
50-
filename: 'node:internal/crypto/pbkdf2',
51-
lineno: 78,
52-
colno: 17
53-
},
54-
{
55-
function: 'longWork',
56-
filename: '/app/test.js',
57-
lineno: 20,
58-
colno: 29
59-
},
60-
{
61-
function: '?',
62-
filename: '/app/test.js',
63-
lineno: 24,
64-
colno: 1
65-
}
66-
],
67-
'2': [
68-
{
69-
function: 'from',
70-
filename: 'node:buffer',
71-
lineno: 298,
72-
colno: 28
73-
},
74-
{
75-
function: 'pbkdf2Sync',
76-
filename: 'node:internal/crypto/pbkdf2',
77-
lineno: 78,
78-
colno: 17
79-
},
80-
{
81-
function: 'longWork',
82-
filename: '/app/worker.js',
83-
lineno: 10,
84-
colno: 29
85-
},
86-
{
87-
function: '?',
88-
filename: '/app/worker.js',
89-
lineno: 14,
90-
colno: 1
91-
}
92-
]
45+
'0': { // Main thread has ID '0'
46+
frames: [
47+
{
48+
function: 'from',
49+
filename: 'node:buffer',
50+
lineno: 298,
51+
colno: 28
52+
},
53+
{
54+
function: 'pbkdf2Sync',
55+
filename: 'node:internal/crypto/pbkdf2',
56+
lineno: 78,
57+
colno: 17
58+
},
59+
{
60+
function: 'longWork',
61+
filename: '/app/test.js',
62+
lineno: 20,
63+
colno: 29
64+
},
65+
{
66+
function: '?',
67+
filename: '/app/test.js',
68+
lineno: 24,
69+
colno: 1
70+
}
71+
]
72+
},
73+
'2': { // Worker thread
74+
frames: [
75+
{
76+
function: 'from',
77+
filename: 'node:buffer',
78+
lineno: 298,
79+
colno: 28
80+
},
81+
{
82+
function: 'pbkdf2Sync',
83+
filename: 'node:internal/crypto/pbkdf2',
84+
lineno: 78,
85+
colno: 17
86+
},
87+
{
88+
function: 'longWork',
89+
filename: '/app/worker.js',
90+
lineno: 10,
91+
colno: 29
92+
},
93+
{
94+
function: '?',
95+
filename: '/app/worker.js',
96+
lineno: 14,
97+
colno: 1
98+
}
99+
]
100+
}
93101
}
94102
```
95103

@@ -179,12 +187,17 @@ type StackFrame = {
179187
};
180188
```
181189

182-
#### `threadPoll<State>(state?: State): void`
190+
#### `threadPoll<State>(state?: State, disableLastSeen?: boolean): void`
183191

184192
Sends a heartbeat from the current thread with optional state information. The
185193
state object will be serialized and included as a JavaScript object with the
186194
corresponding stack trace.
187195

196+
- `state` (optional): An object containing state information to include with the
197+
stack trace.
198+
- `disableLastSeen` (optional): If `true`, disables the tracking of the last
199+
seen time for this thread.
200+
188201
#### `getThreadsLastSeen(): Record<string, number>`
189202

190203
Returns the time in milliseconds since each registered thread called

module.cc

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ void ThreadPoll(const FunctionCallbackInfo<Value> &args) {
249249
auto context = isolate->GetCurrentContext();
250250

251251
std::string state_str;
252-
if (args.Length() == 1 && args[0]->IsValue()) {
252+
if (args.Length() > 0 && args[0]->IsValue()) {
253253
MaybeLocal<String> maybe_json = v8::JSON::Stringify(context, args[0]);
254254
if (!maybe_json.IsEmpty()) {
255255
v8::String::Utf8Value utf8_state(isolate, maybe_json.ToLocalChecked());
@@ -261,14 +261,23 @@ void ThreadPoll(const FunctionCallbackInfo<Value> &args) {
261261
state_str = "";
262262
}
263263

264+
bool disable_last_seen = false;
265+
if (args.Length() > 1 && args[1]->IsBoolean()) {
266+
disable_last_seen = args[1]->BooleanValue(isolate);
267+
}
268+
264269
{
265270
std::lock_guard<std::mutex> lock(threads_mutex);
266271
auto found = threads.find(isolate);
267272
if (found != threads.end()) {
268273
auto &thread_info = found->second;
269274
thread_info.state = state_str;
270-
thread_info.last_seen =
271-
duration_cast<milliseconds>(system_clock::now().time_since_epoch());
275+
if (disable_last_seen) {
276+
thread_info.last_seen = milliseconds::zero();
277+
} else {
278+
thread_info.last_seen =
279+
duration_cast<milliseconds>(system_clock::now().time_since_epoch());
280+
}
272281
}
273282
}
274283
}

src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type StackFrame = {
2525

2626
interface Native {
2727
registerThread(threadName: string): void;
28-
threadPoll(state?: object): void;
28+
threadPoll(state?: object, disableLastSeen?: boolean): void;
2929
captureStackTrace<S = unknown>(): Record<string, Thread<S>>;
3030
getThreadsLastSeen(): Record<string, number>;
3131
}
@@ -187,10 +187,11 @@ export function registerThread(threadName: string = String(threadId)): void {
187187
* Tells the native module that the thread is still running and updates the state.
188188
*
189189
* @param state Optional state to pass to the native module.
190+
* @param disableLastSeen If true, disables the last seen tracking for this thread.
190191
*/
191-
export function threadPoll(state?: object): void {
192-
if (typeof state === 'object') {
193-
native.threadPoll(state);
192+
export function threadPoll(state?: object, disableLastSeen?: boolean): void {
193+
if (typeof state === 'object' || disableLastSeen) {
194+
native.threadPoll(state, disableLastSeen);
194195
} else {
195196
native.threadPoll();
196197
}

test/e2e.test.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,13 @@ describe('e2e Tests', { timeout: 20000 }, () => {
8989

9090
expect(stacks['2'].frames.length).toEqual(1);
9191
});
92+
93+
test('can be disabled', { timeout: 20000 }, () => {
94+
const testFile = join(__dirname, 'stalled-disabled.js');
95+
const result = spawnSync('node', [testFile]);
96+
97+
expect(result.status).toEqual(0);
98+
99+
expect(result.stdout.toString()).toContain('complete');
100+
});
92101
});

test/stalled-disabled.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { Worker } = require('node:worker_threads');
2+
const { longWork } = require('./long-work.js');
3+
const { registerThread, threadPoll } = require('@sentry-internal/node-native-stacktrace');
4+
5+
registerThread();
6+
7+
setInterval(() => {
8+
threadPoll({ some_property: 'some_value' }, true);
9+
}, 200).unref();
10+
11+
const watchdog = new Worker('./test/stalled-watchdog.js');
12+
watchdog.on('exit', () => process.exit(0));
13+
14+
const worker = new Worker('./test/worker-do-nothing.js');
15+
16+
setTimeout(() => {
17+
longWork();
18+
19+
setTimeout(() => {
20+
console.log('complete');
21+
process.exit(0);
22+
}, 1000);
23+
}, 2000);
24+

0 commit comments

Comments
 (0)