Wednesday, March 26, 2008

How to work with jsd (Javascript Debugger)

I was really determined to figure out how firebug and venkman works, so I can write a javascript walker or stepper for my firecruncher plugin. The further I investigate, the more I respect the authors and contributors of these two pieces of seriously written software.

I started with the UI, and then I headed for the core that interacts with the xpcom compionent jsd (Javascript Debugger).

It never seamed easy for me, even though there are some sparse documentation in xulplanet, but the only two examples that I was able to consult was
It really took me a while figuring out how jsd works, as both of the source above have thousands of lines of javascript, a lot of them has interactions with objects outside of the classes or services that are defined in the same file.

After a week's worth of night reading, prototyping and code porting, I finally got it. Well, most of it. ;-)

It turns out that using jsd isn't that hard at all if you know Javascript's event handing mechanism. Well, in jsd, the original designer called them hooks. There are hooks for almost everything from the creation and destruction of a script, errors, to function call and program counters (interrupt or execution). Just think of them as different events that will be fired during a debugger active session. All what we need to do is to write the appropriate handers, and hook back into the jsd.

I will describe a common life cycle of the usage of jsd below:

First, we acquire the @mozilla.org/js/jsd/debugger-service;1 service.
var jsd = Components.classes["@mozilla.org/js/jsd/debugger-service;1"].getService(jsdIDebuggerService);
jsd.on();
Ref: http://www.xulplanet.com/references/xpcomref/ifaces/jsdIDebuggerService.html
With this, we can turn on, off, pause and unpause the debugger, but how do we set a break point? Ha, we are jumping ahead of ourselves. We need a piece of script first, so we can set the break point on top of it. Here is how you track which script has been created and which one has been destroyed.
jsd.scriptHook =
{
onScriptCreated: function(script){/* xxx track the scripts xxx */},
onScriptDestroyed: function(script){/* xxx track the scripts xxx */}
};
You can also enumerate all of the existing scripts that are available before the debugger was turned on by:
jsd.enumerateScripts({enumerateScript: function(script)
{
/* xxx track the scripts xxx */
}});
Now, we have all of the scripts available for us to set the break points. ;-)
Ref: http://www.xulplanet.com/references/xpcomref/ifaces/jsdIScript.html

That's all good until we encounter an error while running the script that we have loaded into the debugger. What do we do then? Ha, the ErrorHook comes handy.
jsd.errorHook = { onError: function(message, fileName, lineNo, pos, flags, errnum, exc){ /* xxx handle the error here xxx */ }};
You can do similar things with debuggerHook and debugHook. So what happens when we encounter a break point? Of course, there is a break point hook as well ;-)
jsd.breakpointHook = { onExecute: function(frame, type, val){ /* xxx handle the break point xxx */} };
We can do something at a break point now, but how do we temporarily suspend the code execution while we are at a break point, and do something like step over? It took me some search to figure this out ;-) We use enterNestedEventLoop to pause, and exitNestedEventLoop to get out of the paused state.
jsd.enterNestedEventLoop({onNest: function() { /* xxx another hook here, so we know that we are in a paused state xxx*/ });
jsd.exitNestedEventLoop(); // Yeh.. we are happily continuing our course of execution.
OK, we now can get a script, handle the error, set the break point, handle the break point, pause at the break point and continue from the break point. So how do we do the stepping? This is done by two other interrupts: functionHook and interruptHook ;-) In the case of step over, we use interruptHook, in the case of step out and step in, we use functionHook. (Ref below is copied from firebug-service.js)
hookFunctions: function()
{
function functionHook(frame, type)
{
switch (type)
{
case TYPE_FUNCTION_CALL:
{
++hookFrameCount;

if (stepMode == STEP_OVER)
jsd.interruptHook = null;

break;
}
case TYPE_FUNCTION_RETURN:
{
--hookFrameCount;

if (hookFrameCount == 0)
fbs.stopStepping();
else if (stepMode == STEP_OVER)
{
if (hookFrameCount <= stepFrameCount)
fbs.hookInterrupts();
}
else if (stepMode == STEP_OUT)
{
if (hookFrameCount < stepFrameCount)
fbs.hookInterrupts();
}

break;
}
}
}

jsd.functionHook = { onCall: functionHook };
},

hookInterrupts: function()
{
function interruptHook(frame, type, rv)
{
// Sometimes the same line will have multiple interrupts, so check
// a unique id for the line and don't break until it changes
var frameLineId = hookFrameCount + frame.script.fileName + frame.line;
if (frameLineId != stepFrameLineId)
return fbs.onBreak(frame, type, rv);
else
return RETURN_CONTINUE;
}

jsd.interruptHook = { onExecute: interruptHook };
},
That's it!

3 comments:

soney said...

Thanks! You just saved me a lot of time diving through the Venkman code

Lei said...

No problem ;-)

Have fun! Let me know if you have any more questions!

reesd said...

This definitely is useful, wish I would have found it sooner.
One thing you seemed to skip was how to actually set a breakpoint. First you said it was too soon, then later you said we had learned how to do so :).
I can see its in setBreakpoint on the jsdIScript interface, but a better understand of what gets passed as the argument would be great.
Thanks again, dave