Skip to content

Commit 70cd10a

Browse files
committed
Support capturing all threads
1 parent b772670 commit 70cd10a

File tree

9 files changed

+181
-148
lines changed

9 files changed

+181
-148
lines changed

README.md

Lines changed: 48 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# `cross-thread-stack-trace`
22

3-
Native Node module to capture stack traces across threads. This allows capturing
4-
main thread stack traces from a worker thread, even if the main thread event
5-
loop is blocked.
3+
Native Node module to capture stack traces from all registered threads.
64

7-
Main thread:
5+
This allows capturing main and worker thread stack traces from another watchdog
6+
thread, even if the event loops are blocked.
7+
8+
In the main or worker threads:
89

910
```ts
10-
const { setMainIsolate } = require("cross-thread-stack-trace");
11+
const { registerThread } = require("cross-thread-stack-trace");
1112

12-
setMainIsolate();
13+
registerThread();
1314
```
1415

15-
Worker thread:
16+
Watchdog thread:
1617

1718
```ts
1819
const { captureStackTrace } = require("cross-thread-stack-trace");
@@ -30,80 +31,58 @@ npm i && npm test
3031
Results in:
3132

3233
```js
33-
[
34-
{
35-
function: "from",
36-
filename: "node:buffer",
37-
lineno: 298,
38-
colno: 28,
39-
},
34+
{
35+
main: [
4036
{
41-
function: "pbkdf2Sync",
42-
filename: "node:internal/crypto/pbkdf2",
43-
lineno: 78,
44-
colno: 17,
37+
function: 'from',
38+
filename: 'node:buffer',
39+
lineno: 298,
40+
colno: 28
4541
},
4642
{
47-
function: "longWork",
48-
filename:
49-
"/Users/tim/Documents/Repositories/cross-thread-stack-trace/test.js",
50-
lineno: 15,
51-
colno: 29,
43+
function: 'pbkdf2Sync',
44+
filename: 'node:internal/crypto/pbkdf2',
45+
lineno: 78,
46+
colno: 17
5247
},
5348
{
54-
function: "?",
55-
filename:
56-
"/Users/tim/Documents/Repositories/cross-thread-stack-trace/test.js",
57-
lineno: 19,
58-
colno: 1,
49+
function: 'longWork',
50+
filename: '/Users/tim/Documents/Repositories/cross-thread-stack-trace/test/test.js',
51+
lineno: 20,
52+
colno: 29
5953
},
6054
{
61-
function: "?",
62-
filename: "node:internal/modules/cjs/loader",
63-
lineno: 1730,
64-
colno: 14,
65-
},
66-
{
67-
function: "?",
68-
filename: "node:internal/modules/cjs/loader",
69-
lineno: 1895,
70-
colno: 10,
71-
},
55+
function: '?',
56+
filename: '/Users/tim/Documents/Repositories/cross-thread-stack-trace/test/test.js',
57+
lineno: 24,
58+
colno: 1
59+
}
60+
],
61+
'worker-2': [
7262
{
73-
function: "?",
74-
filename: "node:internal/modules/cjs/loader",
75-
lineno: 1465,
76-
colno: 32,
63+
function: 'from',
64+
filename: 'node:buffer',
65+
lineno: 298,
66+
colno: 28
7767
},
7868
{
79-
function: "?",
80-
filename: "node:internal/modules/cjs/loader",
81-
lineno: 1282,
82-
colno: 12,
69+
function: 'pbkdf2Sync',
70+
filename: 'node:internal/crypto/pbkdf2',
71+
lineno: 78,
72+
colno: 17
8373
},
8474
{
85-
function: "traceSync",
86-
filename: "node:diagnostics_channel",
87-
lineno: 322,
88-
colno: 14,
75+
function: 'longWork',
76+
filename: '/Users/tim/Documents/Repositories/cross-thread-stack-trace/test/worker.js',
77+
lineno: 10,
78+
colno: 29
8979
},
9080
{
91-
function: "wrapModuleLoad",
92-
filename: "node:internal/modules/cjs/loader",
93-
lineno: 235,
94-
colno: 24,
95-
},
96-
{
97-
function: "executeUserEntryPoint",
98-
filename: "node:internal/modules/run_main",
99-
lineno: 170,
100-
colno: 5,
101-
},
102-
{
103-
function: "?",
104-
filename: "node:internal/main/run_main_module",
105-
lineno: 36,
106-
colno: 49,
107-
},
108-
];
81+
function: '?',
82+
filename: '/Users/tim/Documents/Repositories/cross-thread-stack-trace/test/worker.js',
83+
lineno: 14,
84+
colno: 1
85+
}
86+
]
87+
}
10988
```

index.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
export declare function setMainIsolate(): void;
2+
export declare function registerThread(): void;
33

44
export type StackFrame = {
55
function: string;
@@ -8,4 +8,8 @@ export type StackFrame = {
88
colno: number;
99
};
1010

11-
export declare function captureStackTrace(): StackFrame[];
11+
export type Trace = {
12+
main: StackFrame[];
13+
} & Record<string, StackFrame[]>;
14+
15+
export declare function captureStackTrace(excludeWorkers: boolean): Trace;

index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
const { isMainThread, threadId } = require('node:worker_threads');
12
const native = require('./build/release/cross-thread-stack-trace.node');
23

3-
exports.setMainIsolate = native.setMainIsolate;
4-
exports.captureStackTrace = function () {
5-
return JSON.parse(native.captureStackTrace());
4+
exports.registerThread = function () {
5+
native.registerThread(isMainThread ? -1 : threadId );
6+
};
7+
exports.captureStackTrace = function (excludeWorkers) {
8+
return JSON.parse(native.captureStackTrace(excludeWorkers));
69
};

module.cc

Lines changed: 88 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,37 @@
11
#include <node.h>
22
#include <mutex>
3+
#include <map>
4+
#include <sstream>
5+
#include <future>
36

47
using namespace v8;
58
using namespace node;
69

7-
static v8::Isolate *main_thread_isolate;
8-
9-
static std::mutex interrupt_mutex;
10-
static std::condition_variable interrupt_cv;
11-
static bool interrupt_done = false;
12-
1310
static const int kMaxStackFrames = 255;
14-
static const int kMaxStackJsonSize = 10240;
11+
12+
static std::unordered_map<v8::Isolate *, int> threads = {};
1513

1614
static void ExecutionInterrupted(Isolate *isolate, void *data)
1715
{
18-
char *buffer = static_cast<char *>(data);
19-
20-
v8::RegisterState state;
21-
v8::SampleInfo info;
22-
void *samples[kMaxStackFrames];
16+
auto promise = static_cast<std::promise<std::string> *>(data);
2317

24-
uint32_t pos = 0;
18+
Local<StackTrace> stack = StackTrace::CurrentStackTrace(isolate, kMaxStackFrames, StackTrace::kDetailed);
2519

26-
// Initialise the register state
27-
state.pc = nullptr;
28-
state.fp = &state;
29-
state.sp = &state;
30-
31-
isolate->GetStackSample(state, samples, kMaxStackFrames, &info);
32-
33-
Local<StackTrace> stack = StackTrace::CurrentStackTrace(isolate, 255, StackTrace::kDetailed);
3420
if (stack.IsEmpty())
3521
{
36-
snprintf(buffer, kMaxStackJsonSize, "[]");
22+
promise->set_value("[]");
3723
return;
3824
}
3925

40-
pos += snprintf(&buffer[pos], kMaxStackJsonSize, "[");
41-
int count = stack->GetFrameCount();
26+
std::ostringstream out;
4227

43-
for (int i = 0; i < count; i++)
28+
out << "[";
29+
auto count = stack->GetFrameCount();
30+
31+
for (auto i = 0; i < count; i++)
4432
{
45-
Local<StackFrame> frame = stack->GetFrame(isolate, i);
46-
Local<String> fn_name = frame->GetFunctionName();
33+
auto frame = stack->GetFrame(isolate, i);
34+
auto fn_name = frame->GetFunctionName();
4735

4836
if (frame->IsEval())
4937
{
@@ -60,74 +48,113 @@ static void ExecutionInterrupted(Isolate *isolate, void *data)
6048

6149
String::Utf8Value function_name(isolate, fn_name);
6250
String::Utf8Value script_name(isolate, frame->GetScriptName());
63-
const int line_number = frame->GetLineNumber();
64-
const int column = frame->GetColumn();
51+
auto line_number = frame->GetLineNumber();
52+
auto column = frame->GetColumn();
6553

66-
pos += snprintf(&buffer[pos], kMaxStackJsonSize,
67-
"{\"function\":\"%s\",\"filename\":\"%s\",\"lineno\":%d,\"colno\":%d}",
68-
*function_name,
69-
*script_name,
70-
line_number,
71-
column);
54+
out << "{\"function\":\"" << *function_name
55+
<< "\",\"filename\":\"" << *script_name
56+
<< "\",\"lineno\":" << line_number
57+
<< ",\"colno\":" << column << "}";
7258

7359
if (i < count - 1)
7460
{
75-
pos += snprintf(&buffer[pos], kMaxStackJsonSize, ",");
61+
out << ",";
7662
}
7763
}
7864

79-
pos += snprintf(&buffer[pos], kMaxStackJsonSize, "]");
65+
out << "]";
8066

81-
{
82-
std::lock_guard<std::mutex> lock(interrupt_mutex);
83-
interrupt_done = true;
84-
}
85-
interrupt_cv.notify_one();
67+
promise->set_value(out.str());
8668
}
8769

88-
void CaptureStackTrace(const FunctionCallbackInfo<Value> &args)
70+
std::string CaptureStackTrace(Isolate *isolate)
8971
{
90-
char buffer[kMaxStackJsonSize] = {0};
72+
std::promise<std::string> promise;
73+
auto future = promise.get_future();
74+
75+
isolate->RequestInterrupt(ExecutionInterrupted, &promise);
76+
return future.get();
77+
}
9178

92-
if (auto isolate = main_thread_isolate)
79+
void CaptureStackTraces(const FunctionCallbackInfo<Value> &args)
80+
{
81+
bool exclude_workers = args.Length() == 1 && args[0]->IsBoolean() && args[0].As<Boolean>()->Value();
82+
auto capture_from_isolate = args.GetIsolate();
83+
84+
std::vector<std::future<std::string>> futures;
85+
86+
for (auto &thread : threads)
9387
{
94-
// Reset the interrupt_done flag
88+
auto thread_isolate = thread.first;
89+
if (thread_isolate != capture_from_isolate)
9590
{
96-
std::lock_guard<std::mutex> lock(interrupt_mutex);
97-
interrupt_done = false;
91+
int thread_id = thread.second;
92+
93+
if (exclude_workers && thread_id != -1)
94+
{
95+
continue;
96+
}
97+
98+
auto thread_name = thread_id == -1 ? "main" : "worker-" + std::to_string(thread_id);
99+
100+
futures.emplace_back(std::async(std::launch::async, [thread_name](Isolate *isolate)
101+
{ return "\"" + thread_name + "\":" + CaptureStackTrace(isolate); }, thread_isolate));
98102
}
103+
}
99104

100-
isolate->RequestInterrupt(ExecutionInterrupted, buffer);
105+
std::ostringstream out;
101106

102-
// Wait for the interrupt to complete
103-
std::unique_lock<std::mutex> lock(interrupt_mutex);
104-
interrupt_cv.wait(lock, []
105-
{ return interrupt_done; });
107+
auto count = futures.size();
108+
out << "{";
109+
for (auto &future : futures)
110+
{
111+
out << future.get();
112+
if (--count > 0)
113+
{
114+
out << ",";
115+
}
106116
}
117+
out << "}";
107118

108-
Local<String> result = String::NewFromUtf8(args.GetIsolate(), buffer, NewStringType::kNormal).ToLocalChecked();
109-
args.GetReturnValue().Set(result);
119+
args.GetReturnValue().Set(String::NewFromUtf8(capture_from_isolate, out.str().c_str(), NewStringType::kNormal).ToLocalChecked());
110120
}
111121

112-
void SetMainIsolate(const FunctionCallbackInfo<Value> &args)
122+
void Cleanup(void *arg)
113123
{
114-
main_thread_isolate = args.GetIsolate();
124+
auto isolate = static_cast<Isolate *>(arg);
125+
threads.erase(isolate);
126+
}
127+
128+
void RegisterThread(const FunctionCallbackInfo<Value> &args)
129+
{
130+
auto isolate = args.GetIsolate();
131+
132+
if (args.Length() != 1 || !args[0]->IsNumber())
133+
{
134+
isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, "registerThread() requires a single threadId argument", NewStringType::kInternalized).ToLocalChecked()));
135+
return;
136+
}
137+
138+
int thread_id = args[0].As<Number>()->Value();
139+
140+
threads.emplace(isolate, thread_id);
141+
node::AddEnvironmentCleanupHook(isolate, Cleanup, isolate);
115142
}
116143

117144
extern "C" NODE_MODULE_EXPORT void
118145
NODE_MODULE_INITIALIZER(Local<Object> exports,
119146
Local<Value> module,
120147
Local<Context> context)
121148
{
122-
Isolate *isolate = context->GetIsolate();
149+
auto isolate = context->GetIsolate();
123150

124151
exports->Set(context,
125152
String::NewFromUtf8(isolate, "captureStackTrace", NewStringType::kInternalized).ToLocalChecked(),
126-
FunctionTemplate::New(isolate, CaptureStackTrace)->GetFunction(context).ToLocalChecked())
153+
FunctionTemplate::New(isolate, CaptureStackTraces)->GetFunction(context).ToLocalChecked())
127154
.Check();
128155

129156
exports->Set(context,
130-
String::NewFromUtf8(isolate, "setMainIsolate", NewStringType::kInternalized).ToLocalChecked(),
131-
FunctionTemplate::New(isolate, SetMainIsolate)->GetFunction(context).ToLocalChecked())
157+
String::NewFromUtf8(isolate, "registerThread", NewStringType::kInternalized).ToLocalChecked(),
158+
FunctionTemplate::New(isolate, RegisterThread)->GetFunction(context).ToLocalChecked())
132159
.Check();
133160
}

0 commit comments

Comments
 (0)