michael@0: #! /usr/bin/env perl michael@0: michael@0: # Copyright (c) 1998-2007, Google Inc. michael@0: # All rights reserved. michael@0: # michael@0: # Redistribution and use in source and binary forms, with or without michael@0: # modification, are permitted provided that the following conditions are michael@0: # met: michael@0: # michael@0: # * Redistributions of source code must retain the above copyright michael@0: # notice, this list of conditions and the following disclaimer. michael@0: # * Redistributions in binary form must reproduce the above michael@0: # copyright notice, this list of conditions and the following disclaimer michael@0: # in the documentation and/or other materials provided with the michael@0: # distribution. michael@0: # * Neither the name of Google Inc. nor the names of its michael@0: # contributors may be used to endorse or promote products derived from michael@0: # this software without specific prior written permission. michael@0: # michael@0: # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT michael@0: # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, michael@0: # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT michael@0: # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, michael@0: # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY michael@0: # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT michael@0: # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE michael@0: # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: michael@0: # --- michael@0: # Program for printing the profile generated by common/profiler.cc, michael@0: # or by the heap profiler (common/debugallocation.cc) michael@0: # michael@0: # The profile contains a sequence of entries of the form: michael@0: # michael@0: # This program parses the profile, and generates user-readable michael@0: # output. michael@0: # michael@0: # Examples: michael@0: # michael@0: # % tools/pprof "program" "profile" michael@0: # Enters "interactive" mode michael@0: # michael@0: # % tools/pprof --text "program" "profile" michael@0: # Generates one line per procedure michael@0: # michael@0: # % tools/pprof --gv "program" "profile" michael@0: # Generates annotated call-graph and displays via "gv" michael@0: # michael@0: # % tools/pprof --gv --focus=Mutex "program" "profile" michael@0: # Restrict to code paths that involve an entry that matches "Mutex" michael@0: # michael@0: # % tools/pprof --gv --focus=Mutex --ignore=string "program" "profile" michael@0: # Restrict to code paths that involve an entry that matches "Mutex" michael@0: # and does not match "string" michael@0: # michael@0: # % tools/pprof --list=IBF_CheckDocid "program" "profile" michael@0: # Generates disassembly listing of all routines with at least one michael@0: # sample that match the --list= pattern. The listing is michael@0: # annotated with the flat and cumulative sample counts at each line. michael@0: # michael@0: # % tools/pprof --disasm=IBF_CheckDocid "program" "profile" michael@0: # Generates disassembly listing of all routines with at least one michael@0: # sample that match the --disasm= pattern. The listing is michael@0: # annotated with the flat and cumulative sample counts at each PC value. michael@0: # michael@0: # TODO: Use color to indicate files? michael@0: michael@0: use strict; michael@0: use warnings; michael@0: use Getopt::Long; michael@0: michael@0: my $PPROF_VERSION = "2.0"; michael@0: michael@0: # These are the object tools we use which can come from a michael@0: # user-specified location using --tools, from the PPROF_TOOLS michael@0: # environment variable, or from the environment. michael@0: my %obj_tool_map = ( michael@0: "objdump" => "objdump", michael@0: "nm" => "nm", michael@0: "addr2line" => "addr2line", michael@0: "c++filt" => "c++filt", michael@0: ## ConfigureObjTools may add architecture-specific entries: michael@0: #"nm_pdb" => "nm-pdb", # for reading windows (PDB-format) executables michael@0: #"addr2line_pdb" => "addr2line-pdb", # ditto michael@0: #"otool" => "otool", # equivalent of objdump on OS X michael@0: ); michael@0: # NOTE: these are lists, so you can put in commandline flags if you want. michael@0: my @DOT = ("dot"); # leave non-absolute, since it may be in /usr/local michael@0: my @GV = ("gv"); michael@0: my @EVINCE = ("evince"); # could also be xpdf or perhaps acroread michael@0: my @KCACHEGRIND = ("kcachegrind"); michael@0: my @PS2PDF = ("ps2pdf"); michael@0: # These are used for dynamic profiles michael@0: my @URL_FETCHER = ("curl", "-s"); michael@0: michael@0: # These are the web pages that servers need to support for dynamic profiles michael@0: my $HEAP_PAGE = "/pprof/heap"; michael@0: my $PROFILE_PAGE = "/pprof/profile"; # must support cgi-param "?seconds=#" michael@0: my $PMUPROFILE_PAGE = "/pprof/pmuprofile(?:\\?.*)?"; # must support cgi-param michael@0: # ?seconds=#&event=x&period=n michael@0: my $GROWTH_PAGE = "/pprof/growth"; michael@0: my $CONTENTION_PAGE = "/pprof/contention"; michael@0: my $WALL_PAGE = "/pprof/wall(?:\\?.*)?"; # accepts options like namefilter michael@0: my $FILTEREDPROFILE_PAGE = "/pprof/filteredprofile(?:\\?.*)?"; michael@0: my $CENSUSPROFILE_PAGE = "/pprof/censusprofile(?:\\?.*)?"; # must support cgi-param michael@0: # "?seconds=#", michael@0: # "?tags_regexp=#" and michael@0: # "?type=#". michael@0: my $SYMBOL_PAGE = "/pprof/symbol"; # must support symbol lookup via POST michael@0: my $PROGRAM_NAME_PAGE = "/pprof/cmdline"; michael@0: michael@0: # These are the web pages that can be named on the command line. michael@0: # All the alternatives must begin with /. michael@0: my $PROFILES = "($HEAP_PAGE|$PROFILE_PAGE|$PMUPROFILE_PAGE|" . michael@0: "$GROWTH_PAGE|$CONTENTION_PAGE|$WALL_PAGE|" . michael@0: "$FILTEREDPROFILE_PAGE|$CENSUSPROFILE_PAGE)"; michael@0: michael@0: # default binary name michael@0: my $UNKNOWN_BINARY = "(unknown)"; michael@0: michael@0: # There is a pervasive dependency on the length (in hex characters, michael@0: # i.e., nibbles) of an address, distinguishing between 32-bit and michael@0: # 64-bit profiles. To err on the safe size, default to 64-bit here: michael@0: my $address_length = 16; michael@0: michael@0: my $dev_null = "/dev/null"; michael@0: if (! -e $dev_null && $^O =~ /MSWin/) { # $^O is the OS perl was built for michael@0: $dev_null = "nul"; michael@0: } michael@0: michael@0: # A list of paths to search for shared object files michael@0: my @prefix_list = (); michael@0: michael@0: # Special routine name that should not have any symbols. michael@0: # Used as separator to parse "addr2line -i" output. michael@0: my $sep_symbol = '_fini'; michael@0: my $sep_address = undef; michael@0: michael@0: ##### Argument parsing ##### michael@0: michael@0: sub usage_string { michael@0: return < michael@0: is a space separated list of profile names. michael@0: pprof [options] michael@0: is a list of profile files where each file contains michael@0: the necessary symbol mappings as well as profile data (likely generated michael@0: with --raw). michael@0: pprof [options] michael@0: is a remote form. Symbols are obtained from host:port$SYMBOL_PAGE michael@0: michael@0: Each name can be: michael@0: /path/to/profile - a path to a profile file michael@0: host:port[/] - a location of a service to get profile from michael@0: michael@0: The / can be $HEAP_PAGE, $PROFILE_PAGE, /pprof/pmuprofile, michael@0: $GROWTH_PAGE, $CONTENTION_PAGE, /pprof/wall, michael@0: $CENSUSPROFILE_PAGE, or /pprof/filteredprofile. michael@0: For instance: michael@0: pprof http://myserver.com:80$HEAP_PAGE michael@0: If / is omitted, the service defaults to $PROFILE_PAGE (cpu profiling). michael@0: pprof --symbols michael@0: Maps addresses to symbol names. In this mode, stdin should be a michael@0: list of library mappings, in the same format as is found in the heap- michael@0: and cpu-profile files (this loosely matches that of /proc/self/maps michael@0: on linux), followed by a list of hex addresses to map, one per line. michael@0: michael@0: For more help with querying remote servers, including how to add the michael@0: necessary server-side support code, see this filename (or one like it): michael@0: michael@0: /usr/doc/gperftools-$PPROF_VERSION/pprof_remote_servers.html michael@0: michael@0: Options: michael@0: --cum Sort by cumulative data michael@0: --base= Subtract from before display michael@0: --interactive Run in interactive mode (interactive "help" gives help) [default] michael@0: --seconds= Length of time for dynamic profiles [default=30 secs] michael@0: --add_lib= Read additional symbols and line info from the given library michael@0: --lib_prefix= Comma separated list of library path prefixes michael@0: michael@0: Reporting Granularity: michael@0: --addresses Report at address level michael@0: --lines Report at source line level michael@0: --functions Report at function level [default] michael@0: --files Report at source file level michael@0: michael@0: Output type: michael@0: --text Generate text report michael@0: --callgrind Generate callgrind format to stdout michael@0: --gv Generate Postscript and display michael@0: --evince Generate PDF and display michael@0: --web Generate SVG and display michael@0: --list= Generate source listing of matching routines michael@0: --disasm= Generate disassembly of matching routines michael@0: --symbols Print demangled symbol names found at given addresses michael@0: --dot Generate DOT file to stdout michael@0: --ps Generate Postcript to stdout michael@0: --pdf Generate PDF to stdout michael@0: --svg Generate SVG to stdout michael@0: --gif Generate GIF to stdout michael@0: --raw Generate symbolized pprof data (useful with remote fetch) michael@0: michael@0: Heap-Profile Options: michael@0: --inuse_space Display in-use (mega)bytes [default] michael@0: --inuse_objects Display in-use objects michael@0: --alloc_space Display allocated (mega)bytes michael@0: --alloc_objects Display allocated objects michael@0: --show_bytes Display space in bytes michael@0: --drop_negative Ignore negative differences michael@0: michael@0: Contention-profile options: michael@0: --total_delay Display total delay at each region [default] michael@0: --contentions Display number of delays at each region michael@0: --mean_delay Display mean delay at each region michael@0: michael@0: Call-graph Options: michael@0: --nodecount= Show at most so many nodes [default=80] michael@0: --nodefraction= Hide nodes below *total [default=.005] michael@0: --edgefraction= Hide edges below *total [default=.001] michael@0: --maxdegree= Max incoming/outgoing edges per node [default=8] michael@0: --focus= Focus on nodes matching michael@0: --ignore= Ignore nodes matching michael@0: --scale= Set GV scaling [default=0] michael@0: --heapcheck Make nodes with non-0 object counts michael@0: (i.e. direct leak generators) more visible michael@0: michael@0: Miscellaneous: michael@0: --tools=[,...] \$PATH for object tool pathnames michael@0: --test Run unit tests michael@0: --help This message michael@0: --version Version information michael@0: michael@0: Environment Variables: michael@0: PPROF_TMPDIR Profiles directory. Defaults to \$HOME/pprof michael@0: PPROF_TOOLS Prefix for object tools pathnames michael@0: michael@0: Examples: michael@0: michael@0: pprof /bin/ls ls.prof michael@0: Enters "interactive" mode michael@0: pprof --text /bin/ls ls.prof michael@0: Outputs one line per procedure michael@0: pprof --web /bin/ls ls.prof michael@0: Displays annotated call-graph in web browser michael@0: pprof --gv /bin/ls ls.prof michael@0: Displays annotated call-graph via 'gv' michael@0: pprof --gv --focus=Mutex /bin/ls ls.prof michael@0: Restricts to code paths including a .*Mutex.* entry michael@0: pprof --gv --focus=Mutex --ignore=string /bin/ls ls.prof michael@0: Code paths including Mutex but not string michael@0: pprof --list=getdir /bin/ls ls.prof michael@0: (Per-line) annotated source listing for getdir() michael@0: pprof --disasm=getdir /bin/ls ls.prof michael@0: (Per-PC) annotated disassembly for getdir() michael@0: michael@0: pprof http://localhost:1234/ michael@0: Enters "interactive" mode michael@0: pprof --text localhost:1234 michael@0: Outputs one line per procedure for localhost:1234 michael@0: pprof --raw localhost:1234 > ./local.raw michael@0: pprof --text ./local.raw michael@0: Fetches a remote profile for later analysis and then michael@0: analyzes it in text mode. michael@0: EOF michael@0: } michael@0: michael@0: sub version_string { michael@0: return < \$main::opt_help, michael@0: "version!" => \$main::opt_version, michael@0: "cum!" => \$main::opt_cum, michael@0: "base=s" => \$main::opt_base, michael@0: "seconds=i" => \$main::opt_seconds, michael@0: "add_lib=s" => \$main::opt_lib, michael@0: "lib_prefix=s" => \$main::opt_lib_prefix, michael@0: "functions!" => \$main::opt_functions, michael@0: "lines!" => \$main::opt_lines, michael@0: "addresses!" => \$main::opt_addresses, michael@0: "files!" => \$main::opt_files, michael@0: "text!" => \$main::opt_text, michael@0: "callgrind!" => \$main::opt_callgrind, michael@0: "list=s" => \$main::opt_list, michael@0: "disasm=s" => \$main::opt_disasm, michael@0: "symbols!" => \$main::opt_symbols, michael@0: "gv!" => \$main::opt_gv, michael@0: "evince!" => \$main::opt_evince, michael@0: "web!" => \$main::opt_web, michael@0: "dot!" => \$main::opt_dot, michael@0: "ps!" => \$main::opt_ps, michael@0: "pdf!" => \$main::opt_pdf, michael@0: "svg!" => \$main::opt_svg, michael@0: "gif!" => \$main::opt_gif, michael@0: "raw!" => \$main::opt_raw, michael@0: "interactive!" => \$main::opt_interactive, michael@0: "nodecount=i" => \$main::opt_nodecount, michael@0: "nodefraction=f" => \$main::opt_nodefraction, michael@0: "edgefraction=f" => \$main::opt_edgefraction, michael@0: "maxdegree=i" => \$main::opt_maxdegree, michael@0: "focus=s" => \$main::opt_focus, michael@0: "ignore=s" => \$main::opt_ignore, michael@0: "scale=i" => \$main::opt_scale, michael@0: "heapcheck" => \$main::opt_heapcheck, michael@0: "inuse_space!" => \$main::opt_inuse_space, michael@0: "inuse_objects!" => \$main::opt_inuse_objects, michael@0: "alloc_space!" => \$main::opt_alloc_space, michael@0: "alloc_objects!" => \$main::opt_alloc_objects, michael@0: "show_bytes!" => \$main::opt_show_bytes, michael@0: "drop_negative!" => \$main::opt_drop_negative, michael@0: "total_delay!" => \$main::opt_total_delay, michael@0: "contentions!" => \$main::opt_contentions, michael@0: "mean_delay!" => \$main::opt_mean_delay, michael@0: "tools=s" => \$main::opt_tools, michael@0: "test!" => \$main::opt_test, michael@0: "debug!" => \$main::opt_debug, michael@0: # Undocumented flags used only by unittests: michael@0: "test_stride=i" => \$main::opt_test_stride, michael@0: ) || usage("Invalid option(s)"); michael@0: michael@0: # Deal with the standard --help and --version michael@0: if ($main::opt_help) { michael@0: print usage_string(); michael@0: exit(0); michael@0: } michael@0: michael@0: if ($main::opt_version) { michael@0: print version_string(); michael@0: exit(0); michael@0: } michael@0: michael@0: # Disassembly/listing/symbols mode requires address-level info michael@0: if ($main::opt_disasm || $main::opt_list || $main::opt_symbols) { michael@0: $main::opt_functions = 0; michael@0: $main::opt_lines = 0; michael@0: $main::opt_addresses = 1; michael@0: $main::opt_files = 0; michael@0: } michael@0: michael@0: # Check heap-profiling flags michael@0: if ($main::opt_inuse_space + michael@0: $main::opt_inuse_objects + michael@0: $main::opt_alloc_space + michael@0: $main::opt_alloc_objects > 1) { michael@0: usage("Specify at most on of --inuse/--alloc options"); michael@0: } michael@0: michael@0: # Check output granularities michael@0: my $grains = michael@0: $main::opt_functions + michael@0: $main::opt_lines + michael@0: $main::opt_addresses + michael@0: $main::opt_files + michael@0: 0; michael@0: if ($grains > 1) { michael@0: usage("Only specify one output granularity option"); michael@0: } michael@0: if ($grains == 0) { michael@0: $main::opt_functions = 1; michael@0: } michael@0: michael@0: # Check output modes michael@0: my $modes = michael@0: $main::opt_text + michael@0: $main::opt_callgrind + michael@0: ($main::opt_list eq '' ? 0 : 1) + michael@0: ($main::opt_disasm eq '' ? 0 : 1) + michael@0: ($main::opt_symbols == 0 ? 0 : 1) + michael@0: $main::opt_gv + michael@0: $main::opt_evince + michael@0: $main::opt_web + michael@0: $main::opt_dot + michael@0: $main::opt_ps + michael@0: $main::opt_pdf + michael@0: $main::opt_svg + michael@0: $main::opt_gif + michael@0: $main::opt_raw + michael@0: $main::opt_interactive + michael@0: 0; michael@0: if ($modes > 1) { michael@0: usage("Only specify one output mode"); michael@0: } michael@0: if ($modes == 0) { michael@0: if (-t STDOUT) { # If STDOUT is a tty, activate interactive mode michael@0: $main::opt_interactive = 1; michael@0: } else { michael@0: $main::opt_text = 1; michael@0: } michael@0: } michael@0: michael@0: if ($main::opt_test) { michael@0: RunUnitTests(); michael@0: # Should not return michael@0: exit(1); michael@0: } michael@0: michael@0: # Binary name and profile arguments list michael@0: $main::prog = ""; michael@0: @main::pfile_args = (); michael@0: michael@0: # Remote profiling without a binary (using $SYMBOL_PAGE instead) michael@0: if (@ARGV > 0) { michael@0: if (IsProfileURL($ARGV[0])) { michael@0: $main::use_symbol_page = 1; michael@0: } elsif (IsSymbolizedProfileFile($ARGV[0])) { michael@0: $main::use_symbolized_profile = 1; michael@0: $main::prog = $UNKNOWN_BINARY; # will be set later from the profile file michael@0: } michael@0: } michael@0: michael@0: if ($main::use_symbol_page || $main::use_symbolized_profile) { michael@0: # We don't need a binary! michael@0: my %disabled = ('--lines' => $main::opt_lines, michael@0: '--disasm' => $main::opt_disasm); michael@0: for my $option (keys %disabled) { michael@0: usage("$option cannot be used without a binary") if $disabled{$option}; michael@0: } michael@0: # Set $main::prog later... michael@0: scalar(@ARGV) || usage("Did not specify profile file"); michael@0: } elsif ($main::opt_symbols) { michael@0: # --symbols needs a binary-name (to run nm on, etc) but not profiles michael@0: $main::prog = shift(@ARGV) || usage("Did not specify program"); michael@0: } else { michael@0: $main::prog = shift(@ARGV) || usage("Did not specify program"); michael@0: scalar(@ARGV) || usage("Did not specify profile file"); michael@0: } michael@0: michael@0: # Parse profile file/location arguments michael@0: foreach my $farg (@ARGV) { michael@0: if ($farg =~ m/(.*)\@([0-9]+)(|\/.*)$/ ) { michael@0: my $machine = $1; michael@0: my $num_machines = $2; michael@0: my $path = $3; michael@0: for (my $i = 0; $i < $num_machines; $i++) { michael@0: unshift(@main::pfile_args, "$i.$machine$path"); michael@0: } michael@0: } else { michael@0: unshift(@main::pfile_args, $farg); michael@0: } michael@0: } michael@0: michael@0: if ($main::use_symbol_page) { michael@0: unless (IsProfileURL($main::pfile_args[0])) { michael@0: error("The first profile should be a remote form to use $SYMBOL_PAGE\n"); michael@0: } michael@0: CheckSymbolPage(); michael@0: $main::prog = FetchProgramName(); michael@0: } elsif (!$main::use_symbolized_profile) { # may not need objtools! michael@0: ConfigureObjTools($main::prog) michael@0: } michael@0: michael@0: # Break the opt_lib_prefix into the prefix_list array michael@0: @prefix_list = split (',', $main::opt_lib_prefix); michael@0: michael@0: # Remove trailing / from the prefixes, in the list to prevent michael@0: # searching things like /my/path//lib/mylib.so michael@0: foreach (@prefix_list) { michael@0: s|/+$||; michael@0: } michael@0: } michael@0: michael@0: sub Main() { michael@0: Init(); michael@0: $main::collected_profile = undef; michael@0: @main::profile_files = (); michael@0: $main::op_time = time(); michael@0: michael@0: # Printing symbols is special and requires a lot less info that most. michael@0: if ($main::opt_symbols) { michael@0: PrintSymbols(*STDIN); # Get /proc/maps and symbols output from stdin michael@0: return; michael@0: } michael@0: michael@0: # Fetch all profile data michael@0: FetchDynamicProfiles(); michael@0: michael@0: # this will hold symbols that we read from the profile files michael@0: my $symbol_map = {}; michael@0: michael@0: # Read one profile, pick the last item on the list michael@0: my $data = ReadProfile($main::prog, pop(@main::profile_files)); michael@0: my $profile = $data->{profile}; michael@0: my $pcs = $data->{pcs}; michael@0: my $libs = $data->{libs}; # Info about main program and shared libraries michael@0: $symbol_map = MergeSymbols($symbol_map, $data->{symbols}); michael@0: michael@0: # Add additional profiles, if available. michael@0: if (scalar(@main::profile_files) > 0) { michael@0: foreach my $pname (@main::profile_files) { michael@0: my $data2 = ReadProfile($main::prog, $pname); michael@0: $profile = AddProfile($profile, $data2->{profile}); michael@0: $pcs = AddPcs($pcs, $data2->{pcs}); michael@0: $symbol_map = MergeSymbols($symbol_map, $data2->{symbols}); michael@0: } michael@0: } michael@0: michael@0: # Subtract base from profile, if specified michael@0: if ($main::opt_base ne '') { michael@0: my $base = ReadProfile($main::prog, $main::opt_base); michael@0: $profile = SubtractProfile($profile, $base->{profile}); michael@0: $pcs = AddPcs($pcs, $base->{pcs}); michael@0: $symbol_map = MergeSymbols($symbol_map, $base->{symbols}); michael@0: } michael@0: michael@0: # Get total data in profile michael@0: my $total = TotalProfile($profile); michael@0: michael@0: # Collect symbols michael@0: my $symbols; michael@0: if ($main::use_symbolized_profile) { michael@0: $symbols = FetchSymbols($pcs, $symbol_map); michael@0: } elsif ($main::use_symbol_page) { michael@0: $symbols = FetchSymbols($pcs); michael@0: } else { michael@0: # TODO(csilvers): $libs uses the /proc/self/maps data from profile1, michael@0: # which may differ from the data from subsequent profiles, especially michael@0: # if they were run on different machines. Use appropriate libs for michael@0: # each pc somehow. michael@0: $symbols = ExtractSymbols($libs, $pcs); michael@0: } michael@0: michael@0: # Remove uniniteresting stack items michael@0: $profile = RemoveUninterestingFrames($symbols, $profile); michael@0: michael@0: # Focus? michael@0: if ($main::opt_focus ne '') { michael@0: $profile = FocusProfile($symbols, $profile, $main::opt_focus); michael@0: } michael@0: michael@0: # Ignore? michael@0: if ($main::opt_ignore ne '') { michael@0: $profile = IgnoreProfile($symbols, $profile, $main::opt_ignore); michael@0: } michael@0: michael@0: my $calls = ExtractCalls($symbols, $profile); michael@0: michael@0: # Reduce profiles to required output granularity, and also clean michael@0: # each stack trace so a given entry exists at most once. michael@0: my $reduced = ReduceProfile($symbols, $profile); michael@0: michael@0: # Get derived profiles michael@0: my $flat = FlatProfile($reduced); michael@0: my $cumulative = CumulativeProfile($reduced); michael@0: michael@0: # Print michael@0: if (!$main::opt_interactive) { michael@0: if ($main::opt_disasm) { michael@0: PrintDisassembly($libs, $flat, $cumulative, $main::opt_disasm); michael@0: } elsif ($main::opt_list) { michael@0: PrintListing($total, $libs, $flat, $cumulative, $main::opt_list, 0); michael@0: } elsif ($main::opt_text) { michael@0: # Make sure the output is empty when have nothing to report michael@0: # (only matters when --heapcheck is given but we must be michael@0: # compatible with old branches that did not pass --heapcheck always): michael@0: if ($total != 0) { michael@0: printf("Total: %s %s\n", Unparse($total), Units()); michael@0: } michael@0: PrintText($symbols, $flat, $cumulative, -1); michael@0: } elsif ($main::opt_raw) { michael@0: PrintSymbolizedProfile($symbols, $profile, $main::prog); michael@0: } elsif ($main::opt_callgrind) { michael@0: PrintCallgrind($calls); michael@0: } else { michael@0: if (PrintDot($main::prog, $symbols, $profile, $flat, $cumulative, $total)) { michael@0: if ($main::opt_gv) { michael@0: RunGV(TempName($main::next_tmpfile, "ps"), ""); michael@0: } elsif ($main::opt_evince) { michael@0: RunEvince(TempName($main::next_tmpfile, "pdf"), ""); michael@0: } elsif ($main::opt_web) { michael@0: my $tmp = TempName($main::next_tmpfile, "svg"); michael@0: RunWeb($tmp); michael@0: # The command we run might hand the file name off michael@0: # to an already running browser instance and then exit. michael@0: # Normally, we'd remove $tmp on exit (right now), michael@0: # but fork a child to remove $tmp a little later, so that the michael@0: # browser has time to load it first. michael@0: delete $main::tempnames{$tmp}; michael@0: if (fork() == 0) { michael@0: sleep 5; michael@0: unlink($tmp); michael@0: exit(0); michael@0: } michael@0: } michael@0: } else { michael@0: cleanup(); michael@0: exit(1); michael@0: } michael@0: } michael@0: } else { michael@0: InteractiveMode($profile, $symbols, $libs, $total); michael@0: } michael@0: michael@0: cleanup(); michael@0: exit(0); michael@0: } michael@0: michael@0: ##### Entry Point ##### michael@0: michael@0: Main(); michael@0: michael@0: # Temporary code to detect if we're running on a Goobuntu system. michael@0: # These systems don't have the right stuff installed for the special michael@0: # Readline libraries to work, so as a temporary workaround, we default michael@0: # to using the normal stdio code, rather than the fancier readline-based michael@0: # code michael@0: sub ReadlineMightFail { michael@0: if (-e '/lib/libtermcap.so.2') { michael@0: return 0; # libtermcap exists, so readline should be okay michael@0: } else { michael@0: return 1; michael@0: } michael@0: } michael@0: michael@0: sub RunGV { michael@0: my $fname = shift; michael@0: my $bg = shift; # "" or " &" if we should run in background michael@0: if (!system(ShellEscape(@GV, "--version") . " >$dev_null 2>&1")) { michael@0: # Options using double dash are supported by this gv version. michael@0: # Also, turn on noantialias to better handle bug in gv for michael@0: # postscript files with large dimensions. michael@0: # TODO: Maybe we should not pass the --noantialias flag michael@0: # if the gv version is known to work properly without the flag. michael@0: system(ShellEscape(@GV, "--scale=$main::opt_scale", "--noantialias", $fname) michael@0: . $bg); michael@0: } else { michael@0: # Old gv version - only supports options that use single dash. michael@0: print STDERR ShellEscape(@GV, "-scale", $main::opt_scale) . "\n"; michael@0: system(ShellEscape(@GV, "-scale", "$main::opt_scale", $fname) . $bg); michael@0: } michael@0: } michael@0: michael@0: sub RunEvince { michael@0: my $fname = shift; michael@0: my $bg = shift; # "" or " &" if we should run in background michael@0: system(ShellEscape(@EVINCE, $fname) . $bg); michael@0: } michael@0: michael@0: sub RunWeb { michael@0: my $fname = shift; michael@0: print STDERR "Loading web page file:///$fname\n"; michael@0: michael@0: if (`uname` =~ /Darwin/) { michael@0: # OS X: open will use standard preference for SVG files. michael@0: system("/usr/bin/open", $fname); michael@0: return; michael@0: } michael@0: michael@0: # Some kind of Unix; try generic symlinks, then specific browsers. michael@0: # (Stop once we find one.) michael@0: # Works best if the browser is already running. michael@0: my @alt = ( michael@0: "/etc/alternatives/gnome-www-browser", michael@0: "/etc/alternatives/x-www-browser", michael@0: "google-chrome", michael@0: "firefox", michael@0: ); michael@0: foreach my $b (@alt) { michael@0: if (system($b, $fname) == 0) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: print STDERR "Could not load web browser.\n"; michael@0: } michael@0: michael@0: sub RunKcachegrind { michael@0: my $fname = shift; michael@0: my $bg = shift; # "" or " &" if we should run in background michael@0: print STDERR "Starting '@KCACHEGRIND " . $fname . $bg . "'\n"; michael@0: system(ShellEscape(@KCACHEGRIND, $fname) . $bg); michael@0: } michael@0: michael@0: michael@0: ##### Interactive helper routines ##### michael@0: michael@0: sub InteractiveMode { michael@0: $| = 1; # Make output unbuffered for interactive mode michael@0: my ($orig_profile, $symbols, $libs, $total) = @_; michael@0: michael@0: print STDERR "Welcome to pprof! For help, type 'help'.\n"; michael@0: michael@0: # Use ReadLine if it's installed and input comes from a console. michael@0: if ( -t STDIN && michael@0: !ReadlineMightFail() && michael@0: defined(eval {require Term::ReadLine}) ) { michael@0: my $term = new Term::ReadLine 'pprof'; michael@0: while ( defined ($_ = $term->readline('(pprof) '))) { michael@0: $term->addhistory($_) if /\S/; michael@0: if (!InteractiveCommand($orig_profile, $symbols, $libs, $total, $_)) { michael@0: last; # exit when we get an interactive command to quit michael@0: } michael@0: } michael@0: } else { # don't have readline michael@0: while (1) { michael@0: print STDERR "(pprof) "; michael@0: $_ = ; michael@0: last if ! defined $_ ; michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: michael@0: # Save some flags that might be reset by InteractiveCommand() michael@0: my $save_opt_lines = $main::opt_lines; michael@0: michael@0: if (!InteractiveCommand($orig_profile, $symbols, $libs, $total, $_)) { michael@0: last; # exit when we get an interactive command to quit michael@0: } michael@0: michael@0: # Restore flags michael@0: $main::opt_lines = $save_opt_lines; michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Takes two args: orig profile, and command to run. michael@0: # Returns 1 if we should keep going, or 0 if we were asked to quit michael@0: sub InteractiveCommand { michael@0: my($orig_profile, $symbols, $libs, $total, $command) = @_; michael@0: $_ = $command; # just to make future m//'s easier michael@0: if (!defined($_)) { michael@0: print STDERR "\n"; michael@0: return 0; michael@0: } michael@0: if (m/^\s*quit/) { michael@0: return 0; michael@0: } michael@0: if (m/^\s*help/) { michael@0: InteractiveHelpMessage(); michael@0: return 1; michael@0: } michael@0: # Clear all the mode options -- mode is controlled by "$command" michael@0: $main::opt_text = 0; michael@0: $main::opt_callgrind = 0; michael@0: $main::opt_disasm = 0; michael@0: $main::opt_list = 0; michael@0: $main::opt_gv = 0; michael@0: $main::opt_evince = 0; michael@0: $main::opt_cum = 0; michael@0: michael@0: if (m/^\s*(text|top)(\d*)\s*(.*)/) { michael@0: $main::opt_text = 1; michael@0: michael@0: my $line_limit = ($2 ne "") ? int($2) : 10; michael@0: michael@0: my $routine; michael@0: my $ignore; michael@0: ($routine, $ignore) = ParseInteractiveArgs($3); michael@0: michael@0: my $profile = ProcessProfile($total, $orig_profile, $symbols, "", $ignore); michael@0: my $reduced = ReduceProfile($symbols, $profile); michael@0: michael@0: # Get derived profiles michael@0: my $flat = FlatProfile($reduced); michael@0: my $cumulative = CumulativeProfile($reduced); michael@0: michael@0: PrintText($symbols, $flat, $cumulative, $line_limit); michael@0: return 1; michael@0: } michael@0: if (m/^\s*callgrind\s*([^ \n]*)/) { michael@0: $main::opt_callgrind = 1; michael@0: michael@0: # Get derived profiles michael@0: my $calls = ExtractCalls($symbols, $orig_profile); michael@0: my $filename = $1; michael@0: if ( $1 eq '' ) { michael@0: $filename = TempName($main::next_tmpfile, "callgrind"); michael@0: } michael@0: PrintCallgrind($calls, $filename); michael@0: if ( $1 eq '' ) { michael@0: RunKcachegrind($filename, " & "); michael@0: $main::next_tmpfile++; michael@0: } michael@0: michael@0: return 1; michael@0: } michael@0: if (m/^\s*(web)?list\s*(.+)/) { michael@0: my $html = (defined($1) && ($1 eq "web")); michael@0: $main::opt_list = 1; michael@0: michael@0: my $routine; michael@0: my $ignore; michael@0: ($routine, $ignore) = ParseInteractiveArgs($2); michael@0: michael@0: my $profile = ProcessProfile($total, $orig_profile, $symbols, "", $ignore); michael@0: my $reduced = ReduceProfile($symbols, $profile); michael@0: michael@0: # Get derived profiles michael@0: my $flat = FlatProfile($reduced); michael@0: my $cumulative = CumulativeProfile($reduced); michael@0: michael@0: PrintListing($total, $libs, $flat, $cumulative, $routine, $html); michael@0: return 1; michael@0: } michael@0: if (m/^\s*disasm\s*(.+)/) { michael@0: $main::opt_disasm = 1; michael@0: michael@0: my $routine; michael@0: my $ignore; michael@0: ($routine, $ignore) = ParseInteractiveArgs($1); michael@0: michael@0: # Process current profile to account for various settings michael@0: my $profile = ProcessProfile($total, $orig_profile, $symbols, "", $ignore); michael@0: my $reduced = ReduceProfile($symbols, $profile); michael@0: michael@0: # Get derived profiles michael@0: my $flat = FlatProfile($reduced); michael@0: my $cumulative = CumulativeProfile($reduced); michael@0: michael@0: PrintDisassembly($libs, $flat, $cumulative, $routine); michael@0: return 1; michael@0: } michael@0: if (m/^\s*(gv|web|evince)\s*(.*)/) { michael@0: $main::opt_gv = 0; michael@0: $main::opt_evince = 0; michael@0: $main::opt_web = 0; michael@0: if ($1 eq "gv") { michael@0: $main::opt_gv = 1; michael@0: } elsif ($1 eq "evince") { michael@0: $main::opt_evince = 1; michael@0: } elsif ($1 eq "web") { michael@0: $main::opt_web = 1; michael@0: } michael@0: michael@0: my $focus; michael@0: my $ignore; michael@0: ($focus, $ignore) = ParseInteractiveArgs($2); michael@0: michael@0: # Process current profile to account for various settings michael@0: my $profile = ProcessProfile($total, $orig_profile, $symbols, michael@0: $focus, $ignore); michael@0: my $reduced = ReduceProfile($symbols, $profile); michael@0: michael@0: # Get derived profiles michael@0: my $flat = FlatProfile($reduced); michael@0: my $cumulative = CumulativeProfile($reduced); michael@0: michael@0: if (PrintDot($main::prog, $symbols, $profile, $flat, $cumulative, $total)) { michael@0: if ($main::opt_gv) { michael@0: RunGV(TempName($main::next_tmpfile, "ps"), " &"); michael@0: } elsif ($main::opt_evince) { michael@0: RunEvince(TempName($main::next_tmpfile, "pdf"), " &"); michael@0: } elsif ($main::opt_web) { michael@0: RunWeb(TempName($main::next_tmpfile, "svg")); michael@0: } michael@0: $main::next_tmpfile++; michael@0: } michael@0: return 1; michael@0: } michael@0: if (m/^\s*$/) { michael@0: return 1; michael@0: } michael@0: print STDERR "Unknown command: try 'help'.\n"; michael@0: return 1; michael@0: } michael@0: michael@0: michael@0: sub ProcessProfile { michael@0: my $total_count = shift; michael@0: my $orig_profile = shift; michael@0: my $symbols = shift; michael@0: my $focus = shift; michael@0: my $ignore = shift; michael@0: michael@0: # Process current profile to account for various settings michael@0: my $profile = $orig_profile; michael@0: printf("Total: %s %s\n", Unparse($total_count), Units()); michael@0: if ($focus ne '') { michael@0: $profile = FocusProfile($symbols, $profile, $focus); michael@0: my $focus_count = TotalProfile($profile); michael@0: printf("After focusing on '%s': %s %s of %s (%0.1f%%)\n", michael@0: $focus, michael@0: Unparse($focus_count), Units(), michael@0: Unparse($total_count), ($focus_count*100.0) / $total_count); michael@0: } michael@0: if ($ignore ne '') { michael@0: $profile = IgnoreProfile($symbols, $profile, $ignore); michael@0: my $ignore_count = TotalProfile($profile); michael@0: printf("After ignoring '%s': %s %s of %s (%0.1f%%)\n", michael@0: $ignore, michael@0: Unparse($ignore_count), Units(), michael@0: Unparse($total_count), michael@0: ($ignore_count*100.0) / $total_count); michael@0: } michael@0: michael@0: return $profile; michael@0: } michael@0: michael@0: sub InteractiveHelpMessage { michael@0: print STDERR <{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: if ($#addrs >= 0) { michael@0: my $depth = $#addrs + 1; michael@0: # int(foo / 2**32) is the only reliable way to get rid of bottom michael@0: # 32 bits on both 32- and 64-bit systems. michael@0: print pack('L*', $count & 0xFFFFFFFF, int($count / 2**32)); michael@0: print pack('L*', $depth & 0xFFFFFFFF, int($depth / 2**32)); michael@0: michael@0: foreach my $full_addr (@addrs) { michael@0: my $addr = $full_addr; michael@0: $addr =~ s/0x0*//; # strip off leading 0x, zeroes michael@0: if (length($addr) > 16) { michael@0: print STDERR "Invalid address in profile: $full_addr\n"; michael@0: next; michael@0: } michael@0: my $low_addr = substr($addr, -8); # get last 8 hex chars michael@0: my $high_addr = substr($addr, -16, 8); # get up to 8 more hex chars michael@0: print pack('L*', hex('0x' . $low_addr), hex('0x' . $high_addr)); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Print symbols and profile data michael@0: sub PrintSymbolizedProfile { michael@0: my $symbols = shift; michael@0: my $profile = shift; michael@0: my $prog = shift; michael@0: michael@0: $SYMBOL_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $symbol_marker = $&; michael@0: michael@0: print '--- ', $symbol_marker, "\n"; michael@0: if (defined($prog)) { michael@0: print 'binary=', $prog, "\n"; michael@0: } michael@0: while (my ($pc, $name) = each(%{$symbols})) { michael@0: my $sep = ' '; michael@0: print '0x', $pc; michael@0: # We have a list of function names, which include the inlined michael@0: # calls. They are separated (and terminated) by --, which is michael@0: # illegal in function names. michael@0: for (my $j = 2; $j <= $#{$name}; $j += 3) { michael@0: print $sep, $name->[$j]; michael@0: $sep = '--'; michael@0: } michael@0: print "\n"; michael@0: } michael@0: print '---', "\n"; michael@0: michael@0: $PROFILE_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $profile_marker = $&; michael@0: print '--- ', $profile_marker, "\n"; michael@0: if (defined($main::collected_profile)) { michael@0: # if used with remote fetch, simply dump the collected profile to output. michael@0: open(SRC, "<$main::collected_profile"); michael@0: while () { michael@0: print $_; michael@0: } michael@0: close(SRC); michael@0: } else { michael@0: # dump a cpu-format profile to standard out michael@0: PrintProfileData($profile); michael@0: } michael@0: } michael@0: michael@0: # Print text output michael@0: sub PrintText { michael@0: my $symbols = shift; michael@0: my $flat = shift; michael@0: my $cumulative = shift; michael@0: my $line_limit = shift; michael@0: michael@0: my $total = TotalProfile($flat); michael@0: michael@0: # Which profile to sort by? michael@0: my $s = $main::opt_cum ? $cumulative : $flat; michael@0: michael@0: my $running_sum = 0; michael@0: my $lines = 0; michael@0: foreach my $k (sort { GetEntry($s, $b) <=> GetEntry($s, $a) || $a cmp $b } michael@0: keys(%{$cumulative})) { michael@0: my $f = GetEntry($flat, $k); michael@0: my $c = GetEntry($cumulative, $k); michael@0: $running_sum += $f; michael@0: michael@0: my $sym = $k; michael@0: if (exists($symbols->{$k})) { michael@0: $sym = $symbols->{$k}->[0] . " " . $symbols->{$k}->[1]; michael@0: if ($main::opt_addresses) { michael@0: $sym = $k . " " . $sym; michael@0: } michael@0: } michael@0: michael@0: if ($f != 0 || $c != 0) { michael@0: printf("%8s %6s %6s %8s %6s %s\n", michael@0: Unparse($f), michael@0: Percent($f, $total), michael@0: Percent($running_sum, $total), michael@0: Unparse($c), michael@0: Percent($c, $total), michael@0: $sym); michael@0: } michael@0: $lines++; michael@0: last if ($line_limit >= 0 && $lines >= $line_limit); michael@0: } michael@0: } michael@0: michael@0: # Callgrind format has a compression for repeated function and file michael@0: # names. You show the name the first time, and just use its number michael@0: # subsequently. This can cut down the file to about a third or a michael@0: # quarter of its uncompressed size. $key and $val are the key/value michael@0: # pair that would normally be printed by callgrind; $map is a map from michael@0: # value to number. michael@0: sub CompressedCGName { michael@0: my($key, $val, $map) = @_; michael@0: my $idx = $map->{$val}; michael@0: # For very short keys, providing an index hurts rather than helps. michael@0: if (length($val) <= 3) { michael@0: return "$key=$val\n"; michael@0: } elsif (defined($idx)) { michael@0: return "$key=($idx)\n"; michael@0: } else { michael@0: # scalar(keys $map) gives the number of items in the map. michael@0: $idx = scalar(keys(%{$map})) + 1; michael@0: $map->{$val} = $idx; michael@0: return "$key=($idx) $val\n"; michael@0: } michael@0: } michael@0: michael@0: # Print the call graph in a way that's suiteable for callgrind. michael@0: sub PrintCallgrind { michael@0: my $calls = shift; michael@0: my $filename; michael@0: my %filename_to_index_map; michael@0: my %fnname_to_index_map; michael@0: michael@0: if ($main::opt_interactive) { michael@0: $filename = shift; michael@0: print STDERR "Writing callgrind file to '$filename'.\n" michael@0: } else { michael@0: $filename = "&STDOUT"; michael@0: } michael@0: open(CG, ">$filename"); michael@0: printf CG ("events: Hits\n\n"); michael@0: foreach my $call ( map { $_->[0] } michael@0: sort { $a->[1] cmp $b ->[1] || michael@0: $a->[2] <=> $b->[2] } michael@0: map { /([^:]+):(\d+):([^ ]+)( -> ([^:]+):(\d+):(.+))?/; michael@0: [$_, $1, $2] } michael@0: keys %$calls ) { michael@0: my $count = int($calls->{$call}); michael@0: $call =~ /([^:]+):(\d+):([^ ]+)( -> ([^:]+):(\d+):(.+))?/; michael@0: my ( $caller_file, $caller_line, $caller_function, michael@0: $callee_file, $callee_line, $callee_function ) = michael@0: ( $1, $2, $3, $5, $6, $7 ); michael@0: michael@0: # TODO(csilvers): for better compression, collect all the michael@0: # caller/callee_files and functions first, before printing michael@0: # anything, and only compress those referenced more than once. michael@0: printf CG CompressedCGName("fl", $caller_file, \%filename_to_index_map); michael@0: printf CG CompressedCGName("fn", $caller_function, \%fnname_to_index_map); michael@0: if (defined $6) { michael@0: printf CG CompressedCGName("cfl", $callee_file, \%filename_to_index_map); michael@0: printf CG CompressedCGName("cfn", $callee_function, \%fnname_to_index_map); michael@0: printf CG ("calls=$count $callee_line\n"); michael@0: } michael@0: printf CG ("$caller_line $count\n\n"); michael@0: } michael@0: } michael@0: michael@0: # Print disassembly for all all routines that match $main::opt_disasm michael@0: sub PrintDisassembly { michael@0: my $libs = shift; michael@0: my $flat = shift; michael@0: my $cumulative = shift; michael@0: my $disasm_opts = shift; michael@0: michael@0: my $total = TotalProfile($flat); michael@0: michael@0: foreach my $lib (@{$libs}) { michael@0: my $symbol_table = GetProcedureBoundaries($lib->[0], $disasm_opts); michael@0: my $offset = AddressSub($lib->[1], $lib->[3]); michael@0: foreach my $routine (sort ByName keys(%{$symbol_table})) { michael@0: my $start_addr = $symbol_table->{$routine}->[0]; michael@0: my $end_addr = $symbol_table->{$routine}->[1]; michael@0: # See if there are any samples in this routine michael@0: my $length = hex(AddressSub($end_addr, $start_addr)); michael@0: my $addr = AddressAdd($start_addr, $offset); michael@0: for (my $i = 0; $i < $length; $i++) { michael@0: if (defined($cumulative->{$addr})) { michael@0: PrintDisassembledFunction($lib->[0], $offset, michael@0: $routine, $flat, $cumulative, michael@0: $start_addr, $end_addr, $total); michael@0: last; michael@0: } michael@0: $addr = AddressInc($addr); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Return reference to array of tuples of the form: michael@0: # [start_address, filename, linenumber, instruction, limit_address] michael@0: # E.g., michael@0: # ["0x806c43d", "/foo/bar.cc", 131, "ret", "0x806c440"] michael@0: sub Disassemble { michael@0: my $prog = shift; michael@0: my $offset = shift; michael@0: my $start_addr = shift; michael@0: my $end_addr = shift; michael@0: michael@0: my $objdump = $obj_tool_map{"objdump"}; michael@0: my $cmd = ShellEscape($objdump, "-C", "-d", "-l", "--no-show-raw-insn", michael@0: "--start-address=0x$start_addr", michael@0: "--stop-address=0x$end_addr", $prog); michael@0: open(OBJDUMP, "$cmd |") || error("$cmd: $!\n"); michael@0: my @result = (); michael@0: my $filename = ""; michael@0: my $linenumber = -1; michael@0: my $last = ["", "", "", ""]; michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: chop; michael@0: if (m|\s*([^:\s]+):(\d+)\s*$|) { michael@0: # Location line of the form: michael@0: # : michael@0: $filename = $1; michael@0: $linenumber = $2; michael@0: } elsif (m/^ +([0-9a-f]+):\s*(.*)/) { michael@0: # Disassembly line -- zero-extend address to full length michael@0: my $addr = HexExtend($1); michael@0: my $k = AddressAdd($addr, $offset); michael@0: $last->[4] = $k; # Store ending address for previous instruction michael@0: $last = [$k, $filename, $linenumber, $2, $end_addr]; michael@0: push(@result, $last); michael@0: } michael@0: } michael@0: close(OBJDUMP); michael@0: return @result; michael@0: } michael@0: michael@0: # The input file should contain lines of the form /proc/maps-like michael@0: # output (same format as expected from the profiles) or that looks michael@0: # like hex addresses (like "0xDEADBEEF"). We will parse all michael@0: # /proc/maps output, and for all the hex addresses, we will output michael@0: # "short" symbol names, one per line, in the same order as the input. michael@0: sub PrintSymbols { michael@0: my $maps_and_symbols_file = shift; michael@0: michael@0: # ParseLibraries expects pcs to be in a set. Fine by us... michael@0: my @pclist = (); # pcs in sorted order michael@0: my $pcs = {}; michael@0: my $map = ""; michael@0: foreach my $line (<$maps_and_symbols_file>) { michael@0: $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: if ($line =~ /\b(0x[0-9a-f]+)\b/i) { michael@0: push(@pclist, HexExtend($1)); michael@0: $pcs->{$pclist[-1]} = 1; michael@0: } else { michael@0: $map .= $line; michael@0: } michael@0: } michael@0: michael@0: my $libs = ParseLibraries($main::prog, $map, $pcs); michael@0: my $symbols = ExtractSymbols($libs, $pcs); michael@0: michael@0: foreach my $pc (@pclist) { michael@0: # ->[0] is the shortname, ->[2] is the full name michael@0: print(($symbols->{$pc}->[0] || "??") . "\n"); michael@0: } michael@0: } michael@0: michael@0: michael@0: # For sorting functions by name michael@0: sub ByName { michael@0: return ShortFunctionName($a) cmp ShortFunctionName($b); michael@0: } michael@0: michael@0: # Print source-listing for all all routines that match $list_opts michael@0: sub PrintListing { michael@0: my $total = shift; michael@0: my $libs = shift; michael@0: my $flat = shift; michael@0: my $cumulative = shift; michael@0: my $list_opts = shift; michael@0: my $html = shift; michael@0: michael@0: my $output = \*STDOUT; michael@0: my $fname = ""; michael@0: michael@0: if ($html) { michael@0: # Arrange to write the output to a temporary file michael@0: $fname = TempName($main::next_tmpfile, "html"); michael@0: $main::next_tmpfile++; michael@0: if (!open(TEMP, ">$fname")) { michael@0: print STDERR "$fname: $!\n"; michael@0: return; michael@0: } michael@0: $output = \*TEMP; michael@0: print $output HtmlListingHeader(); michael@0: printf $output ("
%s
Total: %s %s
\n", michael@0: $main::prog, Unparse($total), Units()); michael@0: } michael@0: michael@0: my $listed = 0; michael@0: foreach my $lib (@{$libs}) { michael@0: my $symbol_table = GetProcedureBoundaries($lib->[0], $list_opts); michael@0: my $offset = AddressSub($lib->[1], $lib->[3]); michael@0: foreach my $routine (sort ByName keys(%{$symbol_table})) { michael@0: # Print if there are any samples in this routine michael@0: my $start_addr = $symbol_table->{$routine}->[0]; michael@0: my $end_addr = $symbol_table->{$routine}->[1]; michael@0: my $length = hex(AddressSub($end_addr, $start_addr)); michael@0: my $addr = AddressAdd($start_addr, $offset); michael@0: for (my $i = 0; $i < $length; $i++) { michael@0: if (defined($cumulative->{$addr})) { michael@0: $listed += PrintSource( michael@0: $lib->[0], $offset, michael@0: $routine, $flat, $cumulative, michael@0: $start_addr, $end_addr, michael@0: $html, michael@0: $output); michael@0: last; michael@0: } michael@0: $addr = AddressInc($addr); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if ($html) { michael@0: if ($listed > 0) { michael@0: print $output HtmlListingFooter(); michael@0: close($output); michael@0: RunWeb($fname); michael@0: } else { michael@0: close($output); michael@0: unlink($fname); michael@0: } michael@0: } michael@0: } michael@0: michael@0: sub HtmlListingHeader { michael@0: return <<'EOF'; michael@0: michael@0: michael@0: michael@0: Pprof listing michael@0: michael@0: michael@0: michael@0: michael@0: EOF michael@0: } michael@0: michael@0: sub HtmlListingFooter { michael@0: return <<'EOF'; michael@0: michael@0: michael@0: EOF michael@0: } michael@0: michael@0: sub HtmlEscape { michael@0: my $text = shift; michael@0: $text =~ s/&/&/g; michael@0: $text =~ s//>/g; michael@0: return $text; michael@0: } michael@0: michael@0: # Returns the indentation of the line, if it has any non-whitespace michael@0: # characters. Otherwise, returns -1. michael@0: sub Indentation { michael@0: my $line = shift; michael@0: if (m/^(\s*)\S/) { michael@0: return length($1); michael@0: } else { michael@0: return -1; michael@0: } michael@0: } michael@0: michael@0: # If the symbol table contains inlining info, Disassemble() may tag an michael@0: # instruction with a location inside an inlined function. But for michael@0: # source listings, we prefer to use the location in the function we michael@0: # are listing. So use MapToSymbols() to fetch full location michael@0: # information for each instruction and then pick out the first michael@0: # location from a location list (location list contains callers before michael@0: # callees in case of inlining). michael@0: # michael@0: # After this routine has run, each entry in $instructions contains: michael@0: # [0] start address michael@0: # [1] filename for function we are listing michael@0: # [2] line number for function we are listing michael@0: # [3] disassembly michael@0: # [4] limit address michael@0: # [5] most specific filename (may be different from [1] due to inlining) michael@0: # [6] most specific line number (may be different from [2] due to inlining) michael@0: sub GetTopLevelLineNumbers { michael@0: my ($lib, $offset, $instructions) = @_; michael@0: my $pcs = []; michael@0: for (my $i = 0; $i <= $#{$instructions}; $i++) { michael@0: push(@{$pcs}, $instructions->[$i]->[0]); michael@0: } michael@0: my $symbols = {}; michael@0: MapToSymbols($lib, $offset, $pcs, $symbols); michael@0: for (my $i = 0; $i <= $#{$instructions}; $i++) { michael@0: my $e = $instructions->[$i]; michael@0: push(@{$e}, $e->[1]); michael@0: push(@{$e}, $e->[2]); michael@0: my $addr = $e->[0]; michael@0: my $sym = $symbols->{$addr}; michael@0: if (defined($sym)) { michael@0: if ($#{$sym} >= 2 && $sym->[1] =~ m/^(.*):(\d+)$/) { michael@0: $e->[1] = $1; # File name michael@0: $e->[2] = $2; # Line number michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Print source-listing for one routine michael@0: sub PrintSource { michael@0: my $prog = shift; michael@0: my $offset = shift; michael@0: my $routine = shift; michael@0: my $flat = shift; michael@0: my $cumulative = shift; michael@0: my $start_addr = shift; michael@0: my $end_addr = shift; michael@0: my $html = shift; michael@0: my $output = shift; michael@0: michael@0: # Disassemble all instructions (just to get line numbers) michael@0: my @instructions = Disassemble($prog, $offset, $start_addr, $end_addr); michael@0: GetTopLevelLineNumbers($prog, $offset, \@instructions); michael@0: michael@0: # Hack 1: assume that the first source file encountered in the michael@0: # disassembly contains the routine michael@0: my $filename = undef; michael@0: for (my $i = 0; $i <= $#instructions; $i++) { michael@0: if ($instructions[$i]->[2] >= 0) { michael@0: $filename = $instructions[$i]->[1]; michael@0: last; michael@0: } michael@0: } michael@0: if (!defined($filename)) { michael@0: print STDERR "no filename found in $routine\n"; michael@0: return 0; michael@0: } michael@0: michael@0: # Hack 2: assume that the largest line number from $filename is the michael@0: # end of the procedure. This is typically safe since if P1 contains michael@0: # an inlined call to P2, then P2 usually occurs earlier in the michael@0: # source file. If this does not work, we might have to compute a michael@0: # density profile or just print all regions we find. michael@0: my $lastline = 0; michael@0: for (my $i = 0; $i <= $#instructions; $i++) { michael@0: my $f = $instructions[$i]->[1]; michael@0: my $l = $instructions[$i]->[2]; michael@0: if (($f eq $filename) && ($l > $lastline)) { michael@0: $lastline = $l; michael@0: } michael@0: } michael@0: michael@0: # Hack 3: assume the first source location from "filename" is the start of michael@0: # the source code. michael@0: my $firstline = 1; michael@0: for (my $i = 0; $i <= $#instructions; $i++) { michael@0: if ($instructions[$i]->[1] eq $filename) { michael@0: $firstline = $instructions[$i]->[2]; michael@0: last; michael@0: } michael@0: } michael@0: michael@0: # Hack 4: Extend last line forward until its indentation is less than michael@0: # the indentation we saw on $firstline michael@0: my $oldlastline = $lastline; michael@0: { michael@0: if (!open(FILE, "<$filename")) { michael@0: print STDERR "$filename: $!\n"; michael@0: return 0; michael@0: } michael@0: my $l = 0; michael@0: my $first_indentation = -1; michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: $l++; michael@0: my $indent = Indentation($_); michael@0: if ($l >= $firstline) { michael@0: if ($first_indentation < 0 && $indent >= 0) { michael@0: $first_indentation = $indent; michael@0: last if ($first_indentation == 0); michael@0: } michael@0: } michael@0: if ($l >= $lastline && $indent >= 0) { michael@0: if ($indent >= $first_indentation) { michael@0: $lastline = $l+1; michael@0: } else { michael@0: last; michael@0: } michael@0: } michael@0: } michael@0: close(FILE); michael@0: } michael@0: michael@0: # Assign all samples to the range $firstline,$lastline, michael@0: # Hack 4: If an instruction does not occur in the range, its samples michael@0: # are moved to the next instruction that occurs in the range. michael@0: my $samples1 = {}; # Map from line number to flat count michael@0: my $samples2 = {}; # Map from line number to cumulative count michael@0: my $running1 = 0; # Unassigned flat counts michael@0: my $running2 = 0; # Unassigned cumulative counts michael@0: my $total1 = 0; # Total flat counts michael@0: my $total2 = 0; # Total cumulative counts michael@0: my %disasm = (); # Map from line number to disassembly michael@0: my $running_disasm = ""; # Unassigned disassembly michael@0: my $skip_marker = "---\n"; michael@0: if ($html) { michael@0: $skip_marker = ""; michael@0: for (my $l = $firstline; $l <= $lastline; $l++) { michael@0: $disasm{$l} = ""; michael@0: } michael@0: } michael@0: my $last_dis_filename = ''; michael@0: my $last_dis_linenum = -1; michael@0: my $last_touched_line = -1; # To detect gaps in disassembly for a line michael@0: foreach my $e (@instructions) { michael@0: # Add up counts for all address that fall inside this instruction michael@0: my $c1 = 0; michael@0: my $c2 = 0; michael@0: for (my $a = $e->[0]; $a lt $e->[4]; $a = AddressInc($a)) { michael@0: $c1 += GetEntry($flat, $a); michael@0: $c2 += GetEntry($cumulative, $a); michael@0: } michael@0: michael@0: if ($html) { michael@0: my $dis = sprintf(" %6s %6s \t\t%8s: %s ", michael@0: HtmlPrintNumber($c1), michael@0: HtmlPrintNumber($c2), michael@0: UnparseAddress($offset, $e->[0]), michael@0: CleanDisassembly($e->[3])); michael@0: michael@0: # Append the most specific source line associated with this instruction michael@0: if (length($dis) < 80) { $dis .= (' ' x (80 - length($dis))) }; michael@0: $dis = HtmlEscape($dis); michael@0: my $f = $e->[5]; michael@0: my $l = $e->[6]; michael@0: if ($f ne $last_dis_filename) { michael@0: $dis .= sprintf("%s:%d", michael@0: HtmlEscape(CleanFileName($f)), $l); michael@0: } elsif ($l ne $last_dis_linenum) { michael@0: # De-emphasize the unchanged file name portion michael@0: $dis .= sprintf("%s" . michael@0: ":%d", michael@0: HtmlEscape(CleanFileName($f)), $l); michael@0: } else { michael@0: # De-emphasize the entire location michael@0: $dis .= sprintf("%s:%d", michael@0: HtmlEscape(CleanFileName($f)), $l); michael@0: } michael@0: $last_dis_filename = $f; michael@0: $last_dis_linenum = $l; michael@0: $running_disasm .= $dis; michael@0: $running_disasm .= "\n"; michael@0: } michael@0: michael@0: $running1 += $c1; michael@0: $running2 += $c2; michael@0: $total1 += $c1; michael@0: $total2 += $c2; michael@0: my $file = $e->[1]; michael@0: my $line = $e->[2]; michael@0: if (($file eq $filename) && michael@0: ($line >= $firstline) && michael@0: ($line <= $lastline)) { michael@0: # Assign all accumulated samples to this line michael@0: AddEntry($samples1, $line, $running1); michael@0: AddEntry($samples2, $line, $running2); michael@0: $running1 = 0; michael@0: $running2 = 0; michael@0: if ($html) { michael@0: if ($line != $last_touched_line && $disasm{$line} ne '') { michael@0: $disasm{$line} .= "\n"; michael@0: } michael@0: $disasm{$line} .= $running_disasm; michael@0: $running_disasm = ''; michael@0: $last_touched_line = $line; michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Assign any leftover samples to $lastline michael@0: AddEntry($samples1, $lastline, $running1); michael@0: AddEntry($samples2, $lastline, $running2); michael@0: if ($html) { michael@0: if ($lastline != $last_touched_line && $disasm{$lastline} ne '') { michael@0: $disasm{$lastline} .= "\n"; michael@0: } michael@0: $disasm{$lastline} .= $running_disasm; michael@0: } michael@0: michael@0: if ($html) { michael@0: printf $output ( michael@0: "

%s

%s\n
\n" .
michael@0:       "Total:%6s %6s (flat / cumulative %s)\n",
michael@0:       HtmlEscape(ShortFunctionName($routine)),
michael@0:       HtmlEscape(CleanFileName($filename)),
michael@0:       Unparse($total1),
michael@0:       Unparse($total2),
michael@0:       Units());
michael@0:   } else {
michael@0:     printf $output (
michael@0:       "ROUTINE ====================== %s in %s\n" .
michael@0:       "%6s %6s Total %s (flat / cumulative)\n",
michael@0:       ShortFunctionName($routine),
michael@0:       CleanFileName($filename),
michael@0:       Unparse($total1),
michael@0:       Unparse($total2),
michael@0:       Units());
michael@0:   }
michael@0:   if (!open(FILE, "<$filename")) {
michael@0:     print STDERR "$filename: $!\n";
michael@0:     return 0;
michael@0:   }
michael@0:   my $l = 0;
michael@0:   while () {
michael@0:     s/\r//g;         # turn windows-looking lines into unix-looking lines
michael@0:     $l++;
michael@0:     if ($l >= $firstline - 5 &&
michael@0:         (($l <= $oldlastline + 5) || ($l <= $lastline))) {
michael@0:       chop;
michael@0:       my $text = $_;
michael@0:       if ($l == $firstline) { print $output $skip_marker; }
michael@0:       my $n1 = GetEntry($samples1, $l);
michael@0:       my $n2 = GetEntry($samples2, $l);
michael@0:       if ($html) {
michael@0:         # Emit a span that has one of the following classes:
michael@0:         #    livesrc -- has samples
michael@0:         #    deadsrc -- has disassembly, but with no samples
michael@0:         #    nop     -- has no matching disasembly
michael@0:         # Also emit an optional span containing disassembly.
michael@0:         my $dis = $disasm{$l};
michael@0:         my $asm = "";
michael@0:         if (defined($dis) && $dis ne '') {
michael@0:           $asm = "" . $dis . "";
michael@0:         }
michael@0:         my $source_class = (($n1 + $n2 > 0) 
michael@0:                             ? "livesrc" 
michael@0:                             : (($asm ne "") ? "deadsrc" : "nop"));
michael@0:         printf $output (
michael@0:           "%5d " .
michael@0:           "%6s %6s %s%s\n",
michael@0:           $l, $source_class,
michael@0:           HtmlPrintNumber($n1),
michael@0:           HtmlPrintNumber($n2),
michael@0:           HtmlEscape($text),
michael@0:           $asm);
michael@0:       } else {
michael@0:         printf $output(
michael@0:           "%6s %6s %4d: %s\n",
michael@0:           UnparseAlt($n1),
michael@0:           UnparseAlt($n2),
michael@0:           $l,
michael@0:           $text);
michael@0:       }
michael@0:       if ($l == $lastline)  { print $output $skip_marker; }
michael@0:     };
michael@0:   }
michael@0:   close(FILE);
michael@0:   if ($html) {
michael@0:     print $output "
\n"; michael@0: } michael@0: return 1; michael@0: } michael@0: michael@0: # Return the source line for the specified file/linenumber. michael@0: # Returns undef if not found. michael@0: sub SourceLine { michael@0: my $file = shift; michael@0: my $line = shift; michael@0: michael@0: # Look in cache michael@0: if (!defined($main::source_cache{$file})) { michael@0: if (100 < scalar keys(%main::source_cache)) { michael@0: # Clear the cache when it gets too big michael@0: $main::source_cache = (); michael@0: } michael@0: michael@0: # Read all lines from the file michael@0: if (!open(FILE, "<$file")) { michael@0: print STDERR "$file: $!\n"; michael@0: $main::source_cache{$file} = []; # Cache the negative result michael@0: return undef; michael@0: } michael@0: my $lines = []; michael@0: push(@{$lines}, ""); # So we can use 1-based line numbers as indices michael@0: while () { michael@0: push(@{$lines}, $_); michael@0: } michael@0: close(FILE); michael@0: michael@0: # Save the lines in the cache michael@0: $main::source_cache{$file} = $lines; michael@0: } michael@0: michael@0: my $lines = $main::source_cache{$file}; michael@0: if (($line < 0) || ($line > $#{$lines})) { michael@0: return undef; michael@0: } else { michael@0: return $lines->[$line]; michael@0: } michael@0: } michael@0: michael@0: # Print disassembly for one routine with interspersed source if available michael@0: sub PrintDisassembledFunction { michael@0: my $prog = shift; michael@0: my $offset = shift; michael@0: my $routine = shift; michael@0: my $flat = shift; michael@0: my $cumulative = shift; michael@0: my $start_addr = shift; michael@0: my $end_addr = shift; michael@0: my $total = shift; michael@0: michael@0: # Disassemble all instructions michael@0: my @instructions = Disassemble($prog, $offset, $start_addr, $end_addr); michael@0: michael@0: # Make array of counts per instruction michael@0: my @flat_count = (); michael@0: my @cum_count = (); michael@0: my $flat_total = 0; michael@0: my $cum_total = 0; michael@0: foreach my $e (@instructions) { michael@0: # Add up counts for all address that fall inside this instruction michael@0: my $c1 = 0; michael@0: my $c2 = 0; michael@0: for (my $a = $e->[0]; $a lt $e->[4]; $a = AddressInc($a)) { michael@0: $c1 += GetEntry($flat, $a); michael@0: $c2 += GetEntry($cumulative, $a); michael@0: } michael@0: push(@flat_count, $c1); michael@0: push(@cum_count, $c2); michael@0: $flat_total += $c1; michael@0: $cum_total += $c2; michael@0: } michael@0: michael@0: # Print header with total counts michael@0: printf("ROUTINE ====================== %s\n" . michael@0: "%6s %6s %s (flat, cumulative) %.1f%% of total\n", michael@0: ShortFunctionName($routine), michael@0: Unparse($flat_total), michael@0: Unparse($cum_total), michael@0: Units(), michael@0: ($cum_total * 100.0) / $total); michael@0: michael@0: # Process instructions in order michael@0: my $current_file = ""; michael@0: for (my $i = 0; $i <= $#instructions; ) { michael@0: my $e = $instructions[$i]; michael@0: michael@0: # Print the new file name whenever we switch files michael@0: if ($e->[1] ne $current_file) { michael@0: $current_file = $e->[1]; michael@0: my $fname = $current_file; michael@0: $fname =~ s|^\./||; # Trim leading "./" michael@0: michael@0: # Shorten long file names michael@0: if (length($fname) >= 58) { michael@0: $fname = "..." . substr($fname, -55); michael@0: } michael@0: printf("-------------------- %s\n", $fname); michael@0: } michael@0: michael@0: # TODO: Compute range of lines to print together to deal with michael@0: # small reorderings. michael@0: my $first_line = $e->[2]; michael@0: my $last_line = $first_line; michael@0: my %flat_sum = (); michael@0: my %cum_sum = (); michael@0: for (my $l = $first_line; $l <= $last_line; $l++) { michael@0: $flat_sum{$l} = 0; michael@0: $cum_sum{$l} = 0; michael@0: } michael@0: michael@0: # Find run of instructions for this range of source lines michael@0: my $first_inst = $i; michael@0: while (($i <= $#instructions) && michael@0: ($instructions[$i]->[2] >= $first_line) && michael@0: ($instructions[$i]->[2] <= $last_line)) { michael@0: $e = $instructions[$i]; michael@0: $flat_sum{$e->[2]} += $flat_count[$i]; michael@0: $cum_sum{$e->[2]} += $cum_count[$i]; michael@0: $i++; michael@0: } michael@0: my $last_inst = $i - 1; michael@0: michael@0: # Print source lines michael@0: for (my $l = $first_line; $l <= $last_line; $l++) { michael@0: my $line = SourceLine($current_file, $l); michael@0: if (!defined($line)) { michael@0: $line = "?\n"; michael@0: next; michael@0: } else { michael@0: $line =~ s/^\s+//; michael@0: } michael@0: printf("%6s %6s %5d: %s", michael@0: UnparseAlt($flat_sum{$l}), michael@0: UnparseAlt($cum_sum{$l}), michael@0: $l, michael@0: $line); michael@0: } michael@0: michael@0: # Print disassembly michael@0: for (my $x = $first_inst; $x <= $last_inst; $x++) { michael@0: my $e = $instructions[$x]; michael@0: printf("%6s %6s %8s: %6s\n", michael@0: UnparseAlt($flat_count[$x]), michael@0: UnparseAlt($cum_count[$x]), michael@0: UnparseAddress($offset, $e->[0]), michael@0: CleanDisassembly($e->[3])); michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Print DOT graph michael@0: sub PrintDot { michael@0: my $prog = shift; michael@0: my $symbols = shift; michael@0: my $raw = shift; michael@0: my $flat = shift; michael@0: my $cumulative = shift; michael@0: my $overall_total = shift; michael@0: michael@0: # Get total michael@0: my $local_total = TotalProfile($flat); michael@0: my $nodelimit = int($main::opt_nodefraction * $local_total); michael@0: my $edgelimit = int($main::opt_edgefraction * $local_total); michael@0: my $nodecount = $main::opt_nodecount; michael@0: michael@0: # Find nodes to include michael@0: my @list = (sort { abs(GetEntry($cumulative, $b)) <=> michael@0: abs(GetEntry($cumulative, $a)) michael@0: || $a cmp $b } michael@0: keys(%{$cumulative})); michael@0: my $last = $nodecount - 1; michael@0: if ($last > $#list) { michael@0: $last = $#list; michael@0: } michael@0: while (($last >= 0) && michael@0: (abs(GetEntry($cumulative, $list[$last])) <= $nodelimit)) { michael@0: $last--; michael@0: } michael@0: if ($last < 0) { michael@0: print STDERR "No nodes to print\n"; michael@0: return 0; michael@0: } michael@0: michael@0: if ($nodelimit > 0 || $edgelimit > 0) { michael@0: printf STDERR ("Dropping nodes with <= %s %s; edges with <= %s abs(%s)\n", michael@0: Unparse($nodelimit), Units(), michael@0: Unparse($edgelimit), Units()); michael@0: } michael@0: michael@0: # Open DOT output file michael@0: my $output; michael@0: my $escaped_dot = ShellEscape(@DOT); michael@0: my $escaped_ps2pdf = ShellEscape(@PS2PDF); michael@0: if ($main::opt_gv) { michael@0: my $escaped_outfile = ShellEscape(TempName($main::next_tmpfile, "ps")); michael@0: $output = "| $escaped_dot -Tps2 >$escaped_outfile"; michael@0: } elsif ($main::opt_evince) { michael@0: my $escaped_outfile = ShellEscape(TempName($main::next_tmpfile, "pdf")); michael@0: $output = "| $escaped_dot -Tps2 | $escaped_ps2pdf - $escaped_outfile"; michael@0: } elsif ($main::opt_ps) { michael@0: $output = "| $escaped_dot -Tps2"; michael@0: } elsif ($main::opt_pdf) { michael@0: $output = "| $escaped_dot -Tps2 | $escaped_ps2pdf - -"; michael@0: } elsif ($main::opt_web || $main::opt_svg) { michael@0: # We need to post-process the SVG, so write to a temporary file always. michael@0: my $escaped_outfile = ShellEscape(TempName($main::next_tmpfile, "svg")); michael@0: $output = "| $escaped_dot -Tsvg >$escaped_outfile"; michael@0: } elsif ($main::opt_gif) { michael@0: $output = "| $escaped_dot -Tgif"; michael@0: } else { michael@0: $output = ">&STDOUT"; michael@0: } michael@0: open(DOT, $output) || error("$output: $!\n"); michael@0: michael@0: # Title michael@0: printf DOT ("digraph \"%s; %s %s\" {\n", michael@0: $prog, michael@0: Unparse($overall_total), michael@0: Units()); michael@0: if ($main::opt_pdf) { michael@0: # The output is more printable if we set the page size for dot. michael@0: printf DOT ("size=\"8,11\"\n"); michael@0: } michael@0: printf DOT ("node [width=0.375,height=0.25];\n"); michael@0: michael@0: # Print legend michael@0: printf DOT ("Legend [shape=box,fontsize=24,shape=plaintext," . michael@0: "label=\"%s\\l%s\\l%s\\l%s\\l%s\\l\"];\n", michael@0: $prog, michael@0: sprintf("Total %s: %s", Units(), Unparse($overall_total)), michael@0: sprintf("Focusing on: %s", Unparse($local_total)), michael@0: sprintf("Dropped nodes with <= %s abs(%s)", michael@0: Unparse($nodelimit), Units()), michael@0: sprintf("Dropped edges with <= %s %s", michael@0: Unparse($edgelimit), Units()) michael@0: ); michael@0: michael@0: # Print nodes michael@0: my %node = (); michael@0: my $nextnode = 1; michael@0: foreach my $a (@list[0..$last]) { michael@0: # Pick font size michael@0: my $f = GetEntry($flat, $a); michael@0: my $c = GetEntry($cumulative, $a); michael@0: michael@0: my $fs = 8; michael@0: if ($local_total > 0) { michael@0: $fs = 8 + (50.0 * sqrt(abs($f * 1.0 / $local_total))); michael@0: } michael@0: michael@0: $node{$a} = $nextnode++; michael@0: my $sym = $a; michael@0: $sym =~ s/\s+/\\n/g; michael@0: $sym =~ s/::/\\n/g; michael@0: michael@0: # Extra cumulative info to print for non-leaves michael@0: my $extra = ""; michael@0: if ($f != $c) { michael@0: $extra = sprintf("\\rof %s (%s)", michael@0: Unparse($c), michael@0: Percent($c, $local_total)); michael@0: } michael@0: my $style = ""; michael@0: if ($main::opt_heapcheck) { michael@0: if ($f > 0) { michael@0: # make leak-causing nodes more visible (add a background) michael@0: $style = ",style=filled,fillcolor=gray" michael@0: } elsif ($f < 0) { michael@0: # make anti-leak-causing nodes (which almost never occur) michael@0: # stand out as well (triple border) michael@0: $style = ",peripheries=3" michael@0: } michael@0: } michael@0: michael@0: printf DOT ("N%d [label=\"%s\\n%s (%s)%s\\r" . michael@0: "\",shape=box,fontsize=%.1f%s];\n", michael@0: $node{$a}, michael@0: $sym, michael@0: Unparse($f), michael@0: Percent($f, $local_total), michael@0: $extra, michael@0: $fs, michael@0: $style, michael@0: ); michael@0: } michael@0: michael@0: # Get edges and counts per edge michael@0: my %edge = (); michael@0: my $n; michael@0: my $fullname_to_shortname_map = {}; michael@0: FillFullnameToShortnameMap($symbols, $fullname_to_shortname_map); michael@0: foreach my $k (keys(%{$raw})) { michael@0: # TODO: omit low %age edges michael@0: $n = $raw->{$k}; michael@0: my @translated = TranslateStack($symbols, $fullname_to_shortname_map, $k); michael@0: for (my $i = 1; $i <= $#translated; $i++) { michael@0: my $src = $translated[$i]; michael@0: my $dst = $translated[$i-1]; michael@0: #next if ($src eq $dst); # Avoid self-edges? michael@0: if (exists($node{$src}) && exists($node{$dst})) { michael@0: my $edge_label = "$src\001$dst"; michael@0: if (!exists($edge{$edge_label})) { michael@0: $edge{$edge_label} = 0; michael@0: } michael@0: $edge{$edge_label} += $n; michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Print edges (process in order of decreasing counts) michael@0: my %indegree = (); # Number of incoming edges added per node so far michael@0: my %outdegree = (); # Number of outgoing edges added per node so far michael@0: foreach my $e (sort { $edge{$b} <=> $edge{$a} } keys(%edge)) { michael@0: my @x = split(/\001/, $e); michael@0: $n = $edge{$e}; michael@0: michael@0: # Initialize degree of kept incoming and outgoing edges if necessary michael@0: my $src = $x[0]; michael@0: my $dst = $x[1]; michael@0: if (!exists($outdegree{$src})) { $outdegree{$src} = 0; } michael@0: if (!exists($indegree{$dst})) { $indegree{$dst} = 0; } michael@0: michael@0: my $keep; michael@0: if ($indegree{$dst} == 0) { michael@0: # Keep edge if needed for reachability michael@0: $keep = 1; michael@0: } elsif (abs($n) <= $edgelimit) { michael@0: # Drop if we are below --edgefraction michael@0: $keep = 0; michael@0: } elsif ($outdegree{$src} >= $main::opt_maxdegree || michael@0: $indegree{$dst} >= $main::opt_maxdegree) { michael@0: # Keep limited number of in/out edges per node michael@0: $keep = 0; michael@0: } else { michael@0: $keep = 1; michael@0: } michael@0: michael@0: if ($keep) { michael@0: $outdegree{$src}++; michael@0: $indegree{$dst}++; michael@0: michael@0: # Compute line width based on edge count michael@0: my $fraction = abs($local_total ? (3 * ($n / $local_total)) : 0); michael@0: if ($fraction > 1) { $fraction = 1; } michael@0: my $w = $fraction * 2; michael@0: if ($w < 1 && ($main::opt_web || $main::opt_svg)) { michael@0: # SVG output treats line widths < 1 poorly. michael@0: $w = 1; michael@0: } michael@0: michael@0: # Dot sometimes segfaults if given edge weights that are too large, so michael@0: # we cap the weights at a large value michael@0: my $edgeweight = abs($n) ** 0.7; michael@0: if ($edgeweight > 100000) { $edgeweight = 100000; } michael@0: $edgeweight = int($edgeweight); michael@0: michael@0: my $style = sprintf("setlinewidth(%f)", $w); michael@0: if ($x[1] =~ m/\(inline\)/) { michael@0: $style .= ",dashed"; michael@0: } michael@0: michael@0: # Use a slightly squashed function of the edge count as the weight michael@0: printf DOT ("N%s -> N%s [label=%s, weight=%d, style=\"%s\"];\n", michael@0: $node{$x[0]}, michael@0: $node{$x[1]}, michael@0: Unparse($n), michael@0: $edgeweight, michael@0: $style); michael@0: } michael@0: } michael@0: michael@0: print DOT ("}\n"); michael@0: close(DOT); michael@0: michael@0: if ($main::opt_web || $main::opt_svg) { michael@0: # Rewrite SVG to be more usable inside web browser. michael@0: RewriteSvg(TempName($main::next_tmpfile, "svg")); michael@0: } michael@0: michael@0: return 1; michael@0: } michael@0: michael@0: sub RewriteSvg { michael@0: my $svgfile = shift; michael@0: michael@0: open(SVG, $svgfile) || die "open temp svg: $!"; michael@0: my @svg = ; michael@0: close(SVG); michael@0: unlink $svgfile; michael@0: my $svg = join('', @svg); michael@0: michael@0: # Dot's SVG output is michael@0: # michael@0: # michael@0: # michael@0: # ... michael@0: # michael@0: # michael@0: # michael@0: # Change it to michael@0: # michael@0: # michael@0: # $svg_javascript michael@0: # michael@0: # michael@0: # ... michael@0: # michael@0: # michael@0: # michael@0: michael@0: # Fix width, height; drop viewBox. michael@0: $svg =~ s/(?s) above first michael@0: my $svg_javascript = SvgJavascript(); michael@0: my $viewport = "\n"; michael@0: $svg =~ s/ above . michael@0: $svg =~ s/(.*)(<\/svg>)/$1<\/g>$2/; michael@0: $svg =~ s/$svgfile") || die "open $svgfile: $!"; michael@0: print SVG $svg; michael@0: close(SVG); michael@0: } michael@0: } michael@0: michael@0: sub SvgJavascript { michael@0: return <<'EOF'; michael@0: michael@0: EOF michael@0: } michael@0: michael@0: # Provides a map from fullname to shortname for cases where the michael@0: # shortname is ambiguous. The symlist has both the fullname and michael@0: # shortname for all symbols, which is usually fine, but sometimes -- michael@0: # such as overloaded functions -- two different fullnames can map to michael@0: # the same shortname. In that case, we use the address of the michael@0: # function to disambiguate the two. This function fills in a map that michael@0: # maps fullnames to modified shortnames in such cases. If a fullname michael@0: # is not present in the map, the 'normal' shortname provided by the michael@0: # symlist is the appropriate one to use. michael@0: sub FillFullnameToShortnameMap { michael@0: my $symbols = shift; michael@0: my $fullname_to_shortname_map = shift; michael@0: my $shortnames_seen_once = {}; michael@0: my $shortnames_seen_more_than_once = {}; michael@0: michael@0: foreach my $symlist (values(%{$symbols})) { michael@0: # TODO(csilvers): deal with inlined symbols too. michael@0: my $shortname = $symlist->[0]; michael@0: my $fullname = $symlist->[2]; michael@0: if ($fullname !~ /<[0-9a-fA-F]+>$/) { # fullname doesn't end in an address michael@0: next; # the only collisions we care about are when addresses differ michael@0: } michael@0: if (defined($shortnames_seen_once->{$shortname}) && michael@0: $shortnames_seen_once->{$shortname} ne $fullname) { michael@0: $shortnames_seen_more_than_once->{$shortname} = 1; michael@0: } else { michael@0: $shortnames_seen_once->{$shortname} = $fullname; michael@0: } michael@0: } michael@0: michael@0: foreach my $symlist (values(%{$symbols})) { michael@0: my $shortname = $symlist->[0]; michael@0: my $fullname = $symlist->[2]; michael@0: # TODO(csilvers): take in a list of addresses we care about, and only michael@0: # store in the map if $symlist->[1] is in that list. Saves space. michael@0: next if defined($fullname_to_shortname_map->{$fullname}); michael@0: if (defined($shortnames_seen_more_than_once->{$shortname})) { michael@0: if ($fullname =~ /<0*([^>]*)>$/) { # fullname has address at end of it michael@0: $fullname_to_shortname_map->{$fullname} = "$shortname\@$1"; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Return a small number that identifies the argument. michael@0: # Multiple calls with the same argument will return the same number. michael@0: # Calls with different arguments will return different numbers. michael@0: sub ShortIdFor { michael@0: my $key = shift; michael@0: my $id = $main::uniqueid{$key}; michael@0: if (!defined($id)) { michael@0: $id = keys(%main::uniqueid) + 1; michael@0: $main::uniqueid{$key} = $id; michael@0: } michael@0: return $id; michael@0: } michael@0: michael@0: # Translate a stack of addresses into a stack of symbols michael@0: sub TranslateStack { michael@0: my $symbols = shift; michael@0: my $fullname_to_shortname_map = shift; michael@0: my $k = shift; michael@0: michael@0: my @addrs = split(/\n/, $k); michael@0: my @result = (); michael@0: for (my $i = 0; $i <= $#addrs; $i++) { michael@0: my $a = $addrs[$i]; michael@0: michael@0: # Skip large addresses since they sometimes show up as fake entries on RH9 michael@0: if (length($a) > 8 && $a gt "7fffffffffffffff") { michael@0: next; michael@0: } michael@0: michael@0: if ($main::opt_disasm || $main::opt_list) { michael@0: # We want just the address for the key michael@0: push(@result, $a); michael@0: next; michael@0: } michael@0: michael@0: my $symlist = $symbols->{$a}; michael@0: if (!defined($symlist)) { michael@0: $symlist = [$a, "", $a]; michael@0: } michael@0: michael@0: # We can have a sequence of symbols for a particular entry michael@0: # (more than one symbol in the case of inlining). Callers michael@0: # come before callees in symlist, so walk backwards since michael@0: # the translated stack should contain callees before callers. michael@0: for (my $j = $#{$symlist}; $j >= 2; $j -= 3) { michael@0: my $func = $symlist->[$j-2]; michael@0: my $fileline = $symlist->[$j-1]; michael@0: my $fullfunc = $symlist->[$j]; michael@0: if (defined($fullname_to_shortname_map->{$fullfunc})) { michael@0: $func = $fullname_to_shortname_map->{$fullfunc}; michael@0: } michael@0: if ($j > 2) { michael@0: $func = "$func (inline)"; michael@0: } michael@0: michael@0: # Do not merge nodes corresponding to Callback::Run since that michael@0: # causes confusing cycles in dot display. Instead, we synthesize michael@0: # a unique name for this frame per caller. michael@0: if ($func =~ m/Callback.*::Run$/) { michael@0: my $caller = ($i > 0) ? $addrs[$i-1] : 0; michael@0: $func = "Run#" . ShortIdFor($caller); michael@0: } michael@0: michael@0: if ($main::opt_addresses) { michael@0: push(@result, "$a $func $fileline"); michael@0: } elsif ($main::opt_lines) { michael@0: if ($func eq '??' && $fileline eq '??:0') { michael@0: push(@result, "$a"); michael@0: } else { michael@0: push(@result, "$func $fileline"); michael@0: } michael@0: } elsif ($main::opt_functions) { michael@0: if ($func eq '??') { michael@0: push(@result, "$a"); michael@0: } else { michael@0: push(@result, $func); michael@0: } michael@0: } elsif ($main::opt_files) { michael@0: if ($fileline eq '??:0' || $fileline eq '') { michael@0: push(@result, "$a"); michael@0: } else { michael@0: my $f = $fileline; michael@0: $f =~ s/:\d+$//; michael@0: push(@result, $f); michael@0: } michael@0: } else { michael@0: push(@result, $a); michael@0: last; # Do not print inlined info michael@0: } michael@0: } michael@0: } michael@0: michael@0: # print join(",", @addrs), " => ", join(",", @result), "\n"; michael@0: return @result; michael@0: } michael@0: michael@0: # Generate percent string for a number and a total michael@0: sub Percent { michael@0: my $num = shift; michael@0: my $tot = shift; michael@0: if ($tot != 0) { michael@0: return sprintf("%.1f%%", $num * 100.0 / $tot); michael@0: } else { michael@0: return ($num == 0) ? "nan" : (($num > 0) ? "+inf" : "-inf"); michael@0: } michael@0: } michael@0: michael@0: # Generate pretty-printed form of number michael@0: sub Unparse { michael@0: my $num = shift; michael@0: if ($main::profile_type eq 'heap' || $main::profile_type eq 'growth') { michael@0: if ($main::opt_inuse_objects || $main::opt_alloc_objects) { michael@0: return sprintf("%d", $num); michael@0: } else { michael@0: if ($main::opt_show_bytes) { michael@0: return sprintf("%d", $num); michael@0: } else { michael@0: return sprintf("%.1f", $num / 1048576.0); michael@0: } michael@0: } michael@0: } elsif ($main::profile_type eq 'contention' && !$main::opt_contentions) { michael@0: return sprintf("%.3f", $num / 1e9); # Convert nanoseconds to seconds michael@0: } else { michael@0: return sprintf("%d", $num); michael@0: } michael@0: } michael@0: michael@0: # Alternate pretty-printed form: 0 maps to "." michael@0: sub UnparseAlt { michael@0: my $num = shift; michael@0: if ($num == 0) { michael@0: return "."; michael@0: } else { michael@0: return Unparse($num); michael@0: } michael@0: } michael@0: michael@0: # Alternate pretty-printed form: 0 maps to "" michael@0: sub HtmlPrintNumber { michael@0: my $num = shift; michael@0: if ($num == 0) { michael@0: return ""; michael@0: } else { michael@0: return Unparse($num); michael@0: } michael@0: } michael@0: michael@0: # Return output units michael@0: sub Units { michael@0: if ($main::profile_type eq 'heap' || $main::profile_type eq 'growth') { michael@0: if ($main::opt_inuse_objects || $main::opt_alloc_objects) { michael@0: return "objects"; michael@0: } else { michael@0: if ($main::opt_show_bytes) { michael@0: return "B"; michael@0: } else { michael@0: return "MB"; michael@0: } michael@0: } michael@0: } elsif ($main::profile_type eq 'contention' && !$main::opt_contentions) { michael@0: return "seconds"; michael@0: } else { michael@0: return "samples"; michael@0: } michael@0: } michael@0: michael@0: ##### Profile manipulation code ##### michael@0: michael@0: # Generate flattened profile: michael@0: # If count is charged to stack [a,b,c,d], in generated profile, michael@0: # it will be charged to [a] michael@0: sub FlatProfile { michael@0: my $profile = shift; michael@0: my $result = {}; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: if ($#addrs >= 0) { michael@0: AddEntry($result, $addrs[0], $count); michael@0: } michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # Generate cumulative profile: michael@0: # If count is charged to stack [a,b,c,d], in generated profile, michael@0: # it will be charged to [a], [b], [c], [d] michael@0: sub CumulativeProfile { michael@0: my $profile = shift; michael@0: my $result = {}; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: foreach my $a (@addrs) { michael@0: AddEntry($result, $a, $count); michael@0: } michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # If the second-youngest PC on the stack is always the same, returns michael@0: # that pc. Otherwise, returns undef. michael@0: sub IsSecondPcAlwaysTheSame { michael@0: my $profile = shift; michael@0: michael@0: my $second_pc = undef; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my @addrs = split(/\n/, $k); michael@0: if ($#addrs < 1) { michael@0: return undef; michael@0: } michael@0: if (not defined $second_pc) { michael@0: $second_pc = $addrs[1]; michael@0: } else { michael@0: if ($second_pc ne $addrs[1]) { michael@0: return undef; michael@0: } michael@0: } michael@0: } michael@0: return $second_pc; michael@0: } michael@0: michael@0: sub ExtractSymbolLocation { michael@0: my $symbols = shift; michael@0: my $address = shift; michael@0: # 'addr2line' outputs "??:0" for unknown locations; we do the michael@0: # same to be consistent. michael@0: my $location = "??:0:unknown"; michael@0: if (exists $symbols->{$address}) { michael@0: my $file = $symbols->{$address}->[1]; michael@0: if ($file eq "?") { michael@0: $file = "??:0" michael@0: } michael@0: $location = $file . ":" . $symbols->{$address}->[0]; michael@0: } michael@0: return $location; michael@0: } michael@0: michael@0: # Extracts a graph of calls. michael@0: sub ExtractCalls { michael@0: my $symbols = shift; michael@0: my $profile = shift; michael@0: michael@0: my $calls = {}; michael@0: while( my ($stack_trace, $count) = each %$profile ) { michael@0: my @address = split(/\n/, $stack_trace); michael@0: my $destination = ExtractSymbolLocation($symbols, $address[0]); michael@0: AddEntry($calls, $destination, $count); michael@0: for (my $i = 1; $i <= $#address; $i++) { michael@0: my $source = ExtractSymbolLocation($symbols, $address[$i]); michael@0: my $call = "$source -> $destination"; michael@0: AddEntry($calls, $call, $count); michael@0: $destination = $source; michael@0: } michael@0: } michael@0: michael@0: return $calls; michael@0: } michael@0: michael@0: sub RemoveUninterestingFrames { michael@0: my $symbols = shift; michael@0: my $profile = shift; michael@0: michael@0: # List of function names to skip michael@0: my %skip = (); michael@0: my $skip_regexp = 'NOMATCH'; michael@0: if ($main::profile_type eq 'heap' || $main::profile_type eq 'growth') { michael@0: foreach my $name ('calloc', michael@0: 'cfree', michael@0: 'malloc', michael@0: 'free', michael@0: 'memalign', michael@0: 'posix_memalign', michael@0: 'pvalloc', michael@0: 'valloc', michael@0: 'realloc', michael@0: 'tc_calloc', michael@0: 'tc_cfree', michael@0: 'tc_malloc', michael@0: 'tc_free', michael@0: 'tc_memalign', michael@0: 'tc_posix_memalign', michael@0: 'tc_pvalloc', michael@0: 'tc_valloc', michael@0: 'tc_realloc', michael@0: 'tc_new', michael@0: 'tc_delete', michael@0: 'tc_newarray', michael@0: 'tc_deletearray', michael@0: 'tc_new_nothrow', michael@0: 'tc_newarray_nothrow', michael@0: 'do_malloc', michael@0: '::do_malloc', # new name -- got moved to an unnamed ns michael@0: '::do_malloc_or_cpp_alloc', michael@0: 'DoSampledAllocation', michael@0: 'simple_alloc::allocate', michael@0: '__malloc_alloc_template::allocate', michael@0: '__builtin_delete', michael@0: '__builtin_new', michael@0: '__builtin_vec_delete', michael@0: '__builtin_vec_new', michael@0: 'operator new', michael@0: 'operator new[]', michael@0: # The entry to our memory-allocation routines on OS X michael@0: 'malloc_zone_malloc', michael@0: 'malloc_zone_calloc', michael@0: 'malloc_zone_valloc', michael@0: 'malloc_zone_realloc', michael@0: 'malloc_zone_memalign', michael@0: 'malloc_zone_free', michael@0: # These mark the beginning/end of our custom sections michael@0: '__start_google_malloc', michael@0: '__stop_google_malloc', michael@0: '__start_malloc_hook', michael@0: '__stop_malloc_hook') { michael@0: $skip{$name} = 1; michael@0: $skip{"_" . $name} = 1; # Mach (OS X) adds a _ prefix to everything michael@0: } michael@0: # TODO: Remove TCMalloc once everything has been michael@0: # moved into the tcmalloc:: namespace and we have flushed michael@0: # old code out of the system. michael@0: $skip_regexp = "TCMalloc|^tcmalloc::"; michael@0: } elsif ($main::profile_type eq 'contention') { michael@0: foreach my $vname ('base::RecordLockProfileData', michael@0: 'base::SubmitMutexProfileData', michael@0: 'base::SubmitSpinLockProfileData', michael@0: 'Mutex::Unlock', michael@0: 'Mutex::UnlockSlow', michael@0: 'Mutex::ReaderUnlock', michael@0: 'MutexLock::~MutexLock', michael@0: 'SpinLock::Unlock', michael@0: 'SpinLock::SlowUnlock', michael@0: 'SpinLockHolder::~SpinLockHolder') { michael@0: $skip{$vname} = 1; michael@0: } michael@0: } elsif ($main::profile_type eq 'cpu') { michael@0: # Drop signal handlers used for CPU profile collection michael@0: # TODO(dpeng): this should not be necessary; it's taken michael@0: # care of by the general 2nd-pc mechanism below. michael@0: foreach my $name ('ProfileData::Add', # historical michael@0: 'ProfileData::prof_handler', # historical michael@0: 'CpuProfiler::prof_handler', michael@0: '__FRAME_END__', michael@0: '__pthread_sighandler', michael@0: '__restore') { michael@0: $skip{$name} = 1; michael@0: } michael@0: } else { michael@0: # Nothing skipped for unknown types michael@0: } michael@0: michael@0: if ($main::profile_type eq 'cpu') { michael@0: # If all the second-youngest program counters are the same, michael@0: # this STRONGLY suggests that it is an artifact of measurement, michael@0: # i.e., stack frames pushed by the CPU profiler signal handler. michael@0: # Hence, we delete them. michael@0: # (The topmost PC is read from the signal structure, not from michael@0: # the stack, so it does not get involved.) michael@0: while (my $second_pc = IsSecondPcAlwaysTheSame($profile)) { michael@0: my $result = {}; michael@0: my $func = ''; michael@0: if (exists($symbols->{$second_pc})) { michael@0: $second_pc = $symbols->{$second_pc}->[0]; michael@0: } michael@0: print STDERR "Removing $second_pc from all stack traces.\n"; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: splice @addrs, 1, 1; michael@0: my $reduced_path = join("\n", @addrs); michael@0: AddEntry($result, $reduced_path, $count); michael@0: } michael@0: $profile = $result; michael@0: } michael@0: } michael@0: michael@0: my $result = {}; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: my @path = (); michael@0: foreach my $a (@addrs) { michael@0: if (exists($symbols->{$a})) { michael@0: my $func = $symbols->{$a}->[0]; michael@0: if ($skip{$func} || ($func =~ m/$skip_regexp/)) { michael@0: next; michael@0: } michael@0: } michael@0: push(@path, $a); michael@0: } michael@0: my $reduced_path = join("\n", @path); michael@0: AddEntry($result, $reduced_path, $count); michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # Reduce profile to granularity given by user michael@0: sub ReduceProfile { michael@0: my $symbols = shift; michael@0: my $profile = shift; michael@0: my $result = {}; michael@0: my $fullname_to_shortname_map = {}; michael@0: FillFullnameToShortnameMap($symbols, $fullname_to_shortname_map); michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @translated = TranslateStack($symbols, $fullname_to_shortname_map, $k); michael@0: my @path = (); michael@0: my %seen = (); michael@0: $seen{''} = 1; # So that empty keys are skipped michael@0: foreach my $e (@translated) { michael@0: # To avoid double-counting due to recursion, skip a stack-trace michael@0: # entry if it has already been seen michael@0: if (!$seen{$e}) { michael@0: $seen{$e} = 1; michael@0: push(@path, $e); michael@0: } michael@0: } michael@0: my $reduced_path = join("\n", @path); michael@0: AddEntry($result, $reduced_path, $count); michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # Does the specified symbol array match the regexp? michael@0: sub SymbolMatches { michael@0: my $sym = shift; michael@0: my $re = shift; michael@0: if (defined($sym)) { michael@0: for (my $i = 0; $i < $#{$sym}; $i += 3) { michael@0: if ($sym->[$i] =~ m/$re/ || $sym->[$i+1] =~ m/$re/) { michael@0: return 1; michael@0: } michael@0: } michael@0: } michael@0: return 0; michael@0: } michael@0: michael@0: # Focus only on paths involving specified regexps michael@0: sub FocusProfile { michael@0: my $symbols = shift; michael@0: my $profile = shift; michael@0: my $focus = shift; michael@0: my $result = {}; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: foreach my $a (@addrs) { michael@0: # Reply if it matches either the address/shortname/fileline michael@0: if (($a =~ m/$focus/) || SymbolMatches($symbols->{$a}, $focus)) { michael@0: AddEntry($result, $k, $count); michael@0: last; michael@0: } michael@0: } michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # Focus only on paths not involving specified regexps michael@0: sub IgnoreProfile { michael@0: my $symbols = shift; michael@0: my $profile = shift; michael@0: my $ignore = shift; michael@0: my $result = {}; michael@0: foreach my $k (keys(%{$profile})) { michael@0: my $count = $profile->{$k}; michael@0: my @addrs = split(/\n/, $k); michael@0: my $matched = 0; michael@0: foreach my $a (@addrs) { michael@0: # Reply if it matches either the address/shortname/fileline michael@0: if (($a =~ m/$ignore/) || SymbolMatches($symbols->{$a}, $ignore)) { michael@0: $matched = 1; michael@0: last; michael@0: } michael@0: } michael@0: if (!$matched) { michael@0: AddEntry($result, $k, $count); michael@0: } michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # Get total count in profile michael@0: sub TotalProfile { michael@0: my $profile = shift; michael@0: my $result = 0; michael@0: foreach my $k (keys(%{$profile})) { michael@0: $result += $profile->{$k}; michael@0: } michael@0: return $result; michael@0: } michael@0: michael@0: # Add A to B michael@0: sub AddProfile { michael@0: my $A = shift; michael@0: my $B = shift; michael@0: michael@0: my $R = {}; michael@0: # add all keys in A michael@0: foreach my $k (keys(%{$A})) { michael@0: my $v = $A->{$k}; michael@0: AddEntry($R, $k, $v); michael@0: } michael@0: # add all keys in B michael@0: foreach my $k (keys(%{$B})) { michael@0: my $v = $B->{$k}; michael@0: AddEntry($R, $k, $v); michael@0: } michael@0: return $R; michael@0: } michael@0: michael@0: # Merges symbol maps michael@0: sub MergeSymbols { michael@0: my $A = shift; michael@0: my $B = shift; michael@0: michael@0: my $R = {}; michael@0: foreach my $k (keys(%{$A})) { michael@0: $R->{$k} = $A->{$k}; michael@0: } michael@0: if (defined($B)) { michael@0: foreach my $k (keys(%{$B})) { michael@0: $R->{$k} = $B->{$k}; michael@0: } michael@0: } michael@0: return $R; michael@0: } michael@0: michael@0: michael@0: # Add A to B michael@0: sub AddPcs { michael@0: my $A = shift; michael@0: my $B = shift; michael@0: michael@0: my $R = {}; michael@0: # add all keys in A michael@0: foreach my $k (keys(%{$A})) { michael@0: $R->{$k} = 1 michael@0: } michael@0: # add all keys in B michael@0: foreach my $k (keys(%{$B})) { michael@0: $R->{$k} = 1 michael@0: } michael@0: return $R; michael@0: } michael@0: michael@0: # Subtract B from A michael@0: sub SubtractProfile { michael@0: my $A = shift; michael@0: my $B = shift; michael@0: michael@0: my $R = {}; michael@0: foreach my $k (keys(%{$A})) { michael@0: my $v = $A->{$k} - GetEntry($B, $k); michael@0: if ($v < 0 && $main::opt_drop_negative) { michael@0: $v = 0; michael@0: } michael@0: AddEntry($R, $k, $v); michael@0: } michael@0: if (!$main::opt_drop_negative) { michael@0: # Take care of when subtracted profile has more entries michael@0: foreach my $k (keys(%{$B})) { michael@0: if (!exists($A->{$k})) { michael@0: AddEntry($R, $k, 0 - $B->{$k}); michael@0: } michael@0: } michael@0: } michael@0: return $R; michael@0: } michael@0: michael@0: # Get entry from profile; zero if not present michael@0: sub GetEntry { michael@0: my $profile = shift; michael@0: my $k = shift; michael@0: if (exists($profile->{$k})) { michael@0: return $profile->{$k}; michael@0: } else { michael@0: return 0; michael@0: } michael@0: } michael@0: michael@0: # Add entry to specified profile michael@0: sub AddEntry { michael@0: my $profile = shift; michael@0: my $k = shift; michael@0: my $n = shift; michael@0: if (!exists($profile->{$k})) { michael@0: $profile->{$k} = 0; michael@0: } michael@0: $profile->{$k} += $n; michael@0: } michael@0: michael@0: # Add a stack of entries to specified profile, and add them to the $pcs michael@0: # list. michael@0: sub AddEntries { michael@0: my $profile = shift; michael@0: my $pcs = shift; michael@0: my $stack = shift; michael@0: my $count = shift; michael@0: my @k = (); michael@0: michael@0: foreach my $e (split(/\s+/, $stack)) { michael@0: my $pc = HexExtend($e); michael@0: $pcs->{$pc} = 1; michael@0: push @k, $pc; michael@0: } michael@0: AddEntry($profile, (join "\n", @k), $count); michael@0: } michael@0: michael@0: ##### Code to profile a server dynamically ##### michael@0: michael@0: sub CheckSymbolPage { michael@0: my $url = SymbolPageURL(); michael@0: my $command = ShellEscape(@URL_FETCHER, $url); michael@0: open(SYMBOL, "$command |") or error($command); michael@0: my $line = ; michael@0: $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: close(SYMBOL); michael@0: unless (defined($line)) { michael@0: error("$url doesn't exist\n"); michael@0: } michael@0: michael@0: if ($line =~ /^num_symbols:\s+(\d+)$/) { michael@0: if ($1 == 0) { michael@0: error("Stripped binary. No symbols available.\n"); michael@0: } michael@0: } else { michael@0: error("Failed to get the number of symbols from $url\n"); michael@0: } michael@0: } michael@0: michael@0: sub IsProfileURL { michael@0: my $profile_name = shift; michael@0: if (-f $profile_name) { michael@0: printf STDERR "Using local file $profile_name.\n"; michael@0: return 0; michael@0: } michael@0: return 1; michael@0: } michael@0: michael@0: sub ParseProfileURL { michael@0: my $profile_name = shift; michael@0: michael@0: if (!defined($profile_name) || $profile_name eq "") { michael@0: return (); michael@0: } michael@0: michael@0: # Split profile URL - matches all non-empty strings, so no test. michael@0: $profile_name =~ m,^(https?://)?([^/]+)(.*?)(/|$PROFILES)?$,; michael@0: michael@0: my $proto = $1 || "http://"; michael@0: my $hostport = $2; michael@0: my $prefix = $3; michael@0: my $profile = $4 || "/"; michael@0: michael@0: my $host = $hostport; michael@0: $host =~ s/:.*//; michael@0: michael@0: my $baseurl = "$proto$hostport$prefix"; michael@0: return ($host, $baseurl, $profile); michael@0: } michael@0: michael@0: # We fetch symbols from the first profile argument. michael@0: sub SymbolPageURL { michael@0: my ($host, $baseURL, $path) = ParseProfileURL($main::pfile_args[0]); michael@0: return "$baseURL$SYMBOL_PAGE"; michael@0: } michael@0: michael@0: sub FetchProgramName() { michael@0: my ($host, $baseURL, $path) = ParseProfileURL($main::pfile_args[0]); michael@0: my $url = "$baseURL$PROGRAM_NAME_PAGE"; michael@0: my $command_line = ShellEscape(@URL_FETCHER, $url); michael@0: open(CMDLINE, "$command_line |") or error($command_line); michael@0: my $cmdline = ; michael@0: $cmdline =~ s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: close(CMDLINE); michael@0: error("Failed to get program name from $url\n") unless defined($cmdline); michael@0: $cmdline =~ s/\x00.+//; # Remove argv[1] and latters. michael@0: $cmdline =~ s!\n!!g; # Remove LFs. michael@0: return $cmdline; michael@0: } michael@0: michael@0: # Gee, curl's -L (--location) option isn't reliable at least michael@0: # with its 7.12.3 version. Curl will forget to post data if michael@0: # there is a redirection. This function is a workaround for michael@0: # curl. Redirection happens on borg hosts. michael@0: sub ResolveRedirectionForCurl { michael@0: my $url = shift; michael@0: my $command_line = ShellEscape(@URL_FETCHER, "--head", $url); michael@0: open(CMDLINE, "$command_line |") or error($command_line); michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: if (/^Location: (.*)/) { michael@0: $url = $1; michael@0: } michael@0: } michael@0: close(CMDLINE); michael@0: return $url; michael@0: } michael@0: michael@0: # Add a timeout flat to URL_FETCHER. Returns a new list. michael@0: sub AddFetchTimeout { michael@0: my $timeout = shift; michael@0: my @fetcher = shift; michael@0: if (defined($timeout)) { michael@0: if (join(" ", @fetcher) =~ m/\bcurl -s/) { michael@0: push(@fetcher, "--max-time", sprintf("%d", $timeout)); michael@0: } elsif (join(" ", @fetcher) =~ m/\brpcget\b/) { michael@0: push(@fetcher, sprintf("--deadline=%d", $timeout)); michael@0: } michael@0: } michael@0: return @fetcher; michael@0: } michael@0: michael@0: # Reads a symbol map from the file handle name given as $1, returning michael@0: # the resulting symbol map. Also processes variables relating to symbols. michael@0: # Currently, the only variable processed is 'binary=' which updates michael@0: # $main::prog to have the correct program name. michael@0: sub ReadSymbols { michael@0: my $in = shift; michael@0: my $map = {}; michael@0: while (<$in>) { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: # Removes all the leading zeroes from the symbols, see comment below. michael@0: if (m/^0x0*([0-9a-f]+)\s+(.+)/) { michael@0: $map->{$1} = $2; michael@0: } elsif (m/^---/) { michael@0: last; michael@0: } elsif (m/^([a-z][^=]*)=(.*)$/ ) { michael@0: my ($variable, $value) = ($1, $2); michael@0: for ($variable, $value) { michael@0: s/^\s+//; michael@0: s/\s+$//; michael@0: } michael@0: if ($variable eq "binary") { michael@0: if ($main::prog ne $UNKNOWN_BINARY && $main::prog ne $value) { michael@0: printf STDERR ("Warning: Mismatched binary name '%s', using '%s'.\n", michael@0: $main::prog, $value); michael@0: } michael@0: $main::prog = $value; michael@0: } else { michael@0: printf STDERR ("Ignoring unknown variable in symbols list: " . michael@0: "'%s' = '%s'\n", $variable, $value); michael@0: } michael@0: } michael@0: } michael@0: return $map; michael@0: } michael@0: michael@0: # Fetches and processes symbols to prepare them for use in the profile output michael@0: # code. If the optional 'symbol_map' arg is not given, fetches symbols from michael@0: # $SYMBOL_PAGE for all PC values found in profile. Otherwise, the raw symbols michael@0: # are assumed to have already been fetched into 'symbol_map' and are simply michael@0: # extracted and processed. michael@0: sub FetchSymbols { michael@0: my $pcset = shift; michael@0: my $symbol_map = shift; michael@0: michael@0: my %seen = (); michael@0: my @pcs = grep { !$seen{$_}++ } keys(%$pcset); # uniq michael@0: michael@0: if (!defined($symbol_map)) { michael@0: my $post_data = join("+", sort((map {"0x" . "$_"} @pcs))); michael@0: michael@0: open(POSTFILE, ">$main::tmpfile_sym"); michael@0: print POSTFILE $post_data; michael@0: close(POSTFILE); michael@0: michael@0: my $url = SymbolPageURL(); michael@0: michael@0: my $command_line; michael@0: if (join(" ", @URL_FETCHER) =~ m/\bcurl -s/) { michael@0: $url = ResolveRedirectionForCurl($url); michael@0: $command_line = ShellEscape(@URL_FETCHER, "-d", "\@$main::tmpfile_sym", michael@0: $url); michael@0: } else { michael@0: $command_line = (ShellEscape(@URL_FETCHER, "--post", $url) michael@0: . " < " . ShellEscape($main::tmpfile_sym)); michael@0: } michael@0: # We use c++filt in case $SYMBOL_PAGE gives us mangled symbols. michael@0: my $escaped_cppfilt = ShellEscape($obj_tool_map{"c++filt"}); michael@0: open(SYMBOL, "$command_line | $escaped_cppfilt |") or error($command_line); michael@0: $symbol_map = ReadSymbols(*SYMBOL{IO}); michael@0: close(SYMBOL); michael@0: } michael@0: michael@0: my $symbols = {}; michael@0: foreach my $pc (@pcs) { michael@0: my $fullname; michael@0: # For 64 bits binaries, symbols are extracted with 8 leading zeroes. michael@0: # Then /symbol reads the long symbols in as uint64, and outputs michael@0: # the result with a "0x%08llx" format which get rid of the zeroes. michael@0: # By removing all the leading zeroes in both $pc and the symbols from michael@0: # /symbol, the symbols match and are retrievable from the map. michael@0: my $shortpc = $pc; michael@0: $shortpc =~ s/^0*//; michael@0: # Each line may have a list of names, which includes the function michael@0: # and also other functions it has inlined. They are separated (in michael@0: # PrintSymbolizedProfile), by --, which is illegal in function names. michael@0: my $fullnames; michael@0: if (defined($symbol_map->{$shortpc})) { michael@0: $fullnames = $symbol_map->{$shortpc}; michael@0: } else { michael@0: $fullnames = "0x" . $pc; # Just use addresses michael@0: } michael@0: my $sym = []; michael@0: $symbols->{$pc} = $sym; michael@0: foreach my $fullname (split("--", $fullnames)) { michael@0: my $name = ShortFunctionName($fullname); michael@0: push(@{$sym}, $name, "?", $fullname); michael@0: } michael@0: } michael@0: return $symbols; michael@0: } michael@0: michael@0: sub BaseName { michael@0: my $file_name = shift; michael@0: $file_name =~ s!^.*/!!; # Remove directory name michael@0: return $file_name; michael@0: } michael@0: michael@0: sub MakeProfileBaseName { michael@0: my ($binary_name, $profile_name) = @_; michael@0: my ($host, $baseURL, $path) = ParseProfileURL($profile_name); michael@0: my $binary_shortname = BaseName($binary_name); michael@0: return sprintf("%s.%s.%s", michael@0: $binary_shortname, $main::op_time, $host); michael@0: } michael@0: michael@0: sub FetchDynamicProfile { michael@0: my $binary_name = shift; michael@0: my $profile_name = shift; michael@0: my $fetch_name_only = shift; michael@0: my $encourage_patience = shift; michael@0: michael@0: if (!IsProfileURL($profile_name)) { michael@0: return $profile_name; michael@0: } else { michael@0: my ($host, $baseURL, $path) = ParseProfileURL($profile_name); michael@0: if ($path eq "" || $path eq "/") { michael@0: # Missing type specifier defaults to cpu-profile michael@0: $path = $PROFILE_PAGE; michael@0: } michael@0: michael@0: my $profile_file = MakeProfileBaseName($binary_name, $profile_name); michael@0: michael@0: my $url = "$baseURL$path"; michael@0: my $fetch_timeout = undef; michael@0: if ($path =~ m/$PROFILE_PAGE|$PMUPROFILE_PAGE/) { michael@0: if ($path =~ m/[?]/) { michael@0: $url .= "&"; michael@0: } else { michael@0: $url .= "?"; michael@0: } michael@0: $url .= sprintf("seconds=%d", $main::opt_seconds); michael@0: $fetch_timeout = $main::opt_seconds * 1.01 + 60; michael@0: } else { michael@0: # For non-CPU profiles, we add a type-extension to michael@0: # the target profile file name. michael@0: my $suffix = $path; michael@0: $suffix =~ s,/,.,g; michael@0: $profile_file .= $suffix; michael@0: } michael@0: michael@0: my $profile_dir = $ENV{"PPROF_TMPDIR"} || ($ENV{HOME} . "/pprof"); michael@0: if (! -d $profile_dir) { michael@0: mkdir($profile_dir) michael@0: || die("Unable to create profile directory $profile_dir: $!\n"); michael@0: } michael@0: my $tmp_profile = "$profile_dir/.tmp.$profile_file"; michael@0: my $real_profile = "$profile_dir/$profile_file"; michael@0: michael@0: if ($fetch_name_only > 0) { michael@0: return $real_profile; michael@0: } michael@0: michael@0: my @fetcher = AddFetchTimeout($fetch_timeout, @URL_FETCHER); michael@0: my $cmd = ShellEscape(@fetcher, $url) . " > " . ShellEscape($tmp_profile); michael@0: if ($path =~ m/$PROFILE_PAGE|$PMUPROFILE_PAGE|$CENSUSPROFILE_PAGE/){ michael@0: print STDERR "Gathering CPU profile from $url for $main::opt_seconds seconds to\n ${real_profile}\n"; michael@0: if ($encourage_patience) { michael@0: print STDERR "Be patient...\n"; michael@0: } michael@0: } else { michael@0: print STDERR "Fetching $path profile from $url to\n ${real_profile}\n"; michael@0: } michael@0: michael@0: (system($cmd) == 0) || error("Failed to get profile: $cmd: $!\n"); michael@0: (system("mv", $tmp_profile, $real_profile) == 0) || error("Unable to rename profile\n"); michael@0: print STDERR "Wrote profile to $real_profile\n"; michael@0: $main::collected_profile = $real_profile; michael@0: return $main::collected_profile; michael@0: } michael@0: } michael@0: michael@0: # Collect profiles in parallel michael@0: sub FetchDynamicProfiles { michael@0: my $items = scalar(@main::pfile_args); michael@0: my $levels = log($items) / log(2); michael@0: michael@0: if ($items == 1) { michael@0: $main::profile_files[0] = FetchDynamicProfile($main::prog, $main::pfile_args[0], 0, 1); michael@0: } else { michael@0: # math rounding issues michael@0: if ((2 ** $levels) < $items) { michael@0: $levels++; michael@0: } michael@0: my $count = scalar(@main::pfile_args); michael@0: for (my $i = 0; $i < $count; $i++) { michael@0: $main::profile_files[$i] = FetchDynamicProfile($main::prog, $main::pfile_args[$i], 1, 0); michael@0: } michael@0: print STDERR "Fetching $count profiles, Be patient...\n"; michael@0: FetchDynamicProfilesRecurse($levels, 0, 0); michael@0: $main::collected_profile = join(" \\\n ", @main::profile_files); michael@0: } michael@0: } michael@0: michael@0: # Recursively fork a process to get enough processes michael@0: # collecting profiles michael@0: sub FetchDynamicProfilesRecurse { michael@0: my $maxlevel = shift; michael@0: my $level = shift; michael@0: my $position = shift; michael@0: michael@0: if (my $pid = fork()) { michael@0: $position = 0 | ($position << 1); michael@0: TryCollectProfile($maxlevel, $level, $position); michael@0: wait; michael@0: } else { michael@0: $position = 1 | ($position << 1); michael@0: TryCollectProfile($maxlevel, $level, $position); michael@0: cleanup(); michael@0: exit(0); michael@0: } michael@0: } michael@0: michael@0: # Collect a single profile michael@0: sub TryCollectProfile { michael@0: my $maxlevel = shift; michael@0: my $level = shift; michael@0: my $position = shift; michael@0: michael@0: if ($level >= ($maxlevel - 1)) { michael@0: if ($position < scalar(@main::pfile_args)) { michael@0: FetchDynamicProfile($main::prog, $main::pfile_args[$position], 0, 0); michael@0: } michael@0: } else { michael@0: FetchDynamicProfilesRecurse($maxlevel, $level+1, $position); michael@0: } michael@0: } michael@0: michael@0: ##### Parsing code ##### michael@0: michael@0: # Provide a small streaming-read module to handle very large michael@0: # cpu-profile files. Stream in chunks along a sliding window. michael@0: # Provides an interface to get one 'slot', correctly handling michael@0: # endian-ness differences. A slot is one 32-bit or 64-bit word michael@0: # (depending on the input profile). We tell endianness and bit-size michael@0: # for the profile by looking at the first 8 bytes: in cpu profiles, michael@0: # the second slot is always 3 (we'll accept anything that's not 0). michael@0: BEGIN { michael@0: package CpuProfileStream; michael@0: michael@0: sub new { michael@0: my ($class, $file, $fname) = @_; michael@0: my $self = { file => $file, michael@0: base => 0, michael@0: stride => 512 * 1024, # must be a multiple of bitsize/8 michael@0: slots => [], michael@0: unpack_code => "", # N for big-endian, V for little michael@0: perl_is_64bit => 1, # matters if profile is 64-bit michael@0: }; michael@0: bless $self, $class; michael@0: # Let unittests adjust the stride michael@0: if ($main::opt_test_stride > 0) { michael@0: $self->{stride} = $main::opt_test_stride; michael@0: } michael@0: # Read the first two slots to figure out bitsize and endianness. michael@0: my $slots = $self->{slots}; michael@0: my $str; michael@0: read($self->{file}, $str, 8); michael@0: # Set the global $address_length based on what we see here. michael@0: # 8 is 32-bit (8 hexadecimal chars); 16 is 64-bit (16 hexadecimal chars). michael@0: $address_length = ($str eq (chr(0)x8)) ? 16 : 8; michael@0: if ($address_length == 8) { michael@0: if (substr($str, 6, 2) eq chr(0)x2) { michael@0: $self->{unpack_code} = 'V'; # Little-endian. michael@0: } elsif (substr($str, 4, 2) eq chr(0)x2) { michael@0: $self->{unpack_code} = 'N'; # Big-endian michael@0: } else { michael@0: ::error("$fname: header size >= 2**16\n"); michael@0: } michael@0: @$slots = unpack($self->{unpack_code} . "*", $str); michael@0: } else { michael@0: # If we're a 64-bit profile, check if we're a 64-bit-capable michael@0: # perl. Otherwise, each slot will be represented as a float michael@0: # instead of an int64, losing precision and making all the michael@0: # 64-bit addresses wrong. We won't complain yet, but will michael@0: # later if we ever see a value that doesn't fit in 32 bits. michael@0: my $has_q = 0; michael@0: eval { $has_q = pack("Q", "1") ? 1 : 1; }; michael@0: if (!$has_q) { michael@0: $self->{perl_is_64bit} = 0; michael@0: } michael@0: read($self->{file}, $str, 8); michael@0: if (substr($str, 4, 4) eq chr(0)x4) { michael@0: # We'd love to use 'Q', but it's a) not universal, b) not endian-proof. michael@0: $self->{unpack_code} = 'V'; # Little-endian. michael@0: } elsif (substr($str, 0, 4) eq chr(0)x4) { michael@0: $self->{unpack_code} = 'N'; # Big-endian michael@0: } else { michael@0: ::error("$fname: header size >= 2**32\n"); michael@0: } michael@0: my @pair = unpack($self->{unpack_code} . "*", $str); michael@0: # Since we know one of the pair is 0, it's fine to just add them. michael@0: @$slots = (0, $pair[0] + $pair[1]); michael@0: } michael@0: return $self; michael@0: } michael@0: michael@0: # Load more data when we access slots->get(X) which is not yet in memory. michael@0: sub overflow { michael@0: my ($self) = @_; michael@0: my $slots = $self->{slots}; michael@0: $self->{base} += $#$slots + 1; # skip over data we're replacing michael@0: my $str; michael@0: read($self->{file}, $str, $self->{stride}); michael@0: if ($address_length == 8) { # the 32-bit case michael@0: # This is the easy case: unpack provides 32-bit unpacking primitives. michael@0: @$slots = unpack($self->{unpack_code} . "*", $str); michael@0: } else { michael@0: # We need to unpack 32 bits at a time and combine. michael@0: my @b32_values = unpack($self->{unpack_code} . "*", $str); michael@0: my @b64_values = (); michael@0: for (my $i = 0; $i < $#b32_values; $i += 2) { michael@0: # TODO(csilvers): if this is a 32-bit perl, the math below michael@0: # could end up in a too-large int, which perl will promote michael@0: # to a double, losing necessary precision. Deal with that. michael@0: # Right now, we just die. michael@0: my ($lo, $hi) = ($b32_values[$i], $b32_values[$i+1]); michael@0: if ($self->{unpack_code} eq 'N') { # big-endian michael@0: ($lo, $hi) = ($hi, $lo); michael@0: } michael@0: my $value = $lo + $hi * (2**32); michael@0: if (!$self->{perl_is_64bit} && # check value is exactly represented michael@0: (($value % (2**32)) != $lo || int($value / (2**32)) != $hi)) { michael@0: ::error("Need a 64-bit perl to process this 64-bit profile.\n"); michael@0: } michael@0: push(@b64_values, $value); michael@0: } michael@0: @$slots = @b64_values; michael@0: } michael@0: } michael@0: michael@0: # Access the i-th long in the file (logically), or -1 at EOF. michael@0: sub get { michael@0: my ($self, $idx) = @_; michael@0: my $slots = $self->{slots}; michael@0: while ($#$slots >= 0) { michael@0: if ($idx < $self->{base}) { michael@0: # The only time we expect a reference to $slots[$i - something] michael@0: # after referencing $slots[$i] is reading the very first header. michael@0: # Since $stride > |header|, that shouldn't cause any lookback michael@0: # errors. And everything after the header is sequential. michael@0: print STDERR "Unexpected look-back reading CPU profile"; michael@0: return -1; # shrug, don't know what better to return michael@0: } elsif ($idx > $self->{base} + $#$slots) { michael@0: $self->overflow(); michael@0: } else { michael@0: return $slots->[$idx - $self->{base}]; michael@0: } michael@0: } michael@0: # If we get here, $slots is [], which means we've reached EOF michael@0: return -1; # unique since slots is supposed to hold unsigned numbers michael@0: } michael@0: } michael@0: michael@0: # Reads the top, 'header' section of a profile, and returns the last michael@0: # line of the header, commonly called a 'header line'. The header michael@0: # section of a profile consists of zero or more 'command' lines that michael@0: # are instructions to pprof, which pprof executes when reading the michael@0: # header. All 'command' lines start with a %. After the command michael@0: # lines is the 'header line', which is a profile-specific line that michael@0: # indicates what type of profile it is, and perhaps other global michael@0: # information about the profile. For instance, here's a header line michael@0: # for a heap profile: michael@0: # heap profile: 53: 38236 [ 5525: 1284029] @ heapprofile michael@0: # For historical reasons, the CPU profile does not contain a text- michael@0: # readable header line. If the profile looks like a CPU profile, michael@0: # this function returns "". If no header line could be found, this michael@0: # function returns undef. michael@0: # michael@0: # The following commands are recognized: michael@0: # %warn -- emit the rest of this line to stderr, prefixed by 'WARNING:' michael@0: # michael@0: # The input file should be in binmode. michael@0: sub ReadProfileHeader { michael@0: local *PROFILE = shift; michael@0: my $firstchar = ""; michael@0: my $line = ""; michael@0: read(PROFILE, $firstchar, 1); michael@0: seek(PROFILE, -1, 1); # unread the firstchar michael@0: if ($firstchar !~ /[[:print:]]/) { # is not a text character michael@0: return ""; michael@0: } michael@0: while (defined($line = )) { michael@0: $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: if ($line =~ /^%warn\s+(.*)/) { # 'warn' command michael@0: # Note this matches both '%warn blah\n' and '%warn\n'. michael@0: print STDERR "WARNING: $1\n"; # print the rest of the line michael@0: } elsif ($line =~ /^%/) { michael@0: print STDERR "Ignoring unknown command from profile header: $line"; michael@0: } else { michael@0: # End of commands, must be the header line. michael@0: return $line; michael@0: } michael@0: } michael@0: return undef; # got to EOF without seeing a header line michael@0: } michael@0: michael@0: sub IsSymbolizedProfileFile { michael@0: my $file_name = shift; michael@0: if (!(-e $file_name) || !(-r $file_name)) { michael@0: return 0; michael@0: } michael@0: # Check if the file contains a symbol-section marker. michael@0: open(TFILE, "<$file_name"); michael@0: binmode TFILE; michael@0: my $firstline = ReadProfileHeader(*TFILE); michael@0: close(TFILE); michael@0: if (!$firstline) { michael@0: return 0; michael@0: } michael@0: $SYMBOL_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $symbol_marker = $&; michael@0: return $firstline =~ /^--- *$symbol_marker/; michael@0: } michael@0: michael@0: # Parse profile generated by common/profiler.cc and return a reference michael@0: # to a map: michael@0: # $result->{version} Version number of profile file michael@0: # $result->{period} Sampling period (in microseconds) michael@0: # $result->{profile} Profile object michael@0: # $result->{map} Memory map info from profile michael@0: # $result->{pcs} Hash of all PC values seen, key is hex address michael@0: sub ReadProfile { michael@0: my $prog = shift; michael@0: my $fname = shift; michael@0: my $result; # return value michael@0: michael@0: $CONTENTION_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $contention_marker = $&; michael@0: $GROWTH_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $growth_marker = $&; michael@0: $SYMBOL_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $symbol_marker = $&; michael@0: $PROFILE_PAGE =~ m,[^/]+$,; # matches everything after the last slash michael@0: my $profile_marker = $&; michael@0: michael@0: # Look at first line to see if it is a heap or a CPU profile. michael@0: # CPU profile may start with no header at all, and just binary data michael@0: # (starting with \0\0\0\0) -- in that case, don't try to read the michael@0: # whole firstline, since it may be gigabytes(!) of data. michael@0: open(PROFILE, "<$fname") || error("$fname: $!\n"); michael@0: binmode PROFILE; # New perls do UTF-8 processing michael@0: my $header = ReadProfileHeader(*PROFILE); michael@0: if (!defined($header)) { # means "at EOF" michael@0: error("Profile is empty.\n"); michael@0: } michael@0: michael@0: my $symbols; michael@0: if ($header =~ m/^--- *$symbol_marker/o) { michael@0: # Verify that the user asked for a symbolized profile michael@0: if (!$main::use_symbolized_profile) { michael@0: # we have both a binary and symbolized profiles, abort michael@0: error("FATAL ERROR: Symbolized profile\n $fname\ncannot be used with " . michael@0: "a binary arg. Try again without passing\n $prog\n"); michael@0: } michael@0: # Read the symbol section of the symbolized profile file. michael@0: $symbols = ReadSymbols(*PROFILE{IO}); michael@0: # Read the next line to get the header for the remaining profile. michael@0: $header = ReadProfileHeader(*PROFILE) || ""; michael@0: } michael@0: michael@0: $main::profile_type = ''; michael@0: if ($header =~ m/^heap profile:.*$growth_marker/o) { michael@0: $main::profile_type = 'growth'; michael@0: $result = ReadHeapProfile($prog, *PROFILE, $header); michael@0: } elsif ($header =~ m/^heap profile:/) { michael@0: $main::profile_type = 'heap'; michael@0: $result = ReadHeapProfile($prog, *PROFILE, $header); michael@0: } elsif ($header =~ m/^--- *$contention_marker/o) { michael@0: $main::profile_type = 'contention'; michael@0: $result = ReadSynchProfile($prog, *PROFILE); michael@0: } elsif ($header =~ m/^--- *Stacks:/) { michael@0: print STDERR michael@0: "Old format contention profile: mistakenly reports " . michael@0: "condition variable signals as lock contentions.\n"; michael@0: $main::profile_type = 'contention'; michael@0: $result = ReadSynchProfile($prog, *PROFILE); michael@0: } elsif ($header =~ m/^--- *$profile_marker/) { michael@0: # the binary cpu profile data starts immediately after this line michael@0: $main::profile_type = 'cpu'; michael@0: $result = ReadCPUProfile($prog, $fname, *PROFILE); michael@0: } else { michael@0: if (defined($symbols)) { michael@0: # a symbolized profile contains a format we don't recognize, bail out michael@0: error("$fname: Cannot recognize profile section after symbols.\n"); michael@0: } michael@0: # no ascii header present -- must be a CPU profile michael@0: $main::profile_type = 'cpu'; michael@0: $result = ReadCPUProfile($prog, $fname, *PROFILE); michael@0: } michael@0: michael@0: close(PROFILE); michael@0: michael@0: # if we got symbols along with the profile, return those as well michael@0: if (defined($symbols)) { michael@0: $result->{symbols} = $symbols; michael@0: } michael@0: michael@0: return $result; michael@0: } michael@0: michael@0: # Subtract one from caller pc so we map back to call instr. michael@0: # However, don't do this if we're reading a symbolized profile michael@0: # file, in which case the subtract-one was done when the file michael@0: # was written. michael@0: # michael@0: # We apply the same logic to all readers, though ReadCPUProfile uses an michael@0: # independent implementation. michael@0: sub FixCallerAddresses { michael@0: my $stack = shift; michael@0: if ($main::use_symbolized_profile) { michael@0: return $stack; michael@0: } else { michael@0: $stack =~ /(\s)/; michael@0: my $delimiter = $1; michael@0: my @addrs = split(' ', $stack); michael@0: my @fixedaddrs; michael@0: $#fixedaddrs = $#addrs; michael@0: if ($#addrs >= 0) { michael@0: $fixedaddrs[0] = $addrs[0]; michael@0: } michael@0: for (my $i = 1; $i <= $#addrs; $i++) { michael@0: $fixedaddrs[$i] = AddressSub($addrs[$i], "0x1"); michael@0: } michael@0: return join $delimiter, @fixedaddrs; michael@0: } michael@0: } michael@0: michael@0: # CPU profile reader michael@0: sub ReadCPUProfile { michael@0: my $prog = shift; michael@0: my $fname = shift; # just used for logging michael@0: local *PROFILE = shift; michael@0: my $version; michael@0: my $period; michael@0: my $i; michael@0: my $profile = {}; michael@0: my $pcs = {}; michael@0: michael@0: # Parse string into array of slots. michael@0: my $slots = CpuProfileStream->new(*PROFILE, $fname); michael@0: michael@0: # Read header. The current header version is a 5-element structure michael@0: # containing: michael@0: # 0: header count (always 0) michael@0: # 1: header "words" (after this one: 3) michael@0: # 2: format version (0) michael@0: # 3: sampling period (usec) michael@0: # 4: unused padding (always 0) michael@0: if ($slots->get(0) != 0 ) { michael@0: error("$fname: not a profile file, or old format profile file\n"); michael@0: } michael@0: $i = 2 + $slots->get(1); michael@0: $version = $slots->get(2); michael@0: $period = $slots->get(3); michael@0: # Do some sanity checking on these header values. michael@0: if ($version > (2**32) || $period > (2**32) || $i > (2**32) || $i < 5) { michael@0: error("$fname: not a profile file, or corrupted profile file\n"); michael@0: } michael@0: michael@0: # Parse profile michael@0: while ($slots->get($i) != -1) { michael@0: my $n = $slots->get($i++); michael@0: my $d = $slots->get($i++); michael@0: if ($d > (2**16)) { # TODO(csilvers): what's a reasonable max-stack-depth? michael@0: my $addr = sprintf("0%o", $i * ($address_length == 8 ? 4 : 8)); michael@0: print STDERR "At index $i (address $addr):\n"; michael@0: error("$fname: stack trace depth >= 2**32\n"); michael@0: } michael@0: if ($slots->get($i) == 0) { michael@0: # End of profile data marker michael@0: $i += $d; michael@0: last; michael@0: } michael@0: michael@0: # Make key out of the stack entries michael@0: my @k = (); michael@0: for (my $j = 0; $j < $d; $j++) { michael@0: my $pc = $slots->get($i+$j); michael@0: # Subtract one from caller pc so we map back to call instr. michael@0: # However, don't do this if we're reading a symbolized profile michael@0: # file, in which case the subtract-one was done when the file michael@0: # was written. michael@0: if ($j > 0 && !$main::use_symbolized_profile) { michael@0: $pc--; michael@0: } michael@0: $pc = sprintf("%0*x", $address_length, $pc); michael@0: $pcs->{$pc} = 1; michael@0: push @k, $pc; michael@0: } michael@0: michael@0: AddEntry($profile, (join "\n", @k), $n); michael@0: $i += $d; michael@0: } michael@0: michael@0: # Parse map michael@0: my $map = ''; michael@0: seek(PROFILE, $i * 4, 0); michael@0: read(PROFILE, $map, (stat PROFILE)[7]); michael@0: michael@0: my $r = {}; michael@0: $r->{version} = $version; michael@0: $r->{period} = $period; michael@0: $r->{profile} = $profile; michael@0: $r->{libs} = ParseLibraries($prog, $map, $pcs); michael@0: $r->{pcs} = $pcs; michael@0: michael@0: return $r; michael@0: } michael@0: michael@0: sub ReadHeapProfile { michael@0: my $prog = shift; michael@0: local *PROFILE = shift; michael@0: my $header = shift; michael@0: michael@0: my $index = 1; michael@0: if ($main::opt_inuse_space) { michael@0: $index = 1; michael@0: } elsif ($main::opt_inuse_objects) { michael@0: $index = 0; michael@0: } elsif ($main::opt_alloc_space) { michael@0: $index = 3; michael@0: } elsif ($main::opt_alloc_objects) { michael@0: $index = 2; michael@0: } michael@0: michael@0: # Find the type of this profile. The header line looks like: michael@0: # heap profile: 1246: 8800744 [ 1246: 8800744] @ /266053 michael@0: # There are two pairs , the first inuse objects/space, and the michael@0: # second allocated objects/space. This is followed optionally by a profile michael@0: # type, and if that is present, optionally by a sampling frequency. michael@0: # For remote heap profiles (v1): michael@0: # The interpretation of the sampling frequency is that the profiler, for michael@0: # each sample, calculates a uniformly distributed random integer less than michael@0: # the given value, and records the next sample after that many bytes have michael@0: # been allocated. Therefore, the expected sample interval is half of the michael@0: # given frequency. By default, if not specified, the expected sample michael@0: # interval is 128KB. Only remote-heap-page profiles are adjusted for michael@0: # sample size. michael@0: # For remote heap profiles (v2): michael@0: # The sampling frequency is the rate of a Poisson process. This means that michael@0: # the probability of sampling an allocation of size X with sampling rate Y michael@0: # is 1 - exp(-X/Y) michael@0: # For version 2, a typical header line might look like this: michael@0: # heap profile: 1922: 127792360 [ 1922: 127792360] @ _v2/524288 michael@0: # the trailing number (524288) is the sampling rate. (Version 1 showed michael@0: # double the 'rate' here) michael@0: my $sampling_algorithm = 0; michael@0: my $sample_adjustment = 0; michael@0: chomp($header); michael@0: my $type = "unknown"; michael@0: if ($header =~ m"^heap profile:\s*(\d+):\s+(\d+)\s+\[\s*(\d+):\s+(\d+)\](\s*@\s*([^/]*)(/(\d+))?)?") { michael@0: if (defined($6) && ($6 ne '')) { michael@0: $type = $6; michael@0: my $sample_period = $8; michael@0: # $type is "heapprofile" for profiles generated by the michael@0: # heap-profiler, and either "heap" or "heap_v2" for profiles michael@0: # generated by sampling directly within tcmalloc. It can also michael@0: # be "growth" for heap-growth profiles. The first is typically michael@0: # found for profiles generated locally, and the others for michael@0: # remote profiles. michael@0: if (($type eq "heapprofile") || ($type !~ /heap/) ) { michael@0: # No need to adjust for the sampling rate with heap-profiler-derived data michael@0: $sampling_algorithm = 0; michael@0: } elsif ($type =~ /_v2/) { michael@0: $sampling_algorithm = 2; # version 2 sampling michael@0: if (defined($sample_period) && ($sample_period ne '')) { michael@0: $sample_adjustment = int($sample_period); michael@0: } michael@0: } else { michael@0: $sampling_algorithm = 1; # version 1 sampling michael@0: if (defined($sample_period) && ($sample_period ne '')) { michael@0: $sample_adjustment = int($sample_period)/2; michael@0: } michael@0: } michael@0: } else { michael@0: # We detect whether or not this is a remote-heap profile by checking michael@0: # that the total-allocated stats ($n2,$s2) are exactly the michael@0: # same as the in-use stats ($n1,$s1). It is remotely conceivable michael@0: # that a non-remote-heap profile may pass this check, but it is hard michael@0: # to imagine how that could happen. michael@0: # In this case it's so old it's guaranteed to be remote-heap version 1. michael@0: my ($n1, $s1, $n2, $s2) = ($1, $2, $3, $4); michael@0: if (($n1 == $n2) && ($s1 == $s2)) { michael@0: # This is likely to be a remote-heap based sample profile michael@0: $sampling_algorithm = 1; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if ($sampling_algorithm > 0) { michael@0: # For remote-heap generated profiles, adjust the counts and sizes to michael@0: # account for the sample rate (we sample once every 128KB by default). michael@0: if ($sample_adjustment == 0) { michael@0: # Turn on profile adjustment. michael@0: $sample_adjustment = 128*1024; michael@0: print STDERR "Adjusting heap profiles for 1-in-128KB sampling rate\n"; michael@0: } else { michael@0: printf STDERR ("Adjusting heap profiles for 1-in-%d sampling rate\n", michael@0: $sample_adjustment); michael@0: } michael@0: if ($sampling_algorithm > 1) { michael@0: # We don't bother printing anything for the original version (version 1) michael@0: printf STDERR "Heap version $sampling_algorithm\n"; michael@0: } michael@0: } michael@0: michael@0: my $profile = {}; michael@0: my $pcs = {}; michael@0: my $map = ""; michael@0: michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: if (/^MAPPED_LIBRARIES:/) { michael@0: # Read the /proc/self/maps data michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: $map .= $_; michael@0: } michael@0: last; michael@0: } michael@0: michael@0: if (/^--- Memory map:/) { michael@0: # Read /proc/self/maps data as formatted by DumpAddressMap() michael@0: my $buildvar = ""; michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: # Parse "build=" specification if supplied michael@0: if (m/^\s*build=(.*)\n/) { michael@0: $buildvar = $1; michael@0: } michael@0: michael@0: # Expand "$build" variable if available michael@0: $_ =~ s/\$build\b/$buildvar/g; michael@0: michael@0: $map .= $_; michael@0: } michael@0: last; michael@0: } michael@0: michael@0: # Read entry of the form: michael@0: # : [: ] @ a1 a2 a3 ... an michael@0: s/^\s*//; michael@0: s/\s*$//; michael@0: if (m/^\s*(\d+):\s+(\d+)\s+\[\s*(\d+):\s+(\d+)\]\s+@\s+(.*)$/) { michael@0: my $stack = $5; michael@0: my ($n1, $s1, $n2, $s2) = ($1, $2, $3, $4); michael@0: michael@0: if ($sample_adjustment) { michael@0: if ($sampling_algorithm == 2) { michael@0: # Remote-heap version 2 michael@0: # The sampling frequency is the rate of a Poisson process. michael@0: # This means that the probability of sampling an allocation of michael@0: # size X with sampling rate Y is 1 - exp(-X/Y) michael@0: if ($n1 != 0) { michael@0: my $ratio = (($s1*1.0)/$n1)/($sample_adjustment); michael@0: my $scale_factor = 1/(1 - exp(-$ratio)); michael@0: $n1 *= $scale_factor; michael@0: $s1 *= $scale_factor; michael@0: } michael@0: if ($n2 != 0) { michael@0: my $ratio = (($s2*1.0)/$n2)/($sample_adjustment); michael@0: my $scale_factor = 1/(1 - exp(-$ratio)); michael@0: $n2 *= $scale_factor; michael@0: $s2 *= $scale_factor; michael@0: } michael@0: } else { michael@0: # Remote-heap version 1 michael@0: my $ratio; michael@0: $ratio = (($s1*1.0)/$n1)/($sample_adjustment); michael@0: if ($ratio < 1) { michael@0: $n1 /= $ratio; michael@0: $s1 /= $ratio; michael@0: } michael@0: $ratio = (($s2*1.0)/$n2)/($sample_adjustment); michael@0: if ($ratio < 1) { michael@0: $n2 /= $ratio; michael@0: $s2 /= $ratio; michael@0: } michael@0: } michael@0: } michael@0: michael@0: my @counts = ($n1, $s1, $n2, $s2); michael@0: AddEntries($profile, $pcs, FixCallerAddresses($stack), $counts[$index]); michael@0: } michael@0: } michael@0: michael@0: my $r = {}; michael@0: $r->{version} = "heap"; michael@0: $r->{period} = 1; michael@0: $r->{profile} = $profile; michael@0: $r->{libs} = ParseLibraries($prog, $map, $pcs); michael@0: $r->{pcs} = $pcs; michael@0: return $r; michael@0: } michael@0: michael@0: sub ReadSynchProfile { michael@0: my $prog = shift; michael@0: local *PROFILE = shift; michael@0: my $header = shift; michael@0: michael@0: my $map = ''; michael@0: my $profile = {}; michael@0: my $pcs = {}; michael@0: my $sampling_period = 1; michael@0: my $cyclespernanosec = 2.8; # Default assumption for old binaries michael@0: my $seen_clockrate = 0; michael@0: my $line; michael@0: michael@0: my $index = 0; michael@0: if ($main::opt_total_delay) { michael@0: $index = 0; michael@0: } elsif ($main::opt_contentions) { michael@0: $index = 1; michael@0: } elsif ($main::opt_mean_delay) { michael@0: $index = 2; michael@0: } michael@0: michael@0: while ( $line = ) { michael@0: $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: if ( $line =~ /^\s*(\d+)\s+(\d+) \@\s*(.*?)\s*$/ ) { michael@0: my ($cycles, $count, $stack) = ($1, $2, $3); michael@0: michael@0: # Convert cycles to nanoseconds michael@0: $cycles /= $cyclespernanosec; michael@0: michael@0: # Adjust for sampling done by application michael@0: $cycles *= $sampling_period; michael@0: $count *= $sampling_period; michael@0: michael@0: my @values = ($cycles, $count, $cycles / $count); michael@0: AddEntries($profile, $pcs, FixCallerAddresses($stack), $values[$index]); michael@0: michael@0: } elsif ( $line =~ /^(slow release).*thread \d+ \@\s*(.*?)\s*$/ || michael@0: $line =~ /^\s*(\d+) \@\s*(.*?)\s*$/ ) { michael@0: my ($cycles, $stack) = ($1, $2); michael@0: if ($cycles !~ /^\d+$/) { michael@0: next; michael@0: } michael@0: michael@0: # Convert cycles to nanoseconds michael@0: $cycles /= $cyclespernanosec; michael@0: michael@0: # Adjust for sampling done by application michael@0: $cycles *= $sampling_period; michael@0: michael@0: AddEntries($profile, $pcs, FixCallerAddresses($stack), $cycles); michael@0: michael@0: } elsif ( $line =~ m/^([a-z][^=]*)=(.*)$/ ) { michael@0: my ($variable, $value) = ($1,$2); michael@0: for ($variable, $value) { michael@0: s/^\s+//; michael@0: s/\s+$//; michael@0: } michael@0: if ($variable eq "cycles/second") { michael@0: $cyclespernanosec = $value / 1e9; michael@0: $seen_clockrate = 1; michael@0: } elsif ($variable eq "sampling period") { michael@0: $sampling_period = $value; michael@0: } elsif ($variable eq "ms since reset") { michael@0: # Currently nothing is done with this value in pprof michael@0: # So we just silently ignore it for now michael@0: } elsif ($variable eq "discarded samples") { michael@0: # Currently nothing is done with this value in pprof michael@0: # So we just silently ignore it for now michael@0: } else { michael@0: printf STDERR ("Ignoring unnknown variable in /contention output: " . michael@0: "'%s' = '%s'\n",$variable,$value); michael@0: } michael@0: } else { michael@0: # Memory map entry michael@0: $map .= $line; michael@0: } michael@0: } michael@0: michael@0: if (!$seen_clockrate) { michael@0: printf STDERR ("No cycles/second entry in profile; Guessing %.1f GHz\n", michael@0: $cyclespernanosec); michael@0: } michael@0: michael@0: my $r = {}; michael@0: $r->{version} = 0; michael@0: $r->{period} = $sampling_period; michael@0: $r->{profile} = $profile; michael@0: $r->{libs} = ParseLibraries($prog, $map, $pcs); michael@0: $r->{pcs} = $pcs; michael@0: return $r; michael@0: } michael@0: michael@0: # Given a hex value in the form "0x1abcd" or "1abcd", return either michael@0: # "0001abcd" or "000000000001abcd", depending on the current (global) michael@0: # address length. michael@0: sub HexExtend { michael@0: my $addr = shift; michael@0: michael@0: $addr =~ s/^(0x)?0*//; michael@0: my $zeros_needed = $address_length - length($addr); michael@0: if ($zeros_needed < 0) { michael@0: printf STDERR "Warning: address $addr is longer than address length $address_length\n"; michael@0: return $addr; michael@0: } michael@0: return ("0" x $zeros_needed) . $addr; michael@0: } michael@0: michael@0: ##### Symbol extraction ##### michael@0: michael@0: # Aggressively search the lib_prefix values for the given library michael@0: # If all else fails, just return the name of the library unmodified. michael@0: # If the lib_prefix is "/my/path,/other/path" and $file is "/lib/dir/mylib.so" michael@0: # it will search the following locations in this order, until it finds a file: michael@0: # /my/path/lib/dir/mylib.so michael@0: # /other/path/lib/dir/mylib.so michael@0: # /my/path/dir/mylib.so michael@0: # /other/path/dir/mylib.so michael@0: # /my/path/mylib.so michael@0: # /other/path/mylib.so michael@0: # /lib/dir/mylib.so (returned as last resort) michael@0: sub FindLibrary { michael@0: my $file = shift; michael@0: my $suffix = $file; michael@0: michael@0: # Search for the library as described above michael@0: do { michael@0: foreach my $prefix (@prefix_list) { michael@0: my $fullpath = $prefix . $suffix; michael@0: if (-e $fullpath) { michael@0: return $fullpath; michael@0: } michael@0: } michael@0: } while ($suffix =~ s|^/[^/]+/|/|); michael@0: return $file; michael@0: } michael@0: michael@0: # Return path to library with debugging symbols. michael@0: # For libc libraries, the copy in /usr/lib/debug contains debugging symbols michael@0: sub DebuggingLibrary { michael@0: my $file = shift; michael@0: if ($file =~ m|^/| && -f "/usr/lib/debug$file") { michael@0: return "/usr/lib/debug$file"; michael@0: } michael@0: return undef; michael@0: } michael@0: michael@0: # Parse text section header of a library using objdump michael@0: sub ParseTextSectionHeaderFromObjdump { michael@0: my $lib = shift; michael@0: michael@0: my $size = undef; michael@0: my $vma; michael@0: my $file_offset; michael@0: # Get objdump output from the library file to figure out how to michael@0: # map between mapped addresses and addresses in the library. michael@0: my $cmd = ShellEscape($obj_tool_map{"objdump"}, "-h", $lib); michael@0: open(OBJDUMP, "$cmd |") || error("$cmd: $!\n"); michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: # Idx Name Size VMA LMA File off Algn michael@0: # 10 .text 00104b2c 420156f0 420156f0 000156f0 2**4 michael@0: # For 64-bit objects, VMA and LMA will be 16 hex digits, size and file michael@0: # offset may still be 8. But AddressSub below will still handle that. michael@0: my @x = split; michael@0: if (($#x >= 6) && ($x[1] eq '.text')) { michael@0: $size = $x[2]; michael@0: $vma = $x[3]; michael@0: $file_offset = $x[5]; michael@0: last; michael@0: } michael@0: } michael@0: close(OBJDUMP); michael@0: michael@0: if (!defined($size)) { michael@0: return undef; michael@0: } michael@0: michael@0: my $r = {}; michael@0: $r->{size} = $size; michael@0: $r->{vma} = $vma; michael@0: $r->{file_offset} = $file_offset; michael@0: michael@0: return $r; michael@0: } michael@0: michael@0: # Parse text section header of a library using otool (on OS X) michael@0: sub ParseTextSectionHeaderFromOtool { michael@0: my $lib = shift; michael@0: michael@0: my $size = undef; michael@0: my $vma = undef; michael@0: my $file_offset = undef; michael@0: # Get otool output from the library file to figure out how to michael@0: # map between mapped addresses and addresses in the library. michael@0: my $command = ShellEscape($obj_tool_map{"otool"}, "-l", $lib); michael@0: open(OTOOL, "$command |") || error("$command: $!\n"); michael@0: my $cmd = ""; michael@0: my $sectname = ""; michael@0: my $segname = ""; michael@0: foreach my $line () { michael@0: $line =~ s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: # Load command <#> michael@0: # cmd LC_SEGMENT michael@0: # [...] michael@0: # Section michael@0: # sectname __text michael@0: # segname __TEXT michael@0: # addr 0x000009f8 michael@0: # size 0x00018b9e michael@0: # offset 2552 michael@0: # align 2^2 (4) michael@0: # We will need to strip off the leading 0x from the hex addresses, michael@0: # and convert the offset into hex. michael@0: if ($line =~ /Load command/) { michael@0: $cmd = ""; michael@0: $sectname = ""; michael@0: $segname = ""; michael@0: } elsif ($line =~ /Section/) { michael@0: $sectname = ""; michael@0: $segname = ""; michael@0: } elsif ($line =~ /cmd (\w+)/) { michael@0: $cmd = $1; michael@0: } elsif ($line =~ /sectname (\w+)/) { michael@0: $sectname = $1; michael@0: } elsif ($line =~ /segname (\w+)/) { michael@0: $segname = $1; michael@0: } elsif (!(($cmd eq "LC_SEGMENT" || $cmd eq "LC_SEGMENT_64") && michael@0: $sectname eq "__text" && michael@0: $segname eq "__TEXT")) { michael@0: next; michael@0: } elsif ($line =~ /\baddr 0x([0-9a-fA-F]+)/) { michael@0: $vma = $1; michael@0: } elsif ($line =~ /\bsize 0x([0-9a-fA-F]+)/) { michael@0: $size = $1; michael@0: } elsif ($line =~ /\boffset ([0-9]+)/) { michael@0: $file_offset = sprintf("%016x", $1); michael@0: } michael@0: if (defined($vma) && defined($size) && defined($file_offset)) { michael@0: last; michael@0: } michael@0: } michael@0: close(OTOOL); michael@0: michael@0: if (!defined($vma) || !defined($size) || !defined($file_offset)) { michael@0: return undef; michael@0: } michael@0: michael@0: my $r = {}; michael@0: $r->{size} = $size; michael@0: $r->{vma} = $vma; michael@0: $r->{file_offset} = $file_offset; michael@0: michael@0: return $r; michael@0: } michael@0: michael@0: sub ParseTextSectionHeader { michael@0: # obj_tool_map("otool") is only defined if we're in a Mach-O environment michael@0: if (defined($obj_tool_map{"otool"})) { michael@0: my $r = ParseTextSectionHeaderFromOtool(@_); michael@0: if (defined($r)){ michael@0: return $r; michael@0: } michael@0: } michael@0: # If otool doesn't work, or we don't have it, fall back to objdump michael@0: return ParseTextSectionHeaderFromObjdump(@_); michael@0: } michael@0: michael@0: # Split /proc/pid/maps dump into a list of libraries michael@0: sub ParseLibraries { michael@0: return if $main::use_symbol_page; # We don't need libraries info. michael@0: my $prog = shift; michael@0: my $map = shift; michael@0: my $pcs = shift; michael@0: michael@0: my $result = []; michael@0: my $h = "[a-f0-9]+"; michael@0: my $zero_offset = HexExtend("0"); michael@0: michael@0: my $buildvar = ""; michael@0: foreach my $l (split("\n", $map)) { michael@0: if ($l =~ m/^\s*build=(.*)$/) { michael@0: $buildvar = $1; michael@0: } michael@0: michael@0: my $start; michael@0: my $finish; michael@0: my $offset; michael@0: my $lib; michael@0: if ($l =~ /^($h)-($h)\s+..x.\s+($h)\s+\S+:\S+\s+\d+\s+(\S+\.(so|dll|dylib|bundle)((\.\d+)+\w*(\.\d+){0,3})?)$/i) { michael@0: # Full line from /proc/self/maps. Example: michael@0: # 40000000-40015000 r-xp 00000000 03:01 12845071 /lib/ld-2.3.2.so michael@0: $start = HexExtend($1); michael@0: $finish = HexExtend($2); michael@0: $offset = HexExtend($3); michael@0: $lib = $4; michael@0: $lib =~ s|\\|/|g; # turn windows-style paths into unix-style paths michael@0: } elsif ($l =~ /^\s*($h)-($h):\s*(\S+\.so(\.\d+)*)/) { michael@0: # Cooked line from DumpAddressMap. Example: michael@0: # 40000000-40015000: /lib/ld-2.3.2.so michael@0: $start = HexExtend($1); michael@0: $finish = HexExtend($2); michael@0: $offset = $zero_offset; michael@0: $lib = $3; michael@0: } else { michael@0: next; michael@0: } michael@0: michael@0: # Expand "$build" variable if available michael@0: $lib =~ s/\$build\b/$buildvar/g; michael@0: michael@0: $lib = FindLibrary($lib); michael@0: michael@0: # Check for pre-relocated libraries, which use pre-relocated symbol tables michael@0: # and thus require adjusting the offset that we'll use to translate michael@0: # VM addresses into symbol table addresses. michael@0: # Only do this if we're not going to fetch the symbol table from a michael@0: # debugging copy of the library. michael@0: if (!DebuggingLibrary($lib)) { michael@0: my $text = ParseTextSectionHeader($lib); michael@0: if (defined($text)) { michael@0: my $vma_offset = AddressSub($text->{vma}, $text->{file_offset}); michael@0: $offset = AddressAdd($offset, $vma_offset); michael@0: } michael@0: } michael@0: michael@0: push(@{$result}, [$lib, $start, $finish, $offset]); michael@0: } michael@0: michael@0: # Append special entry for additional library (not relocated) michael@0: if ($main::opt_lib ne "") { michael@0: my $text = ParseTextSectionHeader($main::opt_lib); michael@0: if (defined($text)) { michael@0: my $start = $text->{vma}; michael@0: my $finish = AddressAdd($start, $text->{size}); michael@0: michael@0: push(@{$result}, [$main::opt_lib, $start, $finish, $start]); michael@0: } michael@0: } michael@0: michael@0: # Append special entry for the main program. This covers michael@0: # 0..max_pc_value_seen, so that we assume pc values not found in one michael@0: # of the library ranges will be treated as coming from the main michael@0: # program binary. michael@0: my $min_pc = HexExtend("0"); michael@0: my $max_pc = $min_pc; # find the maximal PC value in any sample michael@0: foreach my $pc (keys(%{$pcs})) { michael@0: if (HexExtend($pc) gt $max_pc) { $max_pc = HexExtend($pc); } michael@0: } michael@0: push(@{$result}, [$prog, $min_pc, $max_pc, $zero_offset]); michael@0: michael@0: return $result; michael@0: } michael@0: michael@0: # Add two hex addresses of length $address_length. michael@0: # Run pprof --test for unit test if this is changed. michael@0: sub AddressAdd { michael@0: my $addr1 = shift; michael@0: my $addr2 = shift; michael@0: my $sum; michael@0: michael@0: if ($address_length == 8) { michael@0: # Perl doesn't cope with wraparound arithmetic, so do it explicitly: michael@0: $sum = (hex($addr1)+hex($addr2)) % (0x10000000 * 16); michael@0: return sprintf("%08x", $sum); michael@0: michael@0: } else { michael@0: # Do the addition in 7-nibble chunks to trivialize carry handling. michael@0: michael@0: if ($main::opt_debug and $main::opt_test) { michael@0: print STDERR "AddressAdd $addr1 + $addr2 = "; michael@0: } michael@0: michael@0: my $a1 = substr($addr1,-7); michael@0: $addr1 = substr($addr1,0,-7); michael@0: my $a2 = substr($addr2,-7); michael@0: $addr2 = substr($addr2,0,-7); michael@0: $sum = hex($a1) + hex($a2); michael@0: my $c = 0; michael@0: if ($sum > 0xfffffff) { michael@0: $c = 1; michael@0: $sum -= 0x10000000; michael@0: } michael@0: my $r = sprintf("%07x", $sum); michael@0: michael@0: $a1 = substr($addr1,-7); michael@0: $addr1 = substr($addr1,0,-7); michael@0: $a2 = substr($addr2,-7); michael@0: $addr2 = substr($addr2,0,-7); michael@0: $sum = hex($a1) + hex($a2) + $c; michael@0: $c = 0; michael@0: if ($sum > 0xfffffff) { michael@0: $c = 1; michael@0: $sum -= 0x10000000; michael@0: } michael@0: $r = sprintf("%07x", $sum) . $r; michael@0: michael@0: $sum = hex($addr1) + hex($addr2) + $c; michael@0: if ($sum > 0xff) { $sum -= 0x100; } michael@0: $r = sprintf("%02x", $sum) . $r; michael@0: michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "$r\n"; } michael@0: michael@0: return $r; michael@0: } michael@0: } michael@0: michael@0: michael@0: # Subtract two hex addresses of length $address_length. michael@0: # Run pprof --test for unit test if this is changed. michael@0: sub AddressSub { michael@0: my $addr1 = shift; michael@0: my $addr2 = shift; michael@0: my $diff; michael@0: michael@0: if ($address_length == 8) { michael@0: # Perl doesn't cope with wraparound arithmetic, so do it explicitly: michael@0: $diff = (hex($addr1)-hex($addr2)) % (0x10000000 * 16); michael@0: return sprintf("%08x", $diff); michael@0: michael@0: } else { michael@0: # Do the addition in 7-nibble chunks to trivialize borrow handling. michael@0: # if ($main::opt_debug) { print STDERR "AddressSub $addr1 - $addr2 = "; } michael@0: michael@0: my $a1 = hex(substr($addr1,-7)); michael@0: $addr1 = substr($addr1,0,-7); michael@0: my $a2 = hex(substr($addr2,-7)); michael@0: $addr2 = substr($addr2,0,-7); michael@0: my $b = 0; michael@0: if ($a2 > $a1) { michael@0: $b = 1; michael@0: $a1 += 0x10000000; michael@0: } michael@0: $diff = $a1 - $a2; michael@0: my $r = sprintf("%07x", $diff); michael@0: michael@0: $a1 = hex(substr($addr1,-7)); michael@0: $addr1 = substr($addr1,0,-7); michael@0: $a2 = hex(substr($addr2,-7)) + $b; michael@0: $addr2 = substr($addr2,0,-7); michael@0: $b = 0; michael@0: if ($a2 > $a1) { michael@0: $b = 1; michael@0: $a1 += 0x10000000; michael@0: } michael@0: $diff = $a1 - $a2; michael@0: $r = sprintf("%07x", $diff) . $r; michael@0: michael@0: $a1 = hex($addr1); michael@0: $a2 = hex($addr2) + $b; michael@0: if ($a2 > $a1) { $a1 += 0x100; } michael@0: $diff = $a1 - $a2; michael@0: $r = sprintf("%02x", $diff) . $r; michael@0: michael@0: # if ($main::opt_debug) { print STDERR "$r\n"; } michael@0: michael@0: return $r; michael@0: } michael@0: } michael@0: michael@0: # Increment a hex addresses of length $address_length. michael@0: # Run pprof --test for unit test if this is changed. michael@0: sub AddressInc { michael@0: my $addr = shift; michael@0: my $sum; michael@0: michael@0: if ($address_length == 8) { michael@0: # Perl doesn't cope with wraparound arithmetic, so do it explicitly: michael@0: $sum = (hex($addr)+1) % (0x10000000 * 16); michael@0: return sprintf("%08x", $sum); michael@0: michael@0: } else { michael@0: # Do the addition in 7-nibble chunks to trivialize carry handling. michael@0: # We are always doing this to step through the addresses in a function, michael@0: # and will almost never overflow the first chunk, so we check for this michael@0: # case and exit early. michael@0: michael@0: # if ($main::opt_debug) { print STDERR "AddressInc $addr1 = "; } michael@0: michael@0: my $a1 = substr($addr,-7); michael@0: $addr = substr($addr,0,-7); michael@0: $sum = hex($a1) + 1; michael@0: my $r = sprintf("%07x", $sum); michael@0: if ($sum <= 0xfffffff) { michael@0: $r = $addr . $r; michael@0: # if ($main::opt_debug) { print STDERR "$r\n"; } michael@0: return HexExtend($r); michael@0: } else { michael@0: $r = "0000000"; michael@0: } michael@0: michael@0: $a1 = substr($addr,-7); michael@0: $addr = substr($addr,0,-7); michael@0: $sum = hex($a1) + 1; michael@0: $r = sprintf("%07x", $sum) . $r; michael@0: if ($sum <= 0xfffffff) { michael@0: $r = $addr . $r; michael@0: # if ($main::opt_debug) { print STDERR "$r\n"; } michael@0: return HexExtend($r); michael@0: } else { michael@0: $r = "00000000000000"; michael@0: } michael@0: michael@0: $sum = hex($addr) + 1; michael@0: if ($sum > 0xff) { $sum -= 0x100; } michael@0: $r = sprintf("%02x", $sum) . $r; michael@0: michael@0: # if ($main::opt_debug) { print STDERR "$r\n"; } michael@0: return $r; michael@0: } michael@0: } michael@0: michael@0: # Extract symbols for all PC values found in profile michael@0: sub ExtractSymbols { michael@0: my $libs = shift; michael@0: my $pcset = shift; michael@0: michael@0: my $symbols = {}; michael@0: michael@0: # Map each PC value to the containing library. To make this faster, michael@0: # we sort libraries by their starting pc value (highest first), and michael@0: # advance through the libraries as we advance the pc. Sometimes the michael@0: # addresses of libraries may overlap with the addresses of the main michael@0: # binary, so to make sure the libraries 'win', we iterate over the michael@0: # libraries in reverse order (which assumes the binary doesn't start michael@0: # in the middle of a library, which seems a fair assumption). michael@0: my @pcs = (sort { $a cmp $b } keys(%{$pcset})); # pcset is 0-extended strings michael@0: foreach my $lib (sort {$b->[1] cmp $a->[1]} @{$libs}) { michael@0: my $libname = $lib->[0]; michael@0: my $start = $lib->[1]; michael@0: my $finish = $lib->[2]; michael@0: my $offset = $lib->[3]; michael@0: michael@0: # Get list of pcs that belong in this library. michael@0: my $contained = []; michael@0: my ($start_pc_index, $finish_pc_index); michael@0: # Find smallest finish_pc_index such that $finish < $pc[$finish_pc_index]. michael@0: for ($finish_pc_index = $#pcs + 1; $finish_pc_index > 0; michael@0: $finish_pc_index--) { michael@0: last if $pcs[$finish_pc_index - 1] le $finish; michael@0: } michael@0: # Find smallest start_pc_index such that $start <= $pc[$start_pc_index]. michael@0: for ($start_pc_index = $finish_pc_index; $start_pc_index > 0; michael@0: $start_pc_index--) { michael@0: last if $pcs[$start_pc_index - 1] lt $start; michael@0: } michael@0: # This keeps PC values higher than $pc[$finish_pc_index] in @pcs, michael@0: # in case there are overlaps in libraries and the main binary. michael@0: @{$contained} = splice(@pcs, $start_pc_index, michael@0: $finish_pc_index - $start_pc_index); michael@0: # Map to symbols michael@0: MapToSymbols($libname, AddressSub($start, $offset), $contained, $symbols); michael@0: } michael@0: michael@0: return $symbols; michael@0: } michael@0: michael@0: # Map list of PC values to symbols for a given image michael@0: sub MapToSymbols { michael@0: my $image = shift; michael@0: my $offset = shift; michael@0: my $pclist = shift; michael@0: my $symbols = shift; michael@0: michael@0: my $debug = 0; michael@0: michael@0: # Ignore empty binaries michael@0: if ($#{$pclist} < 0) { return; } michael@0: michael@0: # Figure out the addr2line command to use michael@0: my $addr2line = $obj_tool_map{"addr2line"}; michael@0: my $cmd = ShellEscape($addr2line, "-f", "-C", "-e", $image); michael@0: if (exists $obj_tool_map{"addr2line_pdb"}) { michael@0: $addr2line = $obj_tool_map{"addr2line_pdb"}; michael@0: $cmd = ShellEscape($addr2line, "--demangle", "-f", "-C", "-e", $image); michael@0: } michael@0: michael@0: # If "addr2line" isn't installed on the system at all, just use michael@0: # nm to get what info we can (function names, but not line numbers). michael@0: if (system(ShellEscape($addr2line, "--help") . " >$dev_null 2>&1") != 0) { michael@0: MapSymbolsWithNM($image, $offset, $pclist, $symbols); michael@0: return; michael@0: } michael@0: michael@0: # "addr2line -i" can produce a variable number of lines per input michael@0: # address, with no separator that allows us to tell when data for michael@0: # the next address starts. So we find the address for a special michael@0: # symbol (_fini) and interleave this address between all real michael@0: # addresses passed to addr2line. The name of this special symbol michael@0: # can then be used as a separator. michael@0: $sep_address = undef; # May be filled in by MapSymbolsWithNM() michael@0: my $nm_symbols = {}; michael@0: MapSymbolsWithNM($image, $offset, $pclist, $nm_symbols); michael@0: if (defined($sep_address)) { michael@0: # Only add " -i" to addr2line if the binary supports it. michael@0: # addr2line --help returns 0, but not if it sees an unknown flag first. michael@0: if (system("$cmd -i --help >$dev_null 2>&1") == 0) { michael@0: $cmd .= " -i"; michael@0: } else { michael@0: $sep_address = undef; # no need for sep_address if we don't support -i michael@0: } michael@0: } michael@0: michael@0: # Make file with all PC values with intervening 'sep_address' so michael@0: # that we can reliably detect the end of inlined function list michael@0: open(ADDRESSES, ">$main::tmpfile_sym") || error("$main::tmpfile_sym: $!\n"); michael@0: if ($debug) { print("---- $image ---\n"); } michael@0: for (my $i = 0; $i <= $#{$pclist}; $i++) { michael@0: # addr2line always reads hex addresses, and does not need '0x' prefix. michael@0: if ($debug) { printf STDERR ("%s\n", $pclist->[$i]); } michael@0: printf ADDRESSES ("%s\n", AddressSub($pclist->[$i], $offset)); michael@0: if (defined($sep_address)) { michael@0: printf ADDRESSES ("%s\n", $sep_address); michael@0: } michael@0: } michael@0: close(ADDRESSES); michael@0: if ($debug) { michael@0: print("----\n"); michael@0: system("cat", $main::tmpfile_sym); michael@0: print("----\n"); michael@0: system("$cmd < " . ShellEscape($main::tmpfile_sym)); michael@0: print("----\n"); michael@0: } michael@0: michael@0: open(SYMBOLS, "$cmd <" . ShellEscape($main::tmpfile_sym) . " |") michael@0: || error("$cmd: $!\n"); michael@0: my $count = 0; # Index in pclist michael@0: while () { michael@0: # Read fullfunction and filelineinfo from next pair of lines michael@0: s/\r?\n$//g; michael@0: my $fullfunction = $_; michael@0: $_ = ; michael@0: s/\r?\n$//g; michael@0: my $filelinenum = $_; michael@0: michael@0: if (defined($sep_address) && $fullfunction eq $sep_symbol) { michael@0: # Terminating marker for data for this address michael@0: $count++; michael@0: next; michael@0: } michael@0: michael@0: $filelinenum =~ s|\\|/|g; # turn windows-style paths into unix-style paths michael@0: michael@0: my $pcstr = $pclist->[$count]; michael@0: my $function = ShortFunctionName($fullfunction); michael@0: my $nms = $nm_symbols->{$pcstr}; michael@0: if (defined($nms)) { michael@0: if ($fullfunction eq '??') { michael@0: # nm found a symbol for us. michael@0: $function = $nms->[0]; michael@0: $fullfunction = $nms->[2]; michael@0: } else { michael@0: # MapSymbolsWithNM tags each routine with its starting address, michael@0: # useful in case the image has multiple occurrences of this michael@0: # routine. (It uses a syntax that resembles template paramters, michael@0: # that are automatically stripped out by ShortFunctionName().) michael@0: # addr2line does not provide the same information. So we check michael@0: # if nm disambiguated our symbol, and if so take the annotated michael@0: # (nm) version of the routine-name. TODO(csilvers): this won't michael@0: # catch overloaded, inlined symbols, which nm doesn't see. michael@0: # Better would be to do a check similar to nm's, in this fn. michael@0: if ($nms->[2] =~ m/^\Q$function\E/) { # sanity check it's the right fn michael@0: $function = $nms->[0]; michael@0: $fullfunction = $nms->[2]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: # Prepend to accumulated symbols for pcstr michael@0: # (so that caller comes before callee) michael@0: my $sym = $symbols->{$pcstr}; michael@0: if (!defined($sym)) { michael@0: $sym = []; michael@0: $symbols->{$pcstr} = $sym; michael@0: } michael@0: unshift(@{$sym}, $function, $filelinenum, $fullfunction); michael@0: if ($debug) { printf STDERR ("%s => [%s]\n", $pcstr, join(" ", @{$sym})); } michael@0: if (!defined($sep_address)) { michael@0: # Inlining is off, so this entry ends immediately michael@0: $count++; michael@0: } michael@0: } michael@0: close(SYMBOLS); michael@0: } michael@0: michael@0: # Use nm to map the list of referenced PCs to symbols. Return true iff we michael@0: # are able to read procedure information via nm. michael@0: sub MapSymbolsWithNM { michael@0: my $image = shift; michael@0: my $offset = shift; michael@0: my $pclist = shift; michael@0: my $symbols = shift; michael@0: michael@0: # Get nm output sorted by increasing address michael@0: my $symbol_table = GetProcedureBoundaries($image, "."); michael@0: if (!%{$symbol_table}) { michael@0: return 0; michael@0: } michael@0: # Start addresses are already the right length (8 or 16 hex digits). michael@0: my @names = sort { $symbol_table->{$a}->[0] cmp $symbol_table->{$b}->[0] } michael@0: keys(%{$symbol_table}); michael@0: michael@0: if ($#names < 0) { michael@0: # No symbols: just use addresses michael@0: foreach my $pc (@{$pclist}) { michael@0: my $pcstr = "0x" . $pc; michael@0: $symbols->{$pc} = [$pcstr, "?", $pcstr]; michael@0: } michael@0: return 0; michael@0: } michael@0: michael@0: # Sort addresses so we can do a join against nm output michael@0: my $index = 0; michael@0: my $fullname = $names[0]; michael@0: my $name = ShortFunctionName($fullname); michael@0: foreach my $pc (sort { $a cmp $b } @{$pclist}) { michael@0: # Adjust for mapped offset michael@0: my $mpc = AddressSub($pc, $offset); michael@0: while (($index < $#names) && ($mpc ge $symbol_table->{$fullname}->[1])){ michael@0: $index++; michael@0: $fullname = $names[$index]; michael@0: $name = ShortFunctionName($fullname); michael@0: } michael@0: if ($mpc lt $symbol_table->{$fullname}->[1]) { michael@0: $symbols->{$pc} = [$name, "?", $fullname]; michael@0: } else { michael@0: my $pcstr = "0x" . $pc; michael@0: $symbols->{$pc} = [$pcstr, "?", $pcstr]; michael@0: } michael@0: } michael@0: return 1; michael@0: } michael@0: michael@0: sub ShortFunctionName { michael@0: my $function = shift; michael@0: while ($function =~ s/\([^()]*\)(\s*const)?//g) { } # Argument types michael@0: while ($function =~ s/<[^<>]*>//g) { } # Remove template arguments michael@0: $function =~ s/^.*\s+(\w+::)/$1/; # Remove leading type michael@0: return $function; michael@0: } michael@0: michael@0: # Trim overly long symbols found in disassembler output michael@0: sub CleanDisassembly { michael@0: my $d = shift; michael@0: while ($d =~ s/\([^()%]*\)(\s*const)?//g) { } # Argument types, not (%rax) michael@0: while ($d =~ s/(\w+)<[^<>]*>/$1/g) { } # Remove template arguments michael@0: return $d; michael@0: } michael@0: michael@0: # Clean file name for display michael@0: sub CleanFileName { michael@0: my ($f) = @_; michael@0: $f =~ s|^/proc/self/cwd/||; michael@0: $f =~ s|^\./||; michael@0: return $f; michael@0: } michael@0: michael@0: # Make address relative to section and clean up for display michael@0: sub UnparseAddress { michael@0: my ($offset, $address) = @_; michael@0: $address = AddressSub($address, $offset); michael@0: $address =~ s/^0x//; michael@0: $address =~ s/^0*//; michael@0: return $address; michael@0: } michael@0: michael@0: ##### Miscellaneous ##### michael@0: michael@0: # Find the right versions of the above object tools to use. The michael@0: # argument is the program file being analyzed, and should be an ELF michael@0: # 32-bit or ELF 64-bit executable file. The location of the tools michael@0: # is determined by considering the following options in this order: michael@0: # 1) --tools option, if set michael@0: # 2) PPROF_TOOLS environment variable, if set michael@0: # 3) the environment michael@0: sub ConfigureObjTools { michael@0: my $prog_file = shift; michael@0: michael@0: # Check for the existence of $prog_file because /usr/bin/file does not michael@0: # predictably return error status in prod. michael@0: (-e $prog_file) || error("$prog_file does not exist.\n"); michael@0: michael@0: my $file_type = undef; michael@0: if (-e "/usr/bin/file") { michael@0: # Follow symlinks (at least for systems where "file" supports that). michael@0: my $escaped_prog_file = ShellEscape($prog_file); michael@0: $file_type = `/usr/bin/file -L $escaped_prog_file 2>$dev_null || michael@0: /usr/bin/file $escaped_prog_file`; michael@0: } elsif ($^O == "MSWin32") { michael@0: $file_type = "MS Windows"; michael@0: } else { michael@0: print STDERR "WARNING: Can't determine the file type of $prog_file"; michael@0: } michael@0: michael@0: if ($file_type =~ /64-bit/) { michael@0: # Change $address_length to 16 if the program file is ELF 64-bit. michael@0: # We can't detect this from many (most?) heap or lock contention michael@0: # profiles, since the actual addresses referenced are generally in low michael@0: # memory even for 64-bit programs. michael@0: $address_length = 16; michael@0: } michael@0: michael@0: if ($file_type =~ /MS Windows/) { michael@0: # For windows, we provide a version of nm and addr2line as part of michael@0: # the opensource release, which is capable of parsing michael@0: # Windows-style PDB executables. It should live in the path, or michael@0: # in the same directory as pprof. michael@0: $obj_tool_map{"nm_pdb"} = "nm-pdb"; michael@0: $obj_tool_map{"addr2line_pdb"} = "addr2line-pdb"; michael@0: } michael@0: michael@0: if ($file_type =~ /Mach-O/) { michael@0: # OS X uses otool to examine Mach-O files, rather than objdump. michael@0: $obj_tool_map{"otool"} = "otool"; michael@0: $obj_tool_map{"addr2line"} = "false"; # no addr2line michael@0: $obj_tool_map{"objdump"} = "false"; # no objdump michael@0: } michael@0: michael@0: # Go fill in %obj_tool_map with the pathnames to use: michael@0: foreach my $tool (keys %obj_tool_map) { michael@0: $obj_tool_map{$tool} = ConfigureTool($obj_tool_map{$tool}); michael@0: } michael@0: } michael@0: michael@0: # Returns the path of a caller-specified object tool. If --tools or michael@0: # PPROF_TOOLS are specified, then returns the full path to the tool michael@0: # with that prefix. Otherwise, returns the path unmodified (which michael@0: # means we will look for it on PATH). michael@0: sub ConfigureTool { michael@0: my $tool = shift; michael@0: my $path; michael@0: michael@0: # --tools (or $PPROF_TOOLS) is a comma separated list, where each michael@0: # item is either a) a pathname prefix, or b) a map of the form michael@0: # :. First we look for an entry of type (b) for our michael@0: # tool. If one is found, we use it. Otherwise, we consider all the michael@0: # pathname prefixes in turn, until one yields an existing file. If michael@0: # none does, we use a default path. michael@0: my $tools = $main::opt_tools || $ENV{"PPROF_TOOLS"} || ""; michael@0: if ($tools =~ m/(,|^)\Q$tool\E:([^,]*)/) { michael@0: $path = $2; michael@0: # TODO(csilvers): sanity-check that $path exists? Hard if it's relative. michael@0: } elsif ($tools ne '') { michael@0: foreach my $prefix (split(',', $tools)) { michael@0: next if ($prefix =~ /:/); # ignore "tool:fullpath" entries in the list michael@0: if (-x $prefix . $tool) { michael@0: $path = $prefix . $tool; michael@0: last; michael@0: } michael@0: } michael@0: if (!$path) { michael@0: error("No '$tool' found with prefix specified by " . michael@0: "--tools (or \$PPROF_TOOLS) '$tools'\n"); michael@0: } michael@0: } else { michael@0: # ... otherwise use the version that exists in the same directory as michael@0: # pprof. If there's nothing there, use $PATH. michael@0: $0 =~ m,[^/]*$,; # this is everything after the last slash michael@0: my $dirname = $`; # this is everything up to and including the last slash michael@0: if (-x "$dirname$tool") { michael@0: $path = "$dirname$tool"; michael@0: } else { michael@0: $path = $tool; michael@0: } michael@0: } michael@0: if ($main::opt_debug) { print STDERR "Using '$path' for '$tool'.\n"; } michael@0: return $path; michael@0: } michael@0: michael@0: sub ShellEscape { michael@0: my @escaped_words = (); michael@0: foreach my $word (@_) { michael@0: my $escaped_word = $word; michael@0: if ($word =~ m![^a-zA-Z0-9/.,_=-]!) { # check for anything not in whitelist michael@0: $escaped_word =~ s/'/'\\''/; michael@0: $escaped_word = "'$escaped_word'"; michael@0: } michael@0: push(@escaped_words, $escaped_word); michael@0: } michael@0: return join(" ", @escaped_words); michael@0: } michael@0: michael@0: sub cleanup { michael@0: unlink($main::tmpfile_sym); michael@0: unlink(keys %main::tempnames); michael@0: michael@0: # We leave any collected profiles in $HOME/pprof in case the user wants michael@0: # to look at them later. We print a message informing them of this. michael@0: if ((scalar(@main::profile_files) > 0) && michael@0: defined($main::collected_profile)) { michael@0: if (scalar(@main::profile_files) == 1) { michael@0: print STDERR "Dynamically gathered profile is in $main::collected_profile\n"; michael@0: } michael@0: print STDERR "If you want to investigate this profile further, you can do:\n"; michael@0: print STDERR "\n"; michael@0: print STDERR " pprof \\\n"; michael@0: print STDERR " $main::prog \\\n"; michael@0: print STDERR " $main::collected_profile\n"; michael@0: print STDERR "\n"; michael@0: } michael@0: } michael@0: michael@0: sub sighandler { michael@0: cleanup(); michael@0: exit(1); michael@0: } michael@0: michael@0: sub error { michael@0: my $msg = shift; michael@0: print STDERR $msg; michael@0: cleanup(); michael@0: exit(1); michael@0: } michael@0: michael@0: michael@0: # Run $nm_command and get all the resulting procedure boundaries whose michael@0: # names match "$regexp" and returns them in a hashtable mapping from michael@0: # procedure name to a two-element vector of [start address, end address] michael@0: sub GetProcedureBoundariesViaNm { michael@0: my $escaped_nm_command = shift; # shell-escaped michael@0: my $regexp = shift; michael@0: michael@0: my $symbol_table = {}; michael@0: open(NM, "$escaped_nm_command |") || error("$escaped_nm_command: $!\n"); michael@0: my $last_start = "0"; michael@0: my $routine = ""; michael@0: while () { michael@0: s/\r//g; # turn windows-looking lines into unix-looking lines michael@0: if (m/^\s*([0-9a-f]+) (.) (..*)/) { michael@0: my $start_val = $1; michael@0: my $type = $2; michael@0: my $this_routine = $3; michael@0: michael@0: # It's possible for two symbols to share the same address, if michael@0: # one is a zero-length variable (like __start_google_malloc) or michael@0: # one symbol is a weak alias to another (like __libc_malloc). michael@0: # In such cases, we want to ignore all values except for the michael@0: # actual symbol, which in nm-speak has type "T". The logic michael@0: # below does this, though it's a bit tricky: what happens when michael@0: # we have a series of lines with the same address, is the first michael@0: # one gets queued up to be processed. However, it won't michael@0: # *actually* be processed until later, when we read a line with michael@0: # a different address. That means that as long as we're reading michael@0: # lines with the same address, we have a chance to replace that michael@0: # item in the queue, which we do whenever we see a 'T' entry -- michael@0: # that is, a line with type 'T'. If we never see a 'T' entry, michael@0: # we'll just go ahead and process the first entry (which never michael@0: # got touched in the queue), and ignore the others. michael@0: if ($start_val eq $last_start && $type =~ /t/i) { michael@0: # We are the 'T' symbol at this address, replace previous symbol. michael@0: $routine = $this_routine; michael@0: next; michael@0: } elsif ($start_val eq $last_start) { michael@0: # We're not the 'T' symbol at this address, so ignore us. michael@0: next; michael@0: } michael@0: michael@0: if ($this_routine eq $sep_symbol) { michael@0: $sep_address = HexExtend($start_val); michael@0: } michael@0: michael@0: # Tag this routine with the starting address in case the image michael@0: # has multiple occurrences of this routine. We use a syntax michael@0: # that resembles template paramters that are automatically michael@0: # stripped out by ShortFunctionName() michael@0: $this_routine .= "<$start_val>"; michael@0: michael@0: if (defined($routine) && $routine =~ m/$regexp/) { michael@0: $symbol_table->{$routine} = [HexExtend($last_start), michael@0: HexExtend($start_val)]; michael@0: } michael@0: $last_start = $start_val; michael@0: $routine = $this_routine; michael@0: } elsif (m/^Loaded image name: (.+)/) { michael@0: # The win32 nm workalike emits information about the binary it is using. michael@0: if ($main::opt_debug) { print STDERR "Using Image $1\n"; } michael@0: } elsif (m/^PDB file name: (.+)/) { michael@0: # The win32 nm workalike emits information about the pdb it is using. michael@0: if ($main::opt_debug) { print STDERR "Using PDB $1\n"; } michael@0: } michael@0: } michael@0: close(NM); michael@0: # Handle the last line in the nm output. Unfortunately, we don't know michael@0: # how big this last symbol is, because we don't know how big the file michael@0: # is. For now, we just give it a size of 0. michael@0: # TODO(csilvers): do better here. michael@0: if (defined($routine) && $routine =~ m/$regexp/) { michael@0: $symbol_table->{$routine} = [HexExtend($last_start), michael@0: HexExtend($last_start)]; michael@0: } michael@0: return $symbol_table; michael@0: } michael@0: michael@0: # Gets the procedure boundaries for all routines in "$image" whose names michael@0: # match "$regexp" and returns them in a hashtable mapping from procedure michael@0: # name to a two-element vector of [start address, end address]. michael@0: # Will return an empty map if nm is not installed or not working properly. michael@0: sub GetProcedureBoundaries { michael@0: my $image = shift; michael@0: my $regexp = shift; michael@0: michael@0: # If $image doesn't start with /, then put ./ in front of it. This works michael@0: # around an obnoxious bug in our probing of nm -f behavior. michael@0: # "nm -f $image" is supposed to fail on GNU nm, but if: michael@0: # michael@0: # a. $image starts with [BbSsPp] (for example, bin/foo/bar), AND michael@0: # b. you have a.out in your current directory (a not uncommon occurence) michael@0: # michael@0: # then "nm -f $image" succeeds because -f only looks at the first letter of michael@0: # the argument, which looks valid because it's [BbSsPp], and then since michael@0: # there's no image provided, it looks for a.out and finds it. michael@0: # michael@0: # This regex makes sure that $image starts with . or /, forcing the -f michael@0: # parsing to fail since . and / are not valid formats. michael@0: $image =~ s#^[^/]#./$&#; michael@0: michael@0: # For libc libraries, the copy in /usr/lib/debug contains debugging symbols michael@0: my $debugging = DebuggingLibrary($image); michael@0: if ($debugging) { michael@0: $image = $debugging; michael@0: } michael@0: michael@0: my $nm = $obj_tool_map{"nm"}; michael@0: my $cppfilt = $obj_tool_map{"c++filt"}; michael@0: michael@0: # nm can fail for two reasons: 1) $image isn't a debug library; 2) nm michael@0: # binary doesn't support --demangle. In addition, for OS X we need michael@0: # to use the -f flag to get 'flat' nm output (otherwise we don't sort michael@0: # properly and get incorrect results). Unfortunately, GNU nm uses -f michael@0: # in an incompatible way. So first we test whether our nm supports michael@0: # --demangle and -f. michael@0: my $demangle_flag = ""; michael@0: my $cppfilt_flag = ""; michael@0: my $to_devnull = ">$dev_null 2>&1"; michael@0: if (system(ShellEscape($nm, "--demangle", "image") . $to_devnull) == 0) { michael@0: # In this mode, we do "nm --demangle " michael@0: $demangle_flag = "--demangle"; michael@0: $cppfilt_flag = ""; michael@0: } elsif (system(ShellEscape($cppfilt, $image) . $to_devnull) == 0) { michael@0: # In this mode, we do "nm | c++filt" michael@0: $cppfilt_flag = " | " . ShellEscape($cppfilt); michael@0: }; michael@0: my $flatten_flag = ""; michael@0: if (system(ShellEscape($nm, "-f", $image) . $to_devnull) == 0) { michael@0: $flatten_flag = "-f"; michael@0: } michael@0: michael@0: # Finally, in the case $imagie isn't a debug library, we try again with michael@0: # -D to at least get *exported* symbols. If we can't use --demangle, michael@0: # we use c++filt instead, if it exists on this system. michael@0: my @nm_commands = (ShellEscape($nm, "-n", $flatten_flag, $demangle_flag, michael@0: $image) . " 2>$dev_null $cppfilt_flag", michael@0: ShellEscape($nm, "-D", "-n", $flatten_flag, $demangle_flag, michael@0: $image) . " 2>$dev_null $cppfilt_flag", michael@0: # 6nm is for Go binaries michael@0: ShellEscape("6nm", "$image") . " 2>$dev_null | sort", michael@0: ); michael@0: michael@0: # If the executable is an MS Windows PDB-format executable, we'll michael@0: # have set up obj_tool_map("nm_pdb"). In this case, we actually michael@0: # want to use both unix nm and windows-specific nm_pdb, since michael@0: # PDB-format executables can apparently include dwarf .o files. michael@0: if (exists $obj_tool_map{"nm_pdb"}) { michael@0: push(@nm_commands, michael@0: ShellEscape($obj_tool_map{"nm_pdb"}, "--demangle", $image) michael@0: . " 2>$dev_null"); michael@0: } michael@0: michael@0: foreach my $nm_command (@nm_commands) { michael@0: my $symbol_table = GetProcedureBoundariesViaNm($nm_command, $regexp); michael@0: return $symbol_table if (%{$symbol_table}); michael@0: } michael@0: my $symbol_table = {}; michael@0: return $symbol_table; michael@0: } michael@0: michael@0: michael@0: # The test vectors for AddressAdd/Sub/Inc are 8-16-nibble hex strings. michael@0: # To make them more readable, we add underscores at interesting places. michael@0: # This routine removes the underscores, producing the canonical representation michael@0: # used by pprof to represent addresses, particularly in the tested routines. michael@0: sub CanonicalHex { michael@0: my $arg = shift; michael@0: return join '', (split '_',$arg); michael@0: } michael@0: michael@0: michael@0: # Unit test for AddressAdd: michael@0: sub AddressAddUnitTest { michael@0: my $test_data_8 = shift; michael@0: my $test_data_16 = shift; michael@0: my $error_count = 0; michael@0: my $fail_count = 0; michael@0: my $pass_count = 0; michael@0: # print STDERR "AddressAddUnitTest: ", 1+$#{$test_data_8}, " tests\n"; michael@0: michael@0: # First a few 8-nibble addresses. Note that this implementation uses michael@0: # plain old arithmetic, so a quick sanity check along with verifying what michael@0: # happens to overflow (we want it to wrap): michael@0: $address_length = 8; michael@0: foreach my $row (@{$test_data_8}) { michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } michael@0: my $sum = AddressAdd ($row->[0], $row->[1]); michael@0: if ($sum ne $row->[2]) { michael@0: printf STDERR "ERROR: %s != %s + %s = %s\n", $sum, michael@0: $row->[0], $row->[1], $row->[2]; michael@0: ++$fail_count; michael@0: } else { michael@0: ++$pass_count; michael@0: } michael@0: } michael@0: printf STDERR "AddressAdd 32-bit tests: %d passes, %d failures\n", michael@0: $pass_count, $fail_count; michael@0: $error_count = $fail_count; michael@0: $fail_count = 0; michael@0: $pass_count = 0; michael@0: michael@0: # Now 16-nibble addresses. michael@0: $address_length = 16; michael@0: foreach my $row (@{$test_data_16}) { michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } michael@0: my $sum = AddressAdd (CanonicalHex($row->[0]), CanonicalHex($row->[1])); michael@0: my $expected = join '', (split '_',$row->[2]); michael@0: if ($sum ne CanonicalHex($row->[2])) { michael@0: printf STDERR "ERROR: %s != %s + %s = %s\n", $sum, michael@0: $row->[0], $row->[1], $row->[2]; michael@0: ++$fail_count; michael@0: } else { michael@0: ++$pass_count; michael@0: } michael@0: } michael@0: printf STDERR "AddressAdd 64-bit tests: %d passes, %d failures\n", michael@0: $pass_count, $fail_count; michael@0: $error_count += $fail_count; michael@0: michael@0: return $error_count; michael@0: } michael@0: michael@0: michael@0: # Unit test for AddressSub: michael@0: sub AddressSubUnitTest { michael@0: my $test_data_8 = shift; michael@0: my $test_data_16 = shift; michael@0: my $error_count = 0; michael@0: my $fail_count = 0; michael@0: my $pass_count = 0; michael@0: # print STDERR "AddressSubUnitTest: ", 1+$#{$test_data_8}, " tests\n"; michael@0: michael@0: # First a few 8-nibble addresses. Note that this implementation uses michael@0: # plain old arithmetic, so a quick sanity check along with verifying what michael@0: # happens to overflow (we want it to wrap): michael@0: $address_length = 8; michael@0: foreach my $row (@{$test_data_8}) { michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } michael@0: my $sum = AddressSub ($row->[0], $row->[1]); michael@0: if ($sum ne $row->[3]) { michael@0: printf STDERR "ERROR: %s != %s - %s = %s\n", $sum, michael@0: $row->[0], $row->[1], $row->[3]; michael@0: ++$fail_count; michael@0: } else { michael@0: ++$pass_count; michael@0: } michael@0: } michael@0: printf STDERR "AddressSub 32-bit tests: %d passes, %d failures\n", michael@0: $pass_count, $fail_count; michael@0: $error_count = $fail_count; michael@0: $fail_count = 0; michael@0: $pass_count = 0; michael@0: michael@0: # Now 16-nibble addresses. michael@0: $address_length = 16; michael@0: foreach my $row (@{$test_data_16}) { michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } michael@0: my $sum = AddressSub (CanonicalHex($row->[0]), CanonicalHex($row->[1])); michael@0: if ($sum ne CanonicalHex($row->[3])) { michael@0: printf STDERR "ERROR: %s != %s - %s = %s\n", $sum, michael@0: $row->[0], $row->[1], $row->[3]; michael@0: ++$fail_count; michael@0: } else { michael@0: ++$pass_count; michael@0: } michael@0: } michael@0: printf STDERR "AddressSub 64-bit tests: %d passes, %d failures\n", michael@0: $pass_count, $fail_count; michael@0: $error_count += $fail_count; michael@0: michael@0: return $error_count; michael@0: } michael@0: michael@0: michael@0: # Unit test for AddressInc: michael@0: sub AddressIncUnitTest { michael@0: my $test_data_8 = shift; michael@0: my $test_data_16 = shift; michael@0: my $error_count = 0; michael@0: my $fail_count = 0; michael@0: my $pass_count = 0; michael@0: # print STDERR "AddressIncUnitTest: ", 1+$#{$test_data_8}, " tests\n"; michael@0: michael@0: # First a few 8-nibble addresses. Note that this implementation uses michael@0: # plain old arithmetic, so a quick sanity check along with verifying what michael@0: # happens to overflow (we want it to wrap): michael@0: $address_length = 8; michael@0: foreach my $row (@{$test_data_8}) { michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } michael@0: my $sum = AddressInc ($row->[0]); michael@0: if ($sum ne $row->[4]) { michael@0: printf STDERR "ERROR: %s != %s + 1 = %s\n", $sum, michael@0: $row->[0], $row->[4]; michael@0: ++$fail_count; michael@0: } else { michael@0: ++$pass_count; michael@0: } michael@0: } michael@0: printf STDERR "AddressInc 32-bit tests: %d passes, %d failures\n", michael@0: $pass_count, $fail_count; michael@0: $error_count = $fail_count; michael@0: $fail_count = 0; michael@0: $pass_count = 0; michael@0: michael@0: # Now 16-nibble addresses. michael@0: $address_length = 16; michael@0: foreach my $row (@{$test_data_16}) { michael@0: if ($main::opt_debug and $main::opt_test) { print STDERR "@{$row}\n"; } michael@0: my $sum = AddressInc (CanonicalHex($row->[0])); michael@0: if ($sum ne CanonicalHex($row->[4])) { michael@0: printf STDERR "ERROR: %s != %s + 1 = %s\n", $sum, michael@0: $row->[0], $row->[4]; michael@0: ++$fail_count; michael@0: } else { michael@0: ++$pass_count; michael@0: } michael@0: } michael@0: printf STDERR "AddressInc 64-bit tests: %d passes, %d failures\n", michael@0: $pass_count, $fail_count; michael@0: $error_count += $fail_count; michael@0: michael@0: return $error_count; michael@0: } michael@0: michael@0: michael@0: # Driver for unit tests. michael@0: # Currently just the address add/subtract/increment routines for 64-bit. michael@0: sub RunUnitTests { michael@0: my $error_count = 0; michael@0: michael@0: # This is a list of tuples [a, b, a+b, a-b, a+1] michael@0: my $unit_test_data_8 = [ michael@0: [qw(aaaaaaaa 50505050 fafafafa 5a5a5a5a aaaaaaab)], michael@0: [qw(50505050 aaaaaaaa fafafafa a5a5a5a6 50505051)], michael@0: [qw(ffffffff aaaaaaaa aaaaaaa9 55555555 00000000)], michael@0: [qw(00000001 ffffffff 00000000 00000002 00000002)], michael@0: [qw(00000001 fffffff0 fffffff1 00000011 00000002)], michael@0: ]; michael@0: my $unit_test_data_16 = [ michael@0: # The implementation handles data in 7-nibble chunks, so those are the michael@0: # interesting boundaries. michael@0: [qw(aaaaaaaa 50505050 michael@0: 00_000000f_afafafa 00_0000005_a5a5a5a 00_000000a_aaaaaab)], michael@0: [qw(50505050 aaaaaaaa michael@0: 00_000000f_afafafa ff_ffffffa_5a5a5a6 00_0000005_0505051)], michael@0: [qw(ffffffff aaaaaaaa michael@0: 00_000001a_aaaaaa9 00_0000005_5555555 00_0000010_0000000)], michael@0: [qw(00000001 ffffffff michael@0: 00_0000010_0000000 ff_ffffff0_0000002 00_0000000_0000002)], michael@0: [qw(00000001 fffffff0 michael@0: 00_000000f_ffffff1 ff_ffffff0_0000011 00_0000000_0000002)], michael@0: michael@0: [qw(00_a00000a_aaaaaaa 50505050 michael@0: 00_a00000f_afafafa 00_a000005_a5a5a5a 00_a00000a_aaaaaab)], michael@0: [qw(0f_fff0005_0505050 aaaaaaaa michael@0: 0f_fff000f_afafafa 0f_ffefffa_5a5a5a6 0f_fff0005_0505051)], michael@0: [qw(00_000000f_fffffff 01_800000a_aaaaaaa michael@0: 01_800001a_aaaaaa9 fe_8000005_5555555 00_0000010_0000000)], michael@0: [qw(00_0000000_0000001 ff_fffffff_fffffff michael@0: 00_0000000_0000000 00_0000000_0000002 00_0000000_0000002)], michael@0: [qw(00_0000000_0000001 ff_fffffff_ffffff0 michael@0: ff_fffffff_ffffff1 00_0000000_0000011 00_0000000_0000002)], michael@0: ]; michael@0: michael@0: $error_count += AddressAddUnitTest($unit_test_data_8, $unit_test_data_16); michael@0: $error_count += AddressSubUnitTest($unit_test_data_8, $unit_test_data_16); michael@0: $error_count += AddressIncUnitTest($unit_test_data_8, $unit_test_data_16); michael@0: if ($error_count > 0) { michael@0: print STDERR $error_count, " errors: FAILED\n"; michael@0: } else { michael@0: print STDERR "PASS\n"; michael@0: } michael@0: exit ($error_count); michael@0: }