Commit 3b181ae2 authored by Tiger Oakes's avatar Tiger Oakes Committed by Commit Bot

Added new redesign for supersize HTML report under flag

supersize html_report (in //tools/binary_size) is being redesigned to use a
tree view to make it easier to navigate. Tree views are more familiar to users
and open up additional features we plan to add, such as rendering size diffs by
showing negative values. Symbols and files in the tree are organized by size,
with larger items on top.

You can preview an example report at:
https://notwoods.github.io/chrome-supersize-reports/

Alternatively, compile your own report by running:
```
tools/binary_size/supersize html_report chrome.size --tree-view-ui --report-dir size-report -v
xdg-open size-report/index.html
```

Design Doc: go/supersize-tree-view-ui

Bug: 847599
Change-Id: I78add67bbc759903b1717585a9463dfd08dacceb
Reviewed-on: https://chromium-review.googlesource.com/1113848
Commit-Queue: Tiger Oakes <tigero@google.com>
Reviewed-by: default avatarEric Stevenson <estevenson@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#570762}
parent e8f2c121
...@@ -4,12 +4,10 @@ ...@@ -4,12 +4,10 @@
"""Creates an html report that allows you to view binary size by component.""" """Creates an html report that allows you to view binary size by component."""
import argparse
import json import json
import logging import logging
import os import os
import shutil import shutil
import sys
import archive import archive
import path_util import path_util
...@@ -32,7 +30,29 @@ _NODE_SYMBOL_SIZE_KEY = 'value' ...@@ -32,7 +30,29 @@ _NODE_SYMBOL_SIZE_KEY = 'value'
_NODE_MAX_DEPTH_KEY = 'maxDepth' _NODE_MAX_DEPTH_KEY = 'maxDepth'
_NODE_LAST_PATH_ELEMENT_KEY = 'lastPathElement' _NODE_LAST_PATH_ELEMENT_KEY = 'lastPathElement'
_COMPACT_FILE_PATH_INDEX_KEY = 'p'
_COMPACT_FILE_COMPONENT_INDEX_KEY = 'c'
_COMPACT_FILE_SYMBOLS_KEY = 's'
_COMPACT_SYMBOL_NAME_KEY = 'n'
_COMPACT_SYMBOL_BYTE_SIZE_KEY = 'b'
_COMPACT_SYMBOL_TYPE_KEY = 't'
_SYMBOL_TYPE_DESCRIPTIONS = {
'b': '.bss',
'd': '.data and .data.*',
'r': '.rodata',
't': '.text',
'v': 'Vtable Entry',
'*': 'Generated Symbols (typeinfo, thunks, etc)',
'x': 'Dex Non-Method Entries',
'm': 'Dex Methods',
'p': 'Locale Pak Entries',
'P': 'Non-Locale Pak Entries',
'o': 'Other Entries',
}
# The display name of the bucket where we put symbols without path. # The display name of the bucket where we put symbols without path.
_NAME_SMALL_SYMBOL_BUCKET = '(Other)'
_NAME_NO_PATH_BUCKET = '(No Path)' _NAME_NO_PATH_BUCKET = '(No Path)'
# Try to keep data buckets smaller than this to avoid killing the # Try to keep data buckets smaller than this to avoid killing the
...@@ -149,11 +169,7 @@ def _MakeCompactTree(symbols, min_symbol_size, method_count_mode): ...@@ -149,11 +169,7 @@ def _MakeCompactTree(symbols, min_symbol_size, method_count_mode):
depth += 1 depth += 1
node = _GetOrMakeChildNode(node, _NODE_TYPE_PATH, path_part) node = _GetOrMakeChildNode(node, _NODE_TYPE_PATH, path_part)
symbol_type = symbol.section symbol_type = _GetSymbolType(symbol)
if symbol.name.endswith('[vtable]'):
symbol_type = _NODE_SYMBOL_TYPE_VTABLE
elif symbol.name.endswith(']'):
symbol_type = _NODE_SYMBOL_TYPE_GENERATED
symbol_size = 1 if method_count_mode else symbol.pss symbol_size = 1 if method_count_mode else symbol.pss
_AddSymbolIntoFileNode(node, symbol_type, symbol.template_name, symbol_size, _AddSymbolIntoFileNode(node, symbol_type, symbol.template_name, symbol_size,
min_symbol_size) min_symbol_size)
...@@ -172,18 +188,117 @@ def _MakeCompactTree(symbols, min_symbol_size, method_count_mode): ...@@ -172,18 +188,117 @@ def _MakeCompactTree(symbols, min_symbol_size, method_count_mode):
return result return result
def _CopyTemplateFiles(dest_dir): def _GetSymbolType(symbol):
symbol_type = symbol.section
if symbol.name.endswith('[vtable]'):
symbol_type = _NODE_SYMBOL_TYPE_VTABLE
elif symbol.name.endswith(']'):
symbol_type = _NODE_SYMBOL_TYPE_GENERATED
return symbol_type
class IndexedSet(object):
"""Set-like object where values are unique and indexed.
Values must be immutable.
"""
def __init__(self):
self._index_dict = {} # Value -> Index dict
self.value_list = [] # List containing all the set items
def GetOrAdd(self, value):
"""Get the index of the value in the list. Append it if not yet present."""
index = self._index_dict.get(value)
if index is None:
self.value_list.append(value)
index = len(self.value_list) - 1
self._index_dict[value] = index
return index
def _MakeTreeViewList(symbols, min_symbol_size):
file_nodes = {}
source_paths = IndexedSet()
components = IndexedSet()
small_symbols = {}
small_file_node = None
if min_symbol_size > 0:
path_index = source_paths.GetOrAdd(_NAME_SMALL_SYMBOL_BUCKET)
small_file_node = {
_COMPACT_FILE_PATH_INDEX_KEY: path_index,
_COMPACT_FILE_COMPONENT_INDEX_KEY: components.GetOrAdd(''),
_COMPACT_FILE_SYMBOLS_KEY: [],
}
file_nodes[_NAME_SMALL_SYMBOL_BUCKET] = small_file_node
for symbol in symbols:
symbol_type = _GetSymbolType(symbol)
if symbol.pss >= min_symbol_size:
path = symbol.source_path or symbol.object_path
file_node = file_nodes.get(path)
if file_node is None:
path_index = source_paths.GetOrAdd(path)
component_index = components.GetOrAdd(symbol.component)
file_node = {
_COMPACT_FILE_PATH_INDEX_KEY: path_index,
_COMPACT_FILE_COMPONENT_INDEX_KEY: component_index,
_COMPACT_FILE_SYMBOLS_KEY: [],
}
file_nodes[path] = file_node
file_node[_COMPACT_FILE_SYMBOLS_KEY].append({
_COMPACT_SYMBOL_NAME_KEY: symbol.template_name,
_COMPACT_SYMBOL_TYPE_KEY: symbol_type,
_COMPACT_SYMBOL_BYTE_SIZE_KEY: symbol.pss,
})
elif min_symbol_size > 0:
if symbol_type not in _SYMBOL_TYPE_DESCRIPTIONS:
symbol_type = 'o'
small_type_symbol = small_symbols.get(symbol_type)
if small_type_symbol is None:
small_type_symbol = {
_COMPACT_SYMBOL_NAME_KEY: _SYMBOL_TYPE_DESCRIPTIONS[symbol_type],
_COMPACT_SYMBOL_TYPE_KEY: symbol_type,
_COMPACT_SYMBOL_BYTE_SIZE_KEY: 0,
}
small_symbols[symbol_type] = small_type_symbol
small_file_node[_COMPACT_FILE_SYMBOLS_KEY].append(small_type_symbol)
small_type_symbol[_COMPACT_SYMBOL_BYTE_SIZE_KEY] += symbol.pss
return {
'file_nodes': list(file_nodes.values()),
'source_paths': source_paths.value_list,
'components': components.value_list,
}
def _CopyTemplateFiles(template_src, dest_dir):
d3_out = os.path.join(dest_dir, 'd3') d3_out = os.path.join(dest_dir, 'd3')
if not os.path.exists(d3_out): if not os.path.exists(d3_out):
os.makedirs(d3_out, 0755) os.makedirs(d3_out, 0755)
d3_src = os.path.join(path_util.SRC_ROOT, 'third_party', 'd3', 'src') d3_src = os.path.join(path_util.SRC_ROOT, 'third_party', 'd3', 'src')
template_src = os.path.join(os.path.dirname(__file__), 'template')
shutil.copy(os.path.join(template_src, 'index.html'), dest_dir)
shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out) 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(d3_src, 'd3.js'), d3_out)
shutil.copy(os.path.join(template_src, 'index.html'), dest_dir)
shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), dest_dir) shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), dest_dir)
def _CopyTreeViewTemplateFiles(template_src, dest_dir):
shutil.copy(os.path.join(template_src, 'index.html'), dest_dir)
shutil.copy(os.path.join(template_src, 'state.js'), dest_dir)
with open(os.path.join(dest_dir, 'tree.js'), 'w') as out_js, \
open(os.path.join(template_src, 'ui.js'), 'r') as ui, \
open(os.path.join(template_src, 'tree-worker.js'), 'r') as worker:
out_js.write(ui.read().replace('--INSERT_WORKER_CODE--', worker.read()))
def AddArguments(parser): def AddArguments(parser):
parser.add_argument('input_file', parser.add_argument('input_file',
help='Path to input .size file.') help='Path to input .size file.')
...@@ -198,6 +313,8 @@ def AddArguments(parser): ...@@ -198,6 +313,8 @@ def AddArguments(parser):
'an independent node.') 'an independent node.')
parser.add_argument('--method-count', action='store_true', parser.add_argument('--method-count', action='store_true',
help='Show dex method count rather than size') help='Show dex method count rather than size')
parser.add_argument('--tree-view-ui', action='store_true',
help='Use the new tree view UI')
def Run(args, parser): def Run(args, parser):
...@@ -212,19 +329,34 @@ def Run(args, parser): ...@@ -212,19 +329,34 @@ def Run(args, parser):
elif not args.include_bss: elif not args.include_bss:
symbols = symbols.WhereInSection('b').Inverted() symbols = symbols.WhereInSection('b').Inverted()
# Copy report boilerplate into output directory. This also proves that the if args.tree_view_ui:
# output directory is safe for writing, so there should be no problems writing template_src = os.path.join(os.path.dirname(__file__), 'template_tree_view')
# the nm.out file later. _CopyTreeViewTemplateFiles(template_src, args.report_dir)
_CopyTemplateFiles(args.report_dir) logging.info('Creating JSON objects')
tree_root = _MakeTreeViewList(symbols, args.min_symbol_size)
logging.info('Creating JSON objects')
tree_root = _MakeCompactTree(symbols, args.min_symbol_size, args.method_count) logging.info('Serializing JSON')
with open(os.path.join(args.report_dir, 'data.js'), 'w') as out_file:
logging.info('Serializing JSON') out_file.write('var tree_data=`')
with open(os.path.join(args.report_dir, 'data.js'), 'w') as out_file: # Use separators without whitespace to get a smaller file.
out_file.write('var tree_data=') json.dump(tree_root, out_file, ensure_ascii=False, check_circular=False,
# Use separators without whitespace to get a smaller file. separators=(',', ':'))
json.dump(tree_root, out_file, ensure_ascii=False, check_circular=False, out_file.write('`')
separators=(',', ':')) else:
# Copy report boilerplate into output directory. This also proves that the
# output directory is safe for writing, so there should be no problems
# writing the nm.out file later.
template_src = os.path.join(os.path.dirname(__file__), 'template')
_CopyTemplateFiles(template_src, args.report_dir)
logging.info('Creating JSON objects')
tree_root = _MakeCompactTree(symbols, args.min_symbol_size,
args.method_count)
logging.info('Serializing JSON')
with open(os.path.join(args.report_dir, 'data.js'), 'w') as out_file:
out_file.write('var tree_data=')
# Use separators without whitespace to get a smaller file.
json.dump(tree_root, out_file, ensure_ascii=False, check_circular=False,
separators=(',', ':'))
logging.warning('Report saved to %s/index.html', args.report_dir) logging.warning('Report saved to %s/index.html', args.report_dir)
This diff is collapsed.
// Copyright 2018 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.
'use strict';
/**
* @fileoverview
* Methods for manipulating the state and the DOM of the page
*/
/** @type {HTMLFormElement} Form containing options and filters */
const form = document.getElementById('options');
/** Utilities for working with the DOM */
const dom = {
/**
* Create a document fragment from the given nodes
* @param {Iterable<Node>} nodes
* @returns {DocumentFragment}
*/
createFragment(nodes) {
const fragment = document.createDocumentFragment();
for (const node of nodes) fragment.appendChild(node);
return fragment;
},
/**
* Removes all the existing children of `parent` and inserts
* `newChild` in their place
* @param {Node} parent
* @param {Node} newChild
*/
replace(parent, newChild) {
while (parent.firstChild) parent.removeChild(parent.firstChild);
parent.appendChild(newChild);
},
};
{
/**
* State is represented in the query string and
* can be manipulated by this object. Keys in the query match with
* input names.
*/
const _filterParams = new URLSearchParams(location.search.slice(1));
/** Utilities for working with the state */
const state = {
/**
* Returns a string from the current query string state.
* Can optionally restrict valid values for the query.
* Values not present in the query will return null, or the default
* value if supplied.
* @param {string} key
* @param {object} [options]
* @param {string} [options.default] Default to use if key is not present
* in the state
* @param {Set<string>} [options.valid] If provided, values must be in this
* set to be returned. Invalid values will return null or `defaultValue`.
* @returns {string | null}
*/
get(key, options = {}) {
let val = _filterParams.get(key);
if (options.valid != null && !options.valid.has(val)) {
val = null;
}
if (options.default != null && val == null) {
val = options.default;
}
return val;
},
/**
* Set the value in the state, overriding any existing value. Afterwards
* display the new state in the URL by replacing the current history entry.
* @param {string} name
* @param {string} value
*/
set(name, value) {
_filterParams.set(name, value);
history.replaceState(null, null, '?' + _filterParams.toString());
},
};
// Update form inputs to reflect the state from URL.
for (const [name, value] of _filterParams) {
if (form[name] != null) form[name].value = value;
}
/**
* Some form inputs can be changed without needing to refresh the tree.
* When these inputs change, update the state and the change event can
* be subscribed to elsewhere in the code.
* @param {Event} event Change event fired when a dynamic value is updated
*/
function _onDynamicValueChange(event) {
const {name, value} = event.currentTarget;
state.set(name, value);
}
for (const dynamicInput of document.querySelectorAll('form [data-dynamic]')) {
dynamicInput.addEventListener('change', _onDynamicValueChange);
}
self.state = state;
}
// Copyright 2018 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.
'use strict';
/**
* @fileoverview
* Web worker code to parse JSON data from binary_size into data for the UI to
* display.
* Note: backticks (\`) are banned from this file as the file contents are
* turned into a Javascript string encapsulated by backticks.
*/
/**
* @typedef {object} DataFile JSON data created by html_report python script
* @prop {FileEntry[]} file_nodes - List of file entries
* @prop {string[]} source_paths - Array of source_paths referenced by index in
* the symbols.
* @prop {string[]} components - Array of components referenced by index in the
* symbols.
*/
/**
* @typedef {object} FileEntry JSON object representing a single file and its
* symbols.
* @prop {number} p - source_path_index
* @prop {number} c - component_index
* @prop {Array<{n:string,b:number,t:string}>} s - Symbols belonging to this
* node. Array of objects.
* n - name of the symbol
* b - size of the symbol in bytes, divided by num_aliases.
* t - single character string to indicate the symbol type
*/
/**
* @typedef {object} TreeNode Node object used to represent the file tree
* @prop {TreeNode[]} children Child tree nodes
* @prop {TreeNode | null} parent Parent tree node. null if this is a root node.
* @prop {string} idPath
* @prop {string} shortName
* @prop {number} size
* @prop {string} type
*/
/**
* Abberivated keys used by FileEntrys in the JSON data file.
* @const
* @enum {string}
*/
const _KEYS = {
SOURCE_PATH_INDEX: 'p',
COMPONENT_INDEX: 'c',
FILE_SYMBOLS: 's',
SYMBOL_NAME: 'n',
SIZE: 'b',
TYPE: 't',
};
const _NO_NAME = '(No path)';
const _DIRECTORY_TYPE = 'D';
const _FILE_TYPE = 'F';
/**
* Return the basename of the pathname 'path'. In a file path, this is the name
* of the file and its extension. In a folder path, this is the name of the
* folder.
* @param {string} path Path to find basename of.
* @param {string} sep Path seperator, such as '/'.
*/
function basename(path, sep) {
const idx = path.lastIndexOf(sep);
return path.substring(idx + 1);
}
/**
* Return the basename of the pathname 'path'. In a file path, this is the
* full path of its folder.
* @param {string} path Path to find dirname of.
* @param {string} sep Path seperator, such as '/'.
*/
function dirname(path, sep) {
const idx = path.lastIndexOf(sep);
return path.substring(0, idx);
}
/**
* Collapse "java"->"com"->"google" into "java/com/google". Nodes will only be
* collapsed if they are the same type, most commonly by merging directories.
* @param {TreeNode} node Node to potentially collapse. Will be modified by
* this function.
* @param {string} sep Path seperator, such as '/'.
*/
function combineSingleChildNodes(node, sep) {
if (node.children.length > 0) {
const [child] = node.children;
// If there is only 1 child and its the same type, merge it in.
if (node.children.length === 1 && node.type === child.type) {
// size & type should be the same, so don't bother copying them.
node.shortName += sep + child.shortName;
node.idPath = child.idPath;
node.children = child.children;
// Search children of this node.
combineSingleChildNodes(node, sep);
} else {
// Search children of this node.
node.children.forEach(child => combineSingleChildNodes(child, sep));
}
}
}
/**
* Sorts nodes in place based on their sizes.
* @param {TreeNode} node Node whose children will be sorted. Will be modified
* by this function.
*/
function sortTree(node) {
node.children.sort((a, b) => b.size - a.size);
node.children.forEach(sortTree);
}
/**
* Link a node to a new parent. Will go up the tree to update parent sizes to
* include the new child.
* @param {TreeNode} node Child node.
* @param {TreeNode} parent New parent node.
*/
function attachToParent(node, parent) {
// Link the nodes together
parent.children.push(node);
node.parent = parent;
// Update the size of all ancestors
const {size} = node;
while (node != null && node.parent != null) {
node.parent.size += size;
node = node.parent;
}
}
/**
* Make a node with some default arguments
* @param {Partial<TreeNode>} options Values to use for the node. If a value is
* omitted, a default will be used instead.
* @param {string} sep Path seperator, such as '/'. Used for creating a default
* shortName.
* @returns {TreeNode}
*/
function createNode(options, sep) {
const {idPath, type, shortName = basename(idPath, sep), size = 0} = options;
return {
children: [],
parent: null,
idPath,
shortName,
size,
type,
};
}
/**
* Build a tree from a list of symbol objects.
* @param {Iterable<FileEntry>} symbols List of basic symbols.
* @param {(symbol: FileEntry) => string} getPath Called to get the id path of
* a symbol.
* @param {string} sep Path seperator used to find parent names.
* Defaults to '/'.
* @returns {TreeNode} Root node of the new tree
*/
function makeTree(symbols, getPath, sep) {
const rootNode = createNode(
{idPath: '/', shortName: '/', type: _DIRECTORY_TYPE},
sep
);
/** @type {Map<string, TreeNode>} Cache for directory nodes */
const parents = new Map();
function getOrMakeParentNode(node) {
// Get idPath of this node's parent.
let parentPath;
if (node.idPath === '') parentPath = _NO_NAME;
else parentPath = dirname(node.idPath, sep);
// check if parent exists
let parentNode;
if (parentPath === '') {
// parent is root node if dirname is ''
parentNode = rootNode;
} else {
// get parent from cache if it exists, otherwise create it
parentNode = parents.get(parentPath);
if (parentNode == null) {
parentNode = createNode(
{idPath: parentPath, type: _DIRECTORY_TYPE},
sep
);
parents.set(parentPath, parentNode);
}
}
// attach node to the newly found parent
attachToParent(node, parentNode);
}
for (const fileNode of symbols) {
// make path for this
const idPath = getPath(fileNode);
// make node for this
const node = createNode({idPath, type: _FILE_TYPE}, sep);
// build child nodes for this file's symbols and attach to self
for (const symbol of fileNode[_KEYS.FILE_SYMBOLS]) {
const symbolNode = createNode(
{
idPath: idPath + ':' + symbol[_KEYS.SYMBOL_NAME],
shortName: symbol[_KEYS.SYMBOL_NAME],
size: symbol[_KEYS.SIZE],
type: symbol[_KEYS.TYPE],
},
sep
);
attachToParent(symbolNode, node);
}
// build parent node and attach to parent
getOrMakeParentNode(node);
}
// build parents for the directories until reaching the root node
for (const directory of parents.values()) {
getOrMakeParentNode(directory);
}
// Collapse nodes such as "java"->"com"->"google" into "java/com/google".
combineSingleChildNodes(rootNode, sep);
// Sort the tree so that larger items are higher.
sortTree(rootNode);
return rootNode;
}
/**
* Assemble a tree when this worker receives a message.
* @param {MessageEvent} event Event for when this worker receives a message.
* @param {object} event.data Event data from the UI thread.
* @param {string} event.data.treeData Stringified JSON representing the symbol
* tree as a flat list of files.
* @param {string} event.data.sep Path seperator used to split file paths, such
* as '/'.
* @param {string} event.data.filters Query string used to filter the resulting
* tree.
*/
self.onmessage = event => {
/** @type {{tree:DataFile,filters:string}} JSON data parsed from string */
const {tree, filters} = JSON.parse(event.data);
const params = new URLSearchParams(filters);
const rootNode = makeTree(
tree.file_nodes,
s => tree.source_paths[s[_KEYS.SOURCE_PATH_INDEX]],
params.get('sep') || '/'
);
// @ts-ignore
self.postMessage(rootNode);
};
// Copyright 2018 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.
'use strict';
/**
* @fileoverview
* UI classes and methods for the Binary Size Analysis HTML report.
*/
{
/**
* @enum {number} Various byte units and the corresponding amount of bytes
* that one unit represents.
*/
const _BYTE_UNITS = {
GiB: 1024 ** 3,
MiB: 1024 ** 2,
KiB: 1024 ** 1,
B: 1024 ** 0,
};
/** Set of all byte units */
const _BYTE_UNITS_SET = new Set(Object.keys(_BYTE_UNITS));
const _icons = document.getElementById('icons');
/**
* @enum {SVGSVGElement} Icon elements that correspond to each symbol type.
*/
const _SYMBOL_ICONS = {
D: _icons.querySelector('.foldericon'),
F: _icons.querySelector('.fileicon'),
b: _icons.querySelector('.bssicon'),
d: _icons.querySelector('.dataicon'),
r: _icons.querySelector('.readonlyicon'),
t: _icons.querySelector('.codeicon'),
v: _icons.querySelector('.vtableicon'),
'*': _icons.querySelector('.generatedicon'),
x: _icons.querySelector('.dexicon'),
m: _icons.querySelector('.dexmethodicon'),
p: _icons.querySelector('.localpakicon'),
P: _icons.querySelector('.nonlocalpakicon'),
o: _icons.querySelector('.othericon'), // used as default icon
};
// Templates for tree nodes in the UI.
const _leafTemplate = document.getElementById('treeitem');
const _treeTemplate = document.getElementById('treefolder');
/**
* @type {WeakMap<HTMLElement, Readonly<TreeNode>>}
* Associates UI nodes with the corresponding tree data object
* so that event listeners and other methods can
* query the original data.
*/
const _uiNodeData = new WeakMap();
/**
* Replace the contexts of the size element for a tree node.
* The unit to use is selected from the current state,
* and the original number of bytes will be displayed as
* hover text over the element.
* @param {HTMLElement} sizeElement Element that shoudl display the byte size
* @param {number} bytes Number of bytes to use for the size text
*/
function _setSizeContents(sizeElement, bytes) {
// Get unit from query string, will fallback for invalid query
const suffix = state.get('byteunit', {
default: 'MiB',
valid: _BYTE_UNITS_SET,
});
const value = _BYTE_UNITS[suffix];
// Format the bytes as a number with 2 digits after the decimal point
const text = (bytes / value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const textNode = document.createTextNode(`${text} `);
// Display the suffix with a smaller font
const suffixElement = document.createElement('small');
suffixElement.textContent = suffix;
// Replace the contents of '.size' and change its title
dom.replace(sizeElement, dom.createFragment([textNode, suffixElement]));
sizeElement.title =
bytes.toLocaleString(undefined, {useGrouping: true}) + ' bytes';
}
/**
* Click event handler to expand or close the child group of a tree.
* @param {Event} event
*/
function _toggleTreeElement(event) {
event.preventDefault();
const link = event.currentTarget;
const element = link.parentElement;
const group = link.nextElementSibling;
const isExpanded = element.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
element.setAttribute('aria-expanded', 'false');
group.setAttribute('hidden', '');
} else {
if (group.children.length === 0) {
const data = _uiNodeData.get(link);
group.appendChild(
dom.createFragment(data.children.map(child => newTreeElement(child)))
);
}
element.setAttribute('aria-expanded', 'true');
group.removeAttribute('hidden');
}
}
/**
* Inflate a template to create an element that represents one tree node.
* The element will represent a tree or a leaf, depending on if the tree
* node object has any children. Trees use a slightly different template
* and have click event listeners attached.
* @param {TreeNode} data Data to use for the UI.
* @returns {HTMLElement}
*/
function newTreeElement(data) {
const isLeaf = data.children.length === 0;
const template = isLeaf ? _leafTemplate : _treeTemplate;
const element = document.importNode(template.content, true);
// Associate clickable node & tree data
const link = element.querySelector('.node');
_uiNodeData.set(link, Object.freeze(data));
// Icons are predefined in the HTML through hidden SVG elements
const iconTemplate = _SYMBOL_ICONS[data.type] || _SYMBOL_ICONS.o;
const icon = iconTemplate.cloneNode(true);
// Insert an SVG icon at the start of the link to represent type
link.insertBefore(icon, link.firstElementChild);
// Set the symbol name and hover text
const symbolName = element.querySelector('.symbol-name');
symbolName.textContent = data.shortName;
symbolName.title = data.idPath;
// Set the byte size and hover text
_setSizeContents(element.querySelector('.size'), data.size);
if (!isLeaf) {
link.addEventListener('click', _toggleTreeElement);
}
return element;
}
// When the `byteunit` state changes, update all .size elements in the page
form.byteunit.addEventListener('change', event => {
// Update existing size elements with the new unit
for (const sizeElement of document.getElementsByClassName('size')) {
const data = _uiNodeData.get(sizeElement.parentElement);
_setSizeContents(sizeElement, data.size);
}
});
function _toggleOptions() {
document.body.classList.toggle('show-options');
}
for (const button of document.getElementsByClassName('toggle-options')) {
button.addEventListener('click', _toggleOptions);
}
self.newTreeElement = newTreeElement;
}
{
const blob = new Blob([
`
--INSERT_WORKER_CODE--
`,
]);
// We use a worker to keep large tree creation logic off the UI thread
const worker = new Worker(URL.createObjectURL(blob));
/**
* Displays the given data as a tree view
* @param {{data:TreeNode}} param0
*/
worker.onmessage = ({data}) => {
const root = newTreeElement(data);
// Expand the root UI node
root.querySelector('.node').click();
dom.replace(document.getElementById('symboltree'), root);
};
/**
* Loads the tree data given on a worker thread and replaces the tree view in
* the UI once complete. Uses query string as state for the filter.
* @param {string} treeData JSON string to be parsed on the worker thread.
*/
function loadTree(treeData) {
// Post as a JSON string for better performance
worker.postMessage(
`{"tree":${treeData}, "filters":"${location.search.slice(1)}"}`
);
}
self.loadTree = loadTree;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment