V8 inspector from an embedder standpoint

It’s been recently that the old V8 debugger API has been removed from V8’s source code in favor of the more modern Inspector API.

This Inspector API is great, and allows me to debug my embedded Android V8 code using Chrome dev tools directly from the browser, or by using an standalone version of them. Profiling, memory dumps, source maps, breakpoints, all works like a charm (except minor bugs here and there mainly related to chrome version though). Unfortunately, there’s not much documentation on this Inspector integration from the embedder point of view.

Inspector integration process

The first to note about the Inspector is that inspection is per Isolate. One single Inspector object instance will be enough to debug all your javascript Context s. The Isolate is thread dependent, and as such you must keep your isolate in scope Isolate::Scope when necessary. That said, the elements that will conform your inspection code are very simple:

InspectorClient

This object will be used to select what Context we are currently debugging, but more importantly, it will handle runMessageLoopOnPause and quitMessageLoopOnPause methods. These two methods are called by V8 debugging internals when you are breaking into js code from Dev Tools. While runMessageLoopOnPause is being called, you must synchronously consume all front end (Dev Tools) debugging messages. If not, you will not get all context information of the code you are debugging. Once V8 knows it has no more inspector messages pending, it will call quitMessageLoopOnPause automatically.

The InspectorClient could do the debugging initialisation process like this:

// create a v8 inspector client: 
// InspectorClientImpl : public v8_inspector::V8InspectorClient
v8_inspector::InspectorClient = new InspectorClientImpl();
// create a v8 inspector instance.
v8_inspector::V8Inspector inspector_ =
v8_inspector::V8Inspector.create( isolate, inspectorClient );
// create a v8 channel. 
// ChannelImpl : public v8_inspector::V8Inspector::Channel
v8_inspector::V8Inspector::Channel channel_ = new ChannelImpl();
v8_inspector::StringView view( ... )
// Create a debugging session by connecting the V8Inspector
// instance to the channel
v8_inspector::V8InspectorSession session_ =
inspector_.connect(
1,
channel,
view);
v8_inspector::StringView ctx_name( /*ctx_name*/ )
// make sure you register Context objects in the V8Inspector.
// ctx_name will be shown in CDT/console. Call this for each context
// your app creates. Normally just one btw.
inspector_->contextCreated(
v8_inspector::V8ContextInfo(
context,
1,
ctx_name);

That’s pretty much it. After this, you’ll have a valid debugging session. How do you, as a dev, interact with each of these elements: V8InspectorClient, V8Inspector, V8Inspector::Channel, V8InspectorSession ? Well, to answer this question, first we call all this code happening in our V8-enabled app the debugging backend, which implicitly means we should have a debugging front-end.

V8InspectorSession

Ideally, the debugging front-end would be Chrome Dev Tools. CDT opens a WebSocket to communicate with the debugging back-end. You can make this happen in Chrome with something like this:

chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:20000/backend

This causes chrome to open a dev tools only tab, w/o most DOM specific stuff. In my case, the 20000 is a forwarded port from my android app to a local port(adb forward tcp:20000 tcp:20000) and /backend in the url is a mount point on the backend WebSocket listener. All front end inspector messages will be received on the backend websocket listening code, and must be forwarded to the debug session:

// msg is a std::string with whatever front sent to back.
// normally a json object with sequence and payload.
v8::internal::Vector<const char> v(msg.c_str(), msg.length());
// inspector session requires a v8_inspector::StringView
v8_inspector::StringView message_view(
reinterpret_cast<const uint8_t *>(v.start()), v.length());
// let the magic happen:
session_->dispatchProtocolMessage( message_view );

The V8InspectorSession object is full of inspection love. I recommend you having a look at v8-inspector.h header file. While all interaction happens from CDT front end, you’ll recognise a lot of functionality there like breakProgram, pause or resume methods.

V8Inspector::Channel

All inspector protocol handling happens automagically. You don’t have to worry about front end message id sequences, or their responses. The only missing part is forward inspector session message results from backend to front end. Responses happen in the custom v8_inspector::V8Inspector::Channel object implementation. Both methods:

sendProtocolResponse(int callId,const v8_inspector::StringView& msg)
void sendProtocolNotification(const v8_inspector::StringView& msg)

will handle inspector protocol responses from commands received from inspection front end. Just convert msg from StringView to std::string (or whatever your code requires) and send to front end

Diagram

This is a small diagram of how things work:

V8 Inspector initialisation flow.

Result

At the end of the process, you’ll get a full browser-enabled remote v8 debugging session. Here’s an screenshot of a sample app. All objects but console are custom bound native objects. In this sample screenshot, the host application OS is Android.