#!/usr/bin/perl -w # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # bloattable [-debug] [-source] [-byte n|-obj n|-ref n] <file1> <file2> ... <filen> > <html-file> # # file1, file2, ... filen should be successive BloatView files generated from the same run. # Summarize them in an HTML table. Output the HTML to the standard output. # # If -debug is set, create a slightly larger html file which is more suitable for debugging this script. # If -source is set, create an html file that prints the html source as the output # If -byte n, -obj n, or -ref n is given, make the page default to showing byte, object, or reference statistics, # respectively, and sort by the nth column (n is zero-based, so the first column has n==0). # # See http://lxr.mozilla.org/mozilla/source/xpcom/doc/MemoryTools.html use 5.004; use strict; use diagnostics; use File::Basename; use Getopt::Long; # The generated HTML is almost entirely generated by a script. Only the <HTML>, <HEAD>, and <BODY> elements are explicit # because a <SCRIPT> element cannot officially be a direct descendant of an <HTML> element. # The script itself is almost all generated by an eval of a large string. This allows the script to reproduce itself # when making a new page using document.write's. Re-sorting the page causes it to regenerate itself in this way. # Return the file's modification date. sub fileModDate($) { my ($pathName) = @_; my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat $pathName or die "Can't stat '$pathName'"; return $mtime; } sub fileCoreName($) { my ($pathName) = @_; my $fileName = basename($pathName, ""); $fileName =~ s/\..*//; return $fileName; } # Convert a raw string into a single-quoted JavaScript string. sub singleQuoteString($) { local ($_) = @_; s/\\/\\\\/g; s/'/\\'/g; s/\n/\\n/g; s/<\//<\\\//g; return "'$_'"; } # Convert a raw string into a double-quoted JavaScript string. sub doubleQuoteString($) { local ($_) = @_; s/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g; s/<\//<\\\//g; return "\"$_\""; } # Quote special HTML characters in the string. sub quoteHTML($) { local ($_) = @_; s/\&/&/g; s/</</g; s/>/>/g; s/ / /g; s/\n/<BR>\n/g; return $_; } # Write the generated page to the standard output. # The script source code is read from this file past the __END__ marker # @$scriptData is the JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript. # @$persistentScriptData is the same as @scriptData, but persists when the page reloads itself. # If $debug is true, generate the script directly instead of having it eval itself. # If $source is true, generate a script that displays the page's source instead of the page itself. sub generate(\@\@$$$$) { my ($scriptData, $persistentScriptData, $debug, $source, $showMode, $sortColumn) = @_; my @scriptSource = <DATA>; chomp @scriptSource; print <<'EOS'; <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> <HTML> <HEAD> <SCRIPT type="text/javascript"> EOS foreach (@$scriptData) {print "$_\n";} print "\n"; print "var srcArray = [\n"; my @quotedScriptSource = map { my $line = $_; $line =~ s/^\s+//g; # $line =~ s/^\/\/SOURCE\s+//g if $source; $line =~ s/^\/\/.*//g; $line =~ s/\s+$//g; $line eq "" ? () : $line } @$persistentScriptData, @scriptSource; my $lastQuotedLine = pop @quotedScriptSource; foreach (@quotedScriptSource) {print doubleQuoteString($_), ",\n";} print doubleQuoteString($lastQuotedLine), "];\n\n"; if ($debug) { push @quotedScriptSource, $lastQuotedLine; foreach (@quotedScriptSource) { s/<\//<\\\//g; # This fails if a regexp ends with a '<'. Oh well.... print "$_\n"; } print "\n"; } else { print "eval(srcArray.join(\"\\n\"));\n\n"; } print "showMode = $showMode;\n"; print "sortColumn = $sortColumn;\n"; if ($source) { print <<'EOS'; function writeQuotedHTML(s) { document.write(quoteHTML(s.toString()).replace(/\n/g, '<BR>\n')); } var quotingDocument = { write: function () { for (var i = 0; i < arguments.length; i++) writeQuotedHTML(arguments[i]); }, writeln: function () { for (var i = 0; i < arguments.length; i++) writeQuotedHTML(arguments[i]); document.writeln('<BR>'); } }; EOS } else { print "showHead(document);\n"; } print "</SCRIPT>\n"; print "</HEAD>\n\n"; print "<BODY>\n"; if ($source) { print "<P><TT>"; print quoteHTML "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n"; print quoteHTML "<HTML>\n"; print quoteHTML "<HEAD>\n"; print "<SCRIPT type=\"text/javascript\">showHead(quotingDocument);</SCRIPT>\n"; print quoteHTML "</HEAD>\n\n"; print quoteHTML "<BODY>\n"; print "<SCRIPT type=\"text/javascript\">showBody(quotingDocument);</SCRIPT>\n"; print quoteHTML "</BODY>\n"; print quoteHTML "</HTML>\n"; print "</TT></P>\n"; } else { print "<SCRIPT type=\"text/javascript\">showBody(document);</SCRIPT>\n"; } print "</BODY>\n"; print "</HTML>\n"; } # Read the bloat file into hash table $h. The hash table is indexed by class names; # each entry is a list with the following elements: # bytesAlloc Total number of bytes allocated # bytesNet Total number of bytes allocated but not deallocated # objectsAlloc Total number of objects allocated # objectsNet Total number of objects allocated but not deallocated # refsAlloc Total number of references AddRef'd # refsNet Total number of references AddRef'd but not Released # Except for TOTAL, all hash table entries refer to mutually exclusive data. # $sizes is a hash table indexed by class names. Each entry of that table contains the class's instance size. sub readBloatFile($\%\%) { my ($file, $h, $sizes) = @_; local $_; # Needed for 'while (<FILE>)' below. my $readSomething = 0; open FILE, $file; while (<FILE>) { if (my ($name, $size, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet) = /^\s*(?:\d+)\s+([\w:]+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(-?\d+)\s*\([^()]*\)\s*(\d+)\s+(-?\d+)\s*\([^()]*\)\s*$/) { my $bytesAlloc; if ($name eq "TOTAL") { $size = "undefined"; $bytesAlloc = "undefined"; } else { $bytesAlloc = $objectsAlloc * $size; if ($bytesNet != $objectsNet * $size) { print STDERR "In '$file', class $name bytesNet != objectsNet * size: $bytesNet != $objectsNet * $size\n"; } } print STDERR "Duplicate entry $name in '$file'\n" if $$h{$name}; $$h{$name} = [$bytesAlloc, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet]; my $oldSize = $$sizes{$name}; print STDERR "Mismatch of sizes of class $name: $oldSize and $size\n" if defined($oldSize) && $size ne $oldSize; $$sizes{$name} = $size; $readSomething = 1; } elsif (/^\s*(?:\d+)\s+([\w:]+)\s/) { print STDERR "Unable to parse '$file' line: $_"; } } close FILE; print STDERR "No data in '$file'\n" unless $readSomething; return $h; } my %sizes; # <class-name> => <instance-size> my %tables; # <file-name> => <bloat-table>; see readBloatFile for format of <bloat-table> # Generate the JavaScript source code for the row named $c. $l can contain the initial entries of the row. sub genTableRowSource($$) { my ($l, $c) = @_; my $lastE; foreach (@ARGV) { my $e = $tables{$_}{$c}; if (defined($lastE) && !defined($e)) { $e = [0,0,0,0,0,0]; print STDERR "Class $c is defined in an earlier file but not in '$_'\n"; } if (defined $e) { if (defined $lastE) { for (my $i = 0; $i <= $#$e; $i++) { my $n = $$e[$i]; $l .= ($n eq "undefined" ? "undefined" : $n - $$lastE[$i]) . ","; } $l .= " "; } else { $l .= join(",", @$e) . ", "; } $lastE = $e; } else { $l .= "0,0,0,0,0,0, "; } } $l .= join(",", @$lastE); return "[$l]"; } my $debug; my $source; my $showMode; my $sortColumn; my @modeOptions; GetOptions("debug" => \$debug, "source" => \$source, "byte=i" => \$modeOptions[0], "obj=i" => \$modeOptions[1], "ref=i" => \$modeOptions[2]); for (my $i = 0; $i != 3; $i++) { my $modeOption = $modeOptions[$i]; if ($modeOption) { die "Only one of -byte, -obj, or -ref may be given" if defined $showMode; my $nFileColumns = scalar(@ARGV) + 1; die "-byte, -obj, or -ref column number out of range" if $modeOption < 0 || $modeOption >= 2 + 2*$nFileColumns; $showMode = $i; if ($modeOption >= 2) { $modeOption -= 2; $sortColumn = 2 + $showMode*2; if ($modeOption >= $nFileColumns) { $sortColumn++; $modeOption -= $nFileColumns; } $sortColumn += $modeOption*6; } else { $sortColumn = $modeOption; } } } unless (defined $showMode) { $showMode = 0; $sortColumn = 0; } # Read all of the bloat files. foreach (@ARGV) { unless ($tables{$_}) { my $f = $_; my %table; readBloatFile $_, %table, %sizes; $tables{$_} = \%table; } } die "No input" unless %sizes; my @scriptData; # JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript. my @persistentScriptData; # Same as @scriptData, but persists the page reloads itself. # Print a list of bloat file names. push @persistentScriptData, "var nFiles = " . scalar(@ARGV) . ";"; push @persistentScriptData, "var fileTags = [" . join(", ", map {singleQuoteString substr(fileCoreName($_), -10)} @ARGV) . "];"; push @persistentScriptData, "var fileNames = [" . join(", ", map {singleQuoteString $_} @ARGV) . "];"; push @persistentScriptData, "var fileDates = [" . join(", ", map {singleQuoteString localtime fileModDate $_} @ARGV) . "];"; # Print the bloat tables. push @persistentScriptData, "var totals = " . genTableRowSource('"TOTAL", undefined, ', "TOTAL") . ";"; push @scriptData, "var classTables = ["; delete $sizes{"TOTAL"}; my @classes = sort(keys %sizes); for (my $i = 0; $i <= $#classes; $i++) { my $c = $classes[$i]; push @scriptData, genTableRowSource(doubleQuoteString($c).", ".$sizes{$c}.", ", $c) . ($i == $#classes ? "];" : ","); } generate(@scriptData, @persistentScriptData, $debug, $source, $showMode, $sortColumn); 1; # The source of the eval'd JavaScript follows. # Comments starting with // that are alone on a line are stripped by the Perl script. __END__ // showMode: 0=bytes, 1=objects, 2=references var showMode; var modeName; var modeNameUpper; var sortColumn; // Sort according to the sortColumn. Column 0 is sorted alphabetically in ascending order. // All other columns are sorted numerically in descending order, with column 0 used for a secondary sort. // Undefined is always listed last. function sortCompare(x, y) { if (sortColumn) { var xc = x[sortColumn]; var yc = y[sortColumn]; if (xc < yc || xc === undefined && yc !== undefined) return 1; if (yc < xc || yc === undefined && xc !== undefined) return -1; } var x0 = x[0]; var y0 = y[0]; if (x0 > y0 || x0 === undefined && y0 !== undefined) return 1; if (y0 > x0 || y0 === undefined && x0 !== undefined) return -1; return 0; } // Quote special HTML characters in the string. function quoteHTML(s) { s = s.replace(/&/g, '&'); // Can't use /</g because HTML interprets '</g' as ending the script! s = s.replace(/\x3C/g, '<'); s = s.replace(/>/g, '>'); s = s.replace(/ /g, ' '); return s; } function writeFileTable(d) { d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>'); d.writeln('<TR>\n<TH>Name</TH>\n<TH>File</TH>\n<TH>Date</TH>\n</TR>'); for (var i = 0; i < nFiles; i++) d.writeln('<TR>\n<TD>'+quoteHTML(fileTags[i])+'</TD>\n<TD><TT>'+quoteHTML(fileNames[i])+'</TT></TD>\n<TD>'+quoteHTML(fileDates[i])+'</TD>\n</TR>'); d.writeln('</TABLE>'); } function writeReloadLink(d, column, s, rowspan) { d.write(rowspan == 1 ? '<TH>' : '<TH rowspan='+rowspan+'>'); if (column != sortColumn) d.write('<A href="javascript:reloadSelf('+column+','+showMode+')">'); d.write(s); if (column != sortColumn) d.write('</A>'); d.writeln('</TH>'); } function writeClassTableRow(d, row, base, modeName) { if (modeName) { d.writeln('<TR>\n<TH>'+modeName+'</TH>'); } else { d.writeln('<TR>\n<TD><A href="javascript:showRowDetail(\''+row[0]+'\')">'+quoteHTML(row[0])+'</A></TD>'); var v = row[1]; d.writeln('<TD class=num>'+(v === undefined ? '' : v)+'</TD>'); } for (var i = 0; i != 2; i++) { var c = base + i; for (var j = 0; j <= nFiles; j++) { v = row[c]; var style = 'num'; if (j != nFiles) if (v > 0) { style = 'pos'; v = '+'+v; } else style = 'neg'; d.writeln('<TD class='+style+'>'+(v === undefined ? '' : v)+'</TD>'); c += 6; } } d.writeln('</TR>'); } function writeClassTable(d) { var base = 2 + showMode*2; // Make a copy because a sort is destructive. var table = classTables.concat(); table.sort(sortCompare); d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>'); d.writeln('<TR>'); writeReloadLink(d, 0, 'Class Name', 2); writeReloadLink(d, 1, 'Instance<BR>Size', 2); d.writeln('<TH colspan='+(nFiles+1)+'>'+modeNameUpper+'s allocated</TH>'); d.writeln('<TH colspan='+(nFiles+1)+'>'+modeNameUpper+'s allocated but not freed</TH>\n</TR>'); d.writeln('<TR>'); for (var i = 0; i != 2; i++) { var c = base + i; for (var j = 0; j <= nFiles; j++) { writeReloadLink(d, c, j == nFiles ? 'Total' : quoteHTML(fileTags[j]), 1); c += 6; } } d.writeln('</TR>'); writeClassTableRow(d, totals, base, 0); for (var r = 0; r < table.length; r++) writeClassTableRow(d, table[r], base, 0); d.writeln('</TABLE>'); } var modeNames = ["byte", "object", "reference"]; var modeNamesUpper = ["Byte", "Object", "Reference"]; var styleSheet = '<STYLE type="TEXT/CSS">\n'+ 'BODY {background-color: #FFFFFF; color: #000000}\n'+ '.num {text-align: right}\n'+ '.pos {text-align: right; color: #CC0000}\n'+ '.neg {text-align: right; color: #009900}\n'+ '</STYLE>'; function showHead(d) { modeName = modeNames[showMode]; modeNameUpper = modeNamesUpper[showMode]; d.writeln('<TITLE>'+modeNameUpper+' Bloats</TITLE>'); d.writeln(styleSheet); } function showBody(d) { d.writeln('<H1>'+modeNameUpper+' Bloats</H1>'); writeFileTable(d); d.write('<FORM>'); for (var i = 0; i != 3; i++) if (i != showMode) { var newSortColumn = sortColumn; if (sortColumn >= 2) newSortColumn = sortColumn + (i-showMode)*2; d.write('<INPUT type="button" value="Show '+modeNamesUpper[i]+'s" onClick="reloadSelf('+newSortColumn+','+i+')">'); } d.writeln('</FORM>'); d.writeln('<P>The numbers do not include <CODE>malloc</CODE>\'d data such as string contents.</P>'); d.writeln('<P>Click on a column heading to sort by that column. Click on a class name to see details for that class.</P>'); writeClassTable(d); } function showRowDetail(rowName) { var row; var i; if (rowName == "TOTAL") row = totals; else { for (i = 0; i < classTables.length; i++) if (rowName == classTables[i][0]) { row = classTables[i]; break; } } if (row) { var w = window.open("", "ClassTableRowDetails"); var d = w.document; d.open(); d.writeln('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">'); d.writeln('<HTML>\n<HEAD>\n<TITLE>'+quoteHTML(rowName)+' bloat details</TITLE>'); d.writeln(styleSheet); d.writeln('</HEAD>\n\n<BODY>'); d.writeln('<H2>'+quoteHTML(rowName)+'</H2>'); if (row[1] !== undefined) d.writeln('<P>Each instance has '+row[1]+' bytes.</P>'); d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>'); d.writeln('<TR>\n<TH></TH>\n<TH colspan='+(nFiles+1)+'>Allocated</TH>'); d.writeln('<TH colspan='+(nFiles+1)+'>Allocated but not freed</TH>\n</TR>'); d.writeln('<TR>\n<TH></TH>'); for (i = 0; i != 2; i++) for (var j = 0; j <= nFiles; j++) d.writeln('<TH>'+(j == nFiles ? 'Total' : quoteHTML(fileTags[j]))+'</TH>'); d.writeln('</TR>'); for (i = 0; i != 3; i++) writeClassTableRow(d, row, 2+i*2, modeNamesUpper[i]+'s'); d.writeln('</TABLE>\n</BODY>\n</HTML>'); d.close(); } return undefined; } function stringSource(s) { s = s.replace(/\\/g, '\\\\'); s = s.replace(/"/g, '\\"'); s = s.replace(/<\//g, '<\\/'); return '"'+s+'"'; } function reloadSelf(n,m) { // Need to cache these because globals go away on document.open(). var sa = srcArray; var ss = stringSource; var ct = classTables; var i; document.open(); // Uncomment this and comment the document.open() line above to see the reloaded page's source. //var w = window.open("", "NewDoc"); //var d = w.document; //var document = new Object; //document.write = function () { // for (var i = 0; i < arguments.length; i++) { // var s = arguments[i].toString(); // s = s.replace(/&/g, '&'); // s = s.replace(/\x3C/g, '<'); // s = s.replace(/>/g, '>'); // s = s.replace(/ /g, ' '); // d.write(s); // } //}; //document.writeln = function () { // for (var i = 0; i < arguments.length; i++) { // var s = arguments[i].toString(); // s = s.replace(/&/g, '&'); // s = s.replace(/\x3C/g, '<'); // s = s.replace(/>/g, '>'); // s = s.replace(/ /g, ' '); // d.write(s); // } // d.writeln('<BR>'); //}; document.writeln('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">'); document.writeln('<HTML>\n<HEAD>\n<SCRIPT type="text/javascript">'); // Manually copy non-persistent script data if (!ct.length) document.writeln('var classTables = [];'); else { document.writeln('var classTables = ['); for (i = 0; i < ct.length; i++) { var row = ct[i]; document.write('[' + ss(row[0])); for (var j = 1; j < row.length; j++) document.write(',' + row[j]); document.writeln(']' + (i == ct.length-1 ? '];' : ',')); } } document.writeln('var srcArray = ['); for (i = 0; i < sa.length; i++) { document.write(ss(sa[i])); if (i != sa.length-1) document.writeln(','); } document.writeln('];'); document.writeln('eval(srcArray.join("\\n"));'); document.writeln('showMode = '+m+';'); document.writeln('sortColumn = '+n+';'); document.writeln('showHead(document);'); document.writeln('</SCRIPT>\n</HEAD>\n\n<BODY>\n<SCRIPT type="text/javascript">showBody(document);</SCRIPT>\n</BODY>\n</HTML>'); document.close(); return undefined; }