clueless newbie has asked for the wisdom of the Perl Monks concerning the following question:

Greeting Monks,

I’m experimenting with using the built-in `DB` package to build a lightweight call tracer. I’ve previously done something similar using **Hook::LexWrap** and **Module::Info**, but this time I’d like to catch **entry and exit** events for subroutines directly through Perl’s built-in debugging interface.

Here’s the minimal prototype I’ve tried:

+ package Devel::Lite; use strict; use warnings; use Time::HiRes (); our $IN_TRACE = 0; sub DB::sub { return &$DB::sub(@_) if $IN_TRACE; local $IN_TRACE = 1; my $sub = $DB::sub; my @args = @_; print STDERR ">> entering $sub\n"; my @ret; eval { @ret = &$sub(@args); }; print STDERR "<< leaving $sub\n"; die $@ if $@; return wantarray ? @ret : $ret[0]; } 1;

Then I run it like this:

perl -I. -d:TobyLite -e "sub x { print qq{okay\n}; } x();"

…but it loops endlessly and never reaches my test sub.

If I insert a print inside the loop, I see repeated `"Time::HiRes::time"`, suggesting recursion into the debugger hook itself.

I’ve tried things like skipping `Devel::Lite` and `Time::HiRes` calls explicitly, and guarding with `$IN_TRACE`, but the loop persists.

What I’d like to understand is:

1. Why is `DB::sub` being triggered recursively despite the guard?

2. What’s the correct minimal pattern for tracing sub entry/exit safely?

3. Bonus: how best to distinguish user code from CPAN/core subs to keep the trace clean?

Environment:

This is perl 5.38.x built for MSWin32-x64-multi-thread

Running on Windows 10.

Any insight or examples would be appreciated — I’m not trying to build a full debugger, just a simple tracer that logs the call tree.

TIA

PS: So far this has stumped (in no order) ChatGPT, claude, and Gemini.

Replies are listed 'Best First'.
Re: Trouble writing a simple tracer using the DB package (loops endlessly)
by tonyc (Hermit) on Oct 11, 2025 at 01:45 UTC

    Try doing the inner call from package DB:

    eval { package DB; @ret = &$sub(@args); };

    There's a few other issues:

    • You're making a copy of @_ and passing that copy, but perl passes by reference, and some code uses that (using the &$sub; form of function call fixes this)
    • The way you handle $IN_TRACE means you'll never capture inner calls (like if your x() called z()).
    • Your inner call is always in list context.

    I played with it a bit:

    package Devel::TobyLite; use strict; use warnings; use Time::HiRes (); our $IN_TRACE = 0; sub DB::DB {} sub DB::sub { goto &$DB::sub if $IN_TRACE; my $start; my $sub = $DB::sub; { local $IN_TRACE = 1; print STDERR ">> entering $sub\n"; $start = Time::HiRes::time; } my (@ret, $ret); #package DB { if (wantarray) { no strict "refs"; eval { package DB; @ret = &$sub; }; } elsif (defined wantarray) { no strict "refs"; eval { package DB; $ret = &$sub; }; } else { no strict "refs"; eval { package DB; &$sub; }; } #} { local $IN_TRACE = 1; my $end = Time::HiRes::time(); print STDERR "<< leaving $sub ", $end - $start, "\n"; die +(caller), $@, "@_" if $@; } if (wantarray) { @ret; } else { $ret; } } 1;

    Output:

    $ ./perl -Ilib -I.. -d:TobyLite -e 'sub x { print qq(ok\n); } sub z { +x() } z()' >> entering UNIVERSAL::import << leaving UNIVERSAL::import 1.40666961669922e-05 >> entering main::z >> entering main::x ok << leaving main::x 3.29017639160156e-05 << leaving main::z 8.48770141601562e-05

      THANK YOU!