Skip to content

Commit e597308

Browse files
committed
fix(inspector): make the Tracing domain protocol-correct and faster
Ports the iOS tracing agent and takes it further where android's inspector allows. - emit pre-serialized Tracing.dataCollected messages straight from the trace writer, 1000 events per message, replacing the JS snippet that was compiled and run inside the isolate to JSON.parse the whole trace and re-slice it into 20-event chunks - handle the Tracing domain in handleMessageOnSocketThread, so starting a trace and flushing it to the frontend never queues behind the main thread or takes the Locker - answer with real CDP responses instead of echoing the request, always send Tracing.tracingComplete so an empty trace no longer hangs DevTools, and report dataLossOccurred from a ring-wrap heuristic - honor traceConfig.includedCategories and traceBufferSizeInKb, the latter clamped to [2, 16384] ring chunks in double space - enforce single process-wide trace ownership (the TracingController is process-global): a concurrent Tracing.start and a Tracing.end from a non-owner get CDP errors, and disconnect drops an orphaned trace - skip the source map rewrite's full-message scan and copy when sending trace chunks Also fixes a use-after-free that the previous teardown always hit: TracingController::Initialize() swaps trace_buffer_ with no lock and UpdateTraceEventDuration() dereferences it with neither a lock nor a null check, so clearing the buffer at StopTracing faulted every TRACE_EVENT scope still open. The controller now gets an NSTraceBuffer indirection installed once and never replaced; the ring inside it is swapped under its own lock and freed as soon as the trace ends.
1 parent 8a4ac28 commit e597308

4 files changed

Lines changed: 317 additions & 148 deletions

File tree

test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ std::string MaybeRewriteSourceMapURL(const std::string& message) {
192192
} // namespace
193193

194194
JsV8InspectorClient::JsV8InspectorClient(v8::Isolate* isolate)
195-
: isolate_(isolate),
195+
: tracing_agent_(new tns::inspector::TracingAgentImpl()),
196+
isolate_(isolate),
196197
inspector_(nullptr),
197198
session_(nullptr),
198199
connection_(nullptr),
@@ -245,6 +246,10 @@ void JsV8InspectorClient::disconnect() {
245246
resourceStreams_.clear();
246247
}
247248

249+
// Nothing will ever ask for a trace the disconnected frontend started, and
250+
// leaving it running keeps the ring buffer filling forever.
251+
tracing_agent_->stopAndDiscard();
252+
248253
// Reset worker sessions first and without the main-isolate Locker: if the
249254
// main isolate is paused, its nested loop owns the Locker and this thread
250255
// blocks below — workers must still get a clean slate (resume if paused,
@@ -288,8 +293,6 @@ void JsV8InspectorClient::dispatchMessage(const std::string& message) {
288293
auto context = Runtime::GetRuntime(isolate_)->GetContext();
289294
Context::Scope context_scope(context);
290295

291-
std::vector<uint16_t> vector = tns::Util::ToVector(message);
292-
StringView messageView(vector.data(), vector.size());
293296
bool success;
294297

295298
/*
@@ -304,22 +307,8 @@ void JsV8InspectorClient::dispatchMessage(const std::string& message) {
304307
*/
305308

306309

307-
if(message.find("Tracing.start") != std::string::npos) {
308-
tracing_agent_->start();
309-
310-
// echo back the request to notify frontend the action was a success
311-
// todo: send an empty response for the incoming message id instead.
312-
this->sendNotification(StringBuffer::create(messageView));
313-
return;
314-
}
315-
316-
if(message.find("Tracing.end") != std::string::npos) {
317-
tracing_agent_->end();
318-
std::string res = tracing_agent_->getLastTrace();
319-
tracing_agent_->SendToDevtools(context, res);
320-
return;
321-
}
322-
310+
// Note: the Tracing domain is handled in handleMessageOnSocketThread, so it
311+
// never waits on this queue or takes the Locker above.
323312

324313
// parse incoming message as JSON
325314
Local<Value> arg;
@@ -467,6 +456,75 @@ bool JsV8InspectorClient::handleMessageOnSocketThread(const std::string& message
467456
return true;
468457
}
469458

459+
// The Tracing domain never touches the isolate, so it is answered here
460+
// instead of on the main-thread dispatch queue: flushing a large trace to
461+
// the frontend must not wait for (or block) JS. The dataCollected chunks
462+
// are pre-serialized by the trace writer and cannot carry a sessionId
463+
// without re-parsing them, so only the main session is served; V8 traces
464+
// the whole process anyway.
465+
if (sessionId.empty() && method == "Tracing.start") {
466+
std::vector<std::string> categories;
467+
double bufferSizeInKb = 0;
468+
469+
const json* traceConfig = nullptr;
470+
if (parsed.contains("params") && parsed["params"].is_object()) {
471+
const auto& params = parsed["params"];
472+
if (params.contains("traceConfig") && params["traceConfig"].is_object()) {
473+
traceConfig = &params["traceConfig"];
474+
} else if (params.contains("categories") && params["categories"].is_array()) {
475+
// deprecated flat format
476+
traceConfig = &params;
477+
}
478+
}
479+
480+
if (traceConfig != nullptr) {
481+
const char* categoriesKey =
482+
traceConfig->contains("includedCategories") ? "includedCategories" : "categories";
483+
if (traceConfig->contains(categoriesKey) && (*traceConfig)[categoriesKey].is_array()) {
484+
for (const auto& category : (*traceConfig)[categoriesKey]) {
485+
if (category.is_string()) {
486+
categories.push_back(category.get<std::string>());
487+
}
488+
}
489+
}
490+
if (traceConfig->contains("traceBufferSizeInKb") &&
491+
(*traceConfig)["traceBufferSizeInKb"].is_number()) {
492+
bufferSizeInKb = (*traceConfig)["traceBufferSizeInKb"].get<double>();
493+
}
494+
}
495+
496+
json reply = {{"id", msgId}};
497+
if (tracing_agent_->start(categories, bufferSizeInKb)) {
498+
reply["result"] = json::object();
499+
} else {
500+
reply["error"] = {{"code", -32000}, {"message", "Tracing is already started"}};
501+
}
502+
response = JsonDump(reply);
503+
return true;
504+
}
505+
506+
if (sessionId.empty() && method == "Tracing.end") {
507+
tns::inspector::TracingAgentImpl::Result trace;
508+
if (!tracing_agent_->end(trace)) {
509+
json error = {{"id", msgId},
510+
{"error", {{"code", -32000}, {"message", "Tracing is not started"}}}};
511+
response = JsonDump(error);
512+
return true;
513+
}
514+
515+
// The ack must reach the frontend before the events it asked for, so it
516+
// is sent here rather than through `response`.
517+
json ack = {{"id", msgId}, {"result", json::object()}};
518+
this->SendRawToFrontend(JsonDump(ack));
519+
for (const auto& traceMessage : trace.messages) {
520+
this->SendRawToFrontend(traceMessage);
521+
}
522+
json complete = {{"method", "Tracing.tracingComplete"},
523+
{"params", {{"dataLossOccurred", trace.dataLossOccurred}}}};
524+
this->SendRawToFrontend(JsonDump(complete));
525+
return true;
526+
}
527+
470528
// DevTools discovers worker targets through the Target domain: its
471529
// ChildTargetManager sends Target.setAutoAttach {flatten: true} right
472530
// after connecting, and from then on expects Target.attachedToTarget /
@@ -781,6 +839,10 @@ void JsV8InspectorClient::sendNotification(std::unique_ptr<StringBuffer> message
781839
}
782840

783841
void JsV8InspectorClient::SendToFrontend(const std::string& message) {
842+
SendRawToFrontend(MaybeRewriteSourceMapURL(message));
843+
}
844+
845+
void JsV8InspectorClient::SendRawToFrontend(const std::string& msg) {
784846
JEnv env;
785847
JniLocalRef connection;
786848
{
@@ -794,7 +856,6 @@ void JsV8InspectorClient::SendToFrontend(const std::string& message) {
794856
connection = JniLocalRef(env.NewLocalRef(connection_));
795857
}
796858

797-
const std::string msg = MaybeRewriteSourceMapURL(message);
798859
try {
799860
// TODO: Pete: Check if we can use a wide (utf 16) string here
800861
JniLocalRef str(env.NewStringUTF(msg.c_str()));
@@ -831,8 +892,6 @@ void JsV8InspectorClient::init() {
831892

832893
createInspectorSession();
833894

834-
tracing_agent_.reset(new tns::inspector::TracingAgentImpl());
835-
836895
try {
837896
this->registerModules();
838897
} catch (NativeScriptException& e) {

test-app/runtime/src/main/cpp/JsV8InspectorClient.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class JsV8InspectorClient : V8InspectorClient, v8_inspector::V8Inspector::Channe
3535
// socket; serializes against connect/disconnect.
3636
void SendToFrontend(const std::string& message);
3737

38+
// Any thread. As SendToFrontend, but skips the source map rewrite (and
39+
// the full-message scan and copy it costs) for payloads that cannot
40+
// contain a sourceMapURL.
41+
void SendRawToFrontend(const std::string& message);
42+
3843
// Worker target management (Target domain, flat-session protocol).
3944
// Register/Unregister run on the worker's own thread; SchedulePauseInWorker
4045
// runs on the worker thread from a V8 interrupt.

0 commit comments

Comments
 (0)