Debugging with Source Code Instrumentation in Detail
This page gives an overview of the transformations applied to JavaScript source code in order to capture the call stack. The four basic transformations are: 1) Capturing computation progress during the method execution 2) Capturing method-local variables 3) Capturing lexically-bound variables 4) Tracking scopes of created closures
Let's use a very simple example, the "hello world" program in JavaScript:
1) Capturing computation progress during the method execution
Now when executing the example, the execution might stop in-between the two alert statements (e.g. due to a breakpoint). One possible way to capture how far the execution of a method has progressed is to use a program counter in the rewritten code like this:
original code
alert("Hello"); alert("World!");
However, in order to track the progress of executing a complex expression, the expression needs to be broken into simple assignments to temporary variables, so the program counter can be updated after each sub-expression.
rewritten code (example)
var _pc = 0; alert("Hello"); _pc = 1; alert("World!"); _pc = 2;
original code
var c = a() + b();
rewritten code (example)
var _pc = 0; var _t0 = a(); _pc = 1; var _t1 = b(); _pc = 2; var c = _t0 + _t1; _pc = 3;
Unfortunately, this becomes complicated when expressions involve conditional computation.
original code
var e = a() && b() ? c() : d();
rewritten code (example)
var _pc = 0; var _t0 = a(); if (_t0) { _pc = 1; var _t1 = b(); if (_t1) { _pc = 2; _t3 = c(); } else { _pc = 3; _t3 = d(); } _t2 = _t3; } else { _t2 = _t0; } _pc = 4; var e = _t2; _pc = 5;
If the expression above is used as guard condition of a while loop, it gets even more complicated. In the end, this way of tracking the program counter involves transformations which resemble the work a compiler does to break a high level language to low level assembly code. An alternative way is to make use of these intermediate computation results. These are necessary in order to resume the computation of an expression and, additionally, give a hint about the current progress of execution. The initial implementation used an JavaScript object (called "value map") to store these intermediate values under the key of their source code position.
original code
alert("Hello"); alert("World!");
rewritten code (example)
var _ = {}; // an empty value map _["0-14"] = alert("Hello"); _["16-31"] = alert("World!");
To be continued...
However, the amount of object accesses considerably slowed down the computation. That's why the current implementation used a different technique. Instead of using the source code position as key for accessing a temporary object, temporary variables are used. In order to differentiate between an uncompleted evaluation and a completed evalutation which yielded "undefinded", the value variables are initialized with a dedicated constant.
The state of the value map implicates the curent progress of the code execution.
original code
alert("Hello"); alert("World!");
rewritten code (live preview)
var _0_14 = __UNDEF; var _16_31 = __UNDEF; _0_14 = alert("Hello"); _16_31 = alert("World!")
The current progress of execution can be determined in a similar way as above:
Value Map
Code execution (evaluted lines are greyed out)
alert("Hello"); alert("World!");
{}
alert("Hello"); alert("World!");
{"0-14": undefined}
{ "0-14": undefined, "16-31": undefined}
alert("Hello"); alert("World!");
_0_14
Code execution (evaluted lines are greyed out)
_16_31
alert("Hello"); alert("World!");
__UNDEF
__UNDEF
alert("Hello"); alert("World!");
undefined
__UNDEF
undefined
alert("Hello"); alert("World!");
undefined
Disconnected