michael@0: #!/usr/bin/perl -w michael@0: # michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: # bloattable [-debug] [-source] [-byte n|-obj n|-ref n] ... > michael@0: # michael@0: # file1, file2, ... filen should be successive BloatView files generated from the same run. michael@0: # Summarize them in an HTML table. Output the HTML to the standard output. michael@0: # michael@0: # If -debug is set, create a slightly larger html file which is more suitable for debugging this script. michael@0: # If -source is set, create an html file that prints the html source as the output michael@0: # If -byte n, -obj n, or -ref n is given, make the page default to showing byte, object, or reference statistics, michael@0: # respectively, and sort by the nth column (n is zero-based, so the first column has n==0). michael@0: # michael@0: # See http://lxr.mozilla.org/mozilla/source/xpcom/doc/MemoryTools.html michael@0: michael@0: use 5.004; michael@0: use strict; michael@0: use diagnostics; michael@0: use File::Basename; michael@0: use Getopt::Long; michael@0: michael@0: # The generated HTML is almost entirely generated by a script. Only the , , and elements are explicit michael@0: # because a \n"; michael@0: print "\n\n"; michael@0: print "\n"; michael@0: if ($source) { michael@0: print "

"; michael@0: print quoteHTML "\n"; michael@0: print quoteHTML "\n"; michael@0: print quoteHTML "\n"; michael@0: print "\n"; michael@0: print quoteHTML "\n\n"; michael@0: print quoteHTML "\n"; michael@0: print "\n"; michael@0: print quoteHTML "\n"; michael@0: print quoteHTML "\n"; michael@0: print "

\n"; michael@0: } else { michael@0: print "\n"; michael@0: } michael@0: print "\n"; michael@0: print "\n"; michael@0: } michael@0: michael@0: michael@0: michael@0: # Read the bloat file into hash table $h. The hash table is indexed by class names; michael@0: # each entry is a list with the following elements: michael@0: # bytesAlloc Total number of bytes allocated michael@0: # bytesNet Total number of bytes allocated but not deallocated michael@0: # objectsAlloc Total number of objects allocated michael@0: # objectsNet Total number of objects allocated but not deallocated michael@0: # refsAlloc Total number of references AddRef'd michael@0: # refsNet Total number of references AddRef'd but not Released michael@0: # Except for TOTAL, all hash table entries refer to mutually exclusive data. michael@0: # $sizes is a hash table indexed by class names. Each entry of that table contains the class's instance size. michael@0: sub readBloatFile($\%\%) { michael@0: my ($file, $h, $sizes) = @_; michael@0: local $_; # Needed for 'while ()' below. michael@0: michael@0: my $readSomething = 0; michael@0: open FILE, $file; michael@0: while () { michael@0: if (my ($name, $size, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet) = michael@0: /^\s*(?:\d+)\s+([\w:]+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(-?\d+)\s*\([^()]*\)\s*(\d+)\s+(-?\d+)\s*\([^()]*\)\s*$/) { michael@0: my $bytesAlloc; michael@0: if ($name eq "TOTAL") { michael@0: $size = "undefined"; michael@0: $bytesAlloc = "undefined"; michael@0: } else { michael@0: $bytesAlloc = $objectsAlloc * $size; michael@0: if ($bytesNet != $objectsNet * $size) { michael@0: print STDERR "In '$file', class $name bytesNet != objectsNet * size: $bytesNet != $objectsNet * $size\n"; michael@0: } michael@0: } michael@0: print STDERR "Duplicate entry $name in '$file'\n" if $$h{$name}; michael@0: $$h{$name} = [$bytesAlloc, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet]; michael@0: michael@0: my $oldSize = $$sizes{$name}; michael@0: print STDERR "Mismatch of sizes of class $name: $oldSize and $size\n" if defined($oldSize) && $size ne $oldSize; michael@0: $$sizes{$name} = $size; michael@0: $readSomething = 1; michael@0: } elsif (/^\s*(?:\d+)\s+([\w:]+)\s/) { michael@0: print STDERR "Unable to parse '$file' line: $_"; michael@0: } michael@0: } michael@0: close FILE; michael@0: print STDERR "No data in '$file'\n" unless $readSomething; michael@0: return $h; michael@0: } michael@0: michael@0: michael@0: my %sizes; # => michael@0: my %tables; # => ; see readBloatFile for format of michael@0: michael@0: # Generate the JavaScript source code for the row named $c. $l can contain the initial entries of the row. michael@0: sub genTableRowSource($$) { michael@0: my ($l, $c) = @_; michael@0: my $lastE; michael@0: foreach (@ARGV) { michael@0: my $e = $tables{$_}{$c}; michael@0: if (defined($lastE) && !defined($e)) { michael@0: $e = [0,0,0,0,0,0]; michael@0: print STDERR "Class $c is defined in an earlier file but not in '$_'\n"; michael@0: } michael@0: if (defined $e) { michael@0: if (defined $lastE) { michael@0: for (my $i = 0; $i <= $#$e; $i++) { michael@0: my $n = $$e[$i]; michael@0: $l .= ($n eq "undefined" ? "undefined" : $n - $$lastE[$i]) . ","; michael@0: } michael@0: $l .= " "; michael@0: } else { michael@0: $l .= join(",", @$e) . ", "; michael@0: } michael@0: $lastE = $e; michael@0: } else { michael@0: $l .= "0,0,0,0,0,0, "; michael@0: } michael@0: } michael@0: $l .= join(",", @$lastE); michael@0: return "[$l]"; michael@0: } michael@0: michael@0: michael@0: michael@0: my $debug; michael@0: my $source; michael@0: my $showMode; michael@0: my $sortColumn; michael@0: my @modeOptions; michael@0: michael@0: GetOptions("debug" => \$debug, "source" => \$source, "byte=i" => \$modeOptions[0], "obj=i" => \$modeOptions[1], "ref=i" => \$modeOptions[2]); michael@0: for (my $i = 0; $i != 3; $i++) { michael@0: my $modeOption = $modeOptions[$i]; michael@0: if ($modeOption) { michael@0: die "Only one of -byte, -obj, or -ref may be given" if defined $showMode; michael@0: my $nFileColumns = scalar(@ARGV) + 1; michael@0: die "-byte, -obj, or -ref column number out of range" if $modeOption < 0 || $modeOption >= 2 + 2*$nFileColumns; michael@0: $showMode = $i; michael@0: if ($modeOption >= 2) { michael@0: $modeOption -= 2; michael@0: $sortColumn = 2 + $showMode*2; michael@0: if ($modeOption >= $nFileColumns) { michael@0: $sortColumn++; michael@0: $modeOption -= $nFileColumns; michael@0: } michael@0: $sortColumn += $modeOption*6; michael@0: } else { michael@0: $sortColumn = $modeOption; michael@0: } michael@0: } michael@0: } michael@0: unless (defined $showMode) { michael@0: $showMode = 0; michael@0: $sortColumn = 0; michael@0: } michael@0: michael@0: # Read all of the bloat files. michael@0: foreach (@ARGV) { michael@0: unless ($tables{$_}) { michael@0: my $f = $_; michael@0: my %table; michael@0: michael@0: readBloatFile $_, %table, %sizes; michael@0: $tables{$_} = \%table; michael@0: } michael@0: } michael@0: die "No input" unless %sizes; michael@0: michael@0: my @scriptData; # JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript. michael@0: my @persistentScriptData; # Same as @scriptData, but persists the page reloads itself. michael@0: michael@0: # Print a list of bloat file names. michael@0: push @persistentScriptData, "var nFiles = " . scalar(@ARGV) . ";"; michael@0: push @persistentScriptData, "var fileTags = [" . join(", ", map {singleQuoteString substr(fileCoreName($_), -10)} @ARGV) . "];"; michael@0: push @persistentScriptData, "var fileNames = [" . join(", ", map {singleQuoteString $_} @ARGV) . "];"; michael@0: push @persistentScriptData, "var fileDates = [" . join(", ", map {singleQuoteString localtime fileModDate $_} @ARGV) . "];"; michael@0: michael@0: # Print the bloat tables. michael@0: push @persistentScriptData, "var totals = " . genTableRowSource('"TOTAL", undefined, ', "TOTAL") . ";"; michael@0: push @scriptData, "var classTables = ["; michael@0: delete $sizes{"TOTAL"}; michael@0: my @classes = sort(keys %sizes); michael@0: for (my $i = 0; $i <= $#classes; $i++) { michael@0: my $c = $classes[$i]; michael@0: push @scriptData, genTableRowSource(doubleQuoteString($c).", ".$sizes{$c}.", ", $c) . ($i == $#classes ? "];" : ","); michael@0: } michael@0: michael@0: generate(@scriptData, @persistentScriptData, $debug, $source, $showMode, $sortColumn); michael@0: 1; michael@0: michael@0: michael@0: # The source of the eval'd JavaScript follows. michael@0: # Comments starting with // that are alone on a line are stripped by the Perl script. michael@0: __END__ michael@0: michael@0: // showMode: 0=bytes, 1=objects, 2=references michael@0: var showMode; michael@0: var modeName; michael@0: var modeNameUpper; michael@0: michael@0: var sortColumn; michael@0: michael@0: // Sort according to the sortColumn. Column 0 is sorted alphabetically in ascending order. michael@0: // All other columns are sorted numerically in descending order, with column 0 used for a secondary sort. michael@0: // Undefined is always listed last. michael@0: function sortCompare(x, y) { michael@0: if (sortColumn) { michael@0: var xc = x[sortColumn]; michael@0: var yc = y[sortColumn]; michael@0: if (xc < yc || xc === undefined && yc !== undefined) return 1; michael@0: if (yc < xc || yc === undefined && xc !== undefined) return -1; michael@0: } michael@0: michael@0: var x0 = x[0]; michael@0: var y0 = y[0]; michael@0: if (x0 > y0 || x0 === undefined && y0 !== undefined) return 1; michael@0: if (y0 > x0 || y0 === undefined && x0 !== undefined) return -1; michael@0: return 0; michael@0: } michael@0: michael@0: michael@0: // Quote special HTML characters in the string. michael@0: function quoteHTML(s) { michael@0: s = s.replace(/&/g, '&'); michael@0: // Can't use //g, '>'); michael@0: s = s.replace(/ /g, ' '); michael@0: return s; michael@0: } michael@0: michael@0: michael@0: function writeFileTable(d) { michael@0: d.writeln(''); michael@0: d.writeln('\n\n\n\n'); michael@0: for (var i = 0; i < nFiles; i++) michael@0: d.writeln('\n\n\n\n'); michael@0: d.writeln('
NameFileDate
'+quoteHTML(fileTags[i])+''+quoteHTML(fileNames[i])+''+quoteHTML(fileDates[i])+'
'); michael@0: } michael@0: michael@0: michael@0: function writeReloadLink(d, column, s, rowspan) { michael@0: d.write(rowspan == 1 ? '' : ''); michael@0: if (column != sortColumn) michael@0: d.write(''); michael@0: d.write(s); michael@0: if (column != sortColumn) michael@0: d.write(''); michael@0: d.writeln(''); michael@0: } michael@0: michael@0: function writeClassTableRow(d, row, base, modeName) { michael@0: if (modeName) { michael@0: d.writeln('\n'+modeName+''); michael@0: } else { michael@0: d.writeln('\n'+quoteHTML(row[0])+''); michael@0: var v = row[1]; michael@0: d.writeln(''+(v === undefined ? '' : v)+''); michael@0: } michael@0: for (var i = 0; i != 2; i++) { michael@0: var c = base + i; michael@0: for (var j = 0; j <= nFiles; j++) { michael@0: v = row[c]; michael@0: var style = 'num'; michael@0: if (j != nFiles) michael@0: if (v > 0) { michael@0: style = 'pos'; michael@0: v = '+'+v; michael@0: } else michael@0: style = 'neg'; michael@0: d.writeln(''+(v === undefined ? '' : v)+''); michael@0: c += 6; michael@0: } michael@0: } michael@0: d.writeln(''); michael@0: } michael@0: michael@0: function writeClassTable(d) { michael@0: var base = 2 + showMode*2; michael@0: michael@0: // Make a copy because a sort is destructive. michael@0: var table = classTables.concat(); michael@0: table.sort(sortCompare); michael@0: michael@0: d.writeln(''); michael@0: michael@0: d.writeln(''); michael@0: writeReloadLink(d, 0, 'Class Name', 2); michael@0: writeReloadLink(d, 1, 'Instance
Size', 2); michael@0: d.writeln(''); michael@0: d.writeln('\n'); michael@0: d.writeln(''); michael@0: for (var i = 0; i != 2; i++) { michael@0: var c = base + i; michael@0: for (var j = 0; j <= nFiles; j++) { michael@0: writeReloadLink(d, c, j == nFiles ? 'Total' : quoteHTML(fileTags[j]), 1); michael@0: c += 6; michael@0: } michael@0: } michael@0: d.writeln(''); michael@0: michael@0: writeClassTableRow(d, totals, base, 0); michael@0: for (var r = 0; r < table.length; r++) michael@0: writeClassTableRow(d, table[r], base, 0); michael@0: michael@0: d.writeln('
'+modeNameUpper+'s allocated'+modeNameUpper+'s allocated but not freed
'); michael@0: } michael@0: michael@0: michael@0: var modeNames = ["byte", "object", "reference"]; michael@0: var modeNamesUpper = ["Byte", "Object", "Reference"]; michael@0: var styleSheet = ''; michael@0: michael@0: michael@0: function showHead(d) { michael@0: modeName = modeNames[showMode]; michael@0: modeNameUpper = modeNamesUpper[showMode]; michael@0: d.writeln(''+modeNameUpper+' Bloats'); michael@0: d.writeln(styleSheet); michael@0: } michael@0: michael@0: function showBody(d) { michael@0: d.writeln('

'+modeNameUpper+' Bloats

'); michael@0: writeFileTable(d); michael@0: d.write('
'); michael@0: for (var i = 0; i != 3; i++) michael@0: if (i != showMode) { michael@0: var newSortColumn = sortColumn; michael@0: if (sortColumn >= 2) michael@0: newSortColumn = sortColumn + (i-showMode)*2; michael@0: d.write(''); michael@0: } michael@0: d.writeln('
'); michael@0: d.writeln('

The numbers do not include malloc\'d data such as string contents.

'); michael@0: d.writeln('

Click on a column heading to sort by that column. Click on a class name to see details for that class.

'); michael@0: writeClassTable(d); michael@0: } michael@0: michael@0: michael@0: function showRowDetail(rowName) { michael@0: var row; michael@0: var i; michael@0: michael@0: if (rowName == "TOTAL") michael@0: row = totals; michael@0: else { michael@0: for (i = 0; i < classTables.length; i++) michael@0: if (rowName == classTables[i][0]) { michael@0: row = classTables[i]; michael@0: break; michael@0: } michael@0: } michael@0: if (row) { michael@0: var w = window.open("", "ClassTableRowDetails"); michael@0: var d = w.document; michael@0: d.open(); michael@0: d.writeln(''); michael@0: d.writeln('\n\n'+quoteHTML(rowName)+' bloat details'); michael@0: d.writeln(styleSheet); michael@0: d.writeln('\n\n'); michael@0: d.writeln('

'+quoteHTML(rowName)+'

'); michael@0: if (row[1] !== undefined) michael@0: d.writeln('

Each instance has '+row[1]+' bytes.

'); michael@0: michael@0: d.writeln(''); michael@0: d.writeln('\n\n'); michael@0: d.writeln('\n'); michael@0: d.writeln('\n'); michael@0: for (i = 0; i != 2; i++) michael@0: for (var j = 0; j <= nFiles; j++) michael@0: d.writeln(''); michael@0: d.writeln(''); michael@0: michael@0: for (i = 0; i != 3; i++) michael@0: writeClassTableRow(d, row, 2+i*2, modeNamesUpper[i]+'s'); michael@0: michael@0: d.writeln('
AllocatedAllocated but not freed
'+(j == nFiles ? 'Total' : quoteHTML(fileTags[j]))+'
\n\n'); michael@0: d.close(); michael@0: } michael@0: return undefined; michael@0: } michael@0: michael@0: michael@0: function stringSource(s) { michael@0: s = s.replace(/\\/g, '\\\\'); michael@0: s = s.replace(/"/g, '\\"'); michael@0: s = s.replace(/<\//g, '<\\/'); michael@0: return '"'+s+'"'; michael@0: } michael@0: michael@0: function reloadSelf(n,m) { michael@0: // Need to cache these because globals go away on document.open(). michael@0: var sa = srcArray; michael@0: var ss = stringSource; michael@0: var ct = classTables; michael@0: var i; michael@0: michael@0: document.open(); michael@0: // Uncomment this and comment the document.open() line above to see the reloaded page's source. michael@0: //var w = window.open("", "NewDoc"); michael@0: //var d = w.document; michael@0: //var document = new Object; michael@0: //document.write = function () { michael@0: // for (var i = 0; i < arguments.length; i++) { michael@0: // var s = arguments[i].toString(); michael@0: // s = s.replace(/&/g, '&'); michael@0: // s = s.replace(/\x3C/g, '<'); michael@0: // s = s.replace(/>/g, '>'); michael@0: // s = s.replace(/ /g, ' '); michael@0: // d.write(s); michael@0: // } michael@0: //}; michael@0: //document.writeln = function () { michael@0: // for (var i = 0; i < arguments.length; i++) { michael@0: // var s = arguments[i].toString(); michael@0: // s = s.replace(/&/g, '&'); michael@0: // s = s.replace(/\x3C/g, '<'); michael@0: // s = s.replace(/>/g, '>'); michael@0: // s = s.replace(/ /g, ' '); michael@0: // d.write(s); michael@0: // } michael@0: // d.writeln('
'); michael@0: //}; michael@0: michael@0: document.writeln(''); michael@0: document.writeln('\n\n\n\n\n\n\n\n'); michael@0: document.close(); michael@0: return undefined; michael@0: }