From 7c48e67cf07ee41bfde7139a62bb232bd23a4a48 Mon Sep 17 00:00:00 2001 From: Lorry Tar Creator Date: Wed, 6 Jun 2012 16:41:29 +0000 Subject: Imported from /srv/lorry/lorry-area/perl-dbi-tarball/DBI-1.622.tar.gz. --- lib/DBI/Profile.pm | 949 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 949 insertions(+) create mode 100644 lib/DBI/Profile.pm (limited to 'lib/DBI/Profile.pm') diff --git a/lib/DBI/Profile.pm b/lib/DBI/Profile.pm new file mode 100644 index 0000000..a468c05 --- /dev/null +++ b/lib/DBI/Profile.pm @@ -0,0 +1,949 @@ +package DBI::Profile; + +=head1 NAME + +DBI::Profile - Performance profiling and benchmarking for the DBI + +=head1 SYNOPSIS + +The easiest way to enable DBI profiling is to set the DBI_PROFILE +environment variable to 2 and then run your code as usual: + + DBI_PROFILE=2 prog.pl + +This will profile your program and then output a textual summary +grouped by query when the program exits. You can also enable profiling by +setting the Profile attribute of any DBI handle: + + $dbh->{Profile} = 2; + +Then the summary will be printed when the handle is destroyed. + +Many other values apart from are possible - see L<"ENABLING A PROFILE"> below. + +=head1 DESCRIPTION + +The DBI::Profile module provides a simple interface to collect and +report performance and benchmarking data from the DBI. + +For a more elaborate interface, suitable for larger programs, see +L and L. +For Apache/mod_perl applications see +L. + +=head1 OVERVIEW + +Performance data collection for the DBI is built around several +concepts which are important to understand clearly. + +=over 4 + +=item Method Dispatch + +Every method call on a DBI handle passes through a single 'dispatch' +function which manages all the common aspects of DBI method calls, +such as handling the RaiseError attribute. + +=item Data Collection + +If profiling is enabled for a handle then the dispatch code takes +a high-resolution timestamp soon after it is entered. Then, after +calling the appropriate method and just before returning, it takes +another high-resolution timestamp and calls a function to record +the information. That function is passed the two timestamps +plus the DBI handle and the name of the method that was called. +That data about a single DBI method call is called a I. + +=item Data Filtering + +If the method call was invoked by the DBI or by a driver then the call is +ignored for profiling because the time spent will be accounted for by the +original 'outermost' call for your code. + +For example, the calls that the selectrow_arrayref() method makes +to prepare() and execute() etc. are not counted individually +because the time spent in those methods is going to be allocated +to the selectrow_arrayref() method when it returns. If this was not +done then it would be very easy to double count time spent inside +the DBI. + +=item Data Storage Tree + +The profile data is accumulated as 'leaves on a tree'. The 'path' through the +branches of the tree to a particular leaf is determined dynamically for each sample. +This is a key feature of DBI profiling. + +For each profiled method call the DBI walks along the Path and uses each value +in the Path to step into and grow the Data tree. + +For example, if the Path is + + [ 'foo', 'bar', 'baz' ] + +then the new profile sample data will be I into the tree at + + $h->{Profile}->{Data}->{foo}->{bar}->{baz} + +But it's not very useful to merge all the call data into one leaf node (except +to get an overall 'time spent inside the DBI' total). It's more common to want +the Path to include dynamic values such as the current statement text and/or +the name of the method called to show what the time spent inside the DBI was for. + +The Path can contain some 'magic cookie' values that are automatically replaced +by corresponding dynamic values when they're used. These magic cookies always +start with a punctuation character. + +For example a value of 'C' in the Path causes the corresponding +entry in the Data to be the name of the method that was called. +For example, if the Path was: + + [ 'foo', '!MethodName', 'bar' ] + +and the selectall_arrayref() method was called, then the profile sample data +for that call will be merged into the tree at: + + $h->{Profile}->{Data}->{foo}->{selectall_arrayref}->{bar} + +=item Profile Data + +Profile data is stored at the 'leaves' of the tree as references +to an array of numeric values. For example: + + [ + 106, # 0: count of samples at this node + 0.0312958955764771, # 1: total duration + 0.000490069389343262, # 2: first duration + 0.000176072120666504, # 3: shortest duration + 0.00140702724456787, # 4: longest duration + 1023115819.83019, # 5: time of first sample + 1023115819.86576, # 6: time of last sample + ] + +After the first sample, later samples always update elements 0, 1, and 6, and +may update 3 or 4 depending on the duration of the sampled call. + +=back + +=head1 ENABLING A PROFILE + +Profiling is enabled for a handle by assigning to the Profile +attribute. For example: + + $h->{Profile} = DBI::Profile->new(); + +The Profile attribute holds a blessed reference to a hash object +that contains the profile data and attributes relating to it. + +The class the Profile object is blessed into is expected to +provide at least a DESTROY method which will dump the profile data +to the DBI trace file handle (STDERR by default). + +All these examples have the same effect as each other: + + $h->{Profile} = 0; + $h->{Profile} = "/DBI::Profile"; + $h->{Profile} = DBI::Profile->new(); + $h->{Profile} = {}; + $h->{Profile} = { Path => [] }; + +Similarly, these examples have the same effect as each other: + + $h->{Profile} = 6; + $h->{Profile} = "6/DBI::Profile"; + $h->{Profile} = "!Statement:!MethodName/DBI::Profile"; + $h->{Profile} = { Path => [ '!Statement', '!MethodName' ] }; + +If a non-blessed hash reference is given then the DBI::Profile +module is automatically C'd and the reference is blessed +into that class. + +If a string is given then it is processed like this: + + ($path, $module, $args) = split /\//, $string, 3 + + @path = split /:/, $path + @args = split /:/, $args + + eval "require $module" if $module + $module ||= "DBI::Profile" + + $module->new( Path => \@Path, @args ) + +So the first value is used to select the Path to be used (see below). +The second value, if present, is used as the name of a module which +will be loaded and it's C method called. If not present it +defaults to DBI::Profile. Any other values are passed as arguments +to the C method. For example: "C<2/DBIx::OtherProfile/Foo:42>". + +Numbers can be used as a shorthand way to enable common Path values. +The simplest way to explain how the values are interpreted is to show the code: + + push @Path, "DBI" if $path_elem & 0x01; + push @Path, "!Statement" if $path_elem & 0x02; + push @Path, "!MethodName" if $path_elem & 0x04; + push @Path, "!MethodClass" if $path_elem & 0x08; + push @Path, "!Caller2" if $path_elem & 0x10; + +So "2" is the same as "!Statement" and "6" (2+4) is the same as +"!Statement:!Method". Those are the two most commonly used values. Using a +negative number will reverse the path. Thus "-6" will group by method name then +statement. + +The splitting and parsing of string values assigned to the Profile +attribute may seem a little odd, but there's a good reason for it. +Remember that attributes can be embedded in the Data Source Name +string which can be passed in to a script as a parameter. For +example: + + dbi:DriverName(Profile=>2):dbname + dbi:DriverName(Profile=>{Username}:!Statement/MyProfiler/Foo:42):dbname + +And also, if the C environment variable is set then +The DBI arranges for every driver handle to share the same profile +object. When perl exits a single profile summary will be generated +that reflects (as nearly as practical) the total use of the DBI by +the application. + + +=head1 THE PROFILE OBJECT + +The DBI core expects the Profile attribute value to be a hash +reference and if the following values don't exist it will create +them as needed: + +=head2 Data + +A reference to a hash containing the collected profile data. + +=head2 Path + +The Path value is a reference to an array. Each element controls the +value to use at the corresponding level of the profile Data tree. + +If the value of Path is anything other than an array reference, +it is treated as if it was: + + [ '!Statement' ] + +The elements of Path array can be one of the following types: + +=head3 Special Constant + +B + +Use the current Statement text. Typically that's the value of the Statement +attribute for the handle the method was called with. Some methods, like +commit() and rollback(), are unrelated to a particular statement. For those +methods !Statement records an empty string. + +For statement handles this is always simply the string that was +given to prepare() when the handle was created. For database handles +this is the statement that was last prepared or executed on that +database handle. That can lead to a little 'fuzzyness' because, for +example, calls to the quote() method to build a new statement will +typically be associated with the previous statement. In practice +this isn't a significant issue and the dynamic Path mechanism can +be used to setup your own rules. + +B + +Use the name of the DBI method that the profile sample relates to. + +B + +Use the fully qualified name of the DBI method, including +the package, that the profile sample relates to. This shows you +where the method was implemented. For example: + + 'DBD::_::db::selectrow_arrayref' => + 0.022902s + 'DBD::mysql::db::selectrow_arrayref' => + 2.244521s / 99 = 0.022445s avg (first 0.022813s, min 0.022051s, max 0.028932s) + +The "DBD::_::db::selectrow_arrayref" shows that the driver has +inherited the selectrow_arrayref method provided by the DBI. + +But you'll note that there is only one call to +DBD::_::db::selectrow_arrayref but another 99 to +DBD::mysql::db::selectrow_arrayref. Currently the first +call doesn't record the true location. That may change. + +B + +Use a string showing the filename and line number of the code calling the method. + +B + +Use a string showing the filename and line number of the code calling the +method, as for !Caller, but also include filename and line number of the code +that called that. Calls from DBI:: and DBD:: packages are skipped. + +B + +Same as !Caller above except that only the filename is included, not the line number. + +B + +Same as !Caller2 above except that only the filenames are included, not the line number. + +B + +Use the current value of time(). Rarely used. See the more useful C below. + +B + +Where C is an integer. Use the current value of time() but with reduced precision. +The value used is determined in this way: + + int( time() / N ) * N + +This is a useful way to segregate a profile into time slots. For example: + + [ '!Time~60', '!Statement' ] + +=head3 Code Reference + +The subroutine is passed the handle it was called on and the DBI method name. +The current Statement is in $_. The statement string should not be modified, +so most subs start with C. + +The list of values it returns is used at that point in the Profile Path. + +The sub can 'veto' (reject) a profile sample by including a reference to undef +in the returned list. That can be useful when you want to only profile +statements that match a certain pattern, or only profile certain methods. + +=head3 Subroutine Specifier + +A Path element that begins with 'C<&>' is treated as the name of a subroutine +in the DBI::ProfileSubs namespace and replaced with the corresponding code reference. + +Currently this only works when the Path is specified by the C +environment variable. + +Also, currently, the only subroutine in the DBI::ProfileSubs namespace is +C<'&norm_std_n3'>. That's a very handy subroutine when profiling code that +doesn't use placeholders. See L for more information. + +=head3 Attribute Specifier + +A string enclosed in braces, such as 'C<{Username}>', specifies that the current +value of the corresponding database handle attribute should be used at that +point in the Path. + +=head3 Reference to a Scalar + +Specifies that the current value of the referenced scalar be used at that point +in the Path. This provides an efficient way to get 'contextual' values into +your profile. + +=head3 Other Values + +Any other values are stringified and used literally. + +(References, and values that begin with punctuation characters are reserved.) + + +=head1 REPORTING + +=head2 Report Format + +The current accumulated profile data can be formatted and output using + + print $h->{Profile}->format; + +To discard the profile data and start collecting fresh data +you can do: + + $h->{Profile}->{Data} = undef; + + +The default results format looks like this: + + DBI::Profile: 0.001015s 42.7% (5 calls) programname @ YYYY-MM-DD HH:MM:SS + '' => + 0.000024s / 2 = 0.000012s avg (first 0.000015s, min 0.000009s, max 0.000015s) + 'SELECT mode,size,name FROM table' => + 0.000991s / 3 = 0.000330s avg (first 0.000678s, min 0.000009s, max 0.000678s) + +Which shows the total time spent inside the DBI, with a count of +the total number of method calls and the name of the script being +run, then a formatted version of the profile data tree. + +If the results are being formatted when the perl process is exiting +(which is usually the case when the DBI_PROFILE environment variable +is used) then the percentage of time the process spent inside the +DBI is also shown. If the process is not exiting then the percentage is +calculated using the time between the first and last call to the DBI. + +In the example above the paths in the tree are only one level deep and +use the Statement text as the value (that's the default behaviour). + +The merged profile data at the 'leaves' of the tree are presented +as total time spent, count, average time spent (which is simply total +time divided by the count), then the time spent on the first call, +the time spent on the fastest call, and finally the time spent on +the slowest call. + +The 'avg', 'first', 'min' and 'max' times are not particularly +useful when the profile data path only contains the statement text. +Here's an extract of a more detailed example using both statement +text and method name in the path: + + 'SELECT mode,size,name FROM table' => + 'FETCH' => + 0.000076s + 'fetchrow_hashref' => + 0.036203s / 108 = 0.000335s avg (first 0.000490s, min 0.000152s, max 0.002786s) + +Here you can see the 'avg', 'first', 'min' and 'max' for the +108 calls to fetchrow_hashref() become rather more interesting. +Also the data for FETCH just shows a time value because it was only +called once. + +Currently the profile data is output sorted by branch names. That +may change in a later version so the leaf nodes are sorted by total +time per leaf node. + + +=head2 Report Destination + +The default method of reporting is for the DESTROY method of the +Profile object to format the results and write them using: + + DBI->trace_msg($results, 0); # see $ON_DESTROY_DUMP below + +to write them to the DBI trace() filehandle (which defaults to +STDERR). To direct the DBI trace filehandle to write to a file +without enabling tracing the trace() method can be called with a +trace level of 0. For example: + + DBI->trace(0, $filename); + +The same effect can be achieved without changing the code by +setting the C environment variable to C<0=filename>. + +The $DBI::Profile::ON_DESTROY_DUMP variable holds a code ref +that's called to perform the output of the formatted results. +The default value is: + + $ON_DESTROY_DUMP = sub { DBI->trace_msg($results, 0) }; + +Apart from making it easy to send the dump elsewhere, it can also +be useful as a simple way to disable dumping results. + +=head1 CHILD HANDLES + +Child handles inherit a reference to the Profile attribute value +of their parent. So if profiling is enabled for a database handle +then by default the statement handles created from it all contribute +to the same merged profile data tree. + + +=head1 PROFILE OBJECT METHODS + +=head2 format + +See L. + +=head2 as_node_path_list + + @ary = $dbh->{Profile}->as_node_path_list(); + @ary = $dbh->{Profile}->as_node_path_list($node, $path); + +Returns the collected data ($dbh->{Profile}{Data}) restructured into a list of +array refs, one for each leaf node in the Data tree. This 'flat' structure is +often much simpler for applications to work with. + +The first element of each array ref is a reference to the leaf node. +The remaining elements are the 'path' through the data tree to that node. + +For example, given a data tree like this: + + {key1a}{key2a}[node1] + {key1a}{key2b}[node2] + {key1b}{key2a}{key3a}[node3] + +The as_node_path_list() method will return this list: + + [ [node1], 'key1a', 'key2a' ] + [ [node2], 'key1a', 'key2b' ] + [ [node3], 'key1b', 'key2a', 'key3a' ] + +The nodes are ordered by key, depth-first. + +The $node argument can be used to focus on a sub-tree. +If not specified it defaults to $dbh->{Profile}{Data}. + +The $path argument can be used to specify a list of path elements that will be +added to each element of the returned list. If not specified it defaults to a a +ref to an empty array. + +=head2 as_text + + @txt = $dbh->{Profile}->as_text(); + $txt = $dbh->{Profile}->as_text({ + node => undef, + path => [], + separator => " > ", + format => '%1$s: %11$fs / %10$d = %2$fs avg (first %12$fs, min %13$fs, max %14$fs)'."\n"; + sortsub => sub { ... }, + ); + +Returns the collected data ($dbh->{Profile}{Data}) reformatted into a list of formatted strings. +In scalar context the list is returned as a single concatenated string. + +A hashref can be used to pass in arguments, the default values are shown in the example above. + +The C and arguments are passed to as_node_path_list(). + +The C argument is used to join the elements of the path for each leaf node. + +The C argument is used to pass in a ref to a sub that will order the list. +The subroutine will be passed a reference to the array returned by +as_node_path_list() and should sort the contents of the array in place. +The return value from the sub is ignored. For example, to sort the nodes by the +second level key you could use: + + sortsub => sub { my $ary=shift; @$ary = sort { $a->[2] cmp $b->[2] } @$ary } + +The C argument is a C format string that specifies the format +to use for each leaf node. It uses the explicit format parameter index +mechanism to specify which of the arguments should appear where in the string. +The arguments to sprintf are: + + 1: path to node, joined with the separator + 2: average duration (total duration/count) + (3 thru 9 are currently unused) + 10: count + 11: total duration + 12: first duration + 13: smallest duration + 14: largest duration + 15: time of first call + 16: time of first call + +=head1 CUSTOM DATA MANIPULATION + +Recall that C<< $h->{Profile}->{Data} >> is a reference to the collected data. +Either to a 'leaf' array (when the Path is empty, i.e., DBI_PROFILE env var is 1), +or a reference to hash containing values that are either further hash +references or leaf array references. + +Sometimes it's useful to be able to summarise some or all of the collected data. +The dbi_profile_merge_nodes() function can be used to merge leaf node values. + +=head2 dbi_profile_merge_nodes + + use DBI qw(dbi_profile_merge_nodes); + + $time_in_dbi = dbi_profile_merge_nodes(my $totals=[], @$leaves); + +Merges profile data node. Given a reference to a destination array, and zero or +more references to profile data, merges the profile data into the destination array. +For example: + + $time_in_dbi = dbi_profile_merge_nodes( + my $totals=[], + [ 10, 0.51, 0.11, 0.01, 0.22, 1023110000, 1023110010 ], + [ 15, 0.42, 0.12, 0.02, 0.23, 1023110005, 1023110009 ], + ); + +$totals will then contain + + [ 25, 0.93, 0.11, 0.01, 0.23, 1023110000, 1023110010 ] + +and $time_in_dbi will be 0.93; + +The second argument need not be just leaf nodes. If given a reference to a hash +then the hash is recursively searched for for leaf nodes and all those found +are merged. + +For example, to get the time spent 'inside' the DBI during an http request, +your logging code run at the end of the request (i.e. mod_perl LogHandler) +could use: + + my $time_in_dbi = 0; + if (my $Profile = $dbh->{Profile}) { # if DBI profiling is enabled + $time_in_dbi = dbi_profile_merge_nodes(my $total=[], $Profile->{Data}); + $Profile->{Data} = {}; # reset the profile data + } + +If profiling has been enabled then $time_in_dbi will hold the time spent inside +the DBI for that handle (and any other handles that share the same profile data) +since the last request. + +Prior to DBI 1.56 the dbi_profile_merge_nodes() function was called dbi_profile_merge(). +That name still exists as an alias. + +=head1 CUSTOM DATA COLLECTION + +=head2 Using The Path Attribute + + XXX example to be added later using a selectall_arrayref call + XXX nested inside a fetch loop where the first column of the + XXX outer loop is bound to the profile Path using + XXX bind_column(1, \${ $dbh->{Profile}->{Path}->[0] }) + XXX so you end up with separate profiles for each loop + XXX (patches welcome to add this to the docs :) + +=head2 Adding Your Own Samples + +The dbi_profile() function can be used to add extra sample data +into the profile data tree. For example: + + use DBI; + use DBI::Profile (dbi_profile dbi_time); + + my $t1 = dbi_time(); # floating point high-resolution time + + ... execute code you want to profile here ... + + my $t2 = dbi_time(); + dbi_profile($h, $statement, $method, $t1, $t2); + +The $h parameter is the handle the extra profile sample should be +associated with. The $statement parameter is the string to use where +the Path specifies !Statement. If $statement is undef +then $h->{Statement} will be used. Similarly $method is the string +to use if the Path specifies !MethodName. There is no +default value for $method. + +The $h->{Profile}{Path} attribute is processed by dbi_profile() in +the usual way. + +The $h parameter is usually a DBI handle but it can also be a reference to a +hash, in which case the dbi_profile() acts on each defined value in the hash. +This is an efficient way to update multiple profiles with a single sample, +and is used by the L module. + +=head1 SUBCLASSING + +Alternate profile modules must subclass DBI::Profile to help ensure +they work with future versions of the DBI. + + +=head1 CAVEATS + +Applications which generate many different statement strings +(typically because they don't use placeholders) and profile with +!Statement in the Path (the default) will consume memory +in the Profile Data structure for each statement. Use a code ref +in the Path to return an edited (simplified) form of the statement. + +If a method throws an exception itself (not via RaiseError) then +it won't be counted in the profile. + +If a HandleError subroutine throws an exception (rather than returning +0 and letting RaiseError do it) then the method call won't be counted +in the profile. + +Time spent in DESTROY is added to the profile of the parent handle. + +Time spent in DBI->*() methods is not counted. The time spent in +the driver connect method, $drh->connect(), when it's called by +DBI->connect is counted if the DBI_PROFILE environment variable is set. + +Time spent fetching tied variables, $DBI::errstr, is counted. + +Time spent in FETCH for $h->{Profile} is not counted, so getting the profile +data doesn't alter it. + +DBI::PurePerl does not support profiling (though it could in theory). + +For asynchronous queries, time spent while the query is running on the +backend is not counted. + +A few platforms don't support the gettimeofday() high resolution +time function used by the DBI (and available via the dbi_time() function). +In which case you'll get integer resolution time which is mostly useless. + +On Windows platforms the dbi_time() function is limited to millisecond +resolution. Which isn't sufficiently fine for our needs, but still +much better than integer resolution. This limited resolution means +that fast method calls will often register as taking 0 time. And +timings in general will have much more 'jitter' depending on where +within the 'current millisecond' the start and and timing was taken. + +This documentation could be more clear. Probably needs to be reordered +to start with several examples and build from there. Trying to +explain the concepts first seems painful and to lead to just as +many forward references. (Patches welcome!) + +=cut + + +use strict; +use vars qw(@ISA @EXPORT @EXPORT_OK $VERSION); +use Exporter (); +use UNIVERSAL (); +use Carp; + +use DBI qw(dbi_time dbi_profile dbi_profile_merge_nodes dbi_profile_merge); + +$VERSION = sprintf("2.%06d", q$Revision: 15064 $ =~ /(\d+)/o); + + +@ISA = qw(Exporter); +@EXPORT = qw( + DBIprofile_Statement + DBIprofile_MethodName + DBIprofile_MethodClass + dbi_profile + dbi_profile_merge_nodes + dbi_profile_merge + dbi_time +); +@EXPORT_OK = qw( + format_profile_thingy +); + +use constant DBIprofile_Statement => '!Statement'; +use constant DBIprofile_MethodName => '!MethodName'; +use constant DBIprofile_MethodClass => '!MethodClass'; + +our $ON_DESTROY_DUMP = sub { DBI->trace_msg(shift, 0) }; +our $ON_FLUSH_DUMP = sub { DBI->trace_msg(shift, 0) }; + +sub new { + my $class = shift; + my $profile = { @_ }; + return bless $profile => $class; +} + + +sub _auto_new { + my $class = shift; + my ($arg) = @_; + + # This sub is called by DBI internals when a non-hash-ref is + # assigned to the Profile attribute. For example + # dbi:mysql(RaiseError=>1,Profile=>!Statement:!MethodName/DBIx::MyProfile/arg1:arg2):dbname + # This sub works out what to do and returns a suitable hash ref. + + $arg =~ s/^DBI::/2\/DBI::/ + and carp "Automatically changed old-style DBI::Profile specification to $arg"; + + # it's a path/module/k1:v1:k2:v2:... list + my ($path, $package, $args) = split /\//, $arg, 3; + my @args = (defined $args) ? split(/:/, $args, -1) : (); + my @Path; + + for my $element (split /:/, $path) { + if (DBI::looks_like_number($element)) { + my $reverse = ($element < 0) ? ($element=-$element, 1) : 0; + my @p; + # a single "DBI" is special-cased in format() + push @p, "DBI" if $element & 0x01; + push @p, DBIprofile_Statement if $element & 0x02; + push @p, DBIprofile_MethodName if $element & 0x04; + push @p, DBIprofile_MethodClass if $element & 0x08; + push @p, '!Caller2' if $element & 0x10; + push @Path, ($reverse ? reverse @p : @p); + } + elsif ($element =~ m/^&(\w.*)/) { + my $name = "DBI::ProfileSubs::$1"; # capture $1 early + require DBI::ProfileSubs; + my $code = do { no strict; *{$name}{CODE} }; + if (defined $code) { + push @Path, $code; + } + else { + warn "$name: subroutine not found\n"; + push @Path, $element; + } + } + else { + push @Path, $element; + } + } + + eval "require $package" if $package; # sliently ignores errors + $package ||= $class; + + return $package->new(Path => \@Path, @args); +} + + +sub empty { # empty out profile data + my $self = shift; + DBI->trace_msg("profile data discarded\n",0) if $self->{Trace}; + $self->{Data} = undef; +} + +sub filename { # baseclass method, see DBI::ProfileDumper + return undef; +} + +sub flush_to_disk { # baseclass method, see DBI::ProfileDumper & DashProfiler::Core + my $self = shift; + return unless $ON_FLUSH_DUMP; + return unless $self->{Data}; + my $detail = $self->format(); + $ON_FLUSH_DUMP->($detail) if $detail; +} + + +sub as_node_path_list { + my ($self, $node, $path) = @_; + # convert the tree into an array of arrays + # from + # {key1a}{key2a}[node1] + # {key1a}{key2b}[node2] + # {key1b}{key2a}{key3a}[node3] + # to + # [ [node1], 'key1a', 'key2a' ] + # [ [node2], 'key1a', 'key2b' ] + # [ [node3], 'key1b', 'key2a', 'key3a' ] + + $node ||= $self->{Data} or return; + $path ||= []; + if (ref $node eq 'HASH') { # recurse + $path = [ @$path, undef ]; + return map { + $path->[-1] = $_; + ($node->{$_}) ? $self->as_node_path_list($node->{$_}, $path) : () + } sort keys %$node; + } + return [ $node, @$path ]; +} + + +sub as_text { + my ($self, $args_ref) = @_; + my $separator = $args_ref->{separator} || " > "; + my $format_path_element = $args_ref->{format_path_element} + || "%s"; # or e.g., " key%2$d='%s'" + my $format = $args_ref->{format} + || '%1$s: %11$fs / %10$d = %2$fs avg (first %12$fs, min %13$fs, max %14$fs)'."\n"; + + my @node_path_list = $self->as_node_path_list(undef, $args_ref->{path}); + + $args_ref->{sortsub}->(\@node_path_list) if $args_ref->{sortsub}; + + my $eval = "qr/".quotemeta($separator)."/"; + my $separator_re = eval($eval) || quotemeta($separator); + #warn "[$eval] = [$separator_re]"; + my @text; + my @spare_slots = (undef) x 7; + for my $node_path (@node_path_list) { + my ($node, @path) = @$node_path; + my $idx = 0; + for (@path) { + s/[\r\n]+/ /g; + s/$separator_re/ /g; + $_ = sprintf $format_path_element, $_, ++$idx; + } + push @text, sprintf $format, + join($separator, @path), # 1=path + ($node->[0] ? $node->[1]/$node->[0] : 0), # 2=avg + @spare_slots, + @$node; # 10=count, 11=dur, 12=first_dur, 13=min, 14=max, 15=first_called, 16=last_called + } + return @text if wantarray; + return join "", @text; +} + + +sub format { + my $self = shift; + my $class = ref($self) || $self; + + my $prologue = "$class: "; + my $detail = $self->format_profile_thingy( + $self->{Data}, 0, " ", + my $path = [], + my $leaves = [], + )."\n"; + + if (@$leaves) { + dbi_profile_merge_nodes(my $totals=[], @$leaves); + my ($count, $time_in_dbi, undef, undef, undef, $t1, $t2) = @$totals; + (my $progname = $0) =~ s:.*/::; + if ($count) { + $prologue .= sprintf "%fs ", $time_in_dbi; + my $perl_time = ($DBI::PERL_ENDING) ? time() - $^T : $t2-$t1; + $prologue .= sprintf "%.2f%% ", $time_in_dbi/$perl_time*100 if $perl_time; + my @lt = localtime(time); + my $ts = sprintf "%d-%02d-%02d %02d:%02d:%02d", + 1900+$lt[5], $lt[4]+1, @lt[3,2,1,0]; + $prologue .= sprintf "(%d calls) $progname \@ $ts\n", $count; + } + if (@$leaves == 1 && ref($self->{Data}) eq 'HASH' && $self->{Data}->{DBI}) { + $detail = ""; # hide the "DBI" from DBI_PROFILE=1 + } + } + return ($prologue, $detail) if wantarray; + return $prologue.$detail; +} + + +sub format_profile_leaf { + my ($self, $thingy, $depth, $pad, $path, $leaves) = @_; + croak "format_profile_leaf called on non-leaf ($thingy)" + unless UNIVERSAL::isa($thingy,'ARRAY'); + + push @$leaves, $thingy if $leaves; + my ($count, $total_time, $first_time, $min, $max, $first_called, $last_called) = @$thingy; + return sprintf "%s%fs\n", ($pad x $depth), $total_time + if $count <= 1; + return sprintf "%s%fs / %d = %fs avg (first %fs, min %fs, max %fs)\n", + ($pad x $depth), $total_time, $count, $count ? $total_time/$count : 0, + $first_time, $min, $max; +} + + +sub format_profile_branch { + my ($self, $thingy, $depth, $pad, $path, $leaves) = @_; + croak "format_profile_branch called on non-branch ($thingy)" + unless UNIVERSAL::isa($thingy,'HASH'); + my @chunk; + my @keys = sort keys %$thingy; + while ( @keys ) { + my $k = shift @keys; + my $v = $thingy->{$k}; + push @$path, $k; + push @chunk, sprintf "%s'%s' =>\n%s", + ($pad x $depth), $k, + $self->format_profile_thingy($v, $depth+1, $pad, $path, $leaves); + pop @$path; + } + return join "", @chunk; +} + + +sub format_profile_thingy { + my ($self, $thingy, $depth, $pad, $path, $leaves) = @_; + return "undef" if not defined $thingy; + return $self->format_profile_leaf( $thingy, $depth, $pad, $path, $leaves) + if UNIVERSAL::isa($thingy,'ARRAY'); + return $self->format_profile_branch($thingy, $depth, $pad, $path, $leaves) + if UNIVERSAL::isa($thingy,'HASH'); + return "$thingy\n"; +} + + +sub on_destroy { + my $self = shift; + return unless $ON_DESTROY_DUMP; + return unless $self->{Data}; + my $detail = $self->format(); + $ON_DESTROY_DUMP->($detail) if $detail; + $self->{Data} = undef; +} + +sub DESTROY { + my $self = shift; + local $@; + DBI->trace_msg("profile data DESTROY\n",0) + if (($self->{Trace}||0) >= 2); + eval { $self->on_destroy }; + if ($@) { + chomp $@; + my $class = ref($self) || $self; + DBI->trace_msg("$class on_destroy failed: $@", 0); + } +} + +1; + -- cgit v1.2.1