Commit b4abaad4 authored by Tiger Oakes's avatar Tiger Oakes Committed by Commit Bot

SuperSize: Adds highlights for certain flags

Adds flags to the data.ndjson file, and exposed the information in the
UI using custom highlighting options. The UI will additionally list the
flags in the symbol infocard.

Bug: 847599
Change-Id: If2c7b7f5147296cf13e0c0ef9c36d6748820e42f
Reviewed-on: https://chromium-review.googlesource.com/1148795
Commit-Queue: Tiger Oakes <tigero@google.com>
Reviewed-by: default avatarSam Maier <smaier@chromium.org>
Reviewed-by: default avatarEric Stevenson <estevenson@chromium.org>
Cr-Commit-Position: refs/heads/master@{#577923}
parent 020df651
......@@ -27,6 +27,7 @@ _COMPACT_SYMBOL_NAME_KEY = 'n'
_COMPACT_SYMBOL_BYTE_SIZE_KEY = 'b'
_COMPACT_SYMBOL_TYPE_KEY = 't'
_COMPACT_SYMBOL_COUNT_KEY = 'u'
_COMPACT_SYMBOL_FLAGS_KEY = 'f'
_SMALL_SYMBOL_DESCRIPTIONS = {
'b': 'Other small uninitialized data',
......@@ -34,7 +35,6 @@ _SMALL_SYMBOL_DESCRIPTIONS = {
'r': 'Other small readonly data',
't': 'Other small code',
'v': 'Other small vtable entries',
'*': 'Other small generated symbols',
'x': 'Other small dex non-method entries',
'm': 'Other small dex methods',
'p': 'Other small locale pak entries',
......@@ -49,8 +49,6 @@ def _GetSymbolType(symbol):
symbol_type = symbol.section
if symbol.name.endswith('[vtable]'):
symbol_type = _SYMBOL_TYPE_VTABLE
elif symbol.name.endswith(']'):
symbol_type = _SYMBOL_TYPE_GENERATED
if symbol_type not in _SMALL_SYMBOL_DESCRIPTIONS:
symbol_type = _SYMBOL_TYPE_OTHER
return symbol_type
......@@ -147,6 +145,8 @@ def _MakeTreeViewList(symbols, include_all_symbols):
# so this data is only included for methods.
if is_dex_method and symbol_count != 1:
symbol_entry[_COMPACT_SYMBOL_COUNT_KEY] = symbol_count
if symbol.flags:
symbol_entry[_COMPACT_SYMBOL_FLAGS_KEY] = symbol.flags
file_node[_COMPACT_FILE_SYMBOLS_KEY].append(symbol_entry)
for symbol in extra_symbols:
......
......@@ -138,6 +138,9 @@ FLAG_CLONE = 64
# which was removed by name normalization. Occurs when an AFDO profile is
# supplied to the linker.
FLAG_HOT = 128
# Relevant for .text symbols. If a method has this flag, then it was run
# according to the code coverage.
FLAG_COVERED = 256
DIFF_STATUS_UNCHANGED = 0
......
......@@ -34,6 +34,7 @@
align-items: center;
}
.appbar-progress {
box-sizing: border-box;
display: block;
width: 100%;
height: 4px;
......@@ -140,6 +141,7 @@
.icon {
display: block;
margin-right: 6px;
flex: none;
}
/** Tree nodes */
......@@ -179,13 +181,13 @@
.symbol-name {
font-weight: 500;
word-break: break-word;
flex: 1;
}
.count,
.size,
.percent {
margin-left: 16px;
margin-left: auto;
padding-left: 16px;
text-align: right;
color: #5f6368;
white-space: nowrap;
......@@ -200,6 +202,16 @@
text-decoration: line-through;
}
.highlight {
background: #feefc3;
}
.highlight-positive {
background: #ceead6;
}
.highlight-negative {
background: #fad2cf;
}
.diff .size-header::after {
content: " diff";
}
......@@ -220,6 +232,7 @@
<body>
<div class="scrim toggle-options" hidden></div>
<!-- App Toolbar -->
<header class="appbar">
<div class="appbar-inner">
<h1 class="headline">Super Size Tiger View</h1>
......@@ -234,7 +247,8 @@
</svg>
<span class="label-text">Upload data</span>
</label>
<a href="https://chromium.googlesource.com/chromium/src/+/master/tools/binary_size/html_report_faq.md" class="icon-button" title="FAQ" id="faq">
<a href="https://chromium.googlesource.com/chromium/src/+/master/tools/binary_size/html_report_faq.md" class="icon-button"
title="FAQ" id="faq">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#5f6368">
<path d="M11,18h2v-2h-2V18z M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M12,20c-4.41,0-8-3.59-8-8
s3.59-8,8-8s8,3.59,8,8S16.41,20,12,20z M12,6c-2.21,0-4,1.79-4,4h2c0-1.1,0.9-2,2-2s2,0.9,2,2c0,2-3,1.75-3,5h2c0-2.25,3-2.5,3-5
......@@ -264,7 +278,7 @@
</div>
<progress value="0" id="progress" class="appbar-progress"></progress>
</header>
<link href="options.css" rel="stylesheet">
<!-- Options side panel -->
<form id="options" class="options" method="GET">
<header class="form-bar">
<button type="button" class="icon-button toggle-options" title="Close">
......@@ -319,9 +333,9 @@
<p class="input-error" id="include-error"></p>
</div>
<div class="input-wrapper">
<input class="input-regex" type="text" id="excluderegex" name="exclude" placeholder="\(.+\)" aria-describedby="exclude-error">
<input class="input-regex" type="text" id="excluderegex" name="exclude" placeholder="\(.+\)" aria-describedby="exclude-error">
<label class="input-label" for="excluderegex">
Symbols must exclude
Symbols must exclude
</label>
<p class="input-error" id="exclude-error"></p>
</div>
......@@ -359,12 +373,6 @@
Vtable entries
</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="filtergen" name="type" value="*" checked>
<label class="checkbox-label" for="filtergen">
Generated Symbols (typeinfo, thunks, etc)
</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="filterdexnon" name="type" value="x" checked>
<label class="checkbox-label" for="filterdexnon">
......@@ -398,8 +406,34 @@
<button type="button" class="text-button" id="type-all">Select all</button>
<button type="button" class="text-button" id="type-none">Select none</button>
</fieldset>
<fieldset id="highlight-container">
<legend class="subhead">Highlight symbols</legend>
<div class="radio-wrapper">
<input type="radio" id="clearhighlight" name="highlight" value="clear" checked>
<label class="radio-label" for="clearhighlight">None</label>
</div>
<div class="radio-wrapper">
<input type="radio" id="hothighlight" name="highlight" value="hot">
<label class="radio-label" for="hothighlight">Hot code</label>
</div>
<div class="radio-wrapper">
<input type="radio" id="generatedhighlight" name="highlight" value="generated">
<label class="radio-label" for="generatedhighlight">Generated files</label>
</div>
<div class="radio-wrapper">
<input type="radio" id="coveragehightlight" name="highlight" value="coverage">
<label class="radio-label" for="coveragehightlight">Code coverage</label>
</div>
</fieldset>
<div class="checkbox-wrapper">
<input type="checkbox" id="generatedfilter" name="generated_filter" value="on">
<label class="checkbox-label" for="generatedfilter">Show only generated files</label>
</div>
</form>
<div class="symbols">
<!-- Icons for symbols are stored here and cloned. -->
<div hidden id="icons">
<svg class="icon foldericon" height="24" width="24" fill="#5f6368">
<title>Directory</title>
......@@ -449,11 +483,6 @@
<path d="M20,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h15c1.1,0,2-0.9,2-2V5C22,3.9,21.1,3,20,3z M20,5v3H5V5H20z M15,19h-5v-9h5V19z
M5,10h3v9H5V10z M17,19v-9h3v9H17z" />
</svg>
<svg class="icon generatedicon" height="24" width="24" fill="#f439a0">
<title>Generated symbol</title>
<path d="M6 2a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8l-6-6H6zm0 2h7v5h5v11H6V4zm9.5 8l-.6 1.4-1.4.6 1.4.6.6 1.4.6-1.4 1.4-.6-1.4-.6-.6-1.4zm-6 1l-1 2-2 1 2 1 1 2 1-2 2-1-2-1-1-2z"
/>
</svg>
<svg class="icon dexicon" height="24" width="24" fill="#ea4335">
<title>Dex non-method entry</title>
<path d="M6.6 1.44l-.82.83 2.1 2.1A6.96 6.96 0 0 0 5 10v1h14v-1a6.96 6.96 0 0 0-2.88-5.63l2.1-2.1-.82-.83-2.3 2.31a6.81 6.81 0 0 0-6.19 0L6.6 1.44zM9 7a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zm6 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zM5 12v4.27C5 20 8.17 23 12 23s7-3 7-6.73V12H5zm2 2h10v2.27c0 2.6-2.2 4.73-5 4.73s-5-2.13-5-4.73V14z"
......@@ -480,6 +509,7 @@
/>
</svg>
</div>
<!-- Template for trees and leaves -->
<template id="treenode-container">
<li role="treeitem" aria-expanded="false" aria-describedby="infocard-container">
<a class="node" href="#" tabindex="-1" role="presentation">
......@@ -497,6 +527,7 @@
</span>
</li>
</template>
<!-- Tree view -->
<main class="tree-container">
<header class="tree-header">
<span class="subtitle">Name</span>
......@@ -504,6 +535,7 @@
</header>
<ul id="symboltree" class="tree" role="tree" aria-labelledby="headline"></ul>
</main>
<!-- Symbol and container breakdown cards -->
<link href="infocard.css" rel="stylesheet">
<footer class="infocards">
<div class="infocard infocard-container open" id="infocard-container" hidden>
......@@ -514,11 +546,12 @@
<header class="header-info">
<h4 class="subhead size-info"></h4>
<p class="body-2 path-info"></p>
<p class="caption type-info"></p>
<p class="caption type-info type-info-container"></p>
</header>
<table class="type-breakdown-info">
<thead>
<tr>
<th class="subhead-2">Icon</th>
<th class="subhead-2">Type</th>
<th class="subhead-2 count">Count</th>
<th class="subhead-2 size">Total size</th>
......@@ -527,66 +560,118 @@
</thead>
<tbody>
<tr class="bss-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#a142f4">
<path d="M6 2a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8l-6-6H6zm0 2h7v5h5v11H6V4zm4 6v4h2v-4h-2zm0 6v2h2v-2h-2z"
/>
</svg>
</td>
<th scope="row">.bss</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="data-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#fa7b17">
<path d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6H6m0 2h7v5h5v11H6V4m2 8v2h8v-2H8m0 4v2h5v-2z" />
</svg>
</td>
<th scope="row">.data and .data.*</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="rodata-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#24c1e0">
<path d="M6 2a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8l-6-6H6zm0 2h7v5h5v11H6V4zm5.9 8c-2 0-3.7 1.2-4.4 3a4.7 4.7 0 0 0 8.8 0c-.7-1.8-2.4-3-4.4-3zm0 1a2 2 0 0 1 2 2 2 2 0 0 1-2 2 2 2 0 0 1-2-2 2 2 0 0 1 2-2zm0 .8a1.2 1.2 0 0 0-1.2 1.2 1.2 1.2 0 0 0 1.2 1.2 1.2 1.2 0 0 0 1.2-1.2 1.2 1.2 0 0 0-1.2-1.2z"
/>
</svg>
</td>
<th scope="row">.rodata</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="text-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#1a73e8">
<path d="M9.4,16.6L4.8,12l4.6-4.6L8,6l-6,6l6,6L9.4,16.6z M14.6,16.6l4.6-4.6l-4.6-4.6L16,6l6,6l-6,6L14.6,16.6z" />
</svg>
</td>
<th scope="row">.text</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="vtable-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#fbbc04">
<path d="M20,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h15c1.1,0,2-0.9,2-2V5C22,3.9,21.1,3,20,3z M20,5v3H5V5H20z M15,19h-5v-9h5V19z
M5,10h3v9H5V10z M17,19v-9h3v9H17z" />
</svg>
</td>
<th scope="row">Vtable entry</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="gen-info">
<th scope="row">Generated symbols</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="dexnon-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#ea4335">
<path d="M6.6 1.44l-.82.83 2.1 2.1A6.96 6.96 0 0 0 5 10v1h14v-1a6.96 6.96 0 0 0-2.88-5.63l2.1-2.1-.82-.83-2.3 2.31a6.81 6.81 0 0 0-6.19 0L6.6 1.44zM9 7a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zm6 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zM5 12v4.27C5 20 8.17 23 12 23s7-3 7-6.73V12H5zm2 2h10v2.27c0 2.6-2.2 4.73-5 4.73s-5-2.13-5-4.73V14z"
/>
</svg>
</td>
<th scope="row">Dex non-method entries</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="dex-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#a50e0e">
<path d="M6.6 1.44l-.82.83 2.1 2.1A6.96 6.96 0 0 0 5 10v1h14v-1a6.96 6.96 0 0 0-2.88-5.63l2.1-2.1-.82-.83-2.3 2.31a6.81 6.81 0 0 0-6.19 0L6.6 1.44zM9 7a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zm6 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1zM5 12v4.27C5 20 8.17 23 12 23s7-3 7-6.73V12H5zm2 2h10v2.27c0 2.6-2.2 4.73-5 4.73s-5-2.13-5-4.73V14z"
/>
</svg>
</td>
<th scope="row">Dex methods</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="pak-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#34a853">
<path d="M5 3a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm0 2h5v2h2V5h7v14H5V5zm7 2v2h2V7h-2zm0 2h-2v2h2V9zm0 2v2h2v-2h-2zm0 2h-2v2h2v-2zm0 2v2h2v-2h-2z"
/>
</svg>
</td>
<th scope="row">Locale pak entries</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="paknon-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#0d652d">
<path d="M5 3a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm0 2h5v2h2V5h7v14H5V5zm7 2v2h2V7h-2zm0 2h-2v2h2V9zm0 2v2h2v-2h-2zm0 2h-2v2h2v-2zm0 2v2h2v-2h-2z"
/>
</svg>
</td>
<th scope="row">Non-locale pak entries</th>
<td class="count"></td>
<td class="size"></td>
<td class="percent"></td>
</tr>
<tr class="other-info">
<td>
<svg class="icon" viewBox="0 0 24 24" height="18" width="18" fill="#5f6368">
<path d="M10.88 2l-.85 1.36L4 13h6v-2H7.6l3.28-5.23L12.28 8h2.35l-3.75-6zM12 10v10h10V10H12zm2 2h6v6h-6v-6zM2.21 15A5.52 5.52 0 0 0 10 21.4v-2.45A3.48 3.48 0 0 1 7.5 20a3.48 3.48 0 0 1-3.15-5H2.2z"
/>
</svg>
</td>
<th scope="row">Other entries</th>
<td class="count"></td>
<td class="size"></td>
......@@ -596,14 +681,17 @@
</table>
</div>
<div class="infocard infocard-symbol" id="infocard-symbol" hidden>
<div class="icon-info">
<div></div>
</div>
<div class="icon-info">
<div></div>
</div>
<header class="header-info">
<h4 class="subhead size-info"></h4>
<p class="body-2 path-info"></p>
</header>
<p class="caption type-info"></p>
<p class="caption type-info-container">
<span class="type-info"></span>
<span class="flags-info"></span>
</p>
</div>
</footer>
</div>
......
......@@ -14,6 +14,18 @@
const displayInfocard = (() => {
const _CANVAS_RADIUS = 40;
const _FLAG_LABELS = new Map([
[_FLAGS.ANONYMOUS, 'anon'],
[_FLAGS.STARTUP, 'startup'],
[_FLAGS.UNLIKELY, 'unlikely'],
[_FLAGS.REL, 'rel'],
[_FLAGS.REL_LOCAL, 'rel.loc'],
[_FLAGS.GENERATED_SOURCE, 'gen'],
[_FLAGS.CLONE, 'clone'],
[_FLAGS.HOT, 'hot'],
[_FLAGS.COVERAGE, 'covered'],
]);
class Infocard {
/**
* @param {string} id
......@@ -26,7 +38,7 @@ const displayInfocard = (() => {
this._pathInfo = this._infocard.querySelector('.path-info');
/** @type {HTMLDivElement} */
this._iconInfo = this._infocard.querySelector('.icon-info');
/** @type {HTMLParagraphElement} */
/** @type {HTMLSpanElement} */
this._typeInfo = this._infocard.querySelector('.type-info');
/**
......@@ -136,6 +148,12 @@ const displayInfocard = (() => {
}
class SymbolInfocard extends Infocard {
constructor(id) {
super(id);
/** @type {HTMLSpanElement} */
this._flagsInfo = this._infocard.querySelector('.flags-info');
}
/**
* @param {SVGSVGElement} icon Icon to display
*/
......@@ -144,6 +162,31 @@ const displayInfocard = (() => {
super._setTypeContent(icon);
this._iconInfo.style.backgroundColor = color;
}
/**
* Updates the DOM for the info card.
* @param {TreeNode} node
*/
_updateInfocard(node) {
super._updateInfocard(node);
this._flagsInfo.textContent = this._flagsString(node);
}
/**
* Returns a string representing the flags in the node.
* @param {TreeNode} symbolNode
*/
_flagsString(symbolNode) {
if (!symbolNode.flags) {
return '';
}
const flagsString = Array.from(_FLAG_LABELS)
.filter(([flag]) => hasFlag(flag, symbolNode))
.map(([, part]) => part)
.join(',');
return `{${flagsString}}`;
}
}
class ContainerInfocard extends Infocard {
......@@ -162,7 +205,6 @@ const displayInfocard = (() => {
r: this._tableBody.querySelector('.rodata-info'),
t: this._tableBody.querySelector('.text-info'),
v: this._tableBody.querySelector('.vtable-info'),
'*': this._tableBody.querySelector('.gen-info'),
x: this._tableBody.querySelector('.dexnon-info'),
m: this._tableBody.querySelector('.dex-info'),
p: this._tableBody.querySelector('.pak-info'),
......@@ -272,7 +314,7 @@ const displayInfocard = (() => {
* @param {TreeNode} containerNode
*/
_updateInfocard(containerNode) {
const extraRows = {...this._infoRows};
const extraRows = Object.assign({}, this._infoRows);
const statsEntries = Object.entries(containerNode.childStats).sort(
(a, b) => b[1].size - a[1].size
);
......
......@@ -78,7 +78,7 @@
.symbol-name-info {
font-weight: 500;
}
.type-info {
.type-info-container {
grid-area: type;
margin-bottom: 0;
}
......
......@@ -23,6 +23,7 @@
* @prop {number} size Byte size of this node and its children.
* @prop {string} type Type of this node. If this node has children, the string
* may have a second character to denote the most common child.
* @prop {number} flags
* @prop {{[type: string]: {size:number,count:number}}} childStats Stats about
* this node's descendants, organized by symbol type.
*/
......@@ -57,6 +58,20 @@ const _KEYS = Object.freeze({
SIZE: /** @type {'b'} */ ('b'),
TYPE: /** @type {'t'} */ ('t'),
COUNT: /** @type {'u'} */ ('u'),
FLAGS: /** @type {'f'} */ ('f'),
});
/** Abberivated keys used by FileEntrys in the JSON data file. */
const _FLAGS = Object.freeze({
ANONYMOUS: 2 ** 0,
STARTUP: 2 ** 1,
UNLIKELY: 2 ** 2,
REL: 2 ** 3,
REL_LOCAL: 2 ** 4,
GENERATED_SOURCE: 2 ** 5,
CLONE: 2 ** 6,
HOT: 2 ** 7,
COVERAGE: 2 ** 8,
});
/**
......@@ -69,8 +84,6 @@ const _BYTE_UNITS = Object.freeze({
KiB: 1024 ** 1,
B: 1024 ** 0,
});
/** Set of all byte units */
const _BYTE_UNITS_SET = new Set(Object.keys(_BYTE_UNITS));
/**
* Special types used by containers, such as folders and files.
......@@ -83,13 +96,15 @@ const _CONTAINER_TYPES = {
};
const _CONTAINER_TYPE_SET = new Set(Object.values(_CONTAINER_TYPES));
/** Type for a code/.text symbol */
const _CODE_SYMBOL_TYPE = 't';
/** Type for a dex method symbol */
const _DEX_METHOD_SYMBOL_TYPE = 'm';
/** Type for an 'other' symbol */
const _OTHER_SYMBOL_TYPE = 'o';
/** Set of all known symbol types. Container types are not included. */
const _SYMBOL_TYPE_SET = new Set('bdrtv*xmpP' + _OTHER_SYMBOL_TYPE);
const _SYMBOL_TYPE_SET = new Set('bdrtvxmpP' + _OTHER_SYMBOL_TYPE);
/** Name used by a directory created to hold symbols with no name. */
const _NO_NAME = '(No path)';
......@@ -133,9 +148,18 @@ function* types(typesList) {
function debounce(func, wait) {
/** @type {number} */
let timeoutId;
function debounced (...args) {
function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), wait);
};
}
return /** @type {any} */ (debounced);
}
/**
* Returns tree if a symbol has a certain bit flag
* @param {number} flag Bit flag from `_FLAGS`
* @param {TreeNode} symbolNode
*/
function hasFlag(flag, symbolNode) {
return (symbolNode.flags & flag) === flag;
}
......@@ -68,44 +68,11 @@ function _initState() {
const state = Object.freeze({
/**
* 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 = {}) {
const [val = null] = state.getAll(key, {
default: options.default ? [options.default] : null,
valid: options.valid,
});
return val;
},
/**
* Returns all string values for a key from the current query string state.
* Can optionally provide default values used if there are no values.
* @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 be omitted.
* @returns {string[]}
*/
getAll(key, options = {}) {
let vals = _filterParams.getAll(key);
if (options.valid != null) {
vals = vals.filter(val => options.valid.has(val));
}
if (options.default != null && vals.length === 0) {
vals = options.default;
}
return vals;
get(key) {
return _filterParams.get(key);
},
/**
* Checks if a key is present in the query string state.
......@@ -140,7 +107,7 @@ function _initState() {
});
// Update form inputs to reflect the state from URL.
for (const element of form.elements) {
for (const element of Array.from(form.elements)) {
if (element.name) {
const input = /** @type {HTMLInputElement} */ (element);
const values = _filterParams.getAll(input.name);
......@@ -370,12 +337,15 @@ function _makeSizeTextGetter() {
};
} else {
const bytes = node.size;
const unit = state.get('byteunit', {
default: 'MiB',
valid: _BYTE_UNITS_SET,
});
let unit = state.get('byteunit');
let suffix = _BYTE_UNITS[unit];
if (suffix == null) {
unit = 'MiB';
suffix = _BYTE_UNITS.MiB;
}
// Format the bytes as a number with 2 digits after the decimal point
const text = (bytes / _BYTE_UNITS[unit]).toLocaleString(_LOCALE, {
const text = (bytes / suffix).toLocaleString(_LOCALE, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
......
......@@ -24,6 +24,9 @@ const newTreeElement = (() => {
const _treeTemplate = document.getElementById('treenode-container');
const _symbolTree = document.getElementById('symboltree');
const _highlightRadio = document.getElementById('highlight-container');
/** @type {HTMLSelectElement} */
const _byteunitSelect = form.elements.namedItem('byteunit');
/**
* @type {HTMLCollectionOf<HTMLAnchorElement | HTMLSpanElement>}
......@@ -44,6 +47,57 @@ const newTreeElement = (() => {
*/
const _uiNodeData = new WeakMap();
/**
* @type {{[mode:string]: (nameEl: HTMLSpanElement, node: TreeNode) => void}}
*/
const _highlightActions = {
hot(symbolName, symbolNode) {
if (hasFlag(_FLAGS.HOT, symbolNode)) {
symbolName.classList.add('highlight');
}
},
generated(symbolName, symbolNode) {
if (hasFlag(_FLAGS.GENERATED_SOURCE, symbolNode)) {
symbolName.classList.add('highlight');
}
},
coverage(symbolName, symbolNode) {
if (symbolNode.type === _CODE_SYMBOL_TYPE) {
if (hasFlag(_FLAGS.COVERAGE, symbolNode)) {
symbolName.classList.add('highlight-positive');
} else {
symbolName.classList.add('highlight-negative');
}
}
},
reset(symbolName) {
symbolName.classList.remove('highlight');
symbolName.classList.remove('highlight-positive');
symbolName.classList.remove('highlight-negative');
},
};
/**
* Applies highlights to the tree element based on certain flags and state.
* @param {HTMLSpanElement} symbolNameElement
* @param {TreeNode} symbolNode
*/
function _highlightSymbolName(symbolNameElement, symbolNode) {
const isDexMethod = symbolNode.type === _DEX_METHOD_SYMBOL_TYPE;
if (isDexMethod && state.has('method_count')) {
const {count = 0} = symbolNode.childStats[_DEX_METHOD_SYMBOL_TYPE] || {};
if (count < 0) {
symbolNameElement.classList.add('removed');
}
}
const hightlightFunc = _highlightActions[state.get('highlight')];
_highlightActions.reset(symbolNameElement, symbolNode);
if (hightlightFunc) {
hightlightFunc(symbolNameElement, symbolNode);
}
}
/**
* Sets focus to a new tree element while updating the element that last had
* focus. The tabindex property is used to avoid needing to tab through every
......@@ -51,6 +105,7 @@ const newTreeElement = (() => {
* @param {number | HTMLElement} el Index of tree node in `_liveNodeList`
*/
function _focusTreeElement(el) {
/** @type {HTMLElement} */
const lastFocused = document.activeElement;
if (_uiNodeData.has(lastFocused)) {
// Update DOM
......@@ -86,7 +141,9 @@ const newTreeElement = (() => {
let data = _uiNodeData.get(link);
if (data == null || data.children == null) {
const idPath = link.querySelector('.symbol-name').title;
/** @type {HTMLSpanElement} */
const symbolName = link.querySelector('.symbol-name');
const idPath = symbolName.title;
data = await worker.openNode(idPath);
_uiNodeData.set(link, data);
}
......@@ -95,7 +152,9 @@ const newTreeElement = (() => {
if (newElements.length === 1) {
// Open the inner element if it only has a single child.
// Ensures nodes like "java"->"com"->"google" are opened all at once.
newElements[0].querySelector('.node').click();
/** @type {HTMLAnchorElement | HTMLSpanElement} */
const link = newElements[0].querySelector('.node');
link.click();
}
const newElementsFragment = dom.createFragment(newElements);
......@@ -185,7 +244,9 @@ const newTreeElement = (() => {
const groupList = link.parentElement.parentElement;
if (groupList.getAttribute('role') === 'group') {
event.preventDefault();
_focusTreeElement(groupList.previousElementSibling);
/** @type {HTMLAnchorElement} */
const parentLink = groupList.previousElementSibling;
_focusTreeElement(parentLink);
}
}
}
......@@ -292,13 +353,7 @@ const newTreeElement = (() => {
_ZERO_WIDTH_SPACE
);
symbolName.title = data.idPath;
if (state.has('method_count') && type === _DEX_METHOD_SYMBOL_TYPE) {
const {count = 0} = data.childStats[type] || {};
if (count < 0) {
symbolName.classList.add('removed');
}
}
_highlightSymbolName(symbolName, data);
// Set the byte size and hover text
_setSize(element.querySelector('.size'), data);
......@@ -312,7 +367,7 @@ const newTreeElement = (() => {
}
// When the `byteunit` state changes, update all .size elements in the page
form.elements.namedItem('byteunit').addEventListener('change', event => {
_byteunitSelect.addEventListener('change', event => {
event.stopPropagation();
state.set(event.currentTarget.name, event.currentTarget.value);
// Update existing size elements with the new unit
......@@ -321,6 +376,19 @@ const newTreeElement = (() => {
if (data) _setSize(sizeElement, data);
}
});
_highlightRadio.addEventListener('change', event => {
event.stopPropagation();
state.set(event.target.name, event.target.value);
// Update existing symbol elements
for (const link of _liveNodeList) {
const data = _uiNodeData.get(link);
if (data.children && data.children.length === 0) {
/** @type {HTMLSpanElement} */
const symbolName = link.querySelector('.symbol-name');
_highlightSymbolName(symbolName, data);
}
}
});
_symbolTree.addEventListener('keydown', _handleKeyNavigation);
_symbolTree.addEventListener('focusin', event => {
......@@ -337,7 +405,7 @@ const newTreeElement = (() => {
}
});
return newTreeElement
return newTreeElement;
})();
{
......
......@@ -24,6 +24,7 @@
* @prop {string} t Single character string to indicate the symbol type.
* @prop {number} [u] Count value indicating how many symbols this entry
* represents. Negative value when removed in a diff.
* @prop {number} [f] Bit flags, defaults to 0.
*/
/**
* @typedef {object} FileEntry JSON object representing a single file and its
......@@ -82,7 +83,14 @@ function _compareFunc(a, b) {
* @returns {TreeNode}
*/
function createNode(options) {
const {idPath, type, shortNameIndex, size = 0, childStats = {}} = options;
const {
idPath,
type,
shortNameIndex,
size = 0,
flags = 0,
childStats = {},
} = options;
return {
children: [],
parent: null,
......@@ -91,6 +99,7 @@ function createNode(options) {
shortNameIndex,
size,
type,
flags,
};
}
......@@ -143,13 +152,14 @@ class TreeBuilder {
const additionalSize = node.size;
const additionalStats = Object.entries(node.childStats);
const additionalFlags = node.flags;
// Update the size and childStats of all ancestors
while (node.parent != null) {
const {parent} = node;
const [containerType, lastBiggestType] = parent.type;
let {size: lastBiggestSize = 0} =
parent.childStats[lastBiggestType] || {};
parent.childStats[lastBiggestType] || {};
for (const [type, stat] of additionalStats) {
const parentStat = parent.childStats[type] || {size: 0, count: 0};
......@@ -165,6 +175,7 @@ class TreeBuilder {
}
parent.size += additionalSize;
parent.flags |= additionalFlags;
node = parent;
}
}
......@@ -257,15 +268,16 @@ class TreeBuilder {
// If there is 1 child, include it so the UI doesn't need to make a
// roundtrip in order to expand the chain.
children = node.children
.map(n => TreeBuilder.formatNode(n, childDepth))
.sort(_compareFunc);
.map(n => TreeBuilder.formatNode(n, childDepth))
.sort(_compareFunc);
}
return TreeBuilder._joinDexMethodClasses({
...node,
children,
parent: null,
});
return TreeBuilder._joinDexMethodClasses(
Object.assign({}, node, {
children,
parent: null,
})
);
}
/**
......@@ -344,7 +356,8 @@ class TreeBuilder {
idPath: `${idPath}:${symbol[_KEYS.SYMBOL_NAME]}`,
shortNameIndex: idPath.length + 1,
size,
type: symbol[_KEYS.TYPE],
type,
flags: symbol[_KEYS.FLAGS] || 0,
childStats: {[type]: {size, count}},
});
......@@ -548,6 +561,7 @@ function parseOptions(options) {
const groupBy = params.get('group_by') || 'source_path';
const methodCountMode = params.has('method_count');
const filterGeneratedFiles = params.has('generated_filter');
let minSymbolSize = Number(params.get('min_size'));
if (Number.isNaN(minSymbolSize)) {
......@@ -569,19 +583,28 @@ function parseOptions(options) {
}
}
/** @type {Array<(symbolNode: TreeNode) => boolean>} */
/**
* @type {Array<(symbolNode: TreeNode) => boolean>} List of functions that
* check each symbol. If any returns false, the symbol will not be used.
*/
const filters = [];
/** Ensure symbol size is past the minimum */
// Ensure symbol size is past the minimum
if (minSymbolSize > 0) {
filters.push(s => Math.abs(s.size) >= minSymbolSize);
}
/** Ensure the symbol size wasn't filtered out */
// Ensure the symbol size wasn't filtered out
if (typeFilter.size < _SYMBOL_TYPE_SET.size) {
filters.push(s => typeFilter.has(s.type));
}
// Only show generated files
if (filterGeneratedFiles) {
filters.push(s => hasFlag(_FLAGS.GENERATED_SOURCE, s));
}
// Search symbol names using regex
if (includeRegex) {
try {
const regex = new RegExp(includeRegex);
......@@ -692,7 +715,7 @@ async function buildTree(options, onProgress) {
const currentTime = Date.now();
if (currentTime - lastBatchSent > 500) {
postToUi();
await Promise.resolve(); // Pause loop to check for worker messages
await Promise.resolve(); // Pause loop to check for worker messages
lastBatchSent = currentTime;
}
}
......
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