1#!/usr/bin/perl -w 2# 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7# bloattable [-debug] [-source] [-byte n|-obj n|-ref n] <file1> <file2> ... <filen> > <html-file> 8# 9# file1, file2, ... filen should be successive BloatView files generated from the same run. 10# Summarize them in an HTML table. Output the HTML to the standard output. 11# 12# If -debug is set, create a slightly larger html file which is more suitable for debugging this script. 13# If -source is set, create an html file that prints the html source as the output 14# If -byte n, -obj n, or -ref n is given, make the page default to showing byte, object, or reference statistics, 15# respectively, and sort by the nth column (n is zero-based, so the first column has n==0). 16# 17# See http://lxr.mozilla.org/mozilla/source/xpcom/doc/MemoryTools.html 18 19use 5.004; 20use strict; 21use diagnostics; 22use File::Basename; 23use Getopt::Long; 24 25# The generated HTML is almost entirely generated by a script. Only the <HTML>, <HEAD>, and <BODY> elements are explicit 26# because a <SCRIPT> element cannot officially be a direct descendant of an <HTML> element. 27# The script itself is almost all generated by an eval of a large string. This allows the script to reproduce itself 28# when making a new page using document.write's. Re-sorting the page causes it to regenerate itself in this way. 29 30 31 32# Return the file's modification date. 33sub fileModDate($) { 34 my ($pathName) = @_; 35 my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = 36 stat $pathName or die "Can't stat '$pathName'"; 37 return $mtime; 38} 39 40 41sub fileCoreName($) { 42 my ($pathName) = @_; 43 my $fileName = basename($pathName, ""); 44 $fileName =~ s/\..*//; 45 return $fileName; 46} 47 48 49# Convert a raw string into a single-quoted JavaScript string. 50sub singleQuoteString($) { 51 local ($_) = @_; 52 s/\\/\\\\/g; 53 s/'/\\'/g; 54 s/\n/\\n/g; 55 s/<\//<\\\//g; 56 return "'$_'"; 57} 58 59 60# Convert a raw string into a double-quoted JavaScript string. 61sub doubleQuoteString($) { 62 local ($_) = @_; 63 s/\\/\\\\/g; 64 s/"/\\"/g; 65 s/\n/\\n/g; 66 s/<\//<\\\//g; 67 return "\"$_\""; 68} 69 70 71# Quote special HTML characters in the string. 72sub quoteHTML($) { 73 local ($_) = @_; 74 s/\&/&/g; 75 s/</</g; 76 s/>/>/g; 77 s/ / /g; 78 s/\n/<BR>\n/g; 79 return $_; 80} 81 82 83# Write the generated page to the standard output. 84# The script source code is read from this file past the __END__ marker 85# @$scriptData is the JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript. 86# @$persistentScriptData is the same as @scriptData, but persists when the page reloads itself. 87# If $debug is true, generate the script directly instead of having it eval itself. 88# If $source is true, generate a script that displays the page's source instead of the page itself. 89sub generate(\@\@$$$$) { 90 my ($scriptData, $persistentScriptData, $debug, $source, $showMode, $sortColumn) = @_; 91 92 my @scriptSource = <DATA>; 93 chomp @scriptSource; 94 print <<'EOS'; 95<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> 96<HTML> 97<HEAD> 98<SCRIPT type="text/javascript"> 99EOS 100 101 foreach (@$scriptData) {print "$_\n";} 102 print "\n"; 103 104 print "var srcArray = [\n"; 105 my @quotedScriptSource = map { 106 my $line = $_; 107 $line =~ s/^\s+//g; 108 # $line =~ s/^\/\/SOURCE\s+//g if $source; 109 $line =~ s/^\/\/.*//g; 110 $line =~ s/\s+$//g; 111 $line eq "" ? () : $line 112 } @$persistentScriptData, @scriptSource; 113 my $lastQuotedLine = pop @quotedScriptSource; 114 foreach (@quotedScriptSource) {print doubleQuoteString($_), ",\n";} 115 print doubleQuoteString($lastQuotedLine), "];\n\n"; 116 117 if ($debug) { 118 push @quotedScriptSource, $lastQuotedLine; 119 foreach (@quotedScriptSource) { 120 s/<\//<\\\//g; # This fails if a regexp ends with a '<'. Oh well.... 121 print "$_\n"; 122 } 123 print "\n"; 124 } else { 125 print "eval(srcArray.join(\"\\n\"));\n\n"; 126 } 127 print "showMode = $showMode;\n"; 128 print "sortColumn = $sortColumn;\n"; 129 if ($source) { 130 print <<'EOS'; 131function writeQuotedHTML(s) { 132 document.write(quoteHTML(s.toString()).replace(/\n/g, '<BR>\n')); 133} 134 135var quotingDocument = { 136 write: function () { 137 for (var i = 0; i < arguments.length; i++) 138 writeQuotedHTML(arguments[i]); 139 }, 140 writeln: function () { 141 for (var i = 0; i < arguments.length; i++) 142 writeQuotedHTML(arguments[i]); 143 document.writeln('<BR>'); 144 } 145}; 146EOS 147 } else { 148 print "showHead(document);\n"; 149 } 150 print "</SCRIPT>\n"; 151 print "</HEAD>\n\n"; 152 print "<BODY>\n"; 153 if ($source) { 154 print "<P><TT>"; 155 print quoteHTML "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n"; 156 print quoteHTML "<HTML>\n"; 157 print quoteHTML "<HEAD>\n"; 158 print "<SCRIPT type=\"text/javascript\">showHead(quotingDocument);</SCRIPT>\n"; 159 print quoteHTML "</HEAD>\n\n"; 160 print quoteHTML "<BODY>\n"; 161 print "<SCRIPT type=\"text/javascript\">showBody(quotingDocument);</SCRIPT>\n"; 162 print quoteHTML "</BODY>\n"; 163 print quoteHTML "</HTML>\n"; 164 print "</TT></P>\n"; 165 } else { 166 print "<SCRIPT type=\"text/javascript\">showBody(document);</SCRIPT>\n"; 167 } 168 print "</BODY>\n"; 169 print "</HTML>\n"; 170} 171 172 173 174# Read the bloat file into hash table $h. The hash table is indexed by class names; 175# each entry is a list with the following elements: 176# bytesAlloc Total number of bytes allocated 177# bytesNet Total number of bytes allocated but not deallocated 178# objectsAlloc Total number of objects allocated 179# objectsNet Total number of objects allocated but not deallocated 180# refsAlloc Total number of references AddRef'd 181# refsNet Total number of references AddRef'd but not Released 182# Except for TOTAL, all hash table entries refer to mutually exclusive data. 183# $sizes is a hash table indexed by class names. Each entry of that table contains the class's instance size. 184sub readBloatFile($\%\%) { 185 my ($file, $h, $sizes) = @_; 186 local $_; # Needed for 'while (<FILE>)' below. 187 188 my $readSomething = 0; 189 open FILE, $file; 190 while (<FILE>) { 191 if (my ($name, $size, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet) = 192 /^\s*(?:\d+)\s+([\w:]+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(-?\d+)\s*\([^()]*\)\s*(\d+)\s+(-?\d+)\s*\([^()]*\)\s*$/) { 193 my $bytesAlloc; 194 if ($name eq "TOTAL") { 195 $size = "undefined"; 196 $bytesAlloc = "undefined"; 197 } else { 198 $bytesAlloc = $objectsAlloc * $size; 199 if ($bytesNet != $objectsNet * $size) { 200 print STDERR "In '$file', class $name bytesNet != objectsNet * size: $bytesNet != $objectsNet * $size\n"; 201 } 202 } 203 print STDERR "Duplicate entry $name in '$file'\n" if $$h{$name}; 204 $$h{$name} = [$bytesAlloc, $bytesNet, $objectsAlloc, $objectsNet, $refsAlloc, $refsNet]; 205 206 my $oldSize = $$sizes{$name}; 207 print STDERR "Mismatch of sizes of class $name: $oldSize and $size\n" if defined($oldSize) && $size ne $oldSize; 208 $$sizes{$name} = $size; 209 $readSomething = 1; 210 } elsif (/^\s*(?:\d+)\s+([\w:]+)\s/) { 211 print STDERR "Unable to parse '$file' line: $_"; 212 } 213 } 214 close FILE; 215 print STDERR "No data in '$file'\n" unless $readSomething; 216 return $h; 217} 218 219 220my %sizes; # <class-name> => <instance-size> 221my %tables; # <file-name> => <bloat-table>; see readBloatFile for format of <bloat-table> 222 223# Generate the JavaScript source code for the row named $c. $l can contain the initial entries of the row. 224sub genTableRowSource($$) { 225 my ($l, $c) = @_; 226 my $lastE; 227 foreach (@ARGV) { 228 my $e = $tables{$_}{$c}; 229 if (defined($lastE) && !defined($e)) { 230 $e = [0,0,0,0,0,0]; 231 print STDERR "Class $c is defined in an earlier file but not in '$_'\n"; 232 } 233 if (defined $e) { 234 if (defined $lastE) { 235 for (my $i = 0; $i <= $#$e; $i++) { 236 my $n = $$e[$i]; 237 $l .= ($n eq "undefined" ? "undefined" : $n - $$lastE[$i]) . ","; 238 } 239 $l .= " "; 240 } else { 241 $l .= join(",", @$e) . ", "; 242 } 243 $lastE = $e; 244 } else { 245 $l .= "0,0,0,0,0,0, "; 246 } 247 } 248 $l .= join(",", @$lastE); 249 return "[$l]"; 250} 251 252 253 254my $debug; 255my $source; 256my $showMode; 257my $sortColumn; 258my @modeOptions; 259 260GetOptions("debug" => \$debug, "source" => \$source, "byte=i" => \$modeOptions[0], "obj=i" => \$modeOptions[1], "ref=i" => \$modeOptions[2]); 261for (my $i = 0; $i != 3; $i++) { 262 my $modeOption = $modeOptions[$i]; 263 if ($modeOption) { 264 die "Only one of -byte, -obj, or -ref may be given" if defined $showMode; 265 my $nFileColumns = scalar(@ARGV) + 1; 266 die "-byte, -obj, or -ref column number out of range" if $modeOption < 0 || $modeOption >= 2 + 2*$nFileColumns; 267 $showMode = $i; 268 if ($modeOption >= 2) { 269 $modeOption -= 2; 270 $sortColumn = 2 + $showMode*2; 271 if ($modeOption >= $nFileColumns) { 272 $sortColumn++; 273 $modeOption -= $nFileColumns; 274 } 275 $sortColumn += $modeOption*6; 276 } else { 277 $sortColumn = $modeOption; 278 } 279 } 280} 281unless (defined $showMode) { 282 $showMode = 0; 283 $sortColumn = 0; 284} 285 286# Read all of the bloat files. 287foreach (@ARGV) { 288 unless ($tables{$_}) { 289 my $f = $_; 290 my %table; 291 292 readBloatFile $_, %table, %sizes; 293 $tables{$_} = \%table; 294 } 295} 296die "No input" unless %sizes; 297 298my @scriptData; # JavaScript source for the tables passed to JavaScript. Each entry is one line of JavaScript. 299my @persistentScriptData; # Same as @scriptData, but persists the page reloads itself. 300 301# Print a list of bloat file names. 302push @persistentScriptData, "var nFiles = " . scalar(@ARGV) . ";"; 303push @persistentScriptData, "var fileTags = [" . join(", ", map {singleQuoteString substr(fileCoreName($_), -10)} @ARGV) . "];"; 304push @persistentScriptData, "var fileNames = [" . join(", ", map {singleQuoteString $_} @ARGV) . "];"; 305push @persistentScriptData, "var fileDates = [" . join(", ", map {singleQuoteString localtime fileModDate $_} @ARGV) . "];"; 306 307# Print the bloat tables. 308push @persistentScriptData, "var totals = " . genTableRowSource('"TOTAL", undefined, ', "TOTAL") . ";"; 309push @scriptData, "var classTables = ["; 310delete $sizes{"TOTAL"}; 311my @classes = sort(keys %sizes); 312for (my $i = 0; $i <= $#classes; $i++) { 313 my $c = $classes[$i]; 314 push @scriptData, genTableRowSource(doubleQuoteString($c).", ".$sizes{$c}.", ", $c) . ($i == $#classes ? "];" : ","); 315} 316 317generate(@scriptData, @persistentScriptData, $debug, $source, $showMode, $sortColumn); 3181; 319 320 321# The source of the eval'd JavaScript follows. 322# Comments starting with // that are alone on a line are stripped by the Perl script. 323__END__ 324 325// showMode: 0=bytes, 1=objects, 2=references 326var showMode; 327var modeName; 328var modeNameUpper; 329 330var sortColumn; 331 332// Sort according to the sortColumn. Column 0 is sorted alphabetically in ascending order. 333// All other columns are sorted numerically in descending order, with column 0 used for a secondary sort. 334// Undefined is always listed last. 335function sortCompare(x, y) { 336 if (sortColumn) { 337 var xc = x[sortColumn]; 338 var yc = y[sortColumn]; 339 if (xc < yc || xc === undefined && yc !== undefined) return 1; 340 if (yc < xc || yc === undefined && xc !== undefined) return -1; 341 } 342 343 var x0 = x[0]; 344 var y0 = y[0]; 345 if (x0 > y0 || x0 === undefined && y0 !== undefined) return 1; 346 if (y0 > x0 || y0 === undefined && x0 !== undefined) return -1; 347 return 0; 348} 349 350 351// Quote special HTML characters in the string. 352function quoteHTML(s) { 353 s = s.replace(/&/g, '&'); 354 // Can't use /</g because HTML interprets '</g' as ending the script! 355 s = s.replace(/\x3C/g, '<'); 356 s = s.replace(/>/g, '>'); 357 s = s.replace(/ /g, ' '); 358 return s; 359} 360 361 362function writeFileTable(d) { 363 d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>'); 364 d.writeln('<TR>\n<TH>Name</TH>\n<TH>File</TH>\n<TH>Date</TH>\n</TR>'); 365 for (var i = 0; i < nFiles; i++) 366 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>'); 367 d.writeln('</TABLE>'); 368} 369 370 371function writeReloadLink(d, column, s, rowspan) { 372 d.write(rowspan == 1 ? '<TH>' : '<TH rowspan='+rowspan+'>'); 373 if (column != sortColumn) 374 d.write('<A href="javascript:reloadSelf('+column+','+showMode+')">'); 375 d.write(s); 376 if (column != sortColumn) 377 d.write('</A>'); 378 d.writeln('</TH>'); 379} 380 381function writeClassTableRow(d, row, base, modeName) { 382 if (modeName) { 383 d.writeln('<TR>\n<TH>'+modeName+'</TH>'); 384 } else { 385 d.writeln('<TR>\n<TD><A href="javascript:showRowDetail(\''+row[0]+'\')">'+quoteHTML(row[0])+'</A></TD>'); 386 var v = row[1]; 387 d.writeln('<TD class=num>'+(v === undefined ? '' : v)+'</TD>'); 388 } 389 for (var i = 0; i != 2; i++) { 390 var c = base + i; 391 for (var j = 0; j <= nFiles; j++) { 392 v = row[c]; 393 var style = 'num'; 394 if (j != nFiles) 395 if (v > 0) { 396 style = 'pos'; 397 v = '+'+v; 398 } else 399 style = 'neg'; 400 d.writeln('<TD class='+style+'>'+(v === undefined ? '' : v)+'</TD>'); 401 c += 6; 402 } 403 } 404 d.writeln('</TR>'); 405} 406 407function writeClassTable(d) { 408 var base = 2 + showMode*2; 409 410 // Make a copy because a sort is destructive. 411 var table = classTables.concat(); 412 table.sort(sortCompare); 413 414 d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>'); 415 416 d.writeln('<TR>'); 417 writeReloadLink(d, 0, 'Class Name', 2); 418 writeReloadLink(d, 1, 'Instance<BR>Size', 2); 419 d.writeln('<TH colspan='+(nFiles+1)+'>'+modeNameUpper+'s allocated</TH>'); 420 d.writeln('<TH colspan='+(nFiles+1)+'>'+modeNameUpper+'s allocated but not freed</TH>\n</TR>'); 421 d.writeln('<TR>'); 422 for (var i = 0; i != 2; i++) { 423 var c = base + i; 424 for (var j = 0; j <= nFiles; j++) { 425 writeReloadLink(d, c, j == nFiles ? 'Total' : quoteHTML(fileTags[j]), 1); 426 c += 6; 427 } 428 } 429 d.writeln('</TR>'); 430 431 writeClassTableRow(d, totals, base, 0); 432 for (var r = 0; r < table.length; r++) 433 writeClassTableRow(d, table[r], base, 0); 434 435 d.writeln('</TABLE>'); 436} 437 438 439var modeNames = ["byte", "object", "reference"]; 440var modeNamesUpper = ["Byte", "Object", "Reference"]; 441var styleSheet = '<STYLE type="TEXT/CSS">\n'+ 442 'BODY {background-color: #FFFFFF; color: #000000}\n'+ 443 '.num {text-align: right}\n'+ 444 '.pos {text-align: right; color: #CC0000}\n'+ 445 '.neg {text-align: right; color: #009900}\n'+ 446 '</STYLE>'; 447 448 449function showHead(d) { 450 modeName = modeNames[showMode]; 451 modeNameUpper = modeNamesUpper[showMode]; 452 d.writeln('<TITLE>'+modeNameUpper+' Bloats</TITLE>'); 453 d.writeln(styleSheet); 454} 455 456function showBody(d) { 457 d.writeln('<H1>'+modeNameUpper+' Bloats</H1>'); 458 writeFileTable(d); 459 d.write('<FORM>'); 460 for (var i = 0; i != 3; i++) 461 if (i != showMode) { 462 var newSortColumn = sortColumn; 463 if (sortColumn >= 2) 464 newSortColumn = sortColumn + (i-showMode)*2; 465 d.write('<INPUT type="button" value="Show '+modeNamesUpper[i]+'s" onClick="reloadSelf('+newSortColumn+','+i+')">'); 466 } 467 d.writeln('</FORM>'); 468 d.writeln('<P>The numbers do not include <CODE>malloc</CODE>\'d data such as string contents.</P>'); 469 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>'); 470 writeClassTable(d); 471} 472 473 474function showRowDetail(rowName) { 475 var row; 476 var i; 477 478 if (rowName == "TOTAL") 479 row = totals; 480 else { 481 for (i = 0; i < classTables.length; i++) 482 if (rowName == classTables[i][0]) { 483 row = classTables[i]; 484 break; 485 } 486 } 487 if (row) { 488 var w = window.open("", "ClassTableRowDetails"); 489 var d = w.document; 490 d.open(); 491 d.writeln('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">'); 492 d.writeln('<HTML>\n<HEAD>\n<TITLE>'+quoteHTML(rowName)+' bloat details</TITLE>'); 493 d.writeln(styleSheet); 494 d.writeln('</HEAD>\n\n<BODY>'); 495 d.writeln('<H2>'+quoteHTML(rowName)+'</H2>'); 496 if (row[1] !== undefined) 497 d.writeln('<P>Each instance has '+row[1]+' bytes.</P>'); 498 499 d.writeln('<TABLE border=1 cellspacing=1 cellpadding=0>'); 500 d.writeln('<TR>\n<TH></TH>\n<TH colspan='+(nFiles+1)+'>Allocated</TH>'); 501 d.writeln('<TH colspan='+(nFiles+1)+'>Allocated but not freed</TH>\n</TR>'); 502 d.writeln('<TR>\n<TH></TH>'); 503 for (i = 0; i != 2; i++) 504 for (var j = 0; j <= nFiles; j++) 505 d.writeln('<TH>'+(j == nFiles ? 'Total' : quoteHTML(fileTags[j]))+'</TH>'); 506 d.writeln('</TR>'); 507 508 for (i = 0; i != 3; i++) 509 writeClassTableRow(d, row, 2+i*2, modeNamesUpper[i]+'s'); 510 511 d.writeln('</TABLE>\n</BODY>\n</HTML>'); 512 d.close(); 513 } 514 return undefined; 515} 516 517 518function stringSource(s) { 519 s = s.replace(/\\/g, '\\\\'); 520 s = s.replace(/"/g, '\\"'); 521 s = s.replace(/<\//g, '<\\/'); 522 return '"'+s+'"'; 523} 524 525function reloadSelf(n,m) { 526 // Need to cache these because globals go away on document.open(). 527 var sa = srcArray; 528 var ss = stringSource; 529 var ct = classTables; 530 var i; 531 532 document.open(); 533 // Uncomment this and comment the document.open() line above to see the reloaded page's source. 534 //var w = window.open("", "NewDoc"); 535 //var d = w.document; 536 //var document = new Object; 537 //document.write = function () { 538 // for (var i = 0; i < arguments.length; i++) { 539 // var s = arguments[i].toString(); 540 // s = s.replace(/&/g, '&'); 541 // s = s.replace(/\x3C/g, '<'); 542 // s = s.replace(/>/g, '>'); 543 // s = s.replace(/ /g, ' '); 544 // d.write(s); 545 // } 546 //}; 547 //document.writeln = function () { 548 // for (var i = 0; i < arguments.length; i++) { 549 // var s = arguments[i].toString(); 550 // s = s.replace(/&/g, '&'); 551 // s = s.replace(/\x3C/g, '<'); 552 // s = s.replace(/>/g, '>'); 553 // s = s.replace(/ /g, ' '); 554 // d.write(s); 555 // } 556 // d.writeln('<BR>'); 557 //}; 558 559 document.writeln('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">'); 560 document.writeln('<HTML>\n<HEAD>\n<SCRIPT type="text/javascript">'); 561 562 // Manually copy non-persistent script data 563 if (!ct.length) 564 document.writeln('var classTables = [];'); 565 else { 566 document.writeln('var classTables = ['); 567 for (i = 0; i < ct.length; i++) { 568 var row = ct[i]; 569 document.write('[' + ss(row[0])); 570 for (var j = 1; j < row.length; j++) 571 document.write(',' + row[j]); 572 document.writeln(']' + (i == ct.length-1 ? '];' : ',')); 573 } 574 } 575 576 document.writeln('var srcArray = ['); 577 for (i = 0; i < sa.length; i++) { 578 document.write(ss(sa[i])); 579 if (i != sa.length-1) 580 document.writeln(','); 581 } 582 document.writeln('];'); 583 document.writeln('eval(srcArray.join("\\n"));'); 584 document.writeln('showMode = '+m+';'); 585 document.writeln('sortColumn = '+n+';'); 586 document.writeln('showHead(document);'); 587 document.writeln('</SCRIPT>\n</HEAD>\n\n<BODY>\n<SCRIPT type="text/javascript">showBody(document);</SCRIPT>\n</BODY>\n</HTML>'); 588 document.close(); 589 return undefined; 590} 591