Skip to content

Commit a32a859

Browse files
committed
Add ANR features
1 parent 997a9dd commit a32a859

File tree

4 files changed

+82
-18
lines changed

4 files changed

+82
-18
lines changed

index.d.ts

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

2-
export declare function registerThread(): void;
2+
export declare function registerThread(name?: string): void;
33

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

11-
export type Trace = {
12-
main: StackFrame[];
13-
} & Record<string, StackFrame[]>;
11+
export type Trace = Record<string, StackFrame[]>;
1412

15-
export declare function captureStackTrace(excludeWorkers: boolean): Trace;
13+
export declare function captureStackTrace(excludeWorkers: boolean): Trace;
14+
15+
export declare function getThreadLastSeen(): Record<string, number>;

index.js

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

4-
exports.registerThread = function () {
5-
native.registerThread(isMainThread ? -1 : threadId );
4+
exports.registerThread = function (name = String(threadId)) {
5+
native.registerThread(name);
66
};
77
exports.captureStackTrace = function () {
88
return native.captureStackTrace();
9+
};
10+
exports.getThreadLastSeen = function () {
11+
return native.getThreadLastSeen();
912
};

module.cc

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
#include <node.h>
22
#include <mutex>
33
#include <future>
4+
#include <chrono>
45

56
using namespace v8;
67
using namespace node;
8+
using namespace std::chrono;
79

810
static const int kMaxStackFrames = 255;
911

12+
// Structure to hold information for each thread/isolate
13+
struct ThreadInfo
14+
{
15+
// Thread name
16+
std::string thread_name;
17+
// Last time this thread was seen in milliseconds since epoch
18+
milliseconds last_seen;
19+
};
20+
1021
static std::mutex threads_mutex;
11-
static std::unordered_map<v8::Isolate *, int> threads = {};
22+
// Map to hold all registered threads and their information
23+
static std::unordered_map<v8::Isolate *, ThreadInfo> threads = {};
1224

25+
// Function to be called when an isolate's execution is interrupted
1326
static void ExecutionInterrupted(Isolate *isolate, void *data)
1427
{
1528
auto promise = static_cast<std::promise<Local<Array>> *>(data);
@@ -68,32 +81,38 @@ static void ExecutionInterrupted(Isolate *isolate, void *data)
6881
promise->set_value(frames);
6982
}
7083

84+
// Function to capture the stack trace of a single isolate
7185
Local<Array> CaptureStackTrace(Isolate *isolate)
7286
{
7387
std::promise<Local<Array>> promise;
7488
auto future = promise.get_future();
7589

90+
// The v8 isolate must be interrupted to capture the stack trace
91+
// Execution resumes automatically after ExecutionInterrupted returns
7692
isolate->RequestInterrupt(ExecutionInterrupted, &promise);
7793
return future.get();
7894
}
7995

96+
// Function to capture stack traces from all registered threads
8097
void CaptureStackTraces(const FunctionCallbackInfo<Value> &args)
8198
{
8299
auto capture_from_isolate = args.GetIsolate();
83100

84101
using ThreadResult = std::tuple<std::string, Local<Array>>;
85102
std::vector<std::future<ThreadResult>> futures;
86103

104+
// We collect the futures into a vec so they can be processed in parallel
87105
std::lock_guard<std::mutex> lock(threads_mutex);
88-
for (auto [thread_isolate, thread_id] : threads)
106+
for (auto [thread_isolate, thread_info] : threads)
89107
{
90108
if (thread_isolate == capture_from_isolate)
91109
continue;
92-
auto thread_name = thread_id == -1 ? "main" : "worker-" + std::to_string(thread_id);
110+
auto thread_name = thread_info.thread_name;
93111
futures.emplace_back(std::async(std::launch::async, [thread_name](Isolate *isolate) -> ThreadResult
94112
{ return std::make_tuple(thread_name, CaptureStackTrace(isolate)); }, thread_isolate));
95113
}
96114

115+
// We wait for all futures to complete and collect their results into a JavaScript object
97116
Local<Object> result = Object::New(capture_from_isolate);
98117
for (auto &future : futures)
99118
{
@@ -105,30 +124,67 @@ void CaptureStackTraces(const FunctionCallbackInfo<Value> &args)
105124
args.GetReturnValue().Set(result);
106125
}
107126

127+
// Cleanup function to remove the thread from the map when the isolate is destroyed
108128
void Cleanup(void *arg)
109129
{
110130
auto isolate = static_cast<Isolate *>(arg);
111131
std::lock_guard<std::mutex> lock(threads_mutex);
112132
threads.erase(isolate);
113133
}
114134

135+
// Function to register a thread and update it's last seen time
115136
void RegisterThread(const FunctionCallbackInfo<Value> &args)
116137
{
117138
auto isolate = args.GetIsolate();
118139

119-
if (args.Length() != 1 || !args[0]->IsNumber())
140+
if (args.Length() != 1 || !args[0]->IsString())
120141
{
121-
isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, "registerThread() requires a single threadId argument", NewStringType::kInternalized).ToLocalChecked()));
142+
isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, "registerThread(name) requires a single name argument", NewStringType::kInternalized).ToLocalChecked()));
122143
return;
123144
}
124145

125-
auto thread_id = args[0].As<Number>()->Value();
146+
v8::String::Utf8Value utf8(isolate, args[0]);
147+
std::string thread_name(*utf8 ? *utf8 : "");
148+
149+
{
150+
std::lock_guard<std::mutex> lock(threads_mutex);
151+
auto found = threads.find(isolate);
152+
if (found == threads.end())
153+
{
154+
threads.emplace(isolate, ThreadInfo{thread_name, milliseconds::zero()});
155+
// Register a cleanup hook to remove this thread when the isolate is destroyed
156+
node::AddEnvironmentCleanupHook(isolate, Cleanup, isolate);
157+
}
158+
else
159+
{
160+
auto &thread_info = found->second;
161+
thread_info.thread_name = thread_name;
162+
thread_info.last_seen = duration_cast<milliseconds>(system_clock::now().time_since_epoch());
163+
}
164+
}
165+
}
126166

167+
// Function to get the last seen time of all registered threads
168+
void GetThreadLastSeen(const FunctionCallbackInfo<Value> &args)
169+
{
170+
Isolate *isolate = args.GetIsolate();
171+
Local<Object> result = Object::New(isolate);
172+
milliseconds now = duration_cast<milliseconds>(system_clock::now().time_since_epoch());
127173
{
128174
std::lock_guard<std::mutex> lock(threads_mutex);
129-
threads.emplace(isolate, thread_id);
175+
for (const auto &[thread_isolate, info] : threads)
176+
{
177+
if (info.last_seen == milliseconds::zero())
178+
continue; // Skip threads that have not registered more than once
179+
180+
int64_t ms_since = (now - info.last_seen).count();
181+
result->Set(isolate->GetCurrentContext(),
182+
String::NewFromUtf8(isolate, info.thread_name.c_str(), NewStringType::kNormal).ToLocalChecked(),
183+
Number::New(isolate, ms_since))
184+
.Check();
185+
}
130186
}
131-
node::AddEnvironmentCleanupHook(isolate, Cleanup, isolate);
187+
args.GetReturnValue().Set(result);
132188
}
133189

134190
extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(Local<Object> exports,
@@ -146,4 +202,9 @@ extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(Local<Object> exports
146202
String::NewFromUtf8(isolate, "registerThread", NewStringType::kInternalized).ToLocalChecked(),
147203
FunctionTemplate::New(isolate, RegisterThread)->GetFunction(context).ToLocalChecked())
148204
.Check();
205+
206+
exports->Set(context,
207+
String::NewFromUtf8(isolate, "getThreadLastSeen", NewStringType::kInternalized).ToLocalChecked(),
208+
FunctionTemplate::New(isolate, GetThreadLastSeen)->GetFunction(context).ToLocalChecked())
209+
.Check();
149210
}

test/basic.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe("Basic", () => {
1212

1313
const stacks = JSON.parse(result.stdout.toString());
1414

15-
expect(stacks.main).toEqual(expect.arrayContaining([
15+
expect(stacks["0"]).toEqual(expect.arrayContaining([
1616
{
1717
function: 'pbkdf2Sync',
1818
filename: expect.any(String),
@@ -33,7 +33,7 @@ describe("Basic", () => {
3333
},
3434
]));
3535

36-
expect(stacks['worker-2']).toEqual(expect.arrayContaining([
36+
expect(stacks['2']).toEqual(expect.arrayContaining([
3737
{
3838
function: 'pbkdf2Sync',
3939
filename: expect.any(String),

0 commit comments

Comments
 (0)