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' ...@@ -27,6 +27,7 @@ _COMPACT_SYMBOL_NAME_KEY = 'n'
_COMPACT_SYMBOL_BYTE_SIZE_KEY = 'b' _COMPACT_SYMBOL_BYTE_SIZE_KEY = 'b'
_COMPACT_SYMBOL_TYPE_KEY = 't' _COMPACT_SYMBOL_TYPE_KEY = 't'
_COMPACT_SYMBOL_COUNT_KEY = 'u' _COMPACT_SYMBOL_COUNT_KEY = 'u'
_COMPACT_SYMBOL_FLAGS_KEY = 'f'
_SMALL_SYMBOL_DESCRIPTIONS = { _SMALL_SYMBOL_DESCRIPTIONS = {
'b': 'Other small uninitialized data', 'b': 'Other small uninitialized data',
...@@ -34,7 +35,6 @@ _SMALL_SYMBOL_DESCRIPTIONS = { ...@@ -34,7 +35,6 @@ _SMALL_SYMBOL_DESCRIPTIONS = {
'r': 'Other small readonly data', 'r': 'Other small readonly data',
't': 'Other small code', 't': 'Other small code',
'v': 'Other small vtable entries', 'v': 'Other small vtable entries',
'*': 'Other small generated symbols',
'x': 'Other small dex non-method entries', 'x': 'Other small dex non-method entries',
'm': 'Other small dex methods', 'm': 'Other small dex methods',
'p': 'Other small locale pak entries', 'p': 'Other small locale pak entries',
...@@ -49,8 +49,6 @@ def _GetSymbolType(symbol): ...@@ -49,8 +49,6 @@ def _GetSymbolType(symbol):
symbol_type = symbol.section symbol_type = symbol.section
if symbol.name.endswith('[vtable]'): if symbol.name.endswith('[vtable]'):
symbol_type = _SYMBOL_TYPE_VTABLE symbol_type = _SYMBOL_TYPE_VTABLE
elif symbol.name.endswith(']'):
symbol_type = _SYMBOL_TYPE_GENERATED
if symbol_type not in _SMALL_SYMBOL_DESCRIPTIONS: if symbol_type not in _SMALL_SYMBOL_DESCRIPTIONS:
symbol_type = _SYMBOL_TYPE_OTHER symbol_type = _SYMBOL_TYPE_OTHER
return symbol_type return symbol_type
...@@ -147,6 +145,8 @@ def _MakeTreeViewList(symbols, include_all_symbols): ...@@ -147,6 +145,8 @@ def _MakeTreeViewList(symbols, include_all_symbols):
# so this data is only included for methods. # so this data is only included for methods.
if is_dex_method and symbol_count != 1: if is_dex_method and symbol_count != 1:
symbol_entry[_COMPACT_SYMBOL_COUNT_KEY] = symbol_count 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) file_node[_COMPACT_FILE_SYMBOLS_KEY].append(symbol_entry)
for symbol in extra_symbols: for symbol in extra_symbols:
......
...@@ -138,6 +138,9 @@ FLAG_CLONE = 64 ...@@ -138,6 +138,9 @@ FLAG_CLONE = 64
# which was removed by name normalization. Occurs when an AFDO profile is # which was removed by name normalization. Occurs when an AFDO profile is
# supplied to the linker. # supplied to the linker.
FLAG_HOT = 128 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 DIFF_STATUS_UNCHANGED = 0
......
...@@ -14,6 +14,18 @@ ...@@ -14,6 +14,18 @@
const displayInfocard = (() => { const displayInfocard = (() => {
const _CANVAS_RADIUS = 40; 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 { class Infocard {
/** /**
* @param {string} id * @param {string} id
...@@ -26,7 +38,7 @@ const displayInfocard = (() => { ...@@ -26,7 +38,7 @@ const displayInfocard = (() => {
this._pathInfo = this._infocard.querySelector('.path-info'); this._pathInfo = this._infocard.querySelector('.path-info');
/** @type {HTMLDivElement} */ /** @type {HTMLDivElement} */
this._iconInfo = this._infocard.querySelector('.icon-info'); this._iconInfo = this._infocard.querySelector('.icon-info');
/** @type {HTMLParagraphElement} */ /** @type {HTMLSpanElement} */
this._typeInfo = this._infocard.querySelector('.type-info'); this._typeInfo = this._infocard.querySelector('.type-info');
/** /**
...@@ -136,6 +148,12 @@ const displayInfocard = (() => { ...@@ -136,6 +148,12 @@ const displayInfocard = (() => {
} }
class SymbolInfocard extends Infocard { class SymbolInfocard extends Infocard {
constructor(id) {
super(id);
/** @type {HTMLSpanElement} */
this._flagsInfo = this._infocard.querySelector('.flags-info');
}
/** /**
* @param {SVGSVGElement} icon Icon to display * @param {SVGSVGElement} icon Icon to display
*/ */
...@@ -144,6 +162,31 @@ const displayInfocard = (() => { ...@@ -144,6 +162,31 @@ const displayInfocard = (() => {
super._setTypeContent(icon); super._setTypeContent(icon);
this._iconInfo.style.backgroundColor = color; 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 { class ContainerInfocard extends Infocard {
...@@ -162,7 +205,6 @@ const displayInfocard = (() => { ...@@ -162,7 +205,6 @@ const displayInfocard = (() => {
r: this._tableBody.querySelector('.rodata-info'), r: this._tableBody.querySelector('.rodata-info'),
t: this._tableBody.querySelector('.text-info'), t: this._tableBody.querySelector('.text-info'),
v: this._tableBody.querySelector('.vtable-info'), v: this._tableBody.querySelector('.vtable-info'),
'*': this._tableBody.querySelector('.gen-info'),
x: this._tableBody.querySelector('.dexnon-info'), x: this._tableBody.querySelector('.dexnon-info'),
m: this._tableBody.querySelector('.dex-info'), m: this._tableBody.querySelector('.dex-info'),
p: this._tableBody.querySelector('.pak-info'), p: this._tableBody.querySelector('.pak-info'),
...@@ -272,7 +314,7 @@ const displayInfocard = (() => { ...@@ -272,7 +314,7 @@ const displayInfocard = (() => {
* @param {TreeNode} containerNode * @param {TreeNode} containerNode
*/ */
_updateInfocard(containerNode) { _updateInfocard(containerNode) {
const extraRows = {...this._infoRows}; const extraRows = Object.assign({}, this._infoRows);
const statsEntries = Object.entries(containerNode.childStats).sort( const statsEntries = Object.entries(containerNode.childStats).sort(
(a, b) => b[1].size - a[1].size (a, b) => b[1].size - a[1].size
); );
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
.symbol-name-info { .symbol-name-info {
font-weight: 500; font-weight: 500;
} }
.type-info { .type-info-container {
grid-area: type; grid-area: type;
margin-bottom: 0; margin-bottom: 0;
} }
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
* @prop {number} size Byte size of this node and its children. * @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 * @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. * may have a second character to denote the most common child.
* @prop {number} flags
* @prop {{[type: string]: {size:number,count:number}}} childStats Stats about * @prop {{[type: string]: {size:number,count:number}}} childStats Stats about
* this node's descendants, organized by symbol type. * this node's descendants, organized by symbol type.
*/ */
...@@ -57,6 +58,20 @@ const _KEYS = Object.freeze({ ...@@ -57,6 +58,20 @@ const _KEYS = Object.freeze({
SIZE: /** @type {'b'} */ ('b'), SIZE: /** @type {'b'} */ ('b'),
TYPE: /** @type {'t'} */ ('t'), TYPE: /** @type {'t'} */ ('t'),
COUNT: /** @type {'u'} */ ('u'), 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({ ...@@ -69,8 +84,6 @@ const _BYTE_UNITS = Object.freeze({
KiB: 1024 ** 1, KiB: 1024 ** 1,
B: 1024 ** 0, 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. * Special types used by containers, such as folders and files.
...@@ -83,13 +96,15 @@ const _CONTAINER_TYPES = { ...@@ -83,13 +96,15 @@ const _CONTAINER_TYPES = {
}; };
const _CONTAINER_TYPE_SET = new Set(Object.values(_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 */ /** Type for a dex method symbol */
const _DEX_METHOD_SYMBOL_TYPE = 'm'; const _DEX_METHOD_SYMBOL_TYPE = 'm';
/** Type for an 'other' symbol */ /** Type for an 'other' symbol */
const _OTHER_SYMBOL_TYPE = 'o'; const _OTHER_SYMBOL_TYPE = 'o';
/** Set of all known symbol types. Container types are not included. */ /** 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. */ /** Name used by a directory created to hold symbols with no name. */
const _NO_NAME = '(No path)'; const _NO_NAME = '(No path)';
...@@ -133,9 +148,18 @@ function* types(typesList) { ...@@ -133,9 +148,18 @@ function* types(typesList) {
function debounce(func, wait) { function debounce(func, wait) {
/** @type {number} */ /** @type {number} */
let timeoutId; let timeoutId;
function debounced (...args) { function debounced(...args) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), wait); timeoutId = setTimeout(() => func(...args), wait);
}; }
return /** @type {any} */ (debounced); 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() { ...@@ -68,44 +68,11 @@ function _initState() {
const state = Object.freeze({ const state = Object.freeze({
/** /**
* Returns a string from the current query string 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 {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} * @returns {string | null}
*/ */
get(key, options = {}) { get(key) {
const [val = null] = state.getAll(key, { return _filterParams.get(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;
}, },
/** /**
* Checks if a key is present in the query string state. * Checks if a key is present in the query string state.
...@@ -140,7 +107,7 @@ function _initState() { ...@@ -140,7 +107,7 @@ function _initState() {
}); });
// Update form inputs to reflect the state from URL. // 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) { if (element.name) {
const input = /** @type {HTMLInputElement} */ (element); const input = /** @type {HTMLInputElement} */ (element);
const values = _filterParams.getAll(input.name); const values = _filterParams.getAll(input.name);
...@@ -370,12 +337,15 @@ function _makeSizeTextGetter() { ...@@ -370,12 +337,15 @@ function _makeSizeTextGetter() {
}; };
} else { } else {
const bytes = node.size; const bytes = node.size;
const unit = state.get('byteunit', { let unit = state.get('byteunit');
default: 'MiB', let suffix = _BYTE_UNITS[unit];
valid: _BYTE_UNITS_SET, if (suffix == null) {
}); unit = 'MiB';
suffix = _BYTE_UNITS.MiB;
}
// Format the bytes as a number with 2 digits after the decimal point // 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, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
}); });
......
...@@ -24,6 +24,9 @@ const newTreeElement = (() => { ...@@ -24,6 +24,9 @@ const newTreeElement = (() => {
const _treeTemplate = document.getElementById('treenode-container'); const _treeTemplate = document.getElementById('treenode-container');
const _symbolTree = document.getElementById('symboltree'); const _symbolTree = document.getElementById('symboltree');
const _highlightRadio = document.getElementById('highlight-container');
/** @type {HTMLSelectElement} */
const _byteunitSelect = form.elements.namedItem('byteunit');
/** /**
* @type {HTMLCollectionOf<HTMLAnchorElement | HTMLSpanElement>} * @type {HTMLCollectionOf<HTMLAnchorElement | HTMLSpanElement>}
...@@ -44,6 +47,57 @@ const newTreeElement = (() => { ...@@ -44,6 +47,57 @@ const newTreeElement = (() => {
*/ */
const _uiNodeData = new WeakMap(); 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 * 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 * focus. The tabindex property is used to avoid needing to tab through every
...@@ -51,6 +105,7 @@ const newTreeElement = (() => { ...@@ -51,6 +105,7 @@ const newTreeElement = (() => {
* @param {number | HTMLElement} el Index of tree node in `_liveNodeList` * @param {number | HTMLElement} el Index of tree node in `_liveNodeList`
*/ */
function _focusTreeElement(el) { function _focusTreeElement(el) {
/** @type {HTMLElement} */
const lastFocused = document.activeElement; const lastFocused = document.activeElement;
if (_uiNodeData.has(lastFocused)) { if (_uiNodeData.has(lastFocused)) {
// Update DOM // Update DOM
...@@ -86,7 +141,9 @@ const newTreeElement = (() => { ...@@ -86,7 +141,9 @@ const newTreeElement = (() => {
let data = _uiNodeData.get(link); let data = _uiNodeData.get(link);
if (data == null || data.children == null) { 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); data = await worker.openNode(idPath);
_uiNodeData.set(link, data); _uiNodeData.set(link, data);
} }
...@@ -95,7 +152,9 @@ const newTreeElement = (() => { ...@@ -95,7 +152,9 @@ const newTreeElement = (() => {
if (newElements.length === 1) { if (newElements.length === 1) {
// Open the inner element if it only has a single child. // Open the inner element if it only has a single child.
// Ensures nodes like "java"->"com"->"google" are opened all at once. // 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); const newElementsFragment = dom.createFragment(newElements);
...@@ -185,7 +244,9 @@ const newTreeElement = (() => { ...@@ -185,7 +244,9 @@ const newTreeElement = (() => {
const groupList = link.parentElement.parentElement; const groupList = link.parentElement.parentElement;
if (groupList.getAttribute('role') === 'group') { if (groupList.getAttribute('role') === 'group') {
event.preventDefault(); event.preventDefault();
_focusTreeElement(groupList.previousElementSibling); /** @type {HTMLAnchorElement} */
const parentLink = groupList.previousElementSibling;
_focusTreeElement(parentLink);
} }
} }
} }
...@@ -292,13 +353,7 @@ const newTreeElement = (() => { ...@@ -292,13 +353,7 @@ const newTreeElement = (() => {
_ZERO_WIDTH_SPACE _ZERO_WIDTH_SPACE
); );
symbolName.title = data.idPath; symbolName.title = data.idPath;
_highlightSymbolName(symbolName, data);
if (state.has('method_count') && type === _DEX_METHOD_SYMBOL_TYPE) {
const {count = 0} = data.childStats[type] || {};
if (count < 0) {
symbolName.classList.add('removed');
}
}
// Set the byte size and hover text // Set the byte size and hover text
_setSize(element.querySelector('.size'), data); _setSize(element.querySelector('.size'), data);
...@@ -312,7 +367,7 @@ const newTreeElement = (() => { ...@@ -312,7 +367,7 @@ const newTreeElement = (() => {
} }
// When the `byteunit` state changes, update all .size elements in the page // 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(); event.stopPropagation();
state.set(event.currentTarget.name, event.currentTarget.value); state.set(event.currentTarget.name, event.currentTarget.value);
// Update existing size elements with the new unit // Update existing size elements with the new unit
...@@ -321,6 +376,19 @@ const newTreeElement = (() => { ...@@ -321,6 +376,19 @@ const newTreeElement = (() => {
if (data) _setSize(sizeElement, data); 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('keydown', _handleKeyNavigation);
_symbolTree.addEventListener('focusin', event => { _symbolTree.addEventListener('focusin', event => {
...@@ -337,7 +405,7 @@ const newTreeElement = (() => { ...@@ -337,7 +405,7 @@ const newTreeElement = (() => {
} }
}); });
return newTreeElement return newTreeElement;
})(); })();
{ {
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
* @prop {string} t Single character string to indicate the symbol type. * @prop {string} t Single character string to indicate the symbol type.
* @prop {number} [u] Count value indicating how many symbols this entry * @prop {number} [u] Count value indicating how many symbols this entry
* represents. Negative value when removed in a diff. * 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 * @typedef {object} FileEntry JSON object representing a single file and its
...@@ -82,7 +83,14 @@ function _compareFunc(a, b) { ...@@ -82,7 +83,14 @@ function _compareFunc(a, b) {
* @returns {TreeNode} * @returns {TreeNode}
*/ */
function createNode(options) { function createNode(options) {
const {idPath, type, shortNameIndex, size = 0, childStats = {}} = options; const {
idPath,
type,
shortNameIndex,
size = 0,
flags = 0,
childStats = {},
} = options;
return { return {
children: [], children: [],
parent: null, parent: null,
...@@ -91,6 +99,7 @@ function createNode(options) { ...@@ -91,6 +99,7 @@ function createNode(options) {
shortNameIndex, shortNameIndex,
size, size,
type, type,
flags,
}; };
} }
...@@ -143,13 +152,14 @@ class TreeBuilder { ...@@ -143,13 +152,14 @@ class TreeBuilder {
const additionalSize = node.size; const additionalSize = node.size;
const additionalStats = Object.entries(node.childStats); const additionalStats = Object.entries(node.childStats);
const additionalFlags = node.flags;
// Update the size and childStats of all ancestors // Update the size and childStats of all ancestors
while (node.parent != null) { while (node.parent != null) {
const {parent} = node; const {parent} = node;
const [containerType, lastBiggestType] = parent.type; const [containerType, lastBiggestType] = parent.type;
let {size: lastBiggestSize = 0} = let {size: lastBiggestSize = 0} =
parent.childStats[lastBiggestType] || {}; parent.childStats[lastBiggestType] || {};
for (const [type, stat] of additionalStats) { for (const [type, stat] of additionalStats) {
const parentStat = parent.childStats[type] || {size: 0, count: 0}; const parentStat = parent.childStats[type] || {size: 0, count: 0};
...@@ -165,6 +175,7 @@ class TreeBuilder { ...@@ -165,6 +175,7 @@ class TreeBuilder {
} }
parent.size += additionalSize; parent.size += additionalSize;
parent.flags |= additionalFlags;
node = parent; node = parent;
} }
} }
...@@ -257,15 +268,16 @@ class TreeBuilder { ...@@ -257,15 +268,16 @@ class TreeBuilder {
// If there is 1 child, include it so the UI doesn't need to make a // If there is 1 child, include it so the UI doesn't need to make a
// roundtrip in order to expand the chain. // roundtrip in order to expand the chain.
children = node.children children = node.children
.map(n => TreeBuilder.formatNode(n, childDepth)) .map(n => TreeBuilder.formatNode(n, childDepth))
.sort(_compareFunc); .sort(_compareFunc);
} }
return TreeBuilder._joinDexMethodClasses({ return TreeBuilder._joinDexMethodClasses(
...node, Object.assign({}, node, {
children, children,
parent: null, parent: null,
}); })
);
} }
/** /**
...@@ -344,7 +356,8 @@ class TreeBuilder { ...@@ -344,7 +356,8 @@ class TreeBuilder {
idPath: `${idPath}:${symbol[_KEYS.SYMBOL_NAME]}`, idPath: `${idPath}:${symbol[_KEYS.SYMBOL_NAME]}`,
shortNameIndex: idPath.length + 1, shortNameIndex: idPath.length + 1,
size, size,
type: symbol[_KEYS.TYPE], type,
flags: symbol[_KEYS.FLAGS] || 0,
childStats: {[type]: {size, count}}, childStats: {[type]: {size, count}},
}); });
...@@ -548,6 +561,7 @@ function parseOptions(options) { ...@@ -548,6 +561,7 @@ function parseOptions(options) {
const groupBy = params.get('group_by') || 'source_path'; const groupBy = params.get('group_by') || 'source_path';
const methodCountMode = params.has('method_count'); const methodCountMode = params.has('method_count');
const filterGeneratedFiles = params.has('generated_filter');
let minSymbolSize = Number(params.get('min_size')); let minSymbolSize = Number(params.get('min_size'));
if (Number.isNaN(minSymbolSize)) { if (Number.isNaN(minSymbolSize)) {
...@@ -569,19 +583,28 @@ function parseOptions(options) { ...@@ -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 = []; const filters = [];
/** Ensure symbol size is past the minimum */ // Ensure symbol size is past the minimum
if (minSymbolSize > 0) { if (minSymbolSize > 0) {
filters.push(s => Math.abs(s.size) >= minSymbolSize); 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) { if (typeFilter.size < _SYMBOL_TYPE_SET.size) {
filters.push(s => typeFilter.has(s.type)); 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) { if (includeRegex) {
try { try {
const regex = new RegExp(includeRegex); const regex = new RegExp(includeRegex);
...@@ -692,7 +715,7 @@ async function buildTree(options, onProgress) { ...@@ -692,7 +715,7 @@ async function buildTree(options, onProgress) {
const currentTime = Date.now(); const currentTime = Date.now();
if (currentTime - lastBatchSent > 500) { if (currentTime - lastBatchSent > 500) {
postToUi(); postToUi();
await Promise.resolve(); // Pause loop to check for worker messages await Promise.resolve(); // Pause loop to check for worker messages
lastBatchSent = currentTime; 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