File Coverage

blib/lib/Tiny/Prof.pm
Criterion Covered Total %
statement 23 114 20.1
branch 0 26 0.0
condition 0 17 0.0
subroutine 8 21 38.1
pod 1 1 100.0
total 32 179 17.8


line stmt bran cond sub pod time code
1             package Tiny::Prof;
2              
3             =head1 LOGO
4              
5             _____ _ ____ __
6             |_ _(_)_ __ _ _ | _ \ _ __ ___ / _|
7             | | | | '_ \| | | | | |_) | '__/ _ \| |_
8             | | | | | | | |_| | | __/| | | (_) | _|
9             |_| |_|_| |_|\__, | |_| |_| \___/|_|
10             |___/
11              
12             =cut
13              
14 1     1   145853 use 5.006;
  1         5  
15 1     1   11 use strict;
  1         3  
  1         30  
16 1     1   7 use warnings;
  1         2  
  1         99  
17 1     1   7 use File::Path qw( make_path );
  1         4  
  1         103  
18 1     1   1140 use Term::ANSIColor qw( colored );
  1         16569  
  1         1025  
19 1     1   12 use File::Basename qw( basename );
  1         2  
  1         96  
20 1     1   7 use Cwd qw( realpath );
  1         2  
  1         82  
21 1     1   838 use Time::Piece;
  1         20734  
  1         7  
22              
23             our $VERSION = '0.04';
24              
25             =head1 NAME
26              
27             Tiny::Prof - Perl profiling made simple to use.
28              
29             =cut
30              
31             =head1 SYNOPSIS
32              
33             use Tiny::Prof;
34             my $profiler = Tiny::Prof->run;
35              
36             ...
37              
38             # $profiler goes out of scope and
39             # then builds the results page.
40              
41             =cut
42              
43             =head1 DESCRIPTION
44              
45             This module is a tool that is designed to make
46             profiling perl code as easy as can be.
47              
48             =head2 Run Stages
49              
50             When profiling, keep in mind:
51             - The stages described below.
52             - the scope of what should be captured/recorded.
53              
54             Flow of Code Execution:
55              
56             |== <-- Stage 1: Setup environment.
57             |
58             |==== <-- Stage 2: Beginning of code.
59             |
60             |======== <-- Stage 3: Start profiling.
61             |
62             | (Data is collected/recorded ONLY here!)
63             |
64             |======== <-- Stage 4: Stop profiling.
65             |
66             |==== <-- Stage 5: End of code.
67             |
68             |== <-- Stage 6: Restore environment
69             |
70             v
71              
72             =head3 Stage 1: Setup Environment
73              
74             These environmental variables should be setup.
75             Failure to do so may result in missing links
76             and/or data in the results!
77              
78             export PERL5OPT=-d:NYTProf
79             export NYTPROF='trace=0:start=no:slowops=0:addpid=1'
80              
81             # Trace - Set to a higher value like '1' for more details.
82             # Start - Put profiler into "standby" mode
83             # (ready, but not running).
84             # AddPid - Important when there are multiple processes.
85             # SlowOps - Disabled to avoid profiling say
86             # sleep or print.
87              
88             If running as a service, the environmental variables
89             should be stored in the service file instead.
90              
91             On a Debian-based machine/box that may mean:
92              
93             systemctl status MY_SERVICE
94             sudo vi /etc/systemdsystem/MY_SERVICE.service
95              
96             Add this line:
97              
98             Environment="PERL5OPT=-d:NYTProf" "NYTPROF='trace=0:start=no:slowops=0:addpid=1'"
99              
100             Then restsrt the service:
101              
102             systemctl restart MY_SERVICE
103              
104             =head3 Stage 2: Beginning of Code
105              
106             The C at this point is in "standby" mode:
107             - Aware of source files (important for later).
108             - Not actually recording anything yet.
109              
110             =head3 Stage 3: Start Profiling
111              
112             To start profiling is like pressing a global record
113             button. Anything after starting to profile will be
114             stored in a file in a data format
115             (which is mostly in machine-readable format).
116              
117             =head3 Stage 4: Stop Profiling
118              
119             Similary, to stop profiling is to press the global
120             stop button.
121              
122             NOTE: It is important to stop the profile correctly
123             since the results would otherwise be useless.
124             As stated in L:
125              
126             "NYTProf writes some important data to the data file
127             when finishing profiling."
128              
129             =head3 Stage 5: End of Code
130              
131             The C at this point returns again to "standby" mode:
132             - Aware of source files (maybe important for later).
133             - Not actually recording anything anymore.
134              
135             =head3 Stage 6: Restore Environment
136              
137             Once profiling is done, the environment should be
138             restored by using:
139              
140             unset PERL5OPT
141             unset NYTPROF
142              
143             =cut
144              
145             =head1 METHODS
146            
147             =head2 run
148              
149             Run the C and return a special object.
150              
151             my $profiler = Tiny::Prof->run( %Options );
152              
153             Will automatically close the recording data file when the object
154             goes out of scope (by default).
155              
156             =head3 Options
157              
158             name => "my", # Name/title of the results.
159             use_flame_graph => 0, # Generate the flame graph (very slow).
160             root_dir => "mytprof", # Folder with results and work data
161             work_dir => "$root_dir/work", # Folder for active work..
162             log => "$work_dir/log", # Proflier log.
163              
164             =cut
165              
166             # API.
167             sub run {
168 0     0 1   my ( $class, %params ) = @_;
169 0           my $self = bless \%params, $class;
170              
171 0           $self->_init;
172 0           $self->_check_env;
173 0           $self->_clean_work_dir;
174 0           $self->_start_profiling;
175              
176 0           $self;
177             }
178              
179             sub DESTROY {
180 0     0     my ( $self ) = @_;
181              
182 0 0         $self->_stop_profiling if $self->{env_ok};
183              
184 0           close $self->{log_fh};
185             }
186              
187             # Setup.
188             sub _init {
189 0     0     my ( $self ) = @_;
190              
191 0   0       $self->{name} //= 'my';
192 0   0       $self->{use_flame_graph} //= 0;
193 0   0       $self->{root_dir} //= 'nytprof';
194 0   0       $self->{work_dir} //= "$self->{root_dir}/work";
195 0   0       $self->{log} //= "$self->{work_dir}/log";
196              
197 0           make_path( $self->{work_dir} );
198 0           open $self->{log_fh}, ">>", $self->{log};
199              
200 0           $self->{log_fh}->autoflush( 1 );
201 0           STDOUT->autoflush( 1 );
202             }
203              
204             sub _check_env {
205 0     0     my ( $self ) = @_;
206              
207 0           my @needed = qw(
208             PERL5OPT
209             NYTPROF
210             );
211              
212 0           for my $need ( @needed ) {
213 0 0         if ( not $ENV{$need} ) {
214 0           $self->_print( "ERROR not set: \$ENV{$need}", "RED" );
215 0           die "Aborting!\n";
216             }
217             }
218              
219 0           $self->{env_ok} = 1;
220             }
221              
222             sub _clean_work_dir {
223 0     0     my ( $self ) = @_;
224              
225 0           my @files = sort glob qq(
226             "$self->{work_dir}/*.out"
227             "$self->{work_dir}/*.out.*"
228             );
229              
230 0           for my $file ( @files ) {
231 0 0         return if !$self->_remove_file( $file );
232             }
233             }
234              
235             sub _remove_file {
236 0     0     my ( $self, $file ) = @_;
237              
238 0 0         return 1 if !-e $file;
239              
240 0 0         if ( unlink $file ) {
241 0           $self->_print( "Removed: $file" );
242             }
243             else {
244 0           $self->_print( "ERROR: Could not remove $file", "RED" );
245 0           return;
246             }
247              
248 0           return 1;
249             }
250              
251             # DB.
252             sub _start_profiling {
253 0     0     my ( $self ) = @_;
254              
255 0           $self->_print( "_start_profiling" );
256              
257 0           my $raw_file = sprintf( "$self->{work_dir}/$self->{name}_%s.out",
258             $self->_get_timestamp, );
259 0           $self->{raw_file} = $raw_file;
260              
261 0           $self->_print( "==> Profiling Started (writing to '$raw_file.*')",
262             "YELLOW" );
263              
264 0           DB::enable_profile( $raw_file );
265             }
266              
267             sub _stop_profiling {
268 0     0     my ( $self ) = @_;
269              
270 0           DB::finish_profile();
271              
272 0           $self->_print( "_stop_profiling()" );
273              
274             # Rename the output file to easily indentify that its finished.
275 0           my $Old = glob "$self->{raw_file}*";
276 0           my $New = "$Old.finished";
277              
278             # Make sure the output file actually exists.
279 0 0         if ( !-e $Old ) {
280 0           $self->_print(
281             "ERROR: output file '$Old' was not created! (Perhaps someone removed it during profiling?)",
282             "RED"
283             );
284 0           return;
285             }
286              
287             # Make it much easier to identify when finished profiling.
288 0 0         if ( !rename $Old => $New ) {
289 0           $self->_print( "ERROR: Cannot rename '$Old' to '$New': $!", "RED" );
290 0           return;
291             }
292              
293 0           $self->_print( "==> Profiling Finished", "YELLOW" );
294              
295 0           $self->_build_html( $New );
296             }
297              
298             # Output.
299             sub _print {
300 0     0     my ( $self, $msg, $color ) = @_;
301              
302 0 0         $msg = colored( $msg, $color ) if $color;
303              
304             # Enrich message.
305 0           my $TimeStamp = $self->_get_timestamp;
306 0           $msg = "$TimeStamp [$$] $msg\n";
307              
308             # Write to STDOUT and a log.
309 0           print $msg;
310 0           print { $self->{log_fh} } $msg;
  0            
311              
312 0           return;
313             }
314              
315             sub _get_timestamp {
316 0     0     my ( $self ) = @_;
317              
318 0           return localtime->strftime( '%Y-%m-%d_%H-%M-%S' );
319             }
320              
321             # NYTProf.
322             sub _build_html {
323 0     0     my ( $self, $finished_file ) = @_;
324              
325 0           $self->_print( "_build_html($finished_file)" );
326              
327 0           my $html_dir = sprintf( "$self->{root_dir}/%s",
328             basename( $finished_file ) =~ s/ \. out \. .+ //rx, );
329              
330             # Run nytprofhtml.
331             my $ok = $self->_system(
332             sprintf(
333             "nytprofhtml --file '$finished_file' --out '$html_dir' --delete %s",
334 0 0         $self->{use_flame_graph} ? '' : '--no-flame', )
335             );
336              
337             # Still maybe ok if the index.html file can be found.
338 0 0 0       if ( !$ok && -e "$html_dir/index.html" ) {
339 0           $self->_print( "WARNING running nytprofhtml, but index.html was made\n",
340             "YELLOW", );
341 0           $ok = 1;
342             }
343 0 0         return if !$ok;
344              
345 0           $self->_show_html_link( $html_dir );
346             }
347              
348             sub _system {
349 0     0     my ( $self, $command ) = @_;
350              
351 0           $self->_print( colored( "==> Running: ", "YELLOW" )
352             . colored( "$command", "ON_BRIGHT_BLACK" ) );
353              
354 0   0       my $output = qx($command 2>&1) // "";
355              
356 0 0         if ( $? ) {
357 0           $command =~ / ^ (\S+) /x; # Shorter for display.
358 0           $self->_print( "ERROR running '$command': $?", "RED" );
359 0           $self->_print( $output );
360 0           return;
361             }
362              
363 0           return 1;
364             }
365              
366             sub _show_html_link {
367 0     0     my ( $self, $html_dir ) = @_;
368 0           my $index_path = realpath( "$html_dir/index.html" );
369              
370 0 0         if ( !-e $index_path ) {
371 0           $self->_print( "ERROR: Could not create $index_path\n", "RED" );
372 0           return;
373             }
374              
375 0           $self->_print( "Created: file://$index_path", "GREEN" );
376             }
377              
378             # Pod.
379              
380             =head1 BUGS
381              
382             None
383              
384             ... and then came along Ron :)
385              
386             =cut
387              
388             =head1 SUPPORT
389              
390             You can find documentation for this module
391             with the perldoc command.
392              
393             perldoc Tiny::Prof
394              
395             You can also look for information at:
396              
397             L
398              
399             L
400              
401             =cut
402              
403             =head1 AUTHOR
404              
405             Tim Potapov, C<< >> E<0x1f42a>E<0x1f977>
406              
407             =cut
408              
409             =head1 LICENSE AND COPYRIGHT
410              
411             This software is Copyright (c) 2024 by Tim Potapov.
412              
413             This is free software, licensed under:
414              
415             The Artistic License 2.0 (GPL Compatible)
416              
417             =cut
418              
419             "\x{1f42a}\x{1f977}"