Augmenting the Perl debugger for fun and profit
Background
I like the Perl debugger a lot. I use it daily, since understanding code by seeing it execute is much better than guessing at what it does based on its API. I do however seldomly use the actual command line debugger module, but instead use the Perl debugger built into Komodo IDE because it streamlines a lot of the busywork that is necessitated by having a debugger bound to a terminal:
- It automatically shows the variables in the surrounding lexical scope instead of making me type
x $var<ret>
every time i want to see my variables. - It allows me to step in, over or out of statements/subs by clicking, which is a lot faster than typing
s<ret>
,n<ret>
orr<ret>
a lot. - It allows me to have the debugger run to a certain line in the current or also other files by just setting the cursor on the line and hitting the shortcut for "Run to cursor".
- It allows me to define and store breakpoints based on the results of perl expressions without actually altering my source code.
- And more.
Problem
All is not perfect however. Both Komodo IDE and perl5db.pl lack nice interfaces for doing the opposite of a breakpoint, which is ignoring statements. Komodo IDE has flat-out no interface for that. The core perl5db.pl allows the user to define a sub DB::watchfunction() which is executed in the debugger's scope and can skip out, but that's a lot of busywork and means I'd have to specifically write the sub for all of my code.
Why is this important? Consider Web::Simple's role Web::Dispatch::ToApp. This is executed a lot during the dispatch process, which is also where i do a lot of my debugging to make sure my routes are solid. That means whenever a piece of code goes $obj->to_app->()
, i have to step into to_app
in order to be able to step into the actual call
sub of $obj
. Another example would be Moo's Sub::Defer::defer_sub::ANON, which is invoked on almost all Moo attribute accessors. That kind of thing is annoying and a waste of time.
So, what i needed was a module that can be loaded in my code, will be given some import parameters and will cause both the Komodo IDE debugger as well as perl5db.pl to skip over statements based on package or fully qualified sub-routine name.
If you don't care about the implementation, you can skip to the end of the article now, where i show the solution.
Implementation
Thanks to an article by brian d foy (hope i got his name right) I found that the Perl debugger modules are actually fairly simple: "Under the -d switch, perl calls &DB::DB before each statement" This is also documented in more detail in perldebguts.
Armed with that knowledge it's simple to write a function that augments the current DB::DB()
with a bit of code that interrogates the caller before heading into the debugger proper. We can't just directly call the old DB::DB()
function, since it relies on the stack level and makes heavy use of caller()
. So, a simple replacement that stores the old function and uses goto &{$ref}
should do the trick:
my $old_db = \&DB::DB;
*DB::DB = sub {
goto &{ $old_db };
};
Sadly it turns out that the goto makes the whole thing go out of the protected debugger scope in which DB::DB
is not executed for every statement; resulting in something akin to a fork bomb. So before we can actually do interesting stuff, we have to add a bit of code to skip out when we're being called from inside DB
or any other DB::*
module:
my $old_db = \&DB::DB;
*DB::DB = sub {
my $lvl = 0;
while ( my ( $pkg ) = caller( $lvl++ ) ) {
return if $pkg eq "DB" or $pkg =~ /^DB::/;
}
goto &{ $old_db };
};
I think it should be possible to munge with the stack directly as well, so we can just use return $old_db->( @_ )
, but my Perl-fu is not that good and Sub::UpLevel looks fairly daunting. (Suggestions welcome though.) One would think that doing it this way would make the debugger a lot slower, but honestly, i've been using it for a while now and couldn't notice a difference.
So, now we have a piece of code that acts as a pass-through to the debugger and doesn't blow everything up. That means we can add code to actually do things, but this turns out to be fairly trivial.
my %pkg_skip = (...);
my %sub_skip = (...);
my $old_db = \&DB::DB;
*DB::DB = sub {
my $lvl = 0;
while ( my ( $pkg ) = caller( $lvl++ ) ) {
return if $pkg eq "DB" or $pkg =~ /^DB::/;
}
return if $pkg_skip{ (caller)[0] };
my $sub = ( caller(1) )[3];
return if $sub and $sub_skip{$sub};
goto &{ $old_db };
};
There we go, a working piece of code that makes the debugger skip over statements in certain scopes or subs.
Solution
With a bit of parametrizing and cleaning up of this, the result is DB::Skip, a module that you can load in your code when you want to augment the debugger to skip statements. The use is extremely simple and it's made debugging a lot more pleasant for me already:
use DB::Skip (
pkgs => [
qr/^Method::Generate::/,
qw( Sub::Defer Sub::Quote Web::Dispatch::ToApp )
],
subs => [ 'Web::Dispatch::MAGIC_MIDDLEWARE_KEY' ]
);
Wow! Great work. Thanks for sharing.
I've done some work on the core's perl5db.pl as of late and plan to do some more. One thing it could use is more tests under t/perl5db.t. Currently the tests coveraege is not too good.