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
......
......@@ -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,6 +152,7 @@ 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) {
......@@ -165,6 +175,7 @@ class TreeBuilder {
}
parent.size += additionalSize;
parent.flags |= additionalFlags;
node = parent;
}
}
......@@ -261,11 +272,12 @@ class TreeBuilder {
.sort(_compareFunc);
}
return TreeBuilder._joinDexMethodClasses({
...node,
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);
......
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