Platform/JSDebugv2

From MozillaWiki
Jump to navigation Jump to search

This is a DRAFT.

Comments are welcome on dev-tech-js-engine(at)lists.mozilla.org; or, you can send them directly to me at jimb(at)mozilla.com.

js::dbg2: JavaScript Debugging Interface, v2

We'd like to improve the Mozilla platform's debugging facilities, for a number of reasons:

  • Beyond debuggers, we want to encourage the creation of other sorts of monitoring and manipulation tools for web code; "watching programs run" is a broad charge. Think DTrace and SystemTap.
  • Our JavaScript implementation is changing rapidly; we need to do better than falling back to the bytecode interpreter whenever the debugger is enabled.
  • We need to be able to monitor and debug worker threads.
  • We need to be able to monitor and debug JavaScript running on embedded devices.
  • Now that SpiderMonkey is C++, an interface designed in that language can be more expressive and less error-prone than a C interface.

General goals

  • The interface must operate at the source language level, and not expose details of the implementation technique: it should behave the same way regardless of whether the debuggee is being executed by a bytecode interpreter (SpiderMonkey classic), a just-in-time compiler (TraceMonkey), or a whole-method JIT (Jägermonkey). If the implementation compiles to native code, the debugging interface should be independent of the underlying processor architecture. The interface should be sufficiently high-level to allow debugging of (say) JITted code without requiring the implementation to pretend that is still a bytecode interpreter.
  • The interface must support cross-thread debugging: if the client uses the interfaces provided for this purpose, it should be able to debug JavaScript code running in another thread.
  • The interface must be cross-runtime: it should allow full inspection of JavaScript values, including objects, without creating direct inter-runtime object references or otherwise violating the rules for working with multiple runtimes.
  • The interface must be network-transparent: using the appropriate interfaces, a client should be able to inspect the state of a JavaScript program running on another machine.

Since the interface is both is network-transparent and independent of the implementation's machine architecture, this means it can be used for debugging JavaScript running on (say) mobile devices, assuming an appropriate connection can be set up.

Event Handlers and Spheres

A JavaScript debugger connects to a debuggee by expressing to js::dbg2 its interest in events occurring in particular spheres. Events are things like breakpoint or watchpoint hits, completions of single-step operations, exceptions being thrown, or 'eval' being called. Spheres are things like particular global objects, origins (in the HTML5 sense), XUL chrome, worker threads, or other things that identify subdivisions of the system that one might want to select to debug.

(In jsd, the jsdIFilter interface attempts to help the debugger distinguish the events it cares about from those it doesn't, but it bases its decisions on script URL patterns; the debugger user wants to debug a particular web site, which could use code from any number of sources. js::dbg2's filtering by origin (web site) and global (web page) provide a better basis for implementing the behavior the debugger's users actually want.)

It may be helpful to provide events reporting the creation and destruction of spheres (creating new tabs; visiting new web sites); this is something I don't understand well yet.

Frames and Scopes

Like jsd, js::dbg2 represents the control stack as a list of frames.

  • A frame representing a call to a JavaScript function has a source location (a URL,line pair), and a scope (a set of identifier bindings). Given a scope, one can look up an identifier's binding, enumerate the bindings present, find its enclosing scope, evaluate JavaScript expressions in that scope, and so on.
  • A frame representing a call to a host function (implemented in C++, say) will have some appropriate identification.

In this area, js::dbg2's behavior will not differ much from jsd's, except that it will identify the current point of execution using script URLs and line numbers, not a script proxy objects and bytecode offsets; see "High-level Source Positions", below.

Value Proxies

Like jsd, js::dbg2 does not permit the debugger to refer to values in the debuggee directly. Instead, it provides proxy objects (analogous to jsd's jsdIValue) which facilitate inspection, but protect the debugger from inadvertently invoking getters, setters, and the like. js::dbg2 will follow jsd's design here, except that the facilities for examining object properties will more closely resemble ES5's inspection facilities (Object.getOwnPropertyDescriptor, etc.)

js::dbg2 aims to support debugging interfaces that correlate values in JS programs with DOM trees, CSS rules, and content rendered on the screen. Thus, js::dbg2 proxy objects representing DOM nodes and other interesting host objects should provide extended interfaces to support these sorts of XUL-specific exploration.

High-level Source Positions

The js::dbg2 interface will allow the debugger to specify breakpoint positions in terms of script URLs and line numbers, or function names qualified by enclosing scopes, not JSScript objects and bytecode offsets. It will be the responsibility of js::dbg2 to manage the mapping between source locations and trapped bytecodes, and insert and remove trap bytecodes as JSScripts objects are created and destroyed.

Code passed to 'eval' or the 'Function' constructors, or established via DOM manipulation, will be assigned synthetic names; see also "Script Labeling", below.

The jsd interface for setting breakpoints requires the debugger to identify the jsdIScript (a wrapper for JSAPI JSScript objects) and bytecode offset within that script at which the breakpoint should be inserted. The debugger is responsible for tracking the creation and destruction of scripts, mapping source locations to (script, bytecode offset) pairs, and inserting and removing trap points. There are a number of problems with this approach:

  • The script+offset interface is oriented towards one particular implementation of JavaScript, out of the three we now have. The new interface for breakpoint specification is implementation-neutral, as it expresses locations strictly in terms of JavaScript source code.
  • Tracking the creation and destruction of scripts is a source of considerable complexity in the debugger; being able to take advantage of SpiderMonkey's own data structures for managing JSScripts, which may need some revisions, should be a net simplification.
  • If a bug in the debugger causes it to supply an incorrect location for a breakpoint trap bytecode, the debugger can cause the interpreter to crash. (At the moment, the system does not even check that the trap locations provided by the debugger are valid offsets into the script's bytecode, but that could easily be fixed.)
  • For remote debugging, it would be very inefficient to report the creation and destruction of all JSScripts across the communication channel to the debugger.

Remote Debugging

js::dbg2 will provide facilities for connecting to a remote XUL process, either on the same machine or via a network or hardware connection, and enumerating the spheres present in that process, providing human-readable descriptions. If the js::dbg2 client expresses an interest in events occurring in such spheres, a remote debugging session is established.

This communication will be implemented using something resembling V8's Debugger Protocol and Chrome's ChromeDevTools Protocol.

Remote debugging support will make a number of things possible:

  • The debugger UI can move into its own process (say, as a XULrunner application), providing better debugger/debuggee segregation.
  • A debugger running in a separate process will be able to provide better chrome debugging, as the debugger won't be trying to operate on its own chrome.
  • We can use it to debug worker threads, simply by using an intra-process communications channel (and perhaps using the fact that we share the debuggee's architecture and ABI to use a simpler protocol).

Compilation Hooks And Script Interrogation

Instead of jsd's onScriptCreated and onScriptDestroyed hooks, js::dbg2 will provide events for the start and end of each compilation, not individual scripts created by those compilations.

The 'compilation start' event will make available the full text to be compiled (if available; compilation can consume tokens from a <stdio.h> FILE, although I don't think the browser uses this).

The 'compilation end' event will make available a list of the names of functions declared in the compiled script.

Script Labeling

We should provide variants of 'eval' and the 'Function' constructor that allow their callers to provide a URL and line number for the code being evaluated, just as the JSAPI JS_EvaluateScript function does. This is a trivial change that, with cooperation from loaders and debuggers, will improve the debugging experience and allow debuggers to be more robust.

Real-life web code often uses 'loaders': JavaScript programs that retrieve code using an XMLHTTPRequest and pass it to 'eval'. Firebug (and other JavaScript debuggers, apparently) go to great lengths to find such scripts and assign them meaningful names; for example, Firebug searches the script's source code for specially formatted comments at the bottom that supply the script's URL, or generates identifiers based on content hashes. However, a cooperative loader could simply supply an appropriate name or URL for the script that the debugger could display to its users.

Template engines and other code generators are also popular, producing JavaScript code on the fly and passing it to eval or the Function constructor. In these cases, there may be no underlying URL, but it would still be valuable to the user if the debugger could identify the parameters used to produce the code.

Debugging of JITted code

Although debugging may disable just-in-time compilation for the time being, in the long term we would like to support debugging of code that has been compiled by Jägermonkey, and perhaps to some degree by TraceMonkey. Some operations would be restricted, but allowing code to run at full speed under the debugger seems like a valuable feature.

In the case of Jägermonkey, the compiler would need to maintain a mapping from generated machine code instructions to source locations, scope extents, and stack information. JavaScript-level breakpoints could be implemented by placing machine-level breakpoints in the compiled code, and then using a signal handler that uses the instruction address to probe this map, find variable's homes, and walk the stack.

Much of the challenge here will be in handling variable references:

  • Unused variables may not be represented in the machine code at all.
  • Null closures may not provide enough information to find variables in enclosing scopes.
  • The compiler may have made assumptions that restrict what sorts of values can be assigned to a variable, or make it impossible to assign to the variable at all.
  • Allowing the user to add and delete variables by passing 'var' and 'delete' forms to the debugger's 'evaluate-in-frame' command may not be practical.

However, in almost all cases, simply being able to produce a stack trace and show the values of the variables will be sufficient for most users.

Debugging code compiled by TraceMonkey may be more difficult to support, as that compiler seems to generate machine code that is further from the original source, but it's still worth looking into. Again, getting this mostly right will be perfectly fine for many users.

No Cross-Runtime Debugging

The jsd interface only supports debugging programs running in a single Runtime at once. There has been some discussion about whether js::dbg2 should support inter-runtime debugging, but this has been set aside:

  • Intra-runtime debugging isn't required for any of our current plans. Worker threads, Chrome and content all share a single runtime, and there are no plans to change this.
  • Experienced SpiderMonkey developers did not feel that segregating debugger and debuggee in separate runtimes offered much benefit in practice.

Internal Debugging Models

The js::dbg2 debugging interface operates at the JavaScript level, not at the C++ or machine level. It assumes that the JavaScript implementation itself is healthy and responsive: the JavaScript program being executed may have gone wrong, but the JavaScript implementation's internal state must not be corrupt. Bugs in the implementation may cause the debugger to fail; bugs in the interpreted program must not.

Whenever a program's execution is paused, the C++ call stack looks like this (younger frames appear above older frames):

debugger machinery frames
interpreter/JITted frames for debuggee
top-level event loop

In this case, the "debugger machinery" is responsible for reporting the state of the JavaScript debuggee and interacting with the debugger's user interface until the program is continued. When control continues, the "debugger machinery" frame simply returns, and the "interpreter frames for debuggee" resume execution. If the user decides to stop executing the debuggee, the "debugger machinery" frame throws an appropriate, uncatchable exception, allowing the interpreter to clean up its state in an orderly way.

Evaluating User Expressions

If the user asks the debugger to evaluate an expression that requires evaluating JavaScript code (like e.x()), then the C++ stack looks like this:

interpreter/JITted frames for expression given to debugger
debugger machinery frames
interpreter/JITted frames for debuggee
top-level event loop

If evaluation of the expression throws an exception or hits a breakpoint, then the result is a matter of user interface. Either we abandon evaluation of the expression, and C++ control returns to the original machinery frames:

debugger machinery frames
interpreter/JITted frames for debuggee
top-level event loop

Or we treat the event as something to be investigated, just as if it had occurred in the debuggee's normal course of execution:

nested debugger machinery frames
interpreter/JITted frames for expression given to debugger
debugger machinery frames
interpreter/JITted frames for debuggee
top-level event loop

Again, the debugger machinery is not written to tolerate corrupt interpreter data structures or incomplete execution states; it relies on the interpreter's debugging API working correctly.

Same-Stack Debugging

In the current model for debugging Firefox, the debugger runs in the same process as the debuggee. Since the XUL user interface only allows one thread to interact with it, the debugger's user interface must share a thread, and thus a stack, with the debuggee. Thus, when the debuggee is paused and the user is interacting with the debugger's user interface, the C++ stack looks like this:

debugger UI frames
(that is, more interpreted/JITted JS frames)
nested event loop invocation
debugger machinery frames
interpreter/JITted frames for debuggee
top-level event loop

There are a number of complications that arise from this model:

  • The debugger's UI and the debuggee share a DOM, and may interact with each other in unexpected ways through that DOM.
  • The debugger should never refer to the debuggee's objects directly --- it is too easy to introduce bugs and security holes by doing so. However, avoiding this is similar to the problem of ensuring that references between Firefox chrome and content go through the proper wrapper objects. This seems to be challenging in practice.

Remote Debugging

One way to avoid the issues mentioned above is to move the debugger UI into its own process, and have it communicate with the debuggee using a wire protocol. (See Remote Debugging, above.)

This ability is also helpful when the debuggee is running on a device with a limited user interface (say, a mobile phone or tablet computer): it can be valuable to have the debugger's user interface running on a workstation or laptop. In this case, the C++ call stack looks like this:

debug protocol server
nested event loop invocation
debugger machinery frames
interpreter/JITted frames for debuggee
top-level event loop

The stack of the debugger's user interface can be whatever is convenient, as long as it communicates appropriately with the debug server. But one possible arrangement would be to treat the protocol as simply another back end for the js::dbg2 interface; the debugger UI would behave identically regardless of whether the debuggee was local or remote. Thus, the C++ stack in the process running the debugger UI would look like this:

debugger UI frames
nested event loop invocation
debugger machinery frames
debugger back end: debug protocol client
top-level event loop

Remote debugging also enables debugging worker threads: if the worker's top-level event loop responds to messages registering the debugger's interest in the sphere

Remote debugging also prepares us to support debugging content in an architecture which places content JavaScript in separate processes from chrome JavaScript.

Separate Windows Cannot Be Debugged Independently

One interesting consequence of the fact that Firefox uses a single thread for all chrome and content JavaScript is that independent windows (in the sense of an HTML5 "Window" object; tabs are windows) cannot be debugged independently. Suppose we hit a breakpoint in one window:

debugger UI frames
nested event loop invocation
debugger machinery frames
interpreter/JITted frames for first window
top-level event loop

Then we switch to a different window and hit a breakpoint there, as well:

debugger UI frames
nested event loop invocation
debugger machinery frames
interpreter/JITted frames for second window
nested event loop invocation
debugger machinery frames
interpreter/JITted frames for first window
top-level event loop

(I believe Firebug currently forbids this situation from arising, either by refusing to allow debugging to occur in the second window, or by throwing away the first window's JavaScript stack. But the goal here is to point out intrinsic limitations in Firefox's execution model, regardless of how Firebug behaves.)

In this case, we cannot simply switch back to the first window and resume execution there: we must first finish (or abandon) execution in the second window, because its stack frames are on top of the ones we wish to resume.

There are two general solutions. The first would be to change SpiderMonkey to represent the JavaScript stack entirely in the heap, such that no C++ frames accumulate in the above scenario, and then use a separate JavaScript stack for each window. However, aside from the engineering work needed, accomodating native frames mixed with JavaScript frames in this arrangement would be a challenge.

The second is to change Firefox to use a separate C++ stack for each window, by creating a separate thread for each window. These threads would not run concurrently (if properly designed, the functions for passing control from one stack to another can guarantee this), avoiding the sorts of unreproducible behavior that make most multi-threaded, shared memory programming so difficult.

If Firefox evolves towards a process-per-window model, then it will have a separate stack per window, and the debugging restrictions described above can be lifted. However, if the user creates a large number of windows, Firefox may need to have windows share processes; in this case, the multiple, non-mutually-preemptive thread model described above could provide consistency between the process-per-window and several-windows-per-process arrangements.