diff options
author | andrewhayden@chromium.org <andrewhayden@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-16 18:53:18 +0000 |
---|---|---|
committer | andrewhayden@chromium.org <andrewhayden@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-16 18:53:18 +0000 |
commit | a2ee1b2cfcf617b59bcdd06098f4936ded808441 (patch) | |
tree | 7d0ae213a98fb8b55973447439fe733a992ee6ca | |
parent | dce58acfbb8166b6716c2183b334fef6e6be939f (diff) | |
download | chromium_src-a2ee1b2cfcf617b59bcdd06098f4936ded808441.zip chromium_src-a2ee1b2cfcf617b59bcdd06098f4936ded808441.tar.gz chromium_src-a2ee1b2cfcf617b59bcdd06098f4936ded808441.tar.bz2 |
New binary size tool visualization options.
BUG=
NOTRY=true
Review URL: https://codereview.chromium.org/231803002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@264270 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | tools/binary_size/README.txt | 6 | ||||
-rw-r--r-- | tools/binary_size/legacy_template/.gitignore (renamed from tools/binary_size/template/.gitignore) | 0 | ||||
-rw-r--r-- | tools/binary_size/legacy_template/index.html | 190 | ||||
-rwxr-xr-x | tools/binary_size/run_binary_size_analysis.py | 140 | ||||
-rw-r--r-- | tools/binary_size/template/D3SymbolTreeMap.js | 938 | ||||
-rw-r--r-- | tools/binary_size/template/index.html | 683 | ||||
-rw-r--r-- | tools/binary_size/template/test-data-generator.html | 157 |
7 files changed, 1911 insertions, 203 deletions
diff --git a/tools/binary_size/README.txt b/tools/binary_size/README.txt index 1af78d9..a8d204f 100644 --- a/tools/binary_size/README.txt +++ b/tools/binary_size/README.txt @@ -116,8 +116,8 @@ The tool is not perfect and has several shortcomings: Feature Requests and Bug Reports -------------------------------------------------------------------------------- Please file bugs and feature requests here, making sure to use the label -"Binary-Size-Tool": - https://code.google.com/p/chromium/issues/entry?labels=Binary-Size-Tool +"Tools-BinarySize": + https://code.google.com/p/chromium/issues/entry?labels=Tools-BinarySize View all open issues here: - https://code.google.com/p/chromium/issues/list?can=2&q=label:Binary-Size-Tool
\ No newline at end of file + https://code.google.com/p/chromium/issues/list?can=2&q=label:Tools-BinarySize diff --git a/tools/binary_size/template/.gitignore b/tools/binary_size/legacy_template/.gitignore index a6c57f5..a6c57f5 100644 --- a/tools/binary_size/template/.gitignore +++ b/tools/binary_size/legacy_template/.gitignore diff --git a/tools/binary_size/legacy_template/index.html b/tools/binary_size/legacy_template/index.html new file mode 100644 index 0000000..b73958a --- /dev/null +++ b/tools/binary_size/legacy_template/index.html @@ -0,0 +1,190 @@ +<!DOCTYPE html> +<!-- + Copyright 2014 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head> + <title>Binary Size Analysis</title> + <link rel='stylesheet' href='webtreemap/webtreemap.css'> + <style> + body { font-family: sans-serif; } + tt, pre { font-family: WebKitWorkaround, monospace; } + #map { + margin: 0 auto; + position: relative; + cursor: pointer; + -webkit-user-select: none; + } + #table { + border: 1px solid black; + } + .treemaplegend { + margin: 0 auto; + position: relative; + } + .webtreemap-symbol-vtable { + background: #FFFFAA; + } + .webtreemap-node:hover { + border-color: red; + background: linear-gradient(rgb(240,240,200), rgb(180,180,200)); + } + </style> + <script src='webtreemap/webtreemap.js'></script> + <script src='treemap-dump.js'></script> + <script src='largest-symbols.js'></script> + <script src='largest-sources.js'></script> + <script src='largest-vtables.js'></script> +</head> +<body onload='show_report_treemap()'> +<div style='text-align: center; margin-bottom: 2em;'> + <h1>Binary Size Analysis</h1> + <a href='#' onclick='show_report_treemap()'>Spatial Treemap</a> + ~ + <a href='#' onclick='show_report_largest_sources()'>Largest Sources</a> + ~ + <a href='#' onclick='show_report_largest_symbols()'>Largest Symbols</a> + ~ + <a href='#' onclick='show_report_largest_vtables()'>Largest VTables</a> +</div> +<div id='report'></div> +<script> +function escape(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>'); +} + +var treemap_width = 800; +var treemap_height = 450; +function show_report_treemap() { + console.log('displaying treemap') + var div = document.getElementById('report'); + var w = window.treemap_width; + var h = window.treemap_height; + div.innerHTML = '<div style=\'text-align: center;\'>' + + '<button onclick=\'zoomInTreemap()\'>Bigger (More Detail)</button>' + + ', <button onclick=\'zoomOutTreemap()\'>Smaller (Less Detail)</button>' + + ' or resize to: ' + + '<input type=text size=5 id=treemap_width value=' + w + '>x' + + '<input type=text size=5 id=treemap_height value=' + h + '>' + + '<button onclick=\'resizeTreemap()\'>Go</button>' + + '<br><em>Click on a box to zoom in and reveal more detail. ' + + 'Click on the outermost box to zoom out.</em>' + + '<br>Legend: <table border=1 class=\'treemaplegend\' cellborder=1><tr>' + + '<td class=\'webtreemap-symbol-bss\'>BSS</td>' + + '<td class=\'webtreemap-symbol-data\'>Data</td>' + + '<td class=\'webtreemap-symbol-code\'>Code</td>' + + '<td class=\'webtreemap-symbol-read-only_data\'>RO Data</td>' + + '<td class=\'webtreemap-symbol-weak_symbol\'>Weak</td>' + + '<td class=\'webtreemap-symbol-vtable\'>VTable</td>' + + '</tr></table>' + + '<br>' + + '<div id=\'map\' ' + + 'style=\'width: ' + w + 'px; height: ' + h + 'px;\'>' + + '</div></div>'; + var map = document.getElementById('map'); + appendTreemap(map, kTree); +} + +function zoomInTreemap() { + window.treemap_width = Math.round(window.treemap_width * 1.25); + window.treemap_height = Math.round(window.treemap_height * 1.25); + show_report_treemap(); +} + +function zoomOutTreemap() { + window.treemap_width = Math.round(window.treemap_width / 1.25); + window.treemap_height = Math.round(window.treemap_height / 1.25); + show_report_treemap(); +} + +function resizeTreemap() { + window.treemap_width = document.getElementById('treemap_width').value; + window.treemap_height = document.getElementById('treemap_height').value; + show_report_treemap(); +} + +function show_report_largest_symbols() { + console.log('displaying largest-symbols report') + var div = document.getElementById('report'); + div.innerHTML = '<div><table id=\'list\' border=1><tr>' + + '<th>Rank</th><th>Size</th><th>Type</th><th>Source</th>' + + '</tr></table>'; + var list = document.getElementById('list'); + for (var i = 0; i < largestSymbols.length; i++) { + var record = largestSymbols[i]; + var link; + if (record.location.indexOf('out') == 0) { + link = record.location; + } else { + link = '<a href="https://code.google.com/p/chromium/codesearch#chromium/src/' + + record.location + '">' + escape(record.location) + '</a>'; + } + list.innerHTML += '<tr>' + + '<td>' + (i+1) + '</td>' + + '<td>' + escape(record.size) + '</td>' + + '<td style=\'white-space: nowrap;\'>' + escape(record.type) + '</td>' + + '<td>' + link + ':<br>' + + escape(record.symbol) + '</td>' + + '</tr>'; + } +} + +function show_report_largest_sources() { + console.log('displaying largest-sources report') + var div = document.getElementById('report'); + div.innerHTML = '<div><table id=\'list\' border=1><tr>' + + '<th>Rank</th><th>Size</th><th>Symbol Count</th><th>Source</th>' + + '</tr></table>'; + var list = document.getElementById('list'); + for (var i = 0; i < largestSources.length; i++) { + var record = largestSources[i]; + var link; + if (record.location.indexOf('out') == 0) { + link = record.location; + } else { + link = '<a href="https://code.google.com/p/chromium/codesearch#chromium/src/' + + record.location + '">' + escape(record.location) + '</a>'; + } + + list.innerHTML += '<tr>' + + '<td>' + (i+1) + '</td>' + + '<td>' + escape(record.size) + '</td>' + + '<td>' + escape(record.symbol_count) + '</td>' + + '<td>' + link + '</td>' + + '</tr>'; + } +} + +function show_report_largest_vtables() { + console.log('displaying largest-vtables report') + var div = document.getElementById('report'); + div.innerHTML = '<div><table id=\'list\' border=1><tr>' + + '<th>Rank</th><th>Size</th><th>Symbol</th><th>Source</th>' + + '</tr></table>'; + var list = document.getElementById('list'); + for (var i = 0; i < largestVTables.length; i++) { + var record = largestVTables[i]; + var link; + if (record.location.indexOf('out') == 0) { + link = record.location; + } else { + link = '<a href="https://code.google.com/p/chromium/codesearch#chromium/src/' + + record.location + '">' + escape(record.location) + '</a>'; + } + + list.innerHTML += '<tr>' + + '<td>' + (i+1) + '</td>' + + '<td>' + escape(record.size) + '</td>' + + '<td>' + escape(record.symbol) + '</td>' + + '<td>' + link + '</td>' + + '</tr>'; + } +} +</script> +</body> +</html>
\ No newline at end of file diff --git a/tools/binary_size/run_binary_size_analysis.py b/tools/binary_size/run_binary_size_analysis.py index 37c8254..2a75faf 100755 --- a/tools/binary_size/run_binary_size_analysis.py +++ b/tools/binary_size/run_binary_size_analysis.py @@ -23,6 +23,7 @@ import sys import tempfile +# TODO(andrewhayden): Only used for legacy reports. Delete. def FormatBytes(bytes): """Pretty-print a number of bytes.""" if bytes > 1e6: @@ -34,6 +35,7 @@ def FormatBytes(bytes): return str(bytes) +# TODO(andrewhayden): Only used for legacy reports. Delete. def SymbolTypeToHuman(type): """Convert a symbol type as printed by nm into a human-readable name.""" return {'b': 'bss', @@ -70,10 +72,7 @@ def ParseNm(input): if match: size, type, sym = match.groups()[0:3] size = int(size, 16) - type = type.lower() - if type == 'v': - type = 'w' # just call them all weak - if type == 'b': + if type.lower() == 'b': continue # skip all BSS for now path = match.group(4) yield sym, type, size, path @@ -93,6 +92,66 @@ def ParseNm(input): print >>sys.stderr, 'unparsed:', repr(line) +def _MkChild(node, name): + child = None + for test in node['children']: + if test['n'] == name: + child = test + break + if not child: + child = {'n': name, 'children': []} + node['children'].append(child) + return child + + +def MakeCompactTree(symbols): + result = {'n': '/', 'children': [], 'k': 'p', 'maxDepth': 0} + for symbol_name, symbol_type, symbol_size, file_path in symbols: + + if 'vtable for ' in symbol_name: + symbol_type = '@' # hack to categorize these separately + # Take path like '/foo/bar/baz', convert to ['foo', 'bar', 'baz'] + if file_path: + file_path = os.path.normpath(file_path) + else: + file_path = '(No Path)' + + if file_path.startswith('/'): + file_path = file_path[1:] + path_parts = file_path.split('/') + + # Find pre-existing node in tree, or update if it already exists + node = result + depth = 0 + while len(path_parts) > 0: + path_part = path_parts.pop(0) + if len(path_part) == 0: + continue + depth += 1 + node = _MkChild(node, path_part); + node['k'] = 'p' # p for path + + # 'node' is now the file node. Find the symbol-type bucket. + node['lastPathElement'] = True + node = _MkChild(node, symbol_type) + node['t'] = symbol_type + node['k'] = 'b' # b for bucket + depth += 1 + + # 'node' is now the symbol-type bucket. Make the child entry. + node = _MkChild(node, symbol_name) + if 'children' in node: # Only possible if we're adding duplicate entries!!! + del node['children'] + node['value'] = symbol_size + node['t'] = symbol_type + node['k'] = 's' # s for symbol + depth += 1 + result['maxDepth'] = max(result['maxDepth'], depth); + + return result + + +# TODO(andrewhayden): Only used for legacy reports. Delete. def TreeifySymbols(symbols): """Convert symbols into a path-based tree, calculating size information along the way. @@ -188,6 +247,7 @@ def TreeifySymbols(symbols): return dirs +# TODO(andrewhayden): Only used for legacy reports. Delete. def JsonifyTree(tree, name): """Convert TreeifySymbols output to a JSON treemap. @@ -224,7 +284,16 @@ def JsonifyTree(tree, name): 'data': { '$area': tree['size'] }, 'children': children } +def DumpCompactTree(symbols, outfile): + out = open(outfile, 'w') + try: + out.write('var tree_data = ' + json.dumps(MakeCompactTree(symbols))) + finally: + out.flush() + out.close() + +# TODO(andrewhayden): Only used for legacy reports. Delete. def DumpTreemap(symbols, outfile): dirs = TreeifySymbols(symbols) out = open(outfile, 'w') @@ -235,6 +304,7 @@ def DumpTreemap(symbols, outfile): out.close() +# TODO(andrewhayden): Only used for legacy reports. Delete. def DumpLargestSymbols(symbols, outfile, n): # a list of (sym, type, size, path); sort by size. symbols = sorted(symbols, key=lambda x: -x[2]) @@ -278,6 +348,7 @@ def MakeSourceMap(symbols): return sources +# TODO(andrewhayden): Only used for legacy reports. Delete. def DumpLargestSources(symbols, outfile, n): map = MakeSourceMap(symbols) sources = sorted(map.values(), key=lambda x: -x['size']) @@ -300,6 +371,7 @@ def DumpLargestSources(symbols, outfile, n): out.close() +# TODO(andrewhayden): Only used for legacy reports. Delete. def DumpLargestVTables(symbols, outfile, n): vtables = [] for symbol, type, size, path in symbols: @@ -325,6 +397,7 @@ def DumpLargestVTables(symbols, outfile, n): out.close() +# TODO(andrewhayden): Switch to Primiano's python-based version. def RunParallelAddress2Line(outfile, library, arch, jobs, verbose): """Run a parallel addr2line processing engine to dump and resolve symbols.""" out_dir = os.getenv('CHROMIUM_OUT_DIR', 'out') @@ -342,15 +415,15 @@ def RunParallelAddress2Line(outfile, library, arch, jobs, verbose): cmd.append('--verbose') prefix = os.path.join('third_party', 'android_tools', 'ndk', 'toolchains') if arch == 'android-arm': - prefix = os.path.join(prefix, 'arm-linux-androideabi-4.7', 'prebuilt', + prefix = os.path.join(prefix, 'arm-linux-androideabi-4.8', 'prebuilt', 'linux-x86_64', 'bin', 'arm-linux-androideabi-') cmd.extend(['--nm', prefix + 'nm', '--addr2line', prefix + 'addr2line']) elif arch == 'android-mips': - prefix = os.path.join(prefix, 'mipsel-linux-android-4.7', 'prebuilt', + prefix = os.path.join(prefix, 'mipsel-linux-android-4.8', 'prebuilt', 'linux-x86_64', 'bin', 'mipsel-linux-android-') cmd.extend(['--nm', prefix + 'nm', '--addr2line', prefix + 'addr2line']) elif arch == 'android-x86': - prefix = os.path.join(prefix, 'x86-4.7', 'prebuilt', + prefix = os.path.join(prefix, 'x86-4.8', 'prebuilt', 'linux-x86_64', 'bin', 'i686-linux-android-') cmd.extend(['--nm', prefix + 'nm', '--addr2line', prefix + 'addr2line']) # else, use whatever is in PATH (don't pass --nm or --addr2line) @@ -441,6 +514,8 @@ def main(): 'mapped to source locations. By default, a tempfile is ' 'used and is deleted when the program terminates.' 'This argument is only valid when using --library.') + parser.add_option('--legacy', action='store_true', + help='emit legacy binary size report instead of modern') opts, args = parser.parse_args() if ((not opts.library) and (not opts.nm_in)) or (opts.library and opts.nm_in): @@ -464,27 +539,40 @@ def main(): if not os.path.exists(opts.destdir): os.makedirs(opts.destdir, 0755) - DumpTreemap(symbols, os.path.join(opts.destdir, 'treemap-dump.js')) - DumpLargestSymbols(symbols, - os.path.join(opts.destdir, 'largest-symbols.js'), 100) - DumpLargestSources(symbols, - os.path.join(opts.destdir, 'largest-sources.js'), 100) - DumpLargestVTables(symbols, - os.path.join(opts.destdir, 'largest-vtables.js'), 100) - - # TODO(andrewhayden): Switch to D3 for greater flexibility - treemap_out = os.path.join(opts.destdir, 'webtreemap') - if not os.path.exists(treemap_out): - os.makedirs(treemap_out, 0755) - treemap_src = os.path.join('third_party', 'webtreemap', 'src') - shutil.copy(os.path.join(treemap_src, 'COPYING'), treemap_out) - shutil.copy(os.path.join(treemap_src, 'webtreemap.js'), treemap_out) - shutil.copy(os.path.join(treemap_src, 'webtreemap.css'), treemap_out) - shutil.copy(os.path.join('tools', 'binary_size', 'template', 'index.html'), - opts.destdir) + + if opts.legacy: # legacy report + DumpTreemap(symbols, os.path.join(opts.destdir, 'treemap-dump.js')) + DumpLargestSymbols(symbols, + os.path.join(opts.destdir, 'largest-symbols.js'), 100) + DumpLargestSources(symbols, + os.path.join(opts.destdir, 'largest-sources.js'), 100) + DumpLargestVTables(symbols, + os.path.join(opts.destdir, 'largest-vtables.js'), 100) + treemap_out = os.path.join(opts.destdir, 'webtreemap') + if not os.path.exists(treemap_out): + os.makedirs(treemap_out, 0755) + treemap_src = os.path.join('third_party', 'webtreemap', 'src') + shutil.copy(os.path.join(treemap_src, 'COPYING'), treemap_out) + shutil.copy(os.path.join(treemap_src, 'webtreemap.js'), treemap_out) + shutil.copy(os.path.join(treemap_src, 'webtreemap.css'), treemap_out) + shutil.copy(os.path.join('tools', 'binary_size', 'legacy_template', + 'index.html'), opts.destdir) + else: # modern report + DumpCompactTree(symbols, os.path.join(opts.destdir, 'data.js')) + d3_out = os.path.join(opts.destdir, 'd3') + if not os.path.exists(d3_out): + os.makedirs(d3_out, 0755) + d3_src = os.path.join('third_party', 'd3', 'src') + template_src = os.path.join('tools', 'binary_size', + 'template') + shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out) + shutil.copy(os.path.join(d3_src, 'd3.js'), d3_out) + shutil.copy(os.path.join(template_src, 'index.html'), opts.destdir) + shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), opts.destdir) + if opts.verbose: print 'Report saved to ' + opts.destdir + '/index.html' if __name__ == '__main__': - sys.exit(main())
\ No newline at end of file + sys.exit(main()) diff --git a/tools/binary_size/template/D3SymbolTreeMap.js b/tools/binary_size/template/D3SymbolTreeMap.js new file mode 100644 index 0000000..4bbe82f --- /dev/null +++ b/tools/binary_size/template/D3SymbolTreeMap.js @@ -0,0 +1,938 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO: +// 1. Visibility functions: base on boxPadding.t, not 15 +// 2. Track a maxDisplayDepth that is user-settable: +// maxDepth == currentRoot.depth + maxDisplayDepth +function D3SymbolTreeMap(mapWidth, mapHeight, levelsToShow) { + this._mapContainer = undefined; + this._mapWidth = mapWidth; + this._mapHeight = mapHeight; + this.boxPadding = {'l': 5, 'r': 5, 't': 20, 'b': 5}; + this.infobox = undefined; + this._maskContainer = undefined; + this._highlightContainer = undefined; + // Transition in this order: + // 1. Exiting items go away. + // 2. Updated items move. + // 3. New items enter. + this._exitDuration=500; + this._updateDuration=500; + this._enterDuration=500; + this._firstTransition=true; + this._layout = undefined; + this._currentRoot = undefined; + this._currentNodes = undefined; + this._treeData = undefined; + this._maxLevelsToShow = levelsToShow; + this._currentMaxDepth = this._maxLevelsToShow; +} + +/** + * Make a number pretty, with comma separators. + */ +D3SymbolTreeMap._pretty = function(num) { + var asString = String(num); + var result = ''; + var counter = 0; + for (var x = asString.length - 1; x >= 0; x--) { + counter++; + if (counter === 4) { + result = ',' + result; + counter = 1; + } + result = asString.charAt(x) + result; + } + return result; +} + +/** + * Express a number in terms of KiB, MiB, GiB, etc. + * Note that these are powers of 2, not of 10. + */ +D3SymbolTreeMap._byteify = function(num) { + var suffix; + if (num >= 1024) { + if (num >= 1024 * 1024 * 1024) { + suffix = 'GiB'; + num = num / (1024 * 1024 * 1024); + } else if (num >= 1024 * 1024) { + suffix = 'MiB'; + num = num / (1024 * 1024); + } else if (num >= 1024) { + suffix = 'KiB' + num = num / 1024; + } + return num.toFixed(2) + ' ' + suffix; + } + return num + ' B'; +} + +D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS = { + // Definitions concisely derived from the nm 'man' page + 'A': 'Global absolute (A)', + 'B': 'Global uninitialized data (B)', + 'b': 'Local uninitialized data (b)', + 'C': 'Global uninitialized common (C)', + 'D': 'Global initialized data (D)', + 'd': 'Local initialized data (d)', + 'G': 'Global small initialized data (G)', + 'g': 'Local small initialized data (g)', + 'i': 'Indirect function (i)', + 'N': 'Debugging (N)', + 'p': 'Stack unwind (p)', + 'R': 'Global read-only data (R)', + 'r': 'Local read-only data (r)', + 'S': 'Global small uninitialized data (S)', + 's': 'Local small uninitialized data (s)', + 'T': 'Global code (T)', + 't': 'Local code (t)', + 'U': 'Undefined (U)', + 'u': 'Unique (u)', + 'V': 'Global weak object (V)', + 'v': 'Local weak object (v)', + 'W': 'Global weak symbol (W)', + 'w': 'Local weak symbol (w)', + '@': 'Vtable entry (@)', // non-standard, hack. + '-': 'STABS debugging (-)', + '?': 'Unrecognized (?)', +}; +D3SymbolTreeMap._NM_SYMBOL_TYPES = ''; +for (var symbol_type in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) { + D3SymbolTreeMap._NM_SYMBOL_TYPES += symbol_type; +} + +/** + * Given a symbol type code, look up and return a human-readable description + * of that symbol type. If the symbol type does not match one of the known + * types, the unrecognized description (corresponding to symbol type '?') is + * returned instead of null or undefined. + */ +D3SymbolTreeMap._getSymbolDescription = function(type) { + var result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS[type]; + if (result === undefined) { + result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS['?']; + } + return result; +} + +// Qualitative 12-value pastel Brewer palette. +D3SymbolTreeMap._colorArray = [ + 'rgb(141,211,199)', + 'rgb(255,255,179)', + 'rgb(190,186,218)', + 'rgb(251,128,114)', + 'rgb(128,177,211)', + 'rgb(253,180,98)', + 'rgb(179,222,105)', + 'rgb(252,205,229)', + 'rgb(217,217,217)', + 'rgb(188,128,189)', + 'rgb(204,235,197)', + 'rgb(255,237,111)']; + +D3SymbolTreeMap._initColorMap = function() { + var map = {}; + var numColors = D3SymbolTreeMap._colorArray.length; + var count = 0; + for (var key in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) { + var index = count++ % numColors; + map[key] = d3.rgb(D3SymbolTreeMap._colorArray[index]); + } + D3SymbolTreeMap._colorMap = map; +} +D3SymbolTreeMap._initColorMap(); + +D3SymbolTreeMap.getColorForType = function(type) { + var result = D3SymbolTreeMap._colorMap[type]; + if (result === undefined) return d3.rgb('rgb(255,255,255)'); + return result; +} + +D3SymbolTreeMap.prototype.init = function() { + this.infobox = this._createInfoBox(); + this._mapContainer = d3.select('body').append('div') + .style('position', 'relative') + .style('width', this._mapWidth) + .style('height', this._mapHeight) + .style('padding', 0) + .style('margin', 0) + .style('box-shadow', '5px 5px 5px #888'); + this._layout = this._createTreeMapLayout(); + this._setData(tree_data); // TODO: Don't use global 'tree_data' +} + +/** + * Sets the data displayed by the treemap and layint out the map. + */ +D3SymbolTreeMap.prototype._setData = function(data) { + this._treeData = data; + console.time('_crunchStats'); + this._crunchStats(data); + console.timeEnd('_crunchStats'); + this._currentRoot = this._treeData; + this._currentNodes = this._layout.nodes(this._currentRoot); + this._currentMaxDepth = this._maxLevelsToShow; + this._doLayout(); +} + +/** + * Recursively traverses the entire tree starting from the specified node, + * computing statistics and recording metadata as it goes. Call this method + * only once per imported tree. + */ +D3SymbolTreeMap.prototype._crunchStats = function(node) { + var stack = []; + stack.idCounter = 0; + this._crunchStatsHelper(stack, node); +} + +/** + * Invoke the specified visitor function on all data elements currently shown + * in the treemap including any and all of their children, starting at the + * currently-displayed root and descening recursively. The function will be + * passed the datum element representing each node. No traversal guarantees + * are made. + */ +D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) { + this._visit(this._currentRoot, visitor); +} + +/** + * Helper function for visit functions. + */ +D3SymbolTreeMap.prototype._visit = function(datum, visitor) { + visitor.call(this, datum); + if (datum.children) for (var i = 0; i < datum.children.length; i++) { + this._visit(datum.children[i], visitor); + } +} + +D3SymbolTreeMap.prototype._crunchStatsHelper = function(stack, node) { + // Only overwrite the node ID if it isn't already set. + // This allows stats to be crunched multiple times on subsets of data + // without breaking the data-to-ID bindings. New nodes get new IDs. + if (node.id === undefined) node.id = stack.idCounter++; + if (node.children === undefined) { + // Leaf node (symbol); accumulate stats. + for (var i = 0; i < stack.length; i++) { + var ancestor = stack[i]; + if (!ancestor.symbol_stats) ancestor.symbol_stats = {}; + if (ancestor.symbol_stats[node.t] === undefined) { + // New symbol type we haven't seen before, just record. + ancestor.symbol_stats[node.t] = {'count': 1, + 'size': node.value}; + } else { + // Existing symbol type, increment. + ancestor.symbol_stats[node.t].count++; + ancestor.symbol_stats[node.t].size += node.value; + } + } + } else for (var i = 0; i < node.children.length; i++) { + stack.push(node); + this._crunchStatsHelper(stack, node.children[i]); + stack.pop(); + } +} + +D3SymbolTreeMap.prototype._createTreeMapLayout = function() { + var result = d3.layout.treemap() + .padding([this.boxPadding.t, this.boxPadding.r, + this.boxPadding.b, this.boxPadding.l]) + .size([this._mapWidth, this._mapHeight]); + return result; +} + +D3SymbolTreeMap.prototype.resize = function(width, height) { + this._mapWidth = width; + this._mapHeight = height; + this._mapContainer.style('width', width).style('height', height); + this._layout.size([this._mapWidth, this._mapHeight]); + this._currentNodes = this._layout.nodes(this._currentRoot); + this._doLayout(); +} + +D3SymbolTreeMap.prototype._zoomDatum = function(datum) { + if (this._currentRoot === datum) return; // already here + this._hideHighlight(datum); + this._hideInfoBox(datum); + this._currentRoot = datum; + this._currentNodes = this._layout.nodes(this._currentRoot); + this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow; + console.log('zooming into datum ' + this._currentRoot.n); + this._doLayout(); +} + +D3SymbolTreeMap.prototype.setMaxLevels = function(levelsToShow) { + this._maxLevelsToShow = levelsToShow; + this._currentNodes = this._layout.nodes(this._currentRoot); + this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow; + console.log('setting max levels to show: ' + this._maxLevelsToShow); + this._doLayout(); +} + +/** + * Clone the specified tree, returning an independent copy of the data. + * Only the original attributes expected to exist prior to invoking + * _crunchStatsHelper are retained, with the exception of the 'id' attribute + * (which must be retained for proper transitions). + * If the optional filter parameter is provided, it will be called with 'this' + * set to this treemap instance and passed the 'datum' object as an argument. + * When specified, the copy will retain only the data for which the filter + * function returns true. + */ +D3SymbolTreeMap.prototype._clone = function(datum, filter) { + var trackingStats = false; + if (this.__cloneState === undefined) { + console.time('_clone'); + trackingStats = true; + this.__cloneState = {'accepted': 0, 'rejected': 0, + 'forced': 0, 'pruned': 0}; + } + + // Must go depth-first. All parents of children that are accepted by the + // filter must be preserved! + var copy = {'n': datum.n, 'k': datum.k}; + var childAccepted = false; + if (datum.children !== undefined) { + for (var i = 0; i < datum.children.length; i++) { + var copiedChild = this._clone(datum.children[i], filter); + if (copiedChild !== undefined) { + childAccepted = true; // parent must also be accepted. + if (copy.children === undefined) copy.children = []; + copy.children.push(copiedChild); + } + } + } + + // Ignore nodes that don't match the filter, when present. + var accept = false; + if (childAccepted) { + // Parent of an accepted child must also be accepted. + this.__cloneState.forced++; + accept = true; + } else if (filter !== undefined && filter.call(this, datum) !== true) { + this.__cloneState.rejected++; + } else if (datum.children === undefined) { + // Accept leaf nodes that passed the filter + this.__cloneState.accepted++; + accept = true; + } else { + // Non-leaf node. If no children are accepted, prune it. + this.__cloneState.pruned++; + } + + if (accept) { + if (datum.id !== undefined) copy.id = datum.id; + if (datum.lastPathElement !== undefined) { + copy.lastPathElement = datum.lastPathElement; + } + if (datum.t !== undefined) copy.t = datum.t; + if (datum.value !== undefined && datum.children === undefined) { + copy.value = datum.value; + } + } else { + // Discard the copy we were going to return + copy = undefined; + } + + if (trackingStats === true) { + // We are the fist call in the recursive chain. + console.timeEnd('_clone'); + var totalAccepted = this.__cloneState.accepted + + this.__cloneState.forced; + console.log( + totalAccepted + ' nodes retained (' + + this.__cloneState.forced + ' forced by accepted children, ' + + this.__cloneState.accepted + ' accepted on their own merits), ' + + this.__cloneState.rejected + ' nodes (and their children) ' + + 'filtered out,' + + this.__cloneState.pruned + ' nodes pruned because because no ' + + 'children remained.'); + delete this.__cloneState; + } + return copy; +} + +D3SymbolTreeMap.prototype.filter = function(filter) { + // Ensure we have a copy of the original root. + if (this._backupTree === undefined) this._backupTree = this._treeData; + this._mapContainer.selectAll('div').remove(); + this._setData(this._clone(this._backupTree, filter)); +} + +D3SymbolTreeMap.prototype._doLayout = function() { + console.time('_doLayout'); + this._handleInodes(); + this._handleLeaves(); + this._firstTransition = false; + console.timeEnd('_doLayout'); +} + +D3SymbolTreeMap.prototype._highlightElement = function(datum, selection) { + this._showHighlight(datum, selection); +} + +D3SymbolTreeMap.prototype._unhighlightElement = function(datum, selection) { + this._hideHighlight(datum, selection); +} + +D3SymbolTreeMap.prototype._handleInodes = function() { + console.time('_handleInodes'); + var thisTreeMap = this; + var inodes = this._currentNodes.filter(function(datum){ + return (datum.depth <= thisTreeMap._currentMaxDepth) && + datum.children !== undefined; + }); + var cellsEnter = this._mapContainer.selectAll('div.inode') + .data(inodes, function(datum) { return datum.id; }) + .enter() + .append('div').attr('class', 'inode').attr('id', function(datum){ + return 'node-' + datum.id;}); + + + // Define enter/update/exit for inodes + cellsEnter + .append('div') + .attr('class', 'rect inode_rect_entering') + .style('z-index', function(datum) { return datum.id * 2; }) + .style('position', 'absolute') + .style('left', function(datum) { return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum){ return datum.dx; }) + .style('height', function(datum){ return datum.dy; }) + .style('opacity', '0') + .style('border', '1px solid black') + .style('background-image', function(datum) { + return thisTreeMap._makeSymbolBucketBackgroundImage.call( + thisTreeMap, datum); + }) + .style('background-color', function(datum) { + if (datum.t === undefined) return 'rgb(220,220,220)'; + return D3SymbolTreeMap.getColorForType(datum.t).toString(); + }) + .on('mouseover', function(datum){ + thisTreeMap._highlightElement.call( + thisTreeMap, datum, d3.select(this)); + thisTreeMap._showInfoBox.call(thisTreeMap, datum); + }) + .on('mouseout', function(datum){ + thisTreeMap._unhighlightElement.call( + thisTreeMap, datum, d3.select(this)); + thisTreeMap._hideInfoBox.call(thisTreeMap, datum); + }) + .on('mousemove', function(){ + thisTreeMap._moveInfoBox.call(thisTreeMap, event); + }) + .on('dblclick', function(datum){ + if (datum !== thisTreeMap._currentRoot) { + // Zoom into the selection + thisTreeMap._zoomDatum(datum); + } else if (datum.parent) { + console.log('event.shiftKey=' + event.shiftKey); + if (event.shiftKey === true) { + // Back to root + thisTreeMap._zoomDatum(thisTreeMap._treeData); + } else { + // Zoom out of the selection + thisTreeMap._zoomDatum(datum.parent); + } + } + }); + cellsEnter + .append('div') + .attr('class', 'label inode_label_entering') + .style('z-index', function(datum) { return (datum.id * 2) + 1; }) + .style('position', 'absolute') + .style('left', function(datum){ return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum) { return datum.dx; }) + .style('height', function(datum) { return thisTreeMap.boxPadding.t; }) + .style('opacity', '0') + .style('pointer-events', 'none') + .style('-webkit-user-select', 'none') + .style('overflow', 'hidden') // required for ellipsis + .style('white-space', 'nowrap') // required for ellipsis + .style('text-overflow', 'ellipsis') + .style('text-align', 'center') + .style('vertical-align', 'top') + .style('visibility', function(datum) { + return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible'; + }) + .text(function(datum) { + var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']' + var text; + if (datum.k === 'b') { // bucket + if (datum === thisTreeMap._currentRoot) { + text = thisTreeMap.pathFor(datum) + ': ' + + D3SymbolTreeMap._getSymbolDescription(datum.t) + } else { + text = D3SymbolTreeMap._getSymbolDescription(datum.t); + } + } else if (datum === thisTreeMap._currentRoot) { + // The top-most level should always show the complete path + text = thisTreeMap.pathFor(datum); + } else { + // Anything that isn't a bucket or a leaf (symbol) or the + // current root should just show its name. + text = datum.n; + } + return text + sizeish; + } + ); + + // Complicated transition logic: + // For nodes that are entering, we want to fade them in in-place AFTER + // any adjusting nodes have resized and moved around. That way, new nodes + // seamlessly appear in the right spot after their containers have resized + // and moved around. + // To do this we do some trickery: + // 1. Define a '_entering' class on the entering elements + // 2. Use this to select only the entering elements and apply the opacity + // transition. + // 3. Use the same transition to drop the '_entering' suffix, so that they + // will correctly update in later zoom/resize/whatever operations. + // 4. The update transition is achieved by selecting the elements without + // the '_entering_' suffix and applying movement and resizing transition + // effects. + this._mapContainer.selectAll('div.inode_rect_entering').transition() + .duration(thisTreeMap._enterDuration).delay( + this._firstTransition ? 0 : thisTreeMap._exitDuration + + thisTreeMap._updateDuration) + .attr('class', 'rect inode_rect') + .style('opacity', '1') + this._mapContainer.selectAll('div.inode_label_entering').transition() + .duration(thisTreeMap._enterDuration).delay( + this._firstTransition ? 0 : thisTreeMap._exitDuration + + thisTreeMap._updateDuration) + .attr('class', 'label inode_label') + .style('opacity', '1') + this._mapContainer.selectAll('div.inode_rect').transition() + .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration) + .style('opacity', '1') + .style('background-image', function(datum) { + return thisTreeMap._makeSymbolBucketBackgroundImage.call( + thisTreeMap, datum); + }) + .style('left', function(datum) { return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum){ return datum.dx; }) + .style('height', function(datum){ return datum.dy; }); + this._mapContainer.selectAll('div.inode_label').transition() + .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration) + .style('opacity', '1') + .style('visibility', function(datum) { + return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible'; + }) + .style('left', function(datum){ return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum) { return datum.dx; }) + .style('height', function(datum) { return thisTreeMap.boxPadding.t; }) + .text(function(datum) { + var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']' + var text; + if (datum.k === 'b') { + if (datum === thisTreeMap._currentRoot) { + text = thisTreeMap.pathFor(datum) + ': ' + + D3SymbolTreeMap._getSymbolDescription(datum.t) + } else { + text = D3SymbolTreeMap._getSymbolDescription(datum.t); + } + } else if (datum === thisTreeMap._currentRoot) { + // The top-most level should always show the complete path + text = thisTreeMap.pathFor(datum); + } else { + // Anything that isn't a bucket or a leaf (symbol) or the + // current root should just show its name. + text = datum.n; + } + return text + sizeish; + }); + var exit = this._mapContainer.selectAll('div.inode') + .data(inodes, function(datum) { return 'inode-' + datum.id; }) + .exit(); + exit.selectAll('div.inode_rect').transition().duration( + thisTreeMap._exitDuration).style('opacity', 0); + exit.selectAll('div.inode_label').transition().duration( + thisTreeMap._exitDuration).style('opacity', 0); + exit.transition().delay(thisTreeMap._exitDuration + 1).remove(); + + console.log(inodes.length + ' inodes layed out.'); + console.timeEnd('_handleInodes'); +} + +D3SymbolTreeMap.prototype._handleLeaves = function() { + console.time('_handleLeaves'); + var color_fn = d3.scale.category10(); + var thisTreeMap = this; + var leaves = this._currentNodes.filter(function(datum){ + return (datum.depth <= thisTreeMap._currentMaxDepth) && + datum.children === undefined; }); + var cellsEnter = this._mapContainer.selectAll('div.leaf') + .data(leaves, function(datum) { return datum.id; }) + .enter() + .append('div').attr('class', 'leaf').attr('id', function(datum){ + return 'node-' + datum.id; + }); + + // Define enter/update/exit for leaves + cellsEnter + .append('div') + .attr('class', 'rect leaf_rect_entering') + .style('z-index', function(datum) { return datum.id * 2; }) + .style('position', 'absolute') + .style('left', function(datum){ return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum){ return datum.dx; }) + .style('height', function(datum){ return datum.dy; }) + .style('opacity', '0') + .style('background-color', function(datum) { + if (datum.t === undefined) return 'rgb(220,220,220)'; + return D3SymbolTreeMap.getColorForType(datum.t) + .darker(0.3).toString(); + }) + .style('border', '1px solid black') + .on('mouseover', function(datum){ + thisTreeMap._highlightElement.call( + thisTreeMap, datum, d3.select(this)); + thisTreeMap._showInfoBox.call(thisTreeMap, datum); + }) + .on('mouseout', function(datum){ + thisTreeMap._unhighlightElement.call( + thisTreeMap, datum, d3.select(this)); + thisTreeMap._hideInfoBox.call(thisTreeMap, datum); + }) + .on('mousemove', function(){ thisTreeMap._moveInfoBox.call( + thisTreeMap, event); + }); + cellsEnter + .append('div') + .attr('class', 'label leaf_label_entering') + .style('z-index', function(datum) { return (datum.id * 2) + 1; }) + .style('position', 'absolute') + .style('left', function(datum){ return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum) { return datum.dx; }) + .style('height', function(datum) { return datum.dy; }) + .style('opacity', '0') + .style('pointer-events', 'none') + .style('-webkit-user-select', 'none') + .style('overflow', 'hidden') // required for ellipsis + .style('white-space', 'nowrap') // required for ellipsis + .style('text-overflow', 'ellipsis') + .style('text-align', 'center') + .style('vertical-align', 'middle') + .style('visibility', function(datum) { + return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible'; + }) + .text(function(datum) { return datum.n; }); + + // Complicated transition logic: See note in _handleInodes() + this._mapContainer.selectAll('div.leaf_rect_entering').transition() + .duration(thisTreeMap._enterDuration).delay( + this._firstTransition ? 0 : thisTreeMap._exitDuration + + thisTreeMap._updateDuration) + .attr('class', 'rect leaf_rect') + .style('opacity', '1') + this._mapContainer.selectAll('div.leaf_label_entering').transition() + .duration(thisTreeMap._enterDuration).delay( + this._firstTransition ? 0 : thisTreeMap._exitDuration + + thisTreeMap._updateDuration) + .attr('class', 'label leaf_label') + .style('opacity', '1') + this._mapContainer.selectAll('div.leaf_rect').transition() + .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration) + .style('opacity', '1') + .style('left', function(datum){ return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum){ return datum.dx; }) + .style('height', function(datum){ return datum.dy; }); + this._mapContainer.selectAll('div.leaf_label').transition() + .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration) + .style('opacity', '1') + .style('visibility', function(datum) { + return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible'; + }) + .style('left', function(datum){ return datum.x; }) + .style('top', function(datum){ return datum.y; }) + .style('width', function(datum) { return datum.dx; }) + .style('height', function(datum) { return datum.dy; }); + var exit = this._mapContainer.selectAll('div.leaf') + .data(leaves, function(datum) { return 'leaf-' + datum.id; }) + .exit(); + exit.selectAll('div.leaf_rect').transition() + .duration(thisTreeMap._exitDuration) + .style('opacity', 0); + exit.selectAll('div.leaf_label').transition() + .duration(thisTreeMap._exitDuration) + .style('opacity', 0); + exit.transition().delay(thisTreeMap._exitDuration + 1).remove(); + + console.log(leaves.length + ' leaves layed out.'); + console.timeEnd('_handleLeaves'); +} + +D3SymbolTreeMap.prototype._makeSymbolBucketBackgroundImage = function(datum) { + if (!(datum.t === undefined && datum.depth == this._currentMaxDepth)) { + return 'none'; + } + var text = ''; + var lastStop = 0; + for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { + symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); + var stats = datum.symbol_stats[symbol_type]; + if (stats !== undefined) { + if (text.length !== 0) { + text += ', '; + } + var percent = 100 * (stats.size / datum.value); + var nowStop = lastStop + percent; + var tempcolor = D3SymbolTreeMap.getColorForType(symbol_type); + var color = d3.rgb(tempcolor).toString(); + text += color + ' ' + lastStop + '%, ' + color + ' ' + + nowStop + '%'; + lastStop = nowStop; + } + } + return 'linear-gradient(' + (datum.dx > datum.dy ? 'to right' : + 'to bottom') + ', ' + text + ')'; +} + +D3SymbolTreeMap.prototype.pathFor = function(datum) { + if (datum.__path) return datum.__path; + parts=[]; + node = datum; + while (node) { + if (node.k === 'p') { // path node + if(node.n !== '/') parts.unshift(node.n); + } + node = node.parent; + } + datum.__path = '/' + parts.join('/'); + return datum.__path; +} + +D3SymbolTreeMap.prototype._createHighlight = function(datum, selection) { + var x = parseInt(selection.style('left')); + var y = parseInt(selection.style('top')); + var w = parseInt(selection.style('width')); + var h = parseInt(selection.style('height')); + datum.highlight = this._mapContainer.append('div') + .attr('id', 'h-' + datum.id) + .attr('class', 'highlight') + .style('pointer-events', 'none') + .style('-webkit-user-select', 'none') + .style('z-index', '999999') + .style('position', 'absolute') + .style('top', y-2) + .style('left', x-2) + .style('width', w+4) + .style('height', h+4) + .style('margin', 0) + .style('padding', 0) + .style('border', '4px outset rgba(250,40,200,0.9)') + .style('box-sizing', 'border-box') + .style('opacity', 0.0); +} + +D3SymbolTreeMap.prototype._showHighlight = function(datum, selection) { + if (datum === this._currentRoot) return; + if (datum.highlight === undefined) { + this._createHighlight(datum, selection); + } + datum.highlight.transition().duration(200).style('opacity', 1.0); +} + +D3SymbolTreeMap.prototype._hideHighlight = function(datum, selection) { + if (datum.highlight === undefined) return; + datum.highlight.transition().duration(750) + .style('opacity', 0) + .each('end', function(){ + if (datum.highlight) datum.highlight.remove(); + delete datum.highlight; + }); +} + +D3SymbolTreeMap.prototype._createInfoBox = function() { + return d3.select('body') + .append('div') + .attr('id', 'infobox') + .style('z-index', '2147483647') // (2^31) - 1: Hopefully safe :) + .style('position', 'absolute') + .style('visibility', 'hidden') + .style('background-color', 'rgba(255,255,255, 0.9)') + .style('border', '1px solid black') + .style('padding', '10px') + .style('-webkit-user-select', 'none') + .style('box-shadow', '3px 3px rgba(70,70,70,0.5)') + .style('border-radius', '10px') + .style('white-space', 'nowrap'); +} + +D3SymbolTreeMap.prototype._showInfoBox = function(datum) { + this.infobox.text(''); + var numSymbols = 0; + var sizeish = D3SymbolTreeMap._pretty(datum.value) + ' bytes (' + + D3SymbolTreeMap._byteify(datum.value) + ')'; + if (datum.k === 'p' || datum.k === 'b') { // path or bucket + if (datum.symbol_stats) { // can be empty if filters are applied + for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { + symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); + var stats = datum.symbol_stats[symbol_type]; + if (stats !== undefined) numSymbols += stats.count; + } + } + } else if (datum.k === 's') { // symbol + numSymbols = 1; + } + + if (datum.k === 'p' && !datum.lastPathElement) { + this.infobox.append('div').text('Directory: ' + this.pathFor(datum)) + this.infobox.append('div').text('Size: ' + sizeish); + } else { + if (datum.k === 'p') { // path + this.infobox.append('div').text('File: ' + this.pathFor(datum)) + this.infobox.append('div').text('Size: ' + sizeish); + } else if (datum.k === 'b') { // bucket + this.infobox.append('div').text('Symbol Bucket: ' + + D3SymbolTreeMap._getSymbolDescription(datum.t)); + this.infobox.append('div').text('Count: ' + numSymbols); + this.infobox.append('div').text('Size: ' + sizeish); + this.infobox.append('div').text('Location: ' + this.pathFor(datum)) + } else if (datum.k === 's') { // symbol + this.infobox.append('div').text('Symbol: ' + datum.n); + this.infobox.append('div').text('Type: ' + + D3SymbolTreeMap._getSymbolDescription(datum.t)); + this.infobox.append('div').text('Size: ' + sizeish); + this.infobox.append('div').text('Location: ' + this.pathFor(datum)) + } + } + if (datum.k === 'p') { + this.infobox.append('div') + .text('Number of symbols: ' + D3SymbolTreeMap._pretty(numSymbols)); + if (datum.symbol_stats) { // can be empty if filters are applied + var table = this.infobox.append('table') + .attr('border', 1).append('tbody'); + var header = table.append('tr'); + header.append('th').text('Type'); + header.append('th').text('Count'); + header.append('th') + .style('white-space', 'nowrap') + .text('Total Size (Bytes)'); + for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) { + symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x); + var stats = datum.symbol_stats[symbol_type]; + if (stats !== undefined) { + var tr = table.append('tr'); + tr.append('td') + .style('white-space', 'nowrap') + .text(D3SymbolTreeMap._getSymbolDescription( + symbol_type)); + tr.append('td').text(D3SymbolTreeMap._pretty(stats.count)); + tr.append('td').text(D3SymbolTreeMap._pretty(stats.size)); + } + } + } + } + this.infobox.style('visibility', 'visible'); +} + +D3SymbolTreeMap.prototype._hideInfoBox = function(datum) { + this.infobox.style('visibility', 'hidden'); +} + +D3SymbolTreeMap.prototype._moveInfoBox = function(event) { + var element = document.getElementById('infobox'); + var w = element.offsetWidth; + var h = element.offsetHeight; + var offsetLeft = 10; + var offsetTop = 10; + + var rightLimit = window.innerWidth; + var rightEdge = event.pageX + offsetLeft + w; + if (rightEdge > rightLimit) { + // Too close to screen edge, reflect around the cursor + offsetLeft = -1 * (w + offsetLeft); + } + + var bottomLimit = window.innerHeight; + var bottomEdge = event.pageY + offsetTop + h; + if (bottomEdge > bottomLimit) { + // Too close ot screen edge, reflect around the cursor + offsetTop = -1 * (h + offsetTop); + } + + this.infobox.style('top', (event.pageY + offsetTop) + 'px') + .style('left', (event.pageX + offsetLeft) + 'px'); +} + +D3SymbolTreeMap.prototype.biggestSymbols = function(maxRecords) { + var result = undefined; + var smallest = undefined; + var sortFunction = function(a,b) { + var result = b.value - a.value; + if (result !== 0) return result; // sort by size + var pathA = treemap.pathFor(a); // sort by path + var pathB = treemap.pathFor(b); + if (pathA > pathB) return 1; + if (pathB > pathA) return -1; + return a.n - b.n; // sort by symbol name + }; + this.visitFromDisplayedRoot(function(datum) { + if (datum.children) return; // ignore non-leaves + if (!result) { // first element + result = [datum]; + smallest = datum.value; + return; + } + if (result.length < maxRecords) { // filling the array + result.push(datum); + return; + } + if (datum.value > smallest) { // array is already full + result.push(datum); + result.sort(sortFunction); + result.pop(); // get rid of smallest element + smallest = result[maxRecords - 1].value; // new threshold for entry + } + }); + result.sort(sortFunction); + return result; +} + +D3SymbolTreeMap.prototype.biggestPaths = function(maxRecords) { + var result = undefined; + var smallest = undefined; + var sortFunction = function(a,b) { + var result = b.value - a.value; + if (result !== 0) return result; // sort by size + var pathA = treemap.pathFor(a); // sort by path + var pathB = treemap.pathFor(b); + if (pathA > pathB) return 1; + if (pathB > pathA) return -1; + console.log('warning, multiple entries for the same path: ' + pathA); + return 0; // should be impossible + }; + this.visitFromDisplayedRoot(function(datum) { + if (!datum.lastPathElement) return; // ignore non-files + if (!result) { // first element + result = [datum]; + smallest = datum.value; + return; + } + if (result.length < maxRecords) { // filling the array + result.push(datum); + return; + } + if (datum.value > smallest) { // array is already full + result.push(datum); + result.sort(sortFunction); + result.pop(); // get rid of smallest element + smallest = result[maxRecords - 1].value; // new threshold for entry + } + }); + result.sort(sortFunction); + return result; +} diff --git a/tools/binary_size/template/index.html b/tools/binary_size/template/index.html index b73958a..b3a86ad 100644 --- a/tools/binary_size/template/index.html +++ b/tools/binary_size/template/index.html @@ -1,4 +1,3 @@ -<!DOCTYPE html> <!-- Copyright 2014 The Chromium Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be @@ -6,185 +5,521 @@ --> <html> <head> - <title>Binary Size Analysis</title> - <link rel='stylesheet' href='webtreemap/webtreemap.css'> - <style> - body { font-family: sans-serif; } - tt, pre { font-family: WebKitWorkaround, monospace; } - #map { - margin: 0 auto; - position: relative; - cursor: pointer; - -webkit-user-select: none; - } - #table { - border: 1px solid black; - } - .treemaplegend { - margin: 0 auto; - position: relative; - } - .webtreemap-symbol-vtable { - background: #FFFFAA; - } - .webtreemap-node:hover { - border-color: red; - background: linear-gradient(rgb(240,240,200), rgb(180,180,200)); - } - </style> - <script src='webtreemap/webtreemap.js'></script> - <script src='treemap-dump.js'></script> - <script src='largest-symbols.js'></script> - <script src='largest-sources.js'></script> - <script src='largest-vtables.js'></script> -</head> -<body onload='show_report_treemap()'> -<div style='text-align: center; margin-bottom: 2em;'> - <h1>Binary Size Analysis</h1> - <a href='#' onclick='show_report_treemap()'>Spatial Treemap</a> - ~ - <a href='#' onclick='show_report_largest_sources()'>Largest Sources</a> - ~ - <a href='#' onclick='show_report_largest_symbols()'>Largest Symbols</a> - ~ - <a href='#' onclick='show_report_largest_vtables()'>Largest VTables</a> -</div> -<div id='report'></div> +<title>Binary Size Analysis</title> +<script src="d3/d3.js" charset="utf-8"></script> +<script src="D3SymbolTreeMap.js" charset="utf-8"></script> +<script src="data.js" charset="utf-8"></script> +<style> +body { + margin: 0px; + padding: 5px; +} +.swatch { + border: 1px solid rgb(100,100,100); + -webkit-user-select: none; + cursor: default; +} +</style> <script> -function escape(str) { - return str.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/</g, '<') - .replace(/>/g, '>'); -} - -var treemap_width = 800; -var treemap_height = 450; -function show_report_treemap() { - console.log('displaying treemap') - var div = document.getElementById('report'); - var w = window.treemap_width; - var h = window.treemap_height; - div.innerHTML = '<div style=\'text-align: center;\'>' + - '<button onclick=\'zoomInTreemap()\'>Bigger (More Detail)</button>' + - ', <button onclick=\'zoomOutTreemap()\'>Smaller (Less Detail)</button>' + - ' or resize to: ' + - '<input type=text size=5 id=treemap_width value=' + w + '>x' + - '<input type=text size=5 id=treemap_height value=' + h + '>' + - '<button onclick=\'resizeTreemap()\'>Go</button>' + - '<br><em>Click on a box to zoom in and reveal more detail. ' + - 'Click on the outermost box to zoom out.</em>' + - '<br>Legend: <table border=1 class=\'treemaplegend\' cellborder=1><tr>' + - '<td class=\'webtreemap-symbol-bss\'>BSS</td>' + - '<td class=\'webtreemap-symbol-data\'>Data</td>' + - '<td class=\'webtreemap-symbol-code\'>Code</td>' + - '<td class=\'webtreemap-symbol-read-only_data\'>RO Data</td>' + - '<td class=\'webtreemap-symbol-weak_symbol\'>Weak</td>' + - '<td class=\'webtreemap-symbol-vtable\'>VTable</td>' + - '</tr></table>' + - '<br>' + - '<div id=\'map\' ' + - 'style=\'width: ' + w + 'px; height: ' + h + 'px;\'>' + - '</div></div>'; - var map = document.getElementById('map'); - appendTreemap(map, kTree); -} - -function zoomInTreemap() { - window.treemap_width = Math.round(window.treemap_width * 1.25); - window.treemap_height = Math.round(window.treemap_height * 1.25); - show_report_treemap(); -} - -function zoomOutTreemap() { - window.treemap_width = Math.round(window.treemap_width / 1.25); - window.treemap_height = Math.round(window.treemap_height / 1.25); - show_report_treemap(); -} - -function resizeTreemap() { - window.treemap_width = document.getElementById('treemap_width').value; - window.treemap_height = document.getElementById('treemap_height').value; - show_report_treemap(); -} - -function show_report_largest_symbols() { - console.log('displaying largest-symbols report') - var div = document.getElementById('report'); - div.innerHTML = '<div><table id=\'list\' border=1><tr>' + - '<th>Rank</th><th>Size</th><th>Type</th><th>Source</th>' + - '</tr></table>'; - var list = document.getElementById('list'); - for (var i = 0; i < largestSymbols.length; i++) { - var record = largestSymbols[i]; - var link; - if (record.location.indexOf('out') == 0) { - link = record.location; +var treemap; +var filterChanging = false; +var savedSettings = {}; + +function init() { + if (window.metadata !== undefined && window.metadata.subtitle) { + document.getElementById('subtitle').innerHTML = ': ' + escape(metadata.subtitle); + } + initFilterOptions(); + treemap = new D3SymbolTreeMap( + savedSettings.width, + savedSettings.height, + savedSettings.maxLevels); + treemap.init(); +} + +function getIdealSizes() { + var width = window.innerWidth - 20; + var height = window.innerHeight - 70; + return {'width': width, 'height': height}; +} + +function showReport(title, data, headers, dataFunction, styleFunction) { + var div = d3.select('body').append('div') + .style('margin', '0') + .style('padding', '5px') + .style('position', 'absolute') + .style('top', '10%') + .style('left', '10%') + .style('background-color', 'rgba(255,255,255,0.9)') + .style('width', '80%') + .style('height', '80%') + .style('z-index', '2147483647') + .style('border', '3px ridge grey') + .style('box-shadow', '10px 10px 5px rgba(80,80,80,0.7)') + .style('text-align', 'center') + .style('border-radius', '10px'); + var titlebar = div.append('div') + .style('margin', '0') + .style('padding', '5px') + .style('position', 'absolute') + .style('top', '0%') + .style('left', '0%') + .style('width', '100%') + .style('height', '10%') + .style('font-size', 'x-large'); + titlebar.text(title); + var controls = div.append('div') + .style('margin', '0') + .style('padding', '5px') + .style('position', 'absolute') + .style('top', '90%') + .style('left', '0%') + .style('width', '100%') + .style('height', '10%'); + controls.append('input').attr('type', 'button') + .attr('value', 'Dismiss') + .on('click', function(){div.remove();}); + + var tableDiv = div.append('div') + .style('overflow', 'auto') + .style('position', 'absolute') + .style('top', '10%') + .style('left', '0%') + .style('width', '100%') + .style('height', '80%') + .style('border-top', '1px solid rgb(230,230,230)') + .style('border-bottom', '1px solid rgb(230,230,230)'); + var table = tableDiv.append('table') + .attr('border', '1') + .attr('cellspacing', '0') + .attr('cellpadding', '2') + .style('margin-left', 'auto') + .style('margin-right', 'auto'); + var header = table.append('tr'); + for (var i = 0; i < headers.length; i++) { + header.append('th').text(headers[i]); + } + + for (var i = 0; i < data.length; i++) { + var row = table.append('tr'); + for (j = 0; j < headers.length; j++) { + var td = row.append('td'); + if (styleFunction) { + styleFunction.call(this, td, j); + } + dataFunction.call(this, data[i], j, td); + } + } +} + +function bigSymbolsReport() { + var list = treemap.biggestSymbols(100); + var headers = ['Rank', 'Size (Bytes)', 'Type', 'Location']; + var styleFunction = function(selection, index) { + if (index === 3) { + selection.style('font-family', 'monospace'); + } + }; + var recordIndex = 1; + var dataFunction = function(record, index, cell) { + if (index === 0) { + cell.text(recordIndex++); + } else if (index === 1) { + cell.text(D3SymbolTreeMap._pretty(record.value)); + } else if (index === 2) { + cell.text(record.t); + } else { + if (treemap.pathFor(record).indexOf('/out') == 0) { + cell.append('span').text(treemap.pathFor(record)); + cell.append('br'); + cell.append('span').text('Symbol: '); + cell.append('span').text(escape(record.n)); + } else { + var href = 'https://code.google.com/p/chromium/codesearch#chromium/src' + + treemap.pathFor(record) + + '&q=' + + escape(record.n); + cell.append('a') + .attr('href', href) + .attr('target', '_blank') + .text(escape(treemap.pathFor(record))); + cell.append('br'); + cell.append('span').text('Symbol: '); + cell.append('span').text(escape(record.n)); + } + } + }; + showReport('100 Largest Symbols', list, headers, dataFunction, styleFunction); +} + +function bigPathsReport() { + var list = treemap.biggestPaths(100); + var headers = ['Rank', 'Size (Bytes)', 'Location']; + var styleFunction = function(selection, index) { + if (index === 2) { + selection.style('font-family', 'monospace'); + } + }; + var recordIndex = 1; + var dataFunction = function(record, index, cell) { + if (index === 0) { + cell.text(recordIndex++); + } else if (index === 1) { + cell.text(D3SymbolTreeMap._pretty(record.value)); + } else if (index === 2) { + if (treemap.pathFor(record).indexOf('/out') == 0) { + cell.text(treemap.pathFor(record)); + } else { + var href = 'https://code.google.com/p/chromium/codesearch#chromium/src' + treemap.pathFor(record); + cell.append('a') + .attr('href', href) + .attr('target', '_blank') + .text(escape(treemap.pathFor(record))); + } + + } + }; + showReport('100 Largest Paths', list, headers, dataFunction, styleFunction); +} + +function symbolFilterTextChanged() { + if (filterChanging) return true; + filterChanging = true; + var enabled = document.getElementById('symbol_types_filter').value; + for (var x=0; x<=25; x++) { + var checkBox = document.getElementById('check_' + x); + checkBox.checked = (enabled.indexOf(checkBox.value) != -1); + } + filterChanging = false; +} + +function updateFilterText() { + if (filterChanging) return true; + filterChanging = true; + var text = ''; + for (var x=0; x<=25; x++) { + var checkBox = document.getElementById('check_' + x); + if (checkBox.checked) { + text += checkBox.value; + } + } + document.getElementById('symbol_types_filter').value=text; + filterChanging = false; +} + +function initFilterOptions() { + updateFilterText(); + for (var x=0; x<=25; x++) { + var checkBox = document.getElementById('check_' + x); + checkBox.onchange=updateFilterText; + var swatch = document.getElementById('swatch_' + x); + swatch.style.backgroundColor = D3SymbolTreeMap.getColorForType(checkBox.value).toString(); + } + var gteCheckbox = document.getElementById('check_gte'); + gteCheckbox.onchange = function() { + document.getElementById('symbol_filter_gte').disabled = !gteCheckbox.checked; + } + var regexCheckbox = document.getElementById('check_regex'); + regexCheckbox.onchange = function() { + document.getElementById('symbol_filter_regex').disabled = !regexCheckbox.checked; + } + var excludeRegexCheckbox = document.getElementById('check_exclude_regex'); + excludeRegexCheckbox.onchange = function() { + document.getElementById('symbol_filter_exclude_regex').disabled = !excludeRegexCheckbox.checked; + } + var idealSizes = getIdealSizes(); + document.getElementById('width').value = idealSizes.width; + document.getElementById('height').value = idealSizes.height; + saveFilterSettings(); +} + +function filterSetAll(enabled) { + for (var x=0; x<=25; x++) { + var checkBox = document.getElementById('check_' + x); + checkBox.checked = enabled; + } + updateFilterText(); +} + +function showOptions() { + loadFilterSettings(); + var container = document.getElementById('options_container'); + var w = container.offsetWidth; + var h = container.offsetHeight; + container.style.margin = '-' + (h/2) + 'px 0 0 -' + (w/2) + 'px'; + container.style.visibility = 'visible'; +} + +function hideOptions() { + var container = document.getElementById('options_container'); + container.style.visibility = 'hidden'; +} + +function applySettings() { + hideOptions(); + var oldWidth = savedSettings.width; + var oldHeight = savedSettings.height; + var oldSymbols = savedSettings.symbolTypes; + var oldRegex = savedSettings.regex; + var oldExcludeRegex = savedSettings.excludeRegex; + var oldGte = savedSettings.gte; + var oldMaxLevels = savedSettings.maxLevels; + saveFilterSettings(); + var resizeNeeded = oldWidth !== savedSettings.width || oldHeight !== savedSettings.height; + var regexChanged = oldRegex !== savedSettings.regex; + var excludeRegexChanged = oldExcludeRegex !== savedSettings.excludeRegex; + var symbolsChanged = oldSymbols !== savedSettings.symbolTypes; + var gteChanged = oldGte !== savedSettings.gte; + var filterChanged = regexChanged || excludeRegexChanged || symbolsChanged || gteChanged; + var maxLevelsChanged = oldMaxLevels !== savedSettings.maxLevels; + + if (filterChanged) { + // Type filters + typeFilter = function(datum) { + if (datum.depth === 0) return true; // root node + if (datum.t === undefined) return true; + return savedSettings.symbolTypes !== undefined && + savedSettings.symbolTypes.indexOf(datum.t) !== -1; + } + + // Regex filter + var regexFilter = undefined; + if (savedSettings.regex !== undefined && savedSettings.regex.length > 0) { + console.log('filter: regex is "' + savedSettings.regex + '"'); + var regex = new RegExp(savedSettings.regex); + regexFilter = function(datum) { + if (datum.depth === 0) return true; // root node + var fullName = this.pathFor(datum); + if (datum.children === undefined) { // it is a leaf node (symbol) + fullName += ':' + datum.n; + } + return regex.test(fullName); + } + } + + // Exclude regex filter + var excludeRegexFilter = undefined; + if (savedSettings.excludeRegex !== undefined && savedSettings.excludeRegex.length > 0) { + console.log('filter: exclude-regex is "' + savedSettings.excludeRegex + '"'); + var excludeRegex = new RegExp(savedSettings.excludeRegex); + excludeRegexFilter = function(datum) { + if (datum.depth === 0) return true; // root node + var fullName = this.pathFor(datum); + if (datum.children === undefined) { // it is a leaf node (symbol) + fullName += ':' + datum.n; + } + return !excludeRegex.test(fullName); + } + } + + // Size filter + var sizeFilter = undefined; + if (savedSettings.gte !== undefined) { + console.log('filter: minimum size is ' + savedSettings.gte + ' bytes'); + sizeFilter = function(datum) { + if (datum.children !== undefined) return true; // non-leaf + if (datum.value === undefined) console.log('whoops'); + return datum.value >= savedSettings.gte; + } + } + + // Make a filter to apply to the tree + var filter = function(datum) { + if (typeFilter && !typeFilter.call(this, datum)) return false; + if (regexFilter && !regexFilter.call(this, datum)) return false; + if (excludeRegexFilter && !excludeRegexFilter.call(this, datum)) return false; + if (sizeFilter && !sizeFilter.call(this, datum)) return false; + return true; + }; + treemap.filter(filter); + } + + // Adjust levels if needed. + if (maxLevelsChanged) { + treemap.setMaxLevels(savedSettings.maxLevels); + } + + // Resize map if necessary. + if (resizeNeeded) { + console.log('desired treemap dimensions have changed, requesting resize'); + treemap.resize(savedSettings.width, savedSettings.height); + } +} + +function cancelSettings() { + hideOptions(); + loadFilterSettings(); +} + +function saveFilterSettings() { + savedSettings.symbolTypes = document.getElementById('symbol_types_filter').value; + if (document.getElementById('check_regex').checked) { + savedSettings.regex = document.getElementById('symbol_filter_regex').value; } else { - link = '<a href="https://code.google.com/p/chromium/codesearch#chromium/src/' - + record.location + '">' + escape(record.location) + '</a>'; - } - list.innerHTML += '<tr>' - + '<td>' + (i+1) + '</td>' - + '<td>' + escape(record.size) + '</td>' - + '<td style=\'white-space: nowrap;\'>' + escape(record.type) + '</td>' - + '<td>' + link + ':<br>' - + escape(record.symbol) + '</td>' - + '</tr>'; - } -} - -function show_report_largest_sources() { - console.log('displaying largest-sources report') - var div = document.getElementById('report'); - div.innerHTML = '<div><table id=\'list\' border=1><tr>' + - '<th>Rank</th><th>Size</th><th>Symbol Count</th><th>Source</th>' + - '</tr></table>'; - var list = document.getElementById('list'); - for (var i = 0; i < largestSources.length; i++) { - var record = largestSources[i]; - var link; - if (record.location.indexOf('out') == 0) { - link = record.location; + savedSettings.regex = undefined; + } + if (document.getElementById('check_exclude_regex').checked) { + savedSettings.excludeRegex = document.getElementById('symbol_filter_exclude_regex').value; } else { - link = '<a href="https://code.google.com/p/chromium/codesearch#chromium/src/' - + record.location + '">' + escape(record.location) + '</a>'; - } - - list.innerHTML += '<tr>' - + '<td>' + (i+1) + '</td>' - + '<td>' + escape(record.size) + '</td>' - + '<td>' + escape(record.symbol_count) + '</td>' - + '<td>' + link + '</td>' - + '</tr>'; - } -} - -function show_report_largest_vtables() { - console.log('displaying largest-vtables report') - var div = document.getElementById('report'); - div.innerHTML = '<div><table id=\'list\' border=1><tr>' + - '<th>Rank</th><th>Size</th><th>Symbol</th><th>Source</th>' + - '</tr></table>'; - var list = document.getElementById('list'); - for (var i = 0; i < largestVTables.length; i++) { - var record = largestVTables[i]; - var link; - if (record.location.indexOf('out') == 0) { - link = record.location; + savedSettings.excludeRegex = undefined; + } + if (document.getElementById('check_gte').checked) { + savedSettings.gte = parseInt(document.getElementById('symbol_filter_gte').value); } else { - link = '<a href="https://code.google.com/p/chromium/codesearch#chromium/src/' - + record.location + '">' + escape(record.location) + '</a>'; + savedSettings.gte = undefined; } + savedSettings.width = parseInt(document.getElementById('width').value); + savedSettings.height = parseInt(document.getElementById('height').value); + savedSettings.maxLevels = parseInt(document.getElementById('max_levels').value); +} - list.innerHTML += '<tr>' - + '<td>' + (i+1) + '</td>' - + '<td>' + escape(record.size) + '</td>' - + '<td>' + escape(record.symbol) + '</td>' - + '<td>' + link + '</td>' - + '</tr>'; - } +function loadFilterSettings() { + document.getElementById('symbol_types_filter').value = savedSettings.symbolTypes; + symbolFilterTextChanged(); + if (savedSettings.regex !== undefined) { + document.getElementById('check_regex').checked = true; + document.getElementById('symbol_filter_regex').value = savedSettings.regex; + } else { + document.getElementById('check_regex').checked = false; + } + if (savedSettings.excludeRegex !== undefined) { + document.getElementById('check_exclude_regex').checked = true; + document.getElementById('symbol_filter_exclude_regex').value = savedSettings.excludeRegex; + } else { + document.getElementById('check_exclude_regex').checked = false; + } + if (savedSettings.gte !== undefined) { + document.getElementById('check_gte').checked = true; + document.getElementById('symbol_filter_gte').value = savedSettings.gte; + } else { + document.getElementById('check_gte').checked = false; + } + document.getElementById('width').value = savedSettings.width; + document.getElementById('height').value = savedSettings.height; + document.getElementById('max_levels').value = savedSettings.maxLevels; +} + +function escape(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>'); } </script> +</head> +<body onload='init()'> +<div style='position: absolute; top: 5px; left: 5px;'> + <input type='button' onclick='showOptions()' value='Options & Legend...'> + <span style='-webkit-user-select: none; cursor: help;' title='Click to view the symbol legend or to configure filters and options for the treemap'>[?]</span> +</div> +<div style='position: absolute; right: 5px; top: 5px; white-space: nowrap;'> + Reports: + <input type='button' onclick='bigSymbolsReport()' value='Large Symbols' title='Click to view a report of the largest 100 symbols that are with the bounds of the treemap that is currently displayed.'> + <input type='button' onclick='bigPathsReport()' value='Large Files' title='Click to view a report of the largest 100 source files that are with the bounds of the treemap that is currently displayed.'> +</div> +<div style='text-align: center; margin-bottom: 5px;'> + <span style='font-size: x-large; font-weight: bold; font-variant: small-caps'>Binary Size Analysis<span id='subtitle'></span></span> + <br><span style='font-size: small; font-style: italic;'>Double-click a box to zoom in, double-click outermost title to zoom out.</span> +</div> +<table id='options_container' style='visibility: hidden; border: 3px ridge grey; padding: 0px; top: 50%; left: 50%; position: fixed; z-index: 2147483646; overflow: auto; background-color: rgba(255,255,255,0.9); border-radius: 10px; box-shadow: 10px 10px 5px rgba(80,80,80,0.7);'><tr><td style='vertical-align: top'> + <table cellspacing=0 cellborder=0 style='width:100%'> + <tr><th colspan=3 style='padding-bottom: .25em; text-decoration: underline;'>Symbol Types To Show</th></tr> + <tr> + <td style='width: 33%; white-space: nowrap; vertical-align: top;'> + <span class='swatch' id='swatch_0'> </span><input checked type='checkbox' id='check_0' value='A'>Global absolute (A) + <br><span class='swatch' id='swatch_1'> </span><input checked type='checkbox' id='check_1' value='B'>Global uninitialized data (B) + <br><span class='swatch' id='swatch_2'> </span><input checked type='checkbox' id='check_2' value='b'>Local uninitialized data (b) + <br><span class='swatch' id='swatch_3'> </span><input checked type='checkbox' id='check_3' value='C'>Global uninitialized common (C) + <br><span class='swatch' id='swatch_4'> </span><input checked type='checkbox' id='check_4' value='D'>Global initialized data (D) + <br><span class='swatch' id='swatch_5'> </span><input checked type='checkbox' id='check_5' value='d'>Local initialized data (d) + <br><span class='swatch' id='swatch_6'> </span><input checked type='checkbox' id='check_6' value='G'>Global small initialized data (G) + <br><span class='swatch' id='swatch_7'> </span><input checked type='checkbox' id='check_7' value='g'>Local small initialized data (g) + <br><span class='swatch' id='swatch_8'> </span><input checked type='checkbox' id='check_8' value='i'>Indirect function (i) + </td> + <td style='width: 33%; white-space: nowrap; vertical-align: top;'> + <span class='swatch' id='swatch_9'> </span><input checked type='checkbox' id='check_9' value='N'>Debugging (N) + <br><span class='swatch' id='swatch_10'> </span><input checked type='checkbox' id='check_10' value='p'>Stack unwind (p) + <br><span class='swatch' id='swatch_11'> </span><input checked type='checkbox' id='check_11' value='R'>Global read-only data (R) + <br><span class='swatch' id='swatch_12'> </span><input checked type='checkbox' id='check_12' value='r'>Local read-only data (r) + <br><span class='swatch' id='swatch_13'> </span><input checked type='checkbox' id='check_13' value='S'>Global small uninitialized data (S) + <br><span class='swatch' id='swatch_14'> </span><input checked type='checkbox' id='check_14' value='s'>Local small uninitialized data (s) + <br><span class='swatch' id='swatch_15'> </span><input checked type='checkbox' id='check_15' value='T'>Global code (T) + <br><span class='swatch' id='swatch_16'> </span><input checked type='checkbox' id='check_16' value='t'>Local code (t) + <br><span class='swatch' id='swatch_17'> </span><input checked type='checkbox' id='check_17' value='U'>Undefined (U) + </td> + <td style='width: 33%; white-space: nowrap; vertical-align: top;'> + <span class='swatch' id='swatch_18'> </span><input checked type='checkbox' id='check_18' value='u'>Unique (u) + <br><span class='swatch' id='swatch_19'> </span><input checked type='checkbox' id='check_19' value='V'>Global weak object (V) + <br><span class='swatch' id='swatch_20'> </span><input checked type='checkbox' id='check_20' value='v'>Local weak object (v) + <br><span class='swatch' id='swatch_21'> </span><input checked type='checkbox' id='check_21' value='W'>Global weak symbol (W) + <br><span class='swatch' id='swatch_22'> </span><input checked type='checkbox' id='check_22' value='w'>Local weak symbol (w) + <br><span class='swatch' id='swatch_23'> </span><input checked type='checkbox' id='check_23' value='@'>Vtable entry (@) + <br><span class='swatch' id='swatch_24'> </span><input checked type='checkbox' id='check_24' value='-'>STABS debugging (-) + <br><span class='swatch' id='swatch_25'> </span><input checked type='checkbox' id='check_25' value='?'>Unrecognized (?) + </td> + </tr> + <tr><td colspan=3 style='text-align: center; white-space: nowrap; padding-top: 1em;'> + Select <input type='button' onclick='filterSetAll(true)' value='All'>, + <input type='button' onclick='filterSetAll(false)' value='None'>, + or type a string: <input id='symbol_types_filter' size=30 value='' onkeyup='symbolFilterTextChanged()' onblur='updateFilterText()'> + <span style='-webkit-user-select: none; cursor: help;' title='Enter codes from the list above for the symbols you want to see. The checkboxes will update automatically to match the string that you enter.'>[?]</span> + </td></tr> + </table> +</td></tr><tr><td style='vertical-align: top; padding-top: 10px; border-top: 1px solid grey;'> + <table cellspacing=0 cellborder=0 style='width: 100%'> + <tr><th colspan=2 style='padding-bottom: .25em; text-decoration: underline;'>Advanced Options</th></tr> + <tr> + <td style='white-space: nowrap; vertical-align: top;'> + <input type='checkbox' id='check_regex'> + Only include symbols matching this regex: + </td> + <td style='text-align: right; vertical-align: top;'> + <input disabled id='symbol_filter_regex' size=30 value='' style='text-align: right;'> + <span style='-webkit-user-select: none; cursor: help;' title='Enter a javascript regex. Only symbols that match this regex will be shown. This filter applies before any exclusion regex specified below. The format of each symbol is [path]:[symbol_name]'>[?]</span> + </td> + </tr> + <tr> + <td style='white-space: nowrap; vertical-align: top;'> + <input type='checkbox' id='check_exclude_regex'> + Exclude all symbols matching this regex: + </td> + <td style='text-align: right; vertical-align: top;'> + <input disabled id='symbol_filter_exclude_regex' size=30 value='' style='text-align: right;'> + <span style='-webkit-user-select: none; cursor: help;' title='Enter a javascript regex. Symbols that match this tegex will not be shown. This filter applies after any inclusion filter specified above. The format of each symbol is [path]:[symbol_name]'>[?]</span> + </td> + </tr> + <tr> + <td style='white-space: nowrap; vertical-align: top;'> + <input type='checkbox' id='check_gte'> + Only include symbols that are at least <span style='font-style: italic;'>n</span> bytes: + </td> + <td style='text-align: right; vertical-align: top;'> + <input disabled id='symbol_filter_gte' size=8 value='' style='text-align: right;'> + <span style='-webkit-user-select: none; cursor: help;' title='Symbols whose size is less than this value will be hidden.'>[?]</span> + </td> + </tr> + <tr> + <td style='white-space: nowrap vertical-align: top;;'> + Show at most <span style='font-style: italic;'>n</span> levels of detail at a time: + </td> + <td style='text-align: right; vertical-align: top;'> + <input id='max_levels' size=4 value='2' style='text-align: right;'><span style='-webkit-user-select: none; cursor: help;' title='Increasing this value shows more detail without the need to zoom, but uses more computing power.'>[?]</span> + </td> + </tr> + <tr> + <td style='white-space: nowrap vertical-align: top;;'> + Set the size of the treemap to <span style='font-style: italic;'>W x H</span> pixels: + </td> + <td style='text-align: right; vertical-align: top;'> + <input id='width' size=4 value='' style='text-align: right;'> + x <input id='height' size=4 value='' style='text-align: right;'> + </td> + </tr> + </table> +</td></tr> +<tr><td style='padding-top: 10px; text-align: right; border-top: 1px solid grey'> + <input type='button' value='Apply' onclick='applySettings()'> + <input type='button' value='Cancel' onclick='cancelSettings()'> +</td></tr></table> </body> -</html>
\ No newline at end of file +</html> diff --git a/tools/binary_size/template/test-data-generator.html b/tools/binary_size/template/test-data-generator.html new file mode 100644 index 0000000..9c6790a --- /dev/null +++ b/tools/binary_size/template/test-data-generator.html @@ -0,0 +1,157 @@ +<!DOCTYPE html> +<!-- + Copyright 2014 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<html> +<head> +<script> +function rnd(max) { + return Math.round(Math.random()*max); +} + +function gen() { + var dirs1=['usr1', 'etc1', 'var1']; + var dirs2=['aaa2', 'bbb2', 'ccc2', 'ddd2', 'eee2', 'fff2', 'ggg2', 'hhh2', + 'frobozz2', 'kazaam2', 'shazam2']; + var dirs3=['iii3', 'jjj3', 'kkk3', 'lll3', 'mmm3', 'nnn3', 'ooo3', 'ppp3', + 'wonderllama3', 'excelsior3', 'src3']; + var filenames=['awesome.cc', 'rad.h', 'tubular.cxx', 'cool.cc', 'groovy.h', + 'excellent.c', 'gnarly.h', 'green.C', 'articulate.cc']; + //All possible types (we only see a subset in practice): 'ABbCDdGgiNpRrSsTtUuVvWw-?'; + var nm_symbol_types = 'trd'; + var minSize = 4; + var maxSize = 10000; + var numGen = 300000; + var text = 'var nm_data=[\n'; + var vtablePercent = 5; + for (var x=0; x<numGen; x++) { + var path = '/' + + dirs1[rnd(dirs1.length - 1)] + '/' + + dirs2[rnd(dirs2.length - 1)] + '/' + + dirs3[rnd(dirs3.length - 1)] + '/' + + filenames[rnd(filenames.length - 1)]; + var isVtable = Math.floor((Math.random()*100)+1) <= vtablePercent; + var size = rnd(maxSize); + var symbol_name; + var type; + if (!isVtable) { + symbol_name = 'sym' + x.toString(16); + type = nm_symbol_types.charAt(rnd(nm_symbol_types.length - 1)); + } else { + symbol_name = 'vtable for ' + x.toString(16); + type = '@' + } + text = text + "{'n': '" + symbol_name + + "', 't': '" + type + + "', 's': " + size + + ", 'p': '" + path + "'},\n"; + } + text += '];'; + + eval(text); + var treeified = to_d3_tree(nm_data); + generateDownloadLink('tree_data=' + JSON.stringify(treeified)); +} + +function generateDownloadLink(content) { + var blob = new Blob([content], {type: 'text/plain'}); + var link = document.createElement('a'); + link.download = 'generated-content.txt'; + link.href = window.URL.createObjectURL(blob); + link.textContent = 'Download ready, click here.'; + link.dataset.downloadurl = ['text/plain', link.download, link.href].join(':'); + link.onclick = function(e) { + if ('disabled' in this.dataset) { return false; } + link.dataset.disabled = true; + setTimeout(function() { window.URL.revokeObjectURL(link.href); }, 1500); + }; + document.getElementById('linkcontainer').innerHTML = ''; + document.getElementById('linkcontainer').appendChild(link); +} + +/** + * This function takes in an array of nm records and converts them into a + * hierarchical data structure suitable for use in a d3-base treemap layout. + * Leaves are individual symbols. The parents of the leaves are logical + * groupings by common symbol-type (for BSS, read-only data, code, etc). + * Above this, each node represents part of a filesystem path relative + * to the parent node. The root node has the name '/', and represents + * a root (though not necessarily THE root) of a file system traversal. + * The root node also has a special property, 'maxDepth', to which is bound + * the deepest level of nesting that was found during conversion: for the + * record at path /a/b/c/d.foo, the maxDepth will be 6; the file 'd.foo' + * is at depth 4, the type-bucket is depth 5 and the symbols are depth 6. + */ +function to_d3_tree(records) { + var result = {'n': '/', 'children': [], 'k': 'p'}; + var maxDepth = 0; + //{'n': 'symbol1', 't': 'b', 's': 1000, 'p': '/usr/local/foo/foo.cc'}, + for (index in records) { + var record = records[index]; + var parts = record.p.split("/"); + var node = result; + var depth = 0; + // Walk the tree and find the file that is named by the "location" + // field of the record. We create any intermediate nodes required. + // This is directly analogous to "mkdir -p". + while(parts.length > 0) { + var part = parts.shift(); + if (part.length == 0) continue; + depth++; + node = _mk_child(node, part, record.s); + node.k = 'p'; // p for path + } + node.lastPathElement = true; + + // 'node' is now the file node. Find the symbol-type bucket. + node = _mk_child(node, record.t, record.s); + node.t = record.t; + node.k = 'b'; // b for bucket + depth++; + // 'node' is now the symbol-type bucket. Make the child entry. + node = _mk_child(node, record.n, record.s); + delete node.children; + node.value = record.s; + node.t = record.t; + node.k = 's'; // s for symbol + depth++; + + maxDepth = Math.max(maxDepth, depth); + } + result.maxDepth = maxDepth; + return result; +} + +/** + * Given a node and a name, return the child within node.children whose + * name matches the specified name. If necessary, a new child node is + * created and appended to node.children. + * If this method creates a new node, the 'name' attribute is set to the + * specified name and the 'children' attribute is an empty array, and + * total_size is the specified size. Otherwise, the existing node is + * returned and its total_size value is incremented by the specified size. + */ +function _mk_child(node, name, size) { + var child = undefined; + for (child_index in node.children) { + if (node.children[child_index].n == name) { + child = node.children[child_index]; + } + } + if (child === undefined) { + child = {'n': name, 'children': []}; + node.children.push(child); + } + return child; +} +</script> +</head> +<body style='white-space: pre; font-family: monospace;'> +This script generates sample data for use in D3SymbolTreeMap, and can be used +for testing. +<input type=button onclick='gen();' value='Generate data'></input> +<div id='linkcontainer'></div> +</body> +</html> |