Architecture of the Web Inspector

The Web Inspector is WebKit's friendly tool for seeing what's inside of a web page. A fundamental tool for any web developer. The Web Inspector itself is nothing but a web application too. In this post, I review its history, architecture and main features.

Posted by Diego Pino García on December 31, 2015

In Igalia we have been contributing to the WebKit project for many years. Starting with WebKitGTK+ and progressively reaching other areas such as WebCore, improving accessibility, implementing new APIs, tooling, fixing tons of bugs, etc. The Web Inspector is another area were we have contributed, although much more modestly. This is a post I wanted to write since a long time ago. It’s a brief tour through the Web Inspector. Here I discuss its architecture, history and main features. Hopefully this information can be useful for other people who would like to start hacking on this important component of the WebKit project.

What’s the Web Inspector?

The Web Inspector is a tool that is part of WebKit and allows you to inspect web pages. I mean inspect in a broad sense. It doesn’t only allow you to debug your JavaScript code, but it also includes a rich set of tools, usually divided into panels, which provide very valuable information such as loading times of resources (files, images), CSS properties and DOM elements edition, visual identification of elements within a web page, and many things more. It basically answers the question what’s going on inside a web page?

The difference between the Web Inspector and other tools, such as Firefox’s famous Web Developer extension, is that the Web Inspector is not an external plugin, but it’s part of WebKit. That means that every WebKit port features the Web Inspector. Nowadays, all major open-source browsers include their own Web Inspector alike tool. Chrome features its Developer tools and Firefox has its own Developer tools too.

Throughtout the years

The Web Inspector was shipped in WebKit for the first time in January 2006. Since then it has gone through big and small changes.

The first big change came in June 2007. There was a big redesign, the network panel was included for the first time, syntax highlighting, better error reporting, etc.

One year later, in September 2008, the Inspector went through another big redesign and more panels were included (Elements, Resources, Scripts, Profile, Database).

November 2009 brought better edition of DOM elements, inline creation of CSS rules and selectors, a widget for color editing, JSON and CSS syntax highlighting, etc.

In June 2012 a brand-new Web Inspector was introduced but only for Safari 6. Exactly one year later, Safari 6’s Web Inspector was open-sourced. During some time the new and old versions of the Web Inspector lived together in the codebase. This new inspector brought a major visual redesign and new layout elements: toolbar, navigation bar, quick console, content, sidebar, etc. The panels were structured in: Resources, Timeline, Debugger, Styles, Layers and Node. The current Web Inspector it’s still based on this release.

Two months before Apple open-sourced the new Web Inspector, Google forked WebKit and created Blink. The Web Inspector is known as Developer Tools in Chrome. Since Blink was forked two months before the new Web Inspector release, Chrome’s Developer Tools are actually based in the old Web Inspector code. Although probably it has gone through many changes since the last two years. The bottom line is that WebKit’s Web Inspector and Chrome’s Developer Tools look in fact very similar.

A first glimpse

The Web Inspector source code lives at Source/WebInspectorUI. Most of it is composed of HTML, CSS and JavaScript although there are parts in C++ to bridge with WebCore and JavaScriptCore. According to OpenHub, WebKit’s source code is 20% JavaScript. That’s a big amount of code, although not all JavaScript code in WebKit is part of the Web Inspector. There’s JavaScript also in the Layout tests and JavaScriptCore.

The WebInspectorUI, which represents the frontend, is structured in several folders:

  • Base/: The main classes (Bootstrap.js, WebInspector.js, EventListener.js, Object.js, etc).
  • Configurations/: XCode configuration files.
  • Controllers/: Catch events and call Model classes to perform business logic. Important files: XXXManager.js (TimelineManager.js, DebuggerManager.js, StorageManager.js, LogManager.js).
  • Images/: Images for icons, visual elements, etc. The images for GTK are different due to license restrictions.
  • Localications/: Contains file with localized strings in English.
  • Models/: Classes that perform business logic (KeyboardShortcut.js, LogObjet.js, Timeline.js, Color.js, etc).
  • Protocol/: Observers that respond to notifications emitted by the backend.
  • Scripts/: Several scripts in Perl, Ruby and Python to perform automatic tasks (update CodeMirror, update resources, minimize CSS, etc).
  • Tools/PrettyPrinting/: External tool for pretty print source code in the Console tab.
  • UserInterface/External:
    • CodeMirror/ : Very powerful text editor implemented in JavaScript for the browser. Used for code editing (JavaScript, CSS) inside Web Inspector.
    • ESLint/ : Check JavaScript syntax.
    • Esprima/ : JavaScript parser, for code completion in the editor.
  • Versions/: Description of protocols for different iOS Versions.
  • View/: Classes for visual element objects.

Getting started

Think of the Web Inspector as a web application that lives inside WebKit. It’s possible to modify one of its elements (HTML, CSS of JavaScript) and see the change reflected in the UI just by typing the following command:

$ Tools/Scripts/build-webkit --inspector-frontend

But this only works in not CMake ports (Apple). It has the inconvenient of no updating localized strings too. In other ports, such as WebKitGTK+, there’s no –inspector-frontend flag so it’s necessary to build WebKit. As usual, only the changed files are built:

$ Tools/Scripts/build-webkit --gtk
====================================================================
   WebKit  is  now built   (00m:18s).
   To  run MiniBrowser with    this    newly-built code,   use the
   "../Tools/Scripts/run-minibrowser"  script.
====================================================================

Once it’s built, open MiniBrowser and the Inspector (Ctrl+I) to see your changes:

$ Tools/Scripts/run-minibrowser   --gtk

There’s a permanent tag with open bugs for the Web Inspector and the URL http://webkit.org/new-inspector-bug, can be used to file out new bugs related to the Web Inspector.

How to debug?

Definitively when starting hacking in a new project it’s fundamental to be able to see what’s going on inside. If you try to inspect a Web Inspector variable using console.log(var) nothing will be printed out. It’s necessary to build WebKit in debug mode and enable the flag developerExtrasEnabled:

$ Tools/Script/build-webkit --gtk --debug

In the case of the WebKitGTK+ port, developerExtrasEnabled is always set to TRUE.

It’s also possible to do the same in release mode, just by removing an #ifdef block in WebKit2/UIProcess/gtk/WebInspectorProxyGtk.cpp:

#ifndef NDEBUG
   //  Allow   developers  to  inspect the Web Inspector   in  debug   builds
   //  without changing    settings.
   preferences->setDeveloperExtrasEnabled(true);
   preferences->setLogsPageMessagesToSystemConsoleEnabled(true);
#endif

Now all console.log(var) messages will be printed out in the shell. With this setting enabled, it’s also possible to open a new Web Inspector to inspect the Web Inspector. With the Web Inspector open, righ-click on one of its elements and select Inspect Element, just like in a normal web page.

Remote debugging

The Web Inspector is a multi-tier application. It’s divided into 3 layers: a frontend, a backend and a target. This division detaches the inspector from the inspected browser. In other words, it’s possible to use the inspector to inspect a browser running in a remote device. This can be useful to debug an iPhone web application or a WebKitGTK+ based browser running in an embedded environment, such as the RaspberryPi.

On the browser to be inspected, first define the WEB_INSPECTOR_SERVER variable:

$ export WEB_INSPECTOR_SERVER=127.0.0.1:9222
$ Tools/Script/run-minibrowser --gtk

On the client side, open WebKit:

$ Tools/Script/run-minibrowser --gtk

Go to the server URL, in this case http://127.0.0.1:9222, and you will see a Web Inspector which is actually inspecting a remote browser.

Architecture of the Web Inspector

As mentioned before, the Web Inspector is a 3-tier application divided into several layers:

  • Frontend, also known as the client.
  • Target, also known as the debugee.
  • Backend, also known as the server.

The frontend (WebInspectorUI/UserInterface/) is Web Inspector’s user interface. It’s what the user sees and interacts with. It’s implemented in HTML, CSS and JavaScript. Sometimes this frontend is referred as the debug client, whereas the backend is referred as the server.

The target is the program being debugged. In normal operation of the Web Inspector, the target program is the WebKit loaded in the browser. In remote debugging mode, the inspected target is a WebKit loaded in a remote machine.

The backend (JavaScriptCore/inspector/ and /WebCore/inspector) is what mediates between the target and the frontend. It gives access to the elements that live in JavaScriptCore and WebCore. For instance, in order to be able to debug JavaScript code within a web page, it’s necessary to have access to JavaScriptCore’s stackframe. In consequence, JavaScriptCore has to provide hooks to the Web Inspector so it can access the inspected properties. The same happens with DOM elements. Showing up a DOM element properties requires that WebCore provides this information to the inspector.

Frontend

Let’s dive into the frontend first. If you grep for HTML code hoping to find the layout of the inspector elements, you’re not going to find any code actually. The reason why it’s because all the layout elements in the inspector are via DOM operations (createElement, appendChild,…). For instance, in UserInterface/Views/SidebarPanel.js:

WebInspector.SidebarPanel = class SidebarPanel extends WebInspector.Object
{
    constructor(identifier, displayName, element, role, label)
    {
        super();
        ...
        this._element = element || document.createElement("div");
        this._element.classList.add("panel", identifier);

        this._contentElement = document.createElement("div");
        this._contentElement.className = "content";
        this._element.appendChild(this._contentElement);
        ...
     }
}

Everything starts with Main.html (UserInterface/Main.html). This file loads all the elements that compose the inspector UI. First it loads several external components (CodeMirror, JSLint), then CSS files and after that all the JavaScript files that form the inspector. These files can be classified into different types. Some are architectural elements (Base/XXX.js), others implement business logic operations (Model/XXX.js), other implements programmatic logic (Controllers/XXX.js) and others implement visual elements and widgets (View/XXX.js). Usually UI classes have a CSS file of the same associated, for instance Views/SidebarPanel.js and Views/SidebarPanel.css.

The WebInspector namespace (UserInterface/Base/WebInspector.js) is the central element of the frontend. Everything is going to be accessed from there:

var WebInspector = {}; // Namespace

As the model, controller and view classes are processed they are going to hook themselves to the WebInspector namespace. Here’s the definition of Views/DOMDetailsSidebarPanel.js:

WebInspector.DOMDetailsSidebarPanel = class DOMDetailsSidebarPanel extends WebInspector.DetailsSidebarPanel
{
    constructor(identifier, displayName, singularDisplayName, element, dontCreateNavigationItem)
    {
        super(identifier, displayName, singularDisplayName, element, dontCreateNavigationItem);
        this.element.addEventListener("click", this._mouseWasClicked.bind(this), true);
        this._domNode = null;
    }
    ...
}

It’s a classical Model-View-Controller pattern. The view accesses the controller to execute business logic operations, which are implemented by models. On the same files there’s:

_mouseWasClicked(event)
{
    if (this._domNode && this._domNode.ownerDocument) {
        var mainResource = WebInspector.frameResourceManager.resourceForURL(this._domNode.ownerDocument.documentURL);
        if (mainResource)
            var parentFrame = mainResource.parentFrame;
    }

    WebInspector.handlePossibleLinkClick(event, parentFrame);
}

Where frameResourceManager is an instance of Controllers/FrameResourceManager.js. Views can also access Models, not usually to perform operations on them but to query them. It’s the case on the same file of inspect method:

inspect(objects)
{
   // Iterate over the objects to find a WebInspector.DOMNode to inspect.
   for (var i = 0; i < objects.length; ++i) {
      if (objects[i] instanceof WebInspector.DOMNode) {
         nodeToInspect = objects[i];
         break;
      }
   }
}

Where DOMNode is a model class (./Models/DOMNode.js).

One thing that characterizes the frontend code is that it tends to quickly adopt new JavaScript features implemented by JavaScriptCore. All the code is structured in classes, makes use of inheritance, there are getter and setter methods, there are for-of loops, makes use of Map and Set classes, etc. Any new ES2015 feature that lands in JavaScriptCore, if it’s convenient and simplifies code, makes its room into the inspector. And it makes sense to do it, as it’s guaranteed the latest JavaScriptCore version is going to be there. It also makes the inspector a helpful codebase to understand new ES2015 features.

WebKit Remote Debugging Protocol

Until this point, most of the frontend architecture is covered. I mentioned earlier that another layer of the inspector is the backend. The backend is what mediates between the target program and the frontend. It consists of several C++ classes that expose properties of WebCore (WebCore/inspector) and JavaScriptCore (JavaScriptCore/inspector) to the inspector. But how is possible that C++ classes and JavaScript classes can exchange information?

The answer to that is the WebKit Remote Debugging Protocol, a JSON formatted protocol than enables communication between the frontend and the backend, and vice versa. This protocol is based on the JSON-RPC 2.0 specification. Currently there’s an attempt, under the RemoteDebug initiative, to standardize all the remote debugging protocols that major browsers use. Remote debugging is a bidirectional protocol: clients send asynchronous requests to the server, the server responds to these request and/or generates notifications. The protocol is divided into a different number of domains.

  • Console: Defines methods and events for interaction with the JavaScript console.
  • Debugger: Exposes JavaScript debugging functions; allows setting and removing breakpoints, stepping through execution, exploring stack traces, etc.
  • DOM: Exposes DOM read/write operations.
  • DOM Debugger: Allows setting breakpoints on particular DOM operations and events. JavaScript execution will stop on these operations as if there was a regular breakpoint set.
  • Network: Allows tracking network activities of the page; exposes information about HTTP and WebSocket requests and responses, their headers, bodies, raw timing, etc.
  • Page: Actions and events related to the inspected page.
  • Runtime: Exposes JavaScript runtime by means of remote evaluation and mirror objects.
  • Timeline: Provides its clients with instrumentation records that are generated during the page runtime.

Each domain defines a number of commands it implements and events it generates. For instance, when setting a breakpoint in the frontend’s console, the following message is sent:

{
    "id": 10, // <-- command sequence number generated by the caller
    "method": "Debugger.setBreakpointByUrl", // <-- protocol method
    "params": { // <-- named parameters map
        "lineNumber": 23,
        "url": "http://www.webkit.org/index.html"
    }
}

For this command, the backend will generate the following response:

{
    "id": 10, // <-- same id as in the command
    "result": { // <-- command result
        "breakpointId": "http://www.webkit.org/index.html:23",
        "locations": [
            {
                "lineNumber": 23,
                "columnNumber": 10
            }
        ]
    }
}

Frontend-to-backend communication: an example

Let’s use the clearMessages command and messagesCleared event defined in the Console domain (JavaScriptCore/inspector/protocol/Console.json) to illustrate how frontend-to-backend communication works:

{
    domain: Console,
    description: Console domain defines methods and events for interaction with the JavaScript console...
    commands: [ 
    {
        name: clearMessages,
        description: Clears console messages collected in the browser.
    },
    events: [ 
    {
        name: messagesCleared,
        description: Issued when console is cleared. This happens either upon <code>clearMessages</code> command or after page navigation.
    }]
}

In the frontend, the LogManager class (Controllers/LogManager.js) sends a clearMessages command through ConsoleAgent:

requestClearMessages()
{
    this._clearMessagesRequested = true;

    ConsoleAgent.clearMessages();
}

Domain commands are implemented in the backend by agents, which are located at WebCore/inspector/agents/ and JavaScriptCore/inspector/agents/, depending on what information available in the backend they need to access. Agents are accessible from the frontend through the Window object. In Main.js (UserInterface/Base/Main.js):

// Enable the Console Agent after creating the singleton managers.
if (window.ConsoleAgent)
    ConsoleAgent.enable();

The glue code that communicates the frontend with the backend is implemented by a set of dispatcher classes. These classes are generated automatically during the build process, out of the definition of the protocol domains. Here is an excerpt of JavaScriptCore/DerivedSources.make:

INSPECTOR_GENERATOR_SCRIPTS = \
    $(JavaScriptCore)/inspector/scripts/codegen/__init__.py \
    $(JavaScriptCore)/inspector/scripts/codegen/cpp_generator_templates.py \
    $(JavaScriptCore)/inspector/scripts/codegen/cpp_generator.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_backend_dispatcher_header.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_backend_dispatcher_implementation.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_frontend_dispatcher_header.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_frontend_dispatcher_implementation.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_protocol_types_header.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_cpp_protocol_types_implementation.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generate_js_backend_commands.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generator_templates.py \
    $(JavaScriptCore)/inspector/scripts/codegen/generator.py \
    $(JavaScriptCore)/inspector/scripts/codegen/models.py \
    $(JavaScriptCore)/inspector/scripts/generate-inspector-protocol-bindings.py \
    $(JavaScriptCore_SCRIPTS_DIR)/generate-combined-inspector-json.py \

# Inspector Backend Dispatchers, Frontend Dispatchers, Type Builders
InspectorFrontendDispatchers.h : CombinedDomains.json $(INSPECTOR_GENERATOR_SCRIPTS)
    $(PYTHON) $(JavaScriptCore)/inspector/scripts/generate-inspector-protocol-bindings.py --framework JavaScriptCore --outputDir . ./CombinedDomains.json

This is something common in WebKit, where there are many classes for which there is not existing code in the Source/ directory but are generated automatically and placed at Release/DerivedSources/. The InspectorBackendDispatchers.h (WebKitBuild/Release/DerivedSources/JavaScriptCore/inspector/InspectorBackendDispatchers.h) implements an interface for the domain commands, while the InspectorFrontDispatchers.h implements an interface for the domain notifications:

class JS_EXPORT_PRIVATE ConsoleBackendDispatcherHandler {
public:
    virtual void enable(ErrorString&) = 0;
    virtual void disable(ErrorString&) = 0;
    virtual void clearMessages(ErrorString&) = 0;
    virtual void setMonitoringXHREnabled(ErrorString&, bool in_enabled) = 0;
    virtual void addInspectedNode(ErrorString&, int in_nodeId) = 0;
protected:
    virtual ~ConsoleBackendDispatcherHandler();
};

The dispatching of a command is done by matching a command name with a method name and calling that method.

void ConsoleBackendDispatcher::dispatch(long callId, const String& method, Ref<InspectorObject>&& message)
{
    Ref<ConsoleBackendDispatcher> protect(*this);
  
    if (method == "clearMessages")
        clearMessages(callId, message);
    else if
        ...
    else
        m_backendDispatcher->reportProtocolError(&callId, BackendDispatcher::MethodNotFound, makeString('\'', "Console", '.', method, "' was not found"));
}

InspectorConsoleAgent.cpp (JavaScriptCore/inspector/agents/InspectorConsoleAgent.cpp) implements the clearMessages() method. Once it has finished it will send a notification back to the frontend through the frontendDispatcher class.

void InspectorConsoleAgent::clearMessages(ErrorString&)
{   
    m_consoleMessages.clear();
    m_expiredConsoleMessageCount = 0;
    m_previousMessage = nullptr;

    m_injectedScriptManager->releaseObjectGroup(ASCIILiteral("console"));

    if (m_frontendDispatcher && m_enabled)
        m_frontendDispatcher->messagesCleared();
}

Backend-to-frontend communication: response

The frontend dispatcher is the mechanism by which the backend can send information to the backend. The frontend dispatcher implements the protocol notifications of a domain:

// DO NOT EDIT THIS FILE. It is automatically generated from 
// CombinedDomains.json by the script: Source/JavaScriptCore/inspector/
// scripts/generate-inspector-protocol-bindings.py

class JS_EXPORT_PRIVATE ConsoleFrontendDispatcher {
public:
    ConsoleFrontendDispatcher(FrontendChannel* frontendChannel) : 
        m_frontendChannel(frontendChannel) { }
    void messageAdded(RefPtr<Inspector::Protocol::Console::ConsoleMessage> 
        message);
    void messageRepeatCountUpdated(int count);
    void messagesCleared();
private:
    FrontendChannel* m_frontendChannel;
};

In order to react to backend notifications, the frontend needs to register observers of backend events. This registration happens in Main.js:

// Register observers for events from the InspectorBackend.
if (InspectorBackend.registerConsoleDispatcher)
    InspectorBackend.registerConsoleDispatcher(new WebInspector.ConsoleObserver);
if (InspectorBackend.registerInspectorDispatcher)
    InspectorBackend.registerInspectorDispatcher(new WebInspector.InspectorObserver);
if (InspectorBackend.registerPageDispatcher)
    InspectorBackend.registerPageDispatcher(new WebInspector.PageObserver);

The frontend class ConsoleObserver.js (UserInterface/Protocol/ConsoleObserver.js) will react to messagesCleared event and trigger some programmatic or business logic:

WebInspector.ConsoleObserver = class ConsoleObserver
{
    // Events defined by the "Console" domain.
    messagesCleared()
    {
        WebInspector.logManager.messagesCleared();
    }
};

Localization

Web Inspector strings are localized. Localized strings are stored at localizedStrings.js (Localizations/en.lproj/localizedStrings.js). All UI strings are wrapped by the WebInspector.UIString() method, so they are printed localized:

grep -Rn "WebInspector.UIString(\"Resource" 

The contents of localizedStrings.js are not created manually but by running the script update-webkit-localizable-strings. This script parses all the strings marked to be localized and updates localizedStrings.js.

Bear this in mind if you send a patch with a new or modified string.

Sending a patch

When sending a patch subject should be prefixed by Web Inspector:. Before the patch is sent, some style checkers are run (Tools/Scripts/check-webkit-style), to verify the patch complies Web Inspector coding style. It’s also possible to run the script manually:

    Tools/Scripts/check-webkit-style
    Tools/Scripts/prepare-ChangeLog --bug xxxxx
    # edit WebCore/ChangeLog

As usual, modify the updated ChangeLogs and run webkit-patch upload to send your patch.

Community and resources

There’s a very convenient Wiki with very valuable sources of information such as pointers to blog posts, how to debug, open bugs, etc. The Web Inspector has also it’s own IRC channel at freenode: #webkit-inspector. Most of the work in Web Inspector is carried by Timothy Hatcher, Joseph Pecoraro and Brian Burg, with contributions of other WebKit hackers and other collaborators. I can tell patches and bug reports are very welcomed and reviews go very fast.

I also want to mention this post from Brian Burg that discusses Web Inspector architecture and I used as a basis for this post.

Summary

The Web Inspector is an important component of the WebKit project. It’s an application structured in 3 layers: a frontend, a backend and a target. This abstraction allows detaching the inspector from the target (inspected WebKit browser).

The frontend is a web application composed of HTML + CSS + JavaScript. It implements a Model-View-Controller pattern and makes heavy use of ECMA2015 features. The backend exposes information of WebKit’s WebCore and JavaScriptCore elements.

Communication between frontend and backend is provided by several dispatchers that communicate both parts through the WebKit Remote Debugging Protocol (a JSON-RPC 2.0 based protocol). The protocol defines several domains. Each domain defines commands and events which are the messages the frontend and the backend exchange with each other. Actual implementation of backend commands is provided by agent classes that live in WebCore and JavaScriptCore side. On the frontend side, several observer classes can listen and respond backend notifications.

It has been a rather long post. I hope it can serve as a starting point for anyone interested in understanding or hacking in this important component of the WebKit project.


webkit javascript igalia