Home | History | Annotate | Download | only in massif
      1 #! /usr/bin/perl
      2 
      3 ##--------------------------------------------------------------------##
      4 ##--- Massif's results printer                         ms_print.in ---##
      5 ##--------------------------------------------------------------------##
      6 
      7 #  This file is part of Massif, a Valgrind tool for profiling memory
      8 #  usage of programs.
      9 #
     10 #  Copyright (C) 2007-2007 Nicholas Nethercote
     11 #     njn (at] valgrind.org
     12 #
     13 #  This program is free software; you can redistribute it and/or
     14 #  modify it under the terms of the GNU General Public License as
     15 #  published by the Free Software Foundation; either version 2 of the
     16 #  License, or (at your option) any later version.
     17 #
     18 #  This program is distributed in the hope that it will be useful, but
     19 #  WITHOUT ANY WARRANTY; without even the implied warranty of
     20 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     21 #  General Public License for more details.
     22 #
     23 #  You should have received a copy of the GNU General Public License
     24 #  along with this program; if not, write to the Free Software
     25 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
     26 #  02111-1307, USA.
     27 #
     28 #  The GNU General Public License is contained in the file COPYING.
     29 
     30 use warnings;
     31 use strict;
     32 
     33 #----------------------------------------------------------------------------
     34 # Global variables, main data structures
     35 #----------------------------------------------------------------------------
     36 
     37 # Command line of profiled program.
     38 my $cmd;
     39 
     40 # Time unit used in profile.
     41 my $time_unit;
     42 
     43 # Threshold dictating what percentage an entry must represent for us to
     44 # bother showing it.
     45 my $threshold = 1.0;
     46 
     47 # Graph x and y dimensions.
     48 my $graph_x = 72;
     49 my $graph_y = 20;
     50 
     51 # Input file name
     52 my $input_file = undef;
     53 
     54 # Tmp file name.
     55 my $tmp_file = "ms_print.tmp.$$";
     56 
     57 # Version number.
     58 my $version = "3.10.0.SVN";
     59 
     60 # Args passed, for printing.
     61 my $ms_print_args;
     62 
     63 # Usage message.
     64 my $usage = <<END
     65 usage: ms_print [options] massif-out-file
     66 
     67   options for the user, with defaults in [ ], are:
     68     -h --help             show this message
     69     --version             show version
     70     --threshold=<m.n>     significance threshold, in percent [$threshold]
     71     --x=<4..1000>         graph width, in columns [72]
     72     --y=<4..1000>         graph height, in rows [20]
     73 
     74   ms_print is Copyright (C) 2007-2007 Nicholas Nethercote.
     75   and licensed under the GNU General Public License, version 2.
     76   Bug reports, feedback, admiration, abuse, etc, to: njn\@valgrind.org.
     77                                                 
     78 END
     79 ;
     80 
     81 # Used in various places of output.
     82 my $fancy    = '-' x 80;
     83 my $fancy_nl = $fancy . "\n";
     84 
     85 # Returns 0 if the denominator is 0.
     86 sub safe_div_0($$)
     87 {
     88     my ($x, $y) = @_;
     89     return ($y ? $x / $y : 0);
     90 }
     91 
     92 #-----------------------------------------------------------------------------
     93 # Argument and option handling
     94 #-----------------------------------------------------------------------------
     95 sub process_cmd_line() 
     96 {
     97     my @files;
     98 
     99     # Grab a copy of the arguments, for printing later.
    100     for my $arg (@ARGV) { 
    101         $ms_print_args .= " $arg";       # The arguments.
    102     }
    103 
    104     for my $arg (@ARGV) { 
    105 
    106         # Option handling
    107         if ($arg =~ /^-/) {
    108 
    109             # --version
    110             if ($arg =~ /^--version$/) {
    111                 die("ms_print-$version\n");
    112 
    113             # --threshold=X (tolerates a trailing '%')
    114             } elsif ($arg =~ /^--threshold=([\d\.]+)%?$/) {
    115                 $threshold = $1;
    116                 ($1 >= 0 && $1 <= 100) or die($usage);
    117 
    118             } elsif ($arg =~ /^--x=(\d+)$/) {
    119                 $graph_x = $1;
    120                 (4 <= $graph_x && $graph_x <= 1000) or die($usage);
    121 
    122             } elsif ($arg =~ /^--y=(\d+)$/) {
    123                 $graph_y = $1;
    124                 (4 <= $graph_y && $graph_y <= 1000) or die($usage);
    125 
    126             } else {            # -h and --help fall under this case
    127                 die($usage);
    128             }
    129         } else {
    130             # Not an option.  Remember it as a filename. 
    131             push(@files, $arg);
    132         }
    133     }
    134 
    135     # Must have chosen exactly one input file.
    136     if (scalar @files) {
    137         $input_file = $files[0];
    138     } else {
    139         die($usage);
    140     }
    141 }
    142 
    143 #-----------------------------------------------------------------------------
    144 # Reading the input file: auxiliary functions
    145 #-----------------------------------------------------------------------------
    146 
    147 # Gets the next line, stripping comments and skipping blanks.
    148 # Returns undef at EOF.
    149 sub get_line()
    150 {
    151     while (my $line = <INPUTFILE>) {
    152         $line =~ s/#.*$//;          # remove comments
    153         if ($line !~ /^\s*$/) {
    154             return $line;           # return $line if non-empty
    155         }
    156     }
    157     return undef;       # EOF: return undef
    158 }
    159 
    160 sub equals_num_line($$)
    161 {
    162     my ($line, $fieldname) = @_;
    163     defined($line) 
    164         or die("Line $.: expected \"$fieldname\" line, got end of file\n");
    165     $line =~ s/^$fieldname=(.*)\s*$//
    166         or die("Line $.: expected \"$fieldname\" line, got:\n$line");
    167     return $1;
    168 }
    169 
    170 sub is_significant_XPt($$$)
    171 {
    172     my ($is_top_node, $xpt_szB, $total_szB) = @_;
    173     ($xpt_szB <= $total_szB) or die;
    174     # Nb: we always consider the alloc-XPt significant, even if the size is
    175     # zero.
    176     return $is_top_node || 0 == $threshold ||
    177         ( $total_szB != 0 && $xpt_szB * 100 / $total_szB >= $threshold );
    178 }
    179 
    180 #-----------------------------------------------------------------------------
    181 # Reading the input file: reading heap trees
    182 #-----------------------------------------------------------------------------
    183 
    184 # Forward declaration, because it's recursive.
    185 sub read_heap_tree($$$$$);
    186 
    187 # Return pair:  if the tree was significant, both are zero.  If it was
    188 # insignificant, the first element is 1 and the second is the number of
    189 # bytes.
    190 sub read_heap_tree($$$$$)
    191 {
    192     # Read the line and determine if it is significant.
    193     my ($is_top_node, $this_prefix, $child_midfix, $arrow, $mem_total_B) = @_;
    194     my $line = get_line();
    195     (defined $line and $line =~ /^\s*n(\d+):\s*(\d+)(.*)$/)
    196         or die("Line $.: expected a tree node line, got:\n$line\n");
    197     my $n_children = $1;
    198     my $bytes      = $2;
    199     my $details    = $3;
    200     my $perc       = safe_div_0(100 * $bytes, $mem_total_B);
    201     # Nb: we always print the alloc-XPt, even if its size is zero.
    202     my $is_significant = is_significant_XPt($is_top_node, $bytes, $mem_total_B);
    203 
    204     # We precede this node's line with "$this_prefix.$arrow".  We precede
    205     # any children of this node with "$this_prefix$child_midfix$arrow".
    206     if ($is_significant) {
    207         # Nb: $details might have '%' in it, so don't embed directly in the
    208         # format string.
    209         printf(TMPFILE
    210             "$this_prefix$arrow%05.2f%% (%sB)%s\n", $perc, commify($bytes),
    211             $details);
    212     }
    213 
    214     # Now read all the children.
    215     my $n_insig_children = 0;
    216     my $total_insig_children_szB = 0;
    217     my $this_prefix2 = $this_prefix . $child_midfix;
    218     for (my $i = 0; $i < $n_children; $i++) {
    219         # If child is the last sibling, the midfix is empty.
    220         my $child_midfix2 = ( $i+1 == $n_children ? "  " : "| " );
    221         my ($is_child_insignificant, $child_insig_bytes) =
    222             # '0' means it's not the top node of the tree.
    223             read_heap_tree(0, $this_prefix2, $child_midfix2, "->",
    224                 $mem_total_B);
    225         $n_insig_children += $is_child_insignificant;
    226         $total_insig_children_szB += $child_insig_bytes;
    227     }
    228 
    229     if ($is_significant) {
    230         # If this was significant but any children were insignificant, print
    231         # the "in N places" line for them.
    232         if ($n_insig_children > 0) {
    233             $perc = safe_div_0(100 * $total_insig_children_szB, $mem_total_B);
    234             printf(TMPFILE "%s->%05.2f%% (%sB) in %d+ places, all below "
    235                  . "ms_print's threshold (%05.2f%%)\n",
    236                 $this_prefix2, $perc, commify($total_insig_children_szB),
    237                 $n_insig_children, $threshold);
    238             print(TMPFILE "$this_prefix2\n");
    239         }
    240 
    241         # If this node has no children, print an extra (mostly) empty line.
    242         if (0 == $n_children) {
    243             print(TMPFILE "$this_prefix2\n");
    244         }
    245         return (0, 0);
    246 
    247     } else {
    248         return (1, $bytes);
    249     }
    250 }
    251 
    252 #-----------------------------------------------------------------------------
    253 # Reading the input file: main
    254 #-----------------------------------------------------------------------------
    255 
    256 sub max_label_2($$)
    257 {
    258     my ($szB, $szB_scaled) = @_;
    259 
    260     # For the label, if $szB is 999B or below, we print it as an integer.
    261     # Otherwise, we print it as a float with 5 characters (including the '.').
    262     # Examples (for bytes):
    263     #       1 -->     1  B
    264     #     999 -->   999  B
    265     #    1000 --> 0.977 KB
    266     #    1024 --> 1.000 KB
    267     #   10240 --> 10.00 KB
    268     #  102400 --> 100.0 KB
    269     # 1024000 --> 0.977 MB
    270     # 1048576 --> 1.000 MB
    271     #
    272     if    ($szB < 1000)        { return sprintf("%5d",   $szB);        }
    273     elsif ($szB_scaled < 10)   { return sprintf("%5.3f", $szB_scaled); }
    274     elsif ($szB_scaled < 100)  { return sprintf("%5.2f", $szB_scaled); }
    275     else                       { return sprintf("%5.1f", $szB_scaled); }
    276 }
    277 
    278 # Work out the units for the max value, measured in instructions.
    279 sub i_max_label($)
    280 {
    281     my ($nI) = @_;
    282 
    283     # We repeat until the number is less than 1000.
    284     my $nI_scaled = $nI;
    285     my $unit = "i";
    286     # Nb: 'k' is the "kilo" (1000) prefix.
    287     if ($nI_scaled >= 1000) { $unit = "ki"; $nI_scaled /= 1024; }
    288     if ($nI_scaled >= 1000) { $unit = "Mi"; $nI_scaled /= 1024; }
    289     if ($nI_scaled >= 1000) { $unit = "Gi"; $nI_scaled /= 1024; }
    290     if ($nI_scaled >= 1000) { $unit = "Ti"; $nI_scaled /= 1024; }
    291     if ($nI_scaled >= 1000) { $unit = "Pi"; $nI_scaled /= 1024; }
    292     if ($nI_scaled >= 1000) { $unit = "Ei"; $nI_scaled /= 1024; }
    293     if ($nI_scaled >= 1000) { $unit = "Zi"; $nI_scaled /= 1024; }
    294     if ($nI_scaled >= 1000) { $unit = "Yi"; $nI_scaled /= 1024; }
    295 
    296     return (max_label_2($nI, $nI_scaled), $unit);
    297 }
    298 
    299 # Work out the units for the max value, measured in bytes.
    300 sub B_max_label($)
    301 {
    302     my ($szB) = @_;
    303 
    304     # We repeat until the number is less than 1000, but we divide by 1024 on
    305     # each scaling.
    306     my $szB_scaled = $szB;
    307     my $unit = "B";
    308     # Nb: 'K' or 'k' are acceptable as the "binary kilo" (1024) prefix.
    309     # (Strictly speaking, should use "KiB" (kibibyte), "MiB" (mebibyte), etc,
    310     # but they're not in common use.)
    311     if ($szB_scaled >= 1000) { $unit = "KB"; $szB_scaled /= 1024; }
    312     if ($szB_scaled >= 1000) { $unit = "MB"; $szB_scaled /= 1024; }
    313     if ($szB_scaled >= 1000) { $unit = "GB"; $szB_scaled /= 1024; }
    314     if ($szB_scaled >= 1000) { $unit = "TB"; $szB_scaled /= 1024; }
    315     if ($szB_scaled >= 1000) { $unit = "PB"; $szB_scaled /= 1024; }
    316     if ($szB_scaled >= 1000) { $unit = "EB"; $szB_scaled /= 1024; }
    317     if ($szB_scaled >= 1000) { $unit = "ZB"; $szB_scaled /= 1024; }
    318     if ($szB_scaled >= 1000) { $unit = "YB"; $szB_scaled /= 1024; }
    319 
    320     return (max_label_2($szB, $szB_scaled), $unit);
    321 }
    322 
    323 # Work out the units for the max value, measured in ms/s/h.
    324 sub t_max_label($)
    325 {
    326     my ($szB) = @_;
    327 
    328     # We scale from millisecond to seconds to hours.
    329     #
    330     # XXX: this allows a number with 6 chars, eg. "3599.0 s"
    331     my $szB_scaled = $szB;
    332     my $unit = "ms";
    333     if ($szB_scaled >= 1000) { $unit = "s"; $szB_scaled /= 1000; }
    334     if ($szB_scaled >= 3600) { $unit = "h"; $szB_scaled /= 3600; }
    335 
    336     return (max_label_2($szB, $szB_scaled), $unit);
    337 }
    338 
    339 # This prints four things:
    340 #   - the output header
    341 #   - the graph
    342 #   - the snapshot summaries (number, list of detailed ones)
    343 #   - the snapshots
    344 #
    345 # The first three parts can't be printed until we've read the whole input file;
    346 # but the fourth part is much easier to print while we're reading the file.  So
    347 # we print the fourth part to a tmp file, and then dump the tmp file at the
    348 # end.
    349 #
    350 sub read_input_file() 
    351 {
    352     my $desc = "";              # Concatenated description lines.
    353     my $peak_mem_total_szB = 0;
    354 
    355     # Info about each snapshot.
    356     my @snapshot_nums = ();
    357     my @times         = ();
    358     my @mem_total_Bs  = ();
    359     my @is_detaileds  = ();
    360     my $peak_num = -1;      # An initial value that will be ok if no peak
    361                             # entry is in the file.
    362     
    363     #-------------------------------------------------------------------------
    364     # Read start of input file.
    365     #-------------------------------------------------------------------------
    366     open(INPUTFILE, "< $input_file") 
    367          || die "Cannot open $input_file for reading\n";
    368 
    369     # Read "desc:" lines.
    370     my $line;
    371     while ($line = get_line()) {
    372         if ($line =~ s/^desc://) {
    373             $desc .= $line;
    374         } else {
    375             last;
    376         }
    377     }
    378 
    379     # Read "cmd:" line (Nb: will already be in $line from "desc:" loop above).
    380     ($line =~ /^cmd:\s*(.*)$/) or die("Line $.: missing 'cmd' line\n");
    381     $cmd = $1;
    382 
    383     # Read "time_unit:" line.
    384     $line = get_line();
    385     ($line =~ /^time_unit:\s*(.*)$/) or
    386         die("Line $.: missing 'time_unit' line\n");
    387     $time_unit = $1;
    388 
    389     #-------------------------------------------------------------------------
    390     # Print snapshot list header to $tmp_file.
    391     #-------------------------------------------------------------------------
    392     open(TMPFILE, "> $tmp_file") 
    393          || die "Cannot open $tmp_file for reading\n";
    394 
    395     my $time_column = sprintf("%14s", "time($time_unit)");
    396     my $column_format = "%3s %14s %16s %16s %13s %12s\n";
    397     my $header =
    398         $fancy_nl .
    399         sprintf($column_format
    400         ,   "n"
    401         ,   $time_column
    402         ,   "total(B)"
    403         ,   "useful-heap(B)"
    404         ,   "extra-heap(B)"
    405         ,   "stacks(B)"
    406         ) .
    407         $fancy_nl;
    408     print(TMPFILE $header);
    409 
    410     #-------------------------------------------------------------------------
    411     # Read body of input file.
    412     #-------------------------------------------------------------------------
    413     $line = get_line();
    414     while (defined $line) {
    415         my $snapshot_num     = equals_num_line($line,      "snapshot");
    416         my $time             = equals_num_line(get_line(), "time");
    417         my $mem_heap_B       = equals_num_line(get_line(), "mem_heap_B");
    418         my $mem_heap_extra_B = equals_num_line(get_line(), "mem_heap_extra_B");
    419         my $mem_stacks_B     = equals_num_line(get_line(), "mem_stacks_B");
    420         my $mem_total_B      = $mem_heap_B + $mem_heap_extra_B + $mem_stacks_B;
    421         my $heap_tree        = equals_num_line(get_line(), "heap_tree");
    422 
    423         # Print the snapshot data to $tmp_file.
    424         printf(TMPFILE $column_format,
    425         ,   $snapshot_num
    426         ,   commify($time)
    427         ,   commify($mem_total_B)
    428         ,   commify($mem_heap_B)
    429         ,   commify($mem_heap_extra_B)
    430         ,   commify($mem_stacks_B)
    431         );
    432 
    433         # Remember the snapshot data.
    434         push(@snapshot_nums, $snapshot_num);
    435         push(@times,         $time);
    436         push(@mem_total_Bs,  $mem_total_B);
    437         push(@is_detaileds,  ( $heap_tree eq "empty" ? 0 : 1 ));
    438         $peak_mem_total_szB = $mem_total_B
    439             if $mem_total_B > $peak_mem_total_szB;
    440 
    441         # Read the heap tree, and if it's detailed, print it and a subsequent
    442         # snapshot list header to $tmp_file.
    443         if      ($heap_tree eq "empty") {
    444             $line = get_line();
    445         } elsif ($heap_tree =~ "(detailed|peak)") {
    446             # If "peak", remember the number.
    447             if ($heap_tree eq "peak") {
    448                 $peak_num = $snapshot_num;
    449             }
    450             # '1' means it's the top node of the tree.
    451             read_heap_tree(1, "", "", "", $mem_total_B);
    452 
    453             # Print the header, unless there are no more snapshots.
    454             $line = get_line();
    455             if (defined $line) {
    456                 print(TMPFILE $header);
    457             }
    458         } else {
    459             die("Line $.: expected 'empty' or '...' after 'heap_tree='\n");
    460         }
    461     }
    462 
    463     close(INPUTFILE);
    464     close(TMPFILE);
    465 
    466     #-------------------------------------------------------------------------
    467     # Print header.
    468     #-------------------------------------------------------------------------
    469     print($fancy_nl);
    470     print("Command:            $cmd\n");
    471     print("Massif arguments:  $desc");
    472     print("ms_print arguments:$ms_print_args\n");
    473     print($fancy_nl);
    474     print("\n\n");
    475 
    476     #-------------------------------------------------------------------------
    477     # Setup for graph.
    478     #-------------------------------------------------------------------------
    479     # The ASCII graph.
    480     # Row    0 ([0..graph_x][0]) is the X-axis.
    481     # Column 0 ([0][0..graph_y]) is the Y-axis.
    482     # The rest ([1][1]..[graph_x][graph_y]) is the usable graph area.
    483     my @graph;
    484     my $x;
    485     my $y;
    486 
    487     my $n_snapshots = scalar(@snapshot_nums);
    488     ($n_snapshots > 0) or die;
    489     my $end_time = $times[$n_snapshots-1];
    490     ($end_time >= 0) or die;
    491 
    492     # Setup graph[][].
    493     $graph[0][0] = '+';                                     # axes join point
    494     for ($x = 1; $x <= $graph_x; $x++) { $graph[$x][0] = '-'; } # X-axis
    495     for ($y = 1; $y <= $graph_y; $y++) { $graph[0][$y] = '|'; } # Y-axis
    496     $graph[$graph_x][0] = '>';                                  # X-axis arrow
    497     $graph[0][$graph_y] = '^';                                  # Y-axis arrow 
    498     for ($x = 1; $x <= $graph_x; $x++) {                        # usable area
    499        for ($y = 1; $y <= $graph_y; $y++) {
    500           $graph[$x][$y] = ' ';
    501        }
    502     }
    503 
    504     #-------------------------------------------------------------------------
    505     # Write snapshot bars into graph[][].
    506     #-------------------------------------------------------------------------
    507     # Each row represents K bytes, which is 1/graph_y of the peak size
    508     # (and K can be non-integral).  When drawing the column for a snapshot,
    509     # in order to fill the slot in row y (where the first row drawn on is
    510     # row 1) with a full-char (eg. ':'), it must be >= y*K.  For example, if
    511     # K = 10 bytes, then the values 0, 4, 5, 9, 10, 14, 15, 19, 20, 24, 25,
    512     # 29, 30 would be drawn like this (showing one per column):
    513     #
    514     #                       y    y * K
    515     #                       -    -----------
    516     # 30 |            :     3    3 * 10 = 30
    517     # 20 |        :::::     2    2 * 10 = 20
    518     # 10 |    :::::::::     1    1 * 10 = 10
    519     # 0  +-------------
    520 
    521     my $peak_char     = '#';                            
    522     my $detailed_char = '@';
    523     my $normal_char   = ':';
    524 
    525     # Work out how many bytes each row represents.  If the peak size was 0,
    526     # make it 1 so that the Y-axis covers a non-zero range of values.
    527     # Likewise for end_time.
    528     if (0 == $peak_mem_total_szB) { $peak_mem_total_szB = 1; }
    529     if (0 == $end_time          ) { $end_time           = 1; }
    530     my $K = $peak_mem_total_szB / $graph_y;
    531 
    532        $x          = 0;
    533     my $prev_x     = 0;
    534     my $prev_y_max = 0;
    535     my $prev_char  = ':';
    536 
    537     for (my $i = 0; $i < $n_snapshots; $i++) {
    538  
    539         # Work out which column this snapshot belongs to.  
    540         $prev_x = $x;
    541         my $x_pos_frac = ($times[$i] / ($end_time)) * $graph_x;
    542         $x = int($x_pos_frac) + 1;    # +1 due to Y-axis
    543         # The final snapshot will spill over into the n+1th column, which
    544         # doesn't get shown.  So we fudge that one and pull it back a
    545         # column, as if the end_time was actually end_time+epsilon.
    546         if ($times[$i] == $end_time) {
    547             ($x == $graph_x+1) or die;
    548             $x = $graph_x;
    549         }
    550 
    551         # If there was a gap between the previous snapshot's column and this
    552         # one, we draw a horizontal line in the gap (so long as it doesn't
    553         # trash the x-axis).  Without this, graphs with a few sparse
    554         # snapshots look funny -- as if the memory usage is in temporary
    555         # spikes.
    556         if ($prev_y_max > 0) {
    557             for (my $x2 = $prev_x + 1; $x2 < $x; $x2++) {
    558                 $graph[$x2][$prev_y_max] = $prev_char;
    559             }
    560         }
    561 
    562         # Choose the column char.
    563         my $char;
    564         if    ($i == $peak_num)   { $char = $peak_char;     }
    565         elsif ($is_detaileds[$i]) { $char = $detailed_char; }
    566         else                      { $char = $normal_char;   }
    567 
    568         # Grow this snapshot bar from bottom to top.
    569         my $y_max = 0;
    570         for ($y = 1; $y <= $graph_y; $y++) {
    571             if ($mem_total_Bs[$i] >= $y * $K) {
    572                 # Priority order for chars: peak > detailed > normal
    573                 my $should_draw_char = 
    574                     (($char eq $peak_char)
    575                      or
    576                      ($char eq $detailed_char and 
    577                       $graph[$x][$y] ne $peak_char
    578                      )
    579                      or
    580                      ($char eq $normal_char and
    581                       $graph[$x][$y] ne $peak_char and
    582                       $graph[$x][$y] ne $detailed_char
    583                      )
    584                     );
    585 
    586                 if ($should_draw_char) {
    587                     $graph[$x][$y] = $char;
    588                 }
    589                 $y_max = $y;
    590             }
    591         }
    592         $prev_y_max = $y_max;
    593         $prev_char = $char;
    594     }
    595 
    596     #-------------------------------------------------------------------------
    597     # Print graph[][].
    598     #-------------------------------------------------------------------------
    599     my ($y_label, $y_unit) = B_max_label($peak_mem_total_szB);
    600     my ($x_label, $x_unit);
    601     if    ($time_unit eq "i")  { ($x_label, $x_unit) = i_max_label($end_time) }
    602     elsif ($time_unit eq "ms") { ($x_label, $x_unit) = t_max_label($end_time) }
    603     elsif ($time_unit eq "B")  { ($x_label, $x_unit) = B_max_label($end_time) }
    604     else                       { die "bad time_unit: $time_unit\n"; }
    605 
    606     printf("    %2s\n", $y_unit);
    607     for ($y = $graph_y; $y >= 0; $y--) {
    608         if ($graph_y == $y) {            # top row
    609             print($y_label);
    610         } elsif (0 == $y) {              # bottom row
    611             print("   0 ");
    612         } else {                         # anywhere else
    613             print("     ");
    614         }
    615           
    616         # Axis and data for the row.
    617         for ($x = 0; $x <= $graph_x; $x++) {
    618             printf("%s", $graph[$x][$y]);
    619         }
    620         if (0 == $y) {
    621             print("$x_unit\n");
    622         } else {
    623             print("\n");
    624         }
    625     }
    626     printf("     0%s%5s\n", ' ' x ($graph_x-5), $x_label);
    627 
    628     #-------------------------------------------------------------------------
    629     # Print snapshot numbers.
    630     #-------------------------------------------------------------------------
    631     print("\n");
    632     print("Number of snapshots: $n_snapshots\n");
    633     print(" Detailed snapshots: [");
    634     my $first_detailed = 1;
    635     for (my $i = 0; $i < $n_snapshots; $i++) {
    636         if ($is_detaileds[$i]) {
    637             if ($first_detailed) {
    638                 printf("$i");
    639                 $first_detailed = 0;
    640             } else {
    641                 printf(", $i");
    642             }
    643             if ($i == $peak_num) {
    644                 print(" (peak)");
    645             }
    646         }
    647     }
    648     print("]\n\n");
    649 
    650     #-------------------------------------------------------------------------
    651     # Print snapshots, from $tmp_file.
    652     #-------------------------------------------------------------------------
    653     open(TMPFILE, "< $tmp_file") 
    654          || die "Cannot open $tmp_file for reading\n";
    655 
    656     while (my $line = <TMPFILE>) {
    657         print($line);
    658     }
    659     unlink($tmp_file);
    660 }
    661 
    662 #-----------------------------------------------------------------------------
    663 # Misc functions
    664 #-----------------------------------------------------------------------------
    665 sub commify ($) {
    666     my ($val) = @_;
    667     1 while ($val =~ s/^(\d+)(\d{3})/$1,$2/);
    668     return $val;
    669 }
    670 
    671 
    672 #----------------------------------------------------------------------------
    673 # "main()"
    674 #----------------------------------------------------------------------------
    675 process_cmd_line();
    676 read_input_file();
    677 
    678 ##--------------------------------------------------------------------##
    679 ##--- end                                              ms_print.in ---##
    680 ##--------------------------------------------------------------------##
    681