The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Devel::Agent - Agent like debugger interface

SYNOPSIS

  perl -d:Agent -MDevel::Agent::EveryThing myscript.pl

DESCRIPTION

For years people in the perl commnity have been asking for a way to do performance monitoring and tracing of runtime production code. This module attempts to fill this role by implementing a stripped down debugger that is intended to provide an agent or agent like interface for perl 5.34.0+ that is simlilar in nature to the agent interface in other langagues Such as python or java.

This is accomplished by running the script or code in debug mode, "perl -d:Agent" and then turning the debugger on only as needed to record traching and performance metrics. That said this module just provides an agent interface, it does not act on code directly until something turns it on.

Agent interface

The Agent interface is implemented via an on demand debugger that can be turned on/off on the fly. Also it is possible to run multiple different debugger instances with diffrent configurations on different blocks of code. This was done intentionally as perl is fairly complex in its nature and an agent interface isn't very useful unless it is flexible.

The agent interface itself is activated by setting $DB::Agent to an instance of itself.

To turn tracing on:

Make sure you either start your perl script -d:Agent or set PERL5OPT="-d:Agent" before launching your script

Inside you script you can issue the following

  use Data::Dumper;

  my $db=DB->new(
    save_to_stack=>1
  );

  $db->start_trace;

  # run some code
  ...

  # and we are done wathcing
  $db->stop_trace;

  # to dump a very human readable full trace
  print Dumper($db->trace);

But altering you code is far from ideal.

Another option is to load the agent and a module that pre-configures it for use such as the Devel::Trace::EveryThing module, wich provides a stack trace to STDERR in real time as frames begin and exit.

Example using: Devel::Trace::EveryThing

  perl -Ilib -d:Agent -MDevel::Agent::EveryThing examples/everything.pl

Classes that are Agent Aware

Any class that implements the $instance->___db_stack_filter($agent,$frame,$args,$raw_caller) can filter its own current frame prior to execution, or even prevent the frame from being traced at all.

The ___db_stack_filter method is expected to return true, if the call returns false, then the frame should not be traced. Since the frame passed in before it's runtime execution, the duration value will not be set.

A basic implementation that exposes only the top level calls is defined in Devel::Agent::AwareRole. Loading this role into your class will hide all calls made by your class, but not calls made directly to it, this includes child classes that make calls to other classes.

Example:

  package My::Class::That::IS::Mostly::Hidden;

  use Role::Tiny::With; # you can also use Moo Moose or other role implementations
  with 'Devel::Agent::AwareRole';

  1;

If you want to force a class to not show its internals.. say a class like LWP::UserAgent.

  use LWP::UserAgent;
  reuqire Devel::Agent::AwareRole;

  # now only the top level calls to LWP::UserAgent will show up
  *LWP::UserAgent::___db_stack_filter=\&Devel::Agent::AwareRole::___db_stack_filter;

Or if you need to disable filtering on a class that has filtering then you can do the opposite

  *LWP::UserAgent::___db_stack_filter=sub { 1}

To be fully ignored

  *LWP::UserAgent::___db_stack_filter=sub { 0}

Frame information

The following hash represents what is provided as a representation of a frame

  {
    caller_class=>'main',         # class that called this class
    calls=>[],                    # child frames, empty unless $self->save_to_stack is true
    class_method=>'main::test_a', # the resolved class::method
    depth=>1,                     # stack depth, 1 is considered the root
    duration=>undef|Float,        # how long the frame took to execute, only defined when the frame has executed
    end_id=>undef|Int,            # frame final execution order where in the stack it ended 
    line=>2,                      # line number the frame was called from
    no_frame=>0|1,                # when true, this frame would have been filtered but was included for completeness
    order_id=>1,                  # inital frame execution order, where in the stack it started
    owner_id=>0,                  # which order_id frame triggered the execution of this frame
    raw_method=>'main::test_a',   # un-resolved method name
    source=>'test.pl',            # the source file
    t0=>[0,0],                    # Frame Start timestamp in: epoch, microseconds
  }

DB Constructor options

This section documents the %args the be passed to the new DB(%args) or DB->new(%args) call. For each option documented in this section, there is an accesor by that given name that can be called by $self->$name($new_value) or my $current_value=$self->$name.

  • level=>ArrayRef

    This is an auto generated array ref that is use by the internals to track current stack level state information.

  • resolve_constructor=>Bool

    This option is used to turn on or off the resolution of a class name when being constructed and other situations. By default this option is set to true.

  • trace=>[]

    When the object instance is constructed with save_to_stack=>1 ( default is: 0 ) then the stack trace will be saved into a single multi tier data structrure represented by $self->trace.

  • ignore_calling_class_re=>ArrayRef[RegexpRef]

    This option allows a list of calling calsses to be ignored when they match the regular expression.

    Example ignore_calling_class_re being set to [qr{^Do::Not::Track::Me::}] will prevent the debugger for trying to trace or record calls made by any methods within "Do::Not::Track::Me::". This does not prevent this class from showing up fully ina stack trace. If this class calls a class that calls another class that calls a class unlisted in ignore_calling_class_re, then Do::Not::Track::Me::xxx will show up as the owner frame of the calls as a biproduct of correctness in stack tracing.

  • excludes=>HashRef[Int]

    This is a collection of classes that should be ignored when they make calls. The defaults are defined in @DB::EXCLUDE_DEFAULTS and include classes like Data::Dumper and Time::HiRes to name a few. For a full list of the current defaults just perl -MDevel::Agent -MData::Dumper -e 'print Dumper(\@DB::EXCLUDE_DEFAULTS)'

  • last_depth=>Int

    This value is used at runtime to determine the previous point in the stack trace.

  • depths=>ArrayRef

    This is used at runtime to determin the current frame stack depth. Each currently executing frame is kept in order from top to the bottom of the stack.

  • order_id=>Int

    This option acts as the sequence or order of execution counter for frames. When a frame starts $self->order_id is incremented by 1 and set to the frame's oder_id when the frame has completed execution the current $self->order_id is incremented again and set to the frame's end_id.

  • save_to_stack=>Bool

    This option is used to turn on or of the saving of frames details in to a layered structure inside of $self->trace. The default is 0 or false.

  • on_frame_end=>CodeRef

    This code ref is called when a frame is closed. This should act as the default data streaming hook callback. All tracing operations are halted durriong this callback.

    Example:

      sub {
        my ($self,$last)=@_;
    
        # $self: An instance of DB
        # $last: The most currently closed frame
      }
  • trace_id=>Int

    This method provides the current agent tracing pass. This number is incremented at the start of each call to $self->start_trace.

  • ignore_blocks=>HasRef[Int]

    This hashref reprents what perl phazed blocks to ignore, the defaults are.

      {
        BEGIN=>1, 
        END=>1,  
        INIT=>1,
        CHECK=>1,
        UNITCHECK=>1,
      }

    The default values used to generate the hashref contained in in @DB::@PHAZES

  • constructor_methods=>HashRef[Int]

    This is a hash of method names we consider object constructors

    Default:

      {
        # we assume new is an object constructor
        new=>1
      }
  • max_depth=>Int

    When the value is set to something other than -1, the number represents the maxium stack depth to trace too. Once the frame of max_depth exits, then max_depth will again be set to -1.

  • tid=>Int

    This is mostly here for completeness, retuns the tid id this debugger was created in.

    Note Note Note:

    The code was not orginally develpoed without threading in mind, if you wish to trace elemetns within a thread make sure you start an instance of the debugger within that thread.

  • agent_aware=>Bool

    This enables or disables the use of $self->_agent_aware(...) method. See: $self->_agent_aware. Default is true.

  • existing_trace=>Maybe[InstanceOf['DB']]

    This acts as a save point for another existing trace to be exected.

  • process_result=>CodeRef

    This callback can be used to evaluate/modify the results of a traced method in this callback.

    Example callback

      sub {
        my ($self,$type,$frame)=@_;
        # $self:  Instance of Devel::Agent
        # $type:  -1,0,1
        # $frame: The current frame
      }

    Notes on $type and where the current return values are stored

    When $type is: -1 This is in a call to DESTROY( the envy of the programming world! ) Return value is in $DB::ret

      0  This method was called in a scalar context
        Return value is in $DB::ret
    
      1  This method was called in a list context
        Return value is in @DB::ret
  • filter_on_args=>CodeRef

    This allows a code ref to be passed in to filter arguments passed into a method. If the method returns false, the frame is ignored.

    Default always returns true

      sub  { 
        my ($self,$frame,$args,$caller)=@_;
        # $self:   Instance of Devel::Agent
        # $frame:  Current frame hashref
        # $args:   The @_ contents
        # $caller: The contents of caller($depth)
        return 1; 
      }

    This is more or less a global frame filter, see: Classes that are Agent Aware

  • pid=>Int

    By default this is set to $$

API Methods

This section documents general api methods.

Bool=$self->_filter($caller,$args)

This method is the core interface used to decide if a method should show up in the debug/stack trace.

This is where the following constructor options are applied:

  • ignore_calling_class_re

    The $self->ignore_calling_class_re is applied to each calling or caller->[0], if it matches the frame is ignored.

  • excludes

    If $caller->[0] matches an element of excludes, then this frame is ignored.

  • resolve_constructor

    This converts the "class" portion of the frame value class_method to the first argument of a method that matches anything found in resolve_constructor.

  • excludes

    The updated class value is applied to excludes again, if it matches, then the frame is not traced.

$self->resolve_class($class,$method,$caller,$args)

Changes $class to the constructor class if $self->resolve_constructor && exists $self->constructor_methods->{$method}

$self->close_depth($depth)

After a frame ahs finished execution, this method is called. This removes the frame from $self->depths

Sets the following frame option:

  error     the $@ state
  end_id    the order_id this frame ended on
  duration  Execution time in micro seconds

my $id=$self->next_order_id

Returns the next order_id.

$self->close_to($depth,$frame|undef)

Closes down the stack trace to $depth, performs addtional actions if $frame is defiend.

$self->filter($caller,$args)

This method is called by the DB method after the sub method and is used to place a frame on the stack for tracing.

$self->_agent_aware($frame,$args,$raw_caller)

This method is called before a frame is traced if $self->agent_aware is set to true( the default ). The objective of this method is see of the first argument being passed to this method is a blessed instance of that class. When the first argument passed to the class is a blessed object that DOES this class then a call to $args->[0]->___db_stack_filter($agent,$frame,$args,$raw_caller) is made. This allows classes to modify or inspect the frame that will be used in the reace. If the call to $args->[0]->___db_stack_filter($agent,$frame,$args,$raw_caller) returns false, the frame is skipped durring the trace period.

Note note Note:

The call to $args->[0]->___db_stack_filter($agent,$frame,$args,$raw_caller) is never wrapped in a eval and should never do anything heavy or it will impact the metrics provided by the debugger.

Self->save_to($frame)

Saves the frame to the stack trace at the proper depth.

$self->push_to_stack($frame)

This method handles the saving, closing and backfilling logic for a given frame.

my $depth=$self->get_depth

Returns undef or a number representing the stack depth. When the value is undef, the trace would exceed $self->max_depth;

my $frame=$self->caller_to_ref($caller,$depth|undef,$raw_method,$no_frame)

Returns a frame hashref. If $depth is not defined a call to $depth is made, if it exceeds a set value of $self->max_depth undef is returned.

$self->reset

Rsets the internals of the object for starting a new stack trace.

$self->start_trace

Begins the stack trace process.

$self->stop_trace

Ends the current stack trace process.

$self->pause_trace

Turns tracing off until a call to $self->restore_trace is made

$self->restore_trace

Turns tracking back on.

$self->close_sub($res)

This method is called by the sub method. It is used to send notice that a frame has finished execution.

my $frames=$self->grab_missing($depth,$frame);

Returns any skiped frames between $depth and $frame.

$depth is expected to be a number and $frame expected to be a frame hash.

DB(@_)

This method is manditory for the implemntation of a debugger. With the current perl internals it is called every time a breakable point of code is reached. Unfortunatly this overhead is unavoidable.

Example:

  foreach(1,3,4) { # Debugger would normally stop here
    someMethod($_)  # agent tracing only happens when your function is called
  }

  # total calls would be 4 + 3 * 2
  # 1 call when foreach is reached
  # 1 call for each element in the loop
  # 2 times for every method

The as an optimization the agent ensures that tracing only happens on user defined functions. All other operations should be ignored.

The optimization reduces the number of calls to 3 or just once per method.

sub(@_)

THis is a manditory for implementation, this method is called twice per user defined method. Once before the execution once to actually run the function.

Example:

  foreach(1,3,4) { 
    someMethod($_)  # sub is called 2 times per function
  }

  # calls to sub 6

If $AGENT is undef, this method does nothing.

Compile time notes

For perl 5.34.0+

When loading this moduel All features of the debugger are disabled aside from: ( 0x01, 0x02, and 0x20 ) which are requried to force the execution of DB::DB. Please see the perldoc perlvar and the $PERLDB section.

  Which means:    $^P==35

RUNTIME

At runtime, this modue tries to exectue $Devel::Agent::AGENT->filter($caller,$args). If $Devel::Agent::AGENT is not defined, then nothing happens.

  $caller: is the caller information
  $args:   contains an array reference that represents the arguments passed to a given method

TODO

  1. Add Dancer2 trace implementation/example

AUTHOR

Michael Shipper AKALINUX@CPAN.ORG

Silly stuff

Please forgive the typos, this was written on holiday in my spare time.

LICENSE

This code is released under the terms of the perl5 licence itself. Please see LICENSE.md for more details.

See Also

Lots of these internals are based on the following:

DB,Devel::Trace,perldebguts