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/\&/&amp;/g;
75	s/</&lt;/g;
76	s/>/&gt;/g;
77	s/ /&nbsp;/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, '&amp;');
354	// Can't use /</g because HTML interprets '</g' as ending the script!
355	s = s.replace(/\x3C/g, '&lt;');
356	s = s.replace(/>/g, '&gt;');
357	s = s.replace(/ /g, '&nbsp;');
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, '&amp;');
541	//		s = s.replace(/\x3C/g, '&lt;');
542	//		s = s.replace(/>/g, '&gt;');
543	//		s = s.replace(/ /g, '&nbsp;');
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, '&amp;');
551	//		s = s.replace(/\x3C/g, '&lt;');
552	//		s = s.replace(/>/g, '&gt;');
553	//		s = s.replace(/ /g, '&nbsp;');
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