diff options
Diffstat (limited to 'tools/bloatview/bloattable.pl')
-rwxr-xr-x | tools/bloatview/bloattable.pl | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/tools/bloatview/bloattable.pl b/tools/bloatview/bloattable.pl new file mode 100755 index 0000000000..e8acfabed3 --- /dev/null +++ b/tools/bloatview/bloattable.pl @@ -0,0 +1,590 @@ +#!/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; +} |