Skip to content

Commit 6c505ae

Browse files
committed
feat: Capture thread state by calling global function
1 parent d881dbd commit 6c505ae

File tree

6 files changed

+89
-58
lines changed

6 files changed

+89
-58
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module.exports = {
22
extends: ['@sentry-internal/sdk'],
33
env: {
44
node: true,
5-
es6: true,
5+
es2020: true
66
},
77
parserOptions: {
88
sourceType: 'module',

README.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,26 @@ Set up automatic detection of blocked event loops:
107107

108108
### 1. Set up thread heartbeats
109109

110-
Send regular heartbeats with optional state information:
110+
Send regular heartbeats:
111111

112112
```ts
113113
import {
114114
registerThread,
115115
threadPoll,
116116
} from "@sentry-internal/node-native-stacktrace";
117117

118+
// Register a global function called `__get_thread_state_callback__` that is called when
119+
// event loop is blocked to get the current thread state. This must return a serializable object.
120+
globalThis.__get_thread_state_callback__ = () => {
121+
return { some_property: "some_value" };
122+
};
123+
118124
// Register this thread
119125
registerThread();
120126

121-
// Send heartbeats every 200ms with optional state
127+
// Send heartbeats every 200ms
122128
setInterval(() => {
123-
threadPoll({
124-
endpoint: "/api/current-request",
125-
userId: getCurrentUserId(),
126-
});
129+
threadPoll();
127130
}, 200);
128131
```
129132

@@ -187,14 +190,10 @@ type StackFrame = {
187190
};
188191
```
189192

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

192-
Sends a heartbeat from the current thread with optional state information. The
193-
state object will be serialized and included as a JavaScript object with the
194-
corresponding stack trace.
195+
Sends a heartbeat from the current thread.
195196

196-
- `state` (optional): An object containing state information to include with the
197-
stack trace.
198197
- `disableLastSeen` (optional): If `true`, disables the tracking of the last
199198
seen time for this thread.
200199

module.cc

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,14 @@ using namespace v8;
1616
using namespace node;
1717
using namespace std::chrono;
1818

19-
static const int kMaxStackFrames = 255;
19+
static const int kMaxStackFrames = 50;
2020

2121
// Structure to hold information for each thread/isolate
2222
struct ThreadInfo {
2323
// Thread name
2424
std::string thread_name;
2525
// Last time this thread was seen in milliseconds since epoch
2626
milliseconds last_seen;
27-
// Some JSON serialized state for the thread
28-
std::string state;
2927
};
3028

3129
static std::mutex threads_mutex;
@@ -41,21 +39,26 @@ struct JsStackFrame {
4139
};
4240

4341
// Type alias for a vector of JsStackFrame
44-
using JsStackTrace = std::vector<JsStackFrame>;
42+
using JsStackFrames = std::vector<JsStackFrame>;
43+
44+
struct JsStackTrace {
45+
// The frames in the stack trace
46+
std::vector<JsStackFrame> frames;
47+
// JSON serialized string of the state
48+
std::string state;
49+
};
4550

4651
struct ThreadResult {
4752
std::string thread_name;
48-
std::string state;
49-
JsStackTrace stack_frames;
53+
JsStackTrace stack_trace;
5054
};
5155

52-
// Function to be called when an isolate's execution is interrupted
53-
static void ExecutionInterrupted(Isolate *isolate, void *data) {
54-
auto promise = static_cast<std::promise<JsStackTrace> *>(data);
56+
// Function to get stack frames from a V8 stack trace
57+
JsStackFrames GetStackFrames(Isolate *isolate) {
5558
auto stack = StackTrace::CurrentStackTrace(isolate, kMaxStackFrames,
5659
StackTrace::kDetailed);
5760

58-
JsStackTrace frames;
61+
JsStackFrames frames;
5962
if (!stack.IsEmpty()) {
6063
for (int i = 0; i < stack->GetFrameCount(); i++) {
6164
auto frame = stack->GetFrame(isolate, i);
@@ -89,7 +92,45 @@ static void ExecutionInterrupted(Isolate *isolate, void *data) {
8992
}
9093
}
9194

92-
promise->set_value(frames);
95+
return frames;
96+
}
97+
98+
// Function to fetch the thread state from the isolate by calling a global
99+
// function called __get_thread_state_callback__
100+
std::string GetThreadState(Isolate *isolate) {
101+
auto callback_name =
102+
v8::String::NewFromUtf8(isolate, "__get_thread_state_callback__",
103+
v8::NewStringType::kNormal)
104+
.ToLocalChecked();
105+
auto context = isolate->GetCurrentContext();
106+
auto callback =
107+
context->Global()->Get(context, callback_name).ToLocalChecked();
108+
109+
if (callback->IsFunction()) {
110+
v8::TryCatch try_catch(isolate);
111+
112+
auto result = Local<Function>::Cast(callback)->Call(
113+
context, Undefined(isolate), 0, {});
114+
115+
MaybeLocal<String> maybe_json =
116+
v8::JSON::Stringify(context, result.ToLocalChecked());
117+
118+
if (!maybe_json.IsEmpty()) {
119+
v8::String::Utf8Value utf8_state(isolate, maybe_json.ToLocalChecked());
120+
if (*utf8_state) {
121+
return *utf8_state;
122+
}
123+
}
124+
}
125+
126+
return "";
127+
}
128+
129+
// Function to be called when an isolate's execution is interrupted
130+
static void ExecutionInterrupted(Isolate *isolate, void *data) {
131+
auto promise = static_cast<std::promise<JsStackTrace> *>(data);
132+
133+
promise->set_value({GetStackFrames(isolate), GetThreadState(isolate)});
93134
}
94135

95136
// Function to capture the stack trace of a single isolate
@@ -116,12 +157,11 @@ void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
116157
if (thread_isolate == capture_from_isolate)
117158
continue;
118159
auto thread_name = thread_info.thread_name;
119-
auto state = thread_info.state;
120160

121161
futures.emplace_back(std::async(
122162
std::launch::async,
123-
[thread_name, state](Isolate *isolate) -> ThreadResult {
124-
return ThreadResult{thread_name, state, CaptureStackTrace(isolate)};
163+
[thread_name](Isolate *isolate) -> ThreadResult {
164+
return ThreadResult{thread_name, CaptureStackTrace(isolate)};
125165
},
126166
thread_isolate));
127167
}
@@ -137,9 +177,9 @@ void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
137177
.ToLocalChecked();
138178

139179
Local<Array> jsFrames =
140-
Array::New(capture_from_isolate, result.stack_frames.size());
141-
for (size_t i = 0; i < result.stack_frames.size(); ++i) {
142-
const auto &frame = result.stack_frames[i];
180+
Array::New(capture_from_isolate, result.stack_trace.frames.size());
181+
for (size_t i = 0; i < result.stack_trace.frames.size(); ++i) {
182+
const auto &frame = result.stack_trace.frames[i];
143183
Local<Object> frameObj = Object::New(capture_from_isolate);
144184
frameObj
145185
->Set(current_context,
@@ -189,9 +229,10 @@ void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
189229
jsFrames)
190230
.Check();
191231

192-
if (!result.state.empty()) {
232+
if (!result.stack_trace.state.empty()) {
193233
v8::MaybeLocal<v8::String> stateStr = v8::String::NewFromUtf8(
194-
capture_from_isolate, result.state.c_str(), NewStringType::kNormal);
234+
capture_from_isolate, result.stack_trace.state.c_str(),
235+
NewStringType::kNormal);
195236
if (!stateStr.IsEmpty()) {
196237
v8::MaybeLocal<v8::Value> maybeStateVal =
197238
v8::JSON::Parse(current_context, stateStr.ToLocalChecked());
@@ -243,8 +284,7 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
243284
std::lock_guard<std::mutex> lock(threads_mutex);
244285
auto found = threads.find(isolate);
245286
if (found == threads.end()) {
246-
threads.emplace(isolate,
247-
ThreadInfo{thread_name, milliseconds::zero(), ""});
287+
threads.emplace(isolate, ThreadInfo{thread_name, milliseconds::zero()});
248288
// Register a cleanup hook to remove this thread when the isolate is
249289
// destroyed
250290
node::AddEnvironmentCleanupHook(isolate, Cleanup, isolate);
@@ -280,32 +320,17 @@ steady_clock::time_point GetUnbiasedMonotonicTime() {
280320
// Function to track a thread and set its state
281321
void ThreadPoll(const FunctionCallbackInfo<Value> &args) {
282322
auto isolate = args.GetIsolate();
283-
auto context = isolate->GetCurrentContext();
284-
285-
std::string state_str;
286-
if (args.Length() > 0 && args[0]->IsValue()) {
287-
MaybeLocal<String> maybe_json = v8::JSON::Stringify(context, args[0]);
288-
if (!maybe_json.IsEmpty()) {
289-
v8::String::Utf8Value utf8_state(isolate, maybe_json.ToLocalChecked());
290-
state_str = *utf8_state ? *utf8_state : "";
291-
} else {
292-
state_str = "";
293-
}
294-
} else {
295-
state_str = "";
296-
}
297323

298324
bool disable_last_seen = false;
299-
if (args.Length() > 1 && args[1]->IsBoolean()) {
300-
disable_last_seen = args[1]->BooleanValue(isolate);
325+
if (args.Length() > 0 && args[0]->IsBoolean()) {
326+
disable_last_seen = args[0]->BooleanValue(isolate);
301327
}
302328

303329
{
304330
std::lock_guard<std::mutex> lock(threads_mutex);
305331
auto found = threads.find(isolate);
306332
if (found != threads.end()) {
307333
auto &thread_info = found->second;
308-
thread_info.state = state_str;
309334
if (disable_last_seen) {
310335
thread_info.last_seen = milliseconds::zero();
311336
} else {

src/index.ts

Lines changed: 4 additions & 5 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, disableLastSeen?: boolean): void;
28+
threadPoll(disableLastSeen?: boolean): void;
2929
captureStackTrace<S = unknown>(): Record<string, Thread<S>>;
3030
getThreadsLastSeen(): Record<string, number>;
3131
}
@@ -186,12 +186,11 @@ export function registerThread(threadName: string = String(threadId)): void {
186186
/**
187187
* Tells the native module that the thread is still running and updates the state.
188188
*
189-
* @param state Optional state to pass to the native module.
190189
* @param disableLastSeen If true, disables the last seen tracking for this thread.
191190
*/
192-
export function threadPoll(state?: object, disableLastSeen?: boolean): void {
193-
if (typeof state === 'object' || disableLastSeen) {
194-
native.threadPoll(state, disableLastSeen);
191+
export function threadPoll(disableLastSeen?: boolean): void {
192+
if (disableLastSeen) {
193+
native.threadPoll(disableLastSeen);
195194
} else {
196195
native.threadPoll();
197196
}

test/stalled-disabled.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ const { Worker } = require('node:worker_threads');
22
const { longWork } = require('./long-work.js');
33
const { registerThread, threadPoll } = require('@sentry-internal/node-native-stacktrace');
44

5+
globalThis.__get_thread_state_callback__ = () => {
6+
return { some_property: 'some_value' };
7+
}
8+
59
registerThread();
610

711
setInterval(() => {
8-
threadPoll({ some_property: 'some_value' }, true);
12+
threadPoll(true);
913
}, 200).unref();
1014

1115
const watchdog = new Worker('./test/stalled-watchdog.js');

test/stalled.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ const { Worker } = require('node:worker_threads');
22
const { longWork } = require('./long-work.js');
33
const { registerThread, threadPoll } = require('@sentry-internal/node-native-stacktrace');
44

5+
globalThis.__get_thread_state_callback__ = () => {
6+
return { some_property: 'some_value' };
7+
}
8+
59
registerThread();
610

711
setInterval(() => {
8-
threadPoll({ some_property: 'some_value' });
12+
threadPoll();
913
}, 200).unref();
1014

1115
const watchdog = new Worker('./test/stalled-watchdog.js');

0 commit comments

Comments
 (0)