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:
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:
Once it’s built, open MiniBrowser and the Inspector (Ctrl+I) to see your changes:
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:
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:
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:
On the client side, open WebKit:
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
:
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:
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
:
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:
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:
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:
For this command, the backend will generate the following response:
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:
In the frontend, the LogManager class (Controllers/LogManager.js
) sends a clearMessages command through ConsoleAgent:
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
):
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
:
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:
The dispatching of a command is done by matching a command name with a method name and calling that method.
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.
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:
In order to react to backend notifications, the frontend needs to register observers of backend events. This registration happens in Main.js
:
The frontend class ConsoleObserver.js
(UserInterface/Protocol/ConsoleObserver.js
) will react to messagesCleared event and trigger some programmatic or business logic:
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:
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:
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.