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

Supersize: Fix percentages and counts in diff mode

Previously percentages and symbol counts were misreported in diff mode,
as the values did not match up with the total size. Percentages are now
calculated based on the total delta of bytes changed, and the pie chart
has an outer ring to indicate if that type of symbol reduced or
increased.

Counts have been updated to be more accurate now that we have small
symbol buckets. A new property has been added to the data file that
indicated how many symbols a symbol represents. Most symbols omit this
key and default to 1 instead. Buckets indicate how many symbols they
represent. Removed dex methods have a negative count to indicate they
were removed, fixing the method count mode in diff mode.

Bug: 847599
Change-Id: I20073dc20b89481733aaaa319f3b4966217c6096
Reviewed-on: https://chromium-review.googlesource.com/1135698
Commit-Queue: Tiger Oakes <tigero@google.com>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#574981}
parent 8425acd0
...@@ -13,6 +13,7 @@ import shutil ...@@ -13,6 +13,7 @@ import shutil
import archive import archive
import diff import diff
import models
import path_util import path_util
...@@ -39,6 +40,7 @@ _COMPACT_FILE_SYMBOLS_KEY = 's' ...@@ -39,6 +40,7 @@ _COMPACT_FILE_SYMBOLS_KEY = 's'
_COMPACT_SYMBOL_NAME_KEY = 'n' _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'
_SMALL_SYMBOL_DESCRIPTIONS = { _SMALL_SYMBOL_DESCRIPTIONS = {
'b': 'Other small uninitialized data', 'b': 'Other small uninitialized data',
...@@ -261,6 +263,9 @@ def _MakeTreeViewList(symbols, min_symbol_size): ...@@ -261,6 +263,9 @@ def _MakeTreeViewList(symbols, min_symbol_size):
symbol_size = round(symbol.pss, 2) symbol_size = round(symbol.pss, 2)
if symbol_size.is_integer(): if symbol_size.is_integer():
symbol_size = int(symbol_size) symbol_size = int(symbol_size)
symbol_count = 1
if symbol.IsDelta() and symbol.diff_status == models.DIFF_STATUS_REMOVED:
symbol_count = -1
path = symbol.source_path or symbol.object_path path = symbol.source_path or symbol.object_path
file_node = file_nodes.get(path) file_node = file_nodes.get(path)
...@@ -277,11 +282,19 @@ def _MakeTreeViewList(symbols, min_symbol_size): ...@@ -277,11 +282,19 @@ def _MakeTreeViewList(symbols, min_symbol_size):
# UI. It's important to see details on all the methods, and most fall below # UI. It's important to see details on all the methods, and most fall below
# the default byte size. # the default byte size.
if symbol_type == 'm' or abs(symbol_size) >= min_symbol_size: if symbol_type == 'm' or abs(symbol_size) >= min_symbol_size:
file_node[_COMPACT_FILE_SYMBOLS_KEY].append({ symbol_entry = {
_COMPACT_SYMBOL_NAME_KEY: symbol.template_name, _COMPACT_SYMBOL_NAME_KEY: symbol.template_name,
_COMPACT_SYMBOL_TYPE_KEY: symbol_type, _COMPACT_SYMBOL_TYPE_KEY: symbol_type,
_COMPACT_SYMBOL_BYTE_SIZE_KEY: symbol_size, _COMPACT_SYMBOL_BYTE_SIZE_KEY: symbol_size,
}) }
# We use symbol count for the method count mode in the diff mode report.
# Negative values are used to indicate a symbol was removed, so it should
# count as -1 rather than the default, 1.
# We don't care about accurate counts for other symbol types currently,
# so this data is only included for methods.
if symbol_type == 'm' and symbol_count != 1:
symbol_entry[_COMPACT_SYMBOL_COUNT_KEY] = symbol_count
file_node[_COMPACT_FILE_SYMBOLS_KEY].append(symbol_entry)
else: else:
small_type_symbol = small_symbols[path].get(symbol_type) small_type_symbol = small_symbols[path].get(symbol_type)
if small_type_symbol is None: if small_type_symbol is None:
......
...@@ -188,9 +188,15 @@ ...@@ -188,9 +188,15 @@
color: #5f6368; color: #5f6368;
white-space: nowrap; white-space: nowrap;
} }
.negative { .shrunk {
color: #34a853;
}
.grew {
color: #ea4335; color: #ea4335;
} }
.removed {
text-decoration: line-through;
}
.diff .size-header::after { .diff .size-header::after {
content: ' diff'; content: ' diff';
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
* symbols as the user hovers or focuses on them. * symbols as the user hovers or focuses on them.
*/ */
{ const displayInfocard = (() => {
const _CANVAS_RADIUS = 40; const _CANVAS_RADIUS = 40;
class Infocard { class Infocard {
...@@ -43,13 +43,9 @@ ...@@ -43,13 +43,9 @@
* *
* Example: "1,234 bytes (1.23 KiB)" * Example: "1,234 bytes (1.23 KiB)"
* @param {TreeNode} node * @param {TreeNode} node
* @param {GetSize} getSizeLabel
*/ */
_updateSize(node, getSizeLabel) { _updateSize(node) {
const {description, element} = getSizeLabel( const {description, element, value} = getSizeContents(node);
node,
state.get('byteunit', {default: 'MiB', valid: _BYTE_UNITS_SET})
);
const sizeFragment = dom.createFragment([ const sizeFragment = dom.createFragment([
document.createTextNode(`${description} (`), document.createTextNode(`${description} (`),
element, element,
...@@ -57,11 +53,7 @@ ...@@ -57,11 +53,7 @@
]); ]);
// Update DOM // Update DOM
if (node.size < 0) { setSizeClasses(this._sizeInfo, value);
this._sizeInfo.classList.add('negative');
} else {
this._sizeInfo.classList.remove('negative');
}
dom.replace(this._sizeInfo, sizeFragment); dom.replace(this._sizeInfo, sizeFragment);
} }
...@@ -116,13 +108,12 @@ ...@@ -116,13 +108,12 @@
/** /**
* Updates the DOM for the info card. * Updates the DOM for the info card.
* @param {TreeNode} node * @param {TreeNode} node
* @param {GetSize} getSizeLabel
*/ */
_updateInfocard(node, getSizeLabel) { _updateInfocard(node) {
const type = node.type[0]; const type = node.type[0];
// Update DOM // Update DOM
this._updateSize(node, getSizeLabel); this._updateSize(node);
this._updatePath(node); this._updatePath(node);
if (type !== this._lastType) { if (type !== this._lastType) {
// No need to create a new icon if it is identical. // No need to create a new icon if it is identical.
...@@ -135,12 +126,11 @@ ...@@ -135,12 +126,11 @@
/** /**
* Updates the card on the next animation frame. * Updates the card on the next animation frame.
* @param {TreeNode} node * @param {TreeNode} node
* @param {GetSize} getSizeLabel
*/ */
updateInfocard(node, getSizeLabel) { updateInfocard(node) {
cancelAnimationFrame(Infocard._pendingFrame); cancelAnimationFrame(Infocard._pendingFrame);
Infocard._pendingFrame = requestAnimationFrame(() => Infocard._pendingFrame = requestAnimationFrame(() =>
this._updateInfocard(node, getSizeLabel) this._updateInfocard(node)
); );
} }
} }
...@@ -205,16 +195,17 @@ ...@@ -205,16 +195,17 @@
* Draw a slice of a pie chart. * Draw a slice of a pie chart.
* @param {number} angleStart Starting angle, in radians. * @param {number} angleStart Starting angle, in radians.
* @param {number} percentage Percentage of circle to draw. * @param {number} percentage Percentage of circle to draw.
* @param {string} color Color of the pie slice. * @param {string} fillColor Color of the pie slice.
* @param {string} strokeColor Color of the pie slice border.
* @returns {number} Ending angle, in radians. * @returns {number} Ending angle, in radians.
*/ */
_drawSlice(angleStart, percentage, color) { _drawSlice(angleStart, percentage, fillColor, strokeColor) {
const arcLength = percentage * 2 * Math.PI; const arcLength = Math.abs(percentage) * 2 * Math.PI;
const angleEnd = angleStart + arcLength; const angleEnd = angleStart + arcLength;
if (arcLength === 0) return angleEnd; if (arcLength === 0) return angleEnd;
// Update DOM // Update DOM
this._ctx.fillStyle = color; this._ctx.fillStyle = fillColor;
// Move cursor to center, where line will start // Move cursor to center, where line will start
this._ctx.beginPath(); this._ctx.beginPath();
this._ctx.moveTo(40, 40); this._ctx.moveTo(40, 40);
...@@ -224,6 +215,14 @@ ...@@ -224,6 +215,14 @@
this._ctx.closePath(); this._ctx.closePath();
this._ctx.fill(); this._ctx.fill();
if (strokeColor) {
this._ctx.strokeStyle = strokeColor;
this._ctx.lineWidth = 16;
this._ctx.beginPath();
this._ctx.arc(40, 40, _CANVAS_RADIUS, angleStart, angleEnd);
this._ctx.stroke();
}
return angleEnd; return angleEnd;
} }
...@@ -271,23 +270,31 @@ ...@@ -271,23 +270,31 @@
/** /**
* Update DOM for the container infocard * Update DOM for the container infocard
* @param {TreeNode} containerNode * @param {TreeNode} containerNode
* @param {GetSize} getSizeLabel
*/ */
_updateInfocard(containerNode, getSizeLabel) { _updateInfocard(containerNode) {
const extraRows = {...this._infoRows}; const extraRows = {...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
); );
const diffMode = state.has('diff_mode');
let totalSize = 0;
for (const [, stats] of statsEntries) {
totalSize += Math.abs(stats.size);
}
// Update DOM // Update DOM
super._updateInfocard(containerNode, getSizeLabel); super._updateInfocard(containerNode);
let angleStart = 0; let angleStart = 0;
for (const [type, stats] of statsEntries) { for (const [type, stats] of statsEntries) {
delete extraRows[type]; delete extraRows[type];
const {color} = getIconStyle(type); const {color} = getIconStyle(type);
const percentage = stats.size / containerNode.size; const percentage = stats.size / totalSize;
let stroke = '';
if (diffMode) {
stroke = stats.size > 0 ? '#ea4335' : '#34a853';
}
angleStart = this._drawSlice(angleStart, percentage, color); angleStart = this._drawSlice(angleStart, percentage, color, stroke);
this._updateBreakdownRow(this._infoRows[type], stats, percentage); this._updateBreakdownRow(this._infoRows[type], stats, percentage);
} }
...@@ -304,19 +311,18 @@ ...@@ -304,19 +311,18 @@
/** /**
* Displays an infocard for the given symbol on the next frame. * Displays an infocard for the given symbol on the next frame.
* @param {TreeNode} node * @param {TreeNode} node
* @param {GetSize} getSizeLabel
*/ */
function displayInfocard(node, getSizeLabel) { function displayInfocard(node) {
if (_CONTAINER_TYPE_SET.has(node.type[0])) { if (_CONTAINER_TYPE_SET.has(node.type[0])) {
_containerInfo.updateInfocard(node, getSizeLabel); _containerInfo.updateInfocard(node);
_containerInfo.setHidden(false); _containerInfo.setHidden(false);
_symbolInfo.setHidden(true); _symbolInfo.setHidden(true);
} else { } else {
_symbolInfo.updateInfocard(node, getSizeLabel); _symbolInfo.updateInfocard(node);
_symbolInfo.setHidden(false); _symbolInfo.setHidden(false);
_containerInfo.setHidden(true); _containerInfo.setHidden(true);
} }
} }
self.displayInfocard = displayInfocard; return displayInfocard;
} })();
...@@ -64,14 +64,15 @@ ...@@ -64,14 +64,15 @@
} }
.header-info { .header-info {
grid-area: header; grid-area: header;
color: #202124;
} }
.size-info { .size-info {
margin: 0 0 2px; margin: 0 0 2px;
color: #202124;
} }
.path-info { .path-info {
margin: 0 0 8px; margin: 0 0 8px;
word-break: break-word; word-break: break-word;
color: #3c4043;
} }
.symbol-name-info { .symbol-name-info {
font-weight: 500; font-weight: 500;
...@@ -84,7 +85,6 @@ ...@@ -84,7 +85,6 @@
.type-pie-info { .type-pie-info {
height: 80px; height: 80px;
width: 80px; width: 80px;
background: #5f6368;
border-radius: 50%; border-radius: 50%;
} }
.type-breakdown-info { .type-breakdown-info {
......
...@@ -49,14 +49,15 @@ ...@@ -49,14 +49,15 @@
*/ */
/** Abberivated keys used by FileEntrys in the JSON data file. */ /** Abberivated keys used by FileEntrys in the JSON data file. */
const _KEYS = { const _KEYS = Object.freeze({
SOURCE_PATH: 'p', SOURCE_PATH: /** @type {'p'} */ ('p'),
COMPONENT_INDEX: 'c', COMPONENT_INDEX: /** @type {'c'} */ ('c'),
FILE_SYMBOLS: 's', FILE_SYMBOLS: /** @type {'s'} */ ('s'),
SYMBOL_NAME: 'n', SYMBOL_NAME: /** @type {'n'} */ ('n'),
SIZE: 'b', SIZE: /** @type {'b'} */ ('b'),
TYPE: 't', TYPE: /** @type {'t'} */ ('t'),
}; COUNT: /** @type {'u'} */ ('u'),
});
/** /**
* @enum {number} Various byte units and the corresponding amount of bytes * @enum {number} Various byte units and the corresponding amount of bytes
......
...@@ -124,6 +124,18 @@ function _initState() { ...@@ -124,6 +124,18 @@ function _initState() {
if (types.length > 0) copy.set(_TYPE_STATE_KEY, types.join('')); if (types.length > 0) copy.set(_TYPE_STATE_KEY, types.join(''));
return `?${copy.toString()}`; return `?${copy.toString()}`;
}, },
/**
* Saves a key and value into a temporary state not displayed in the URL.
* @param {string} key
* @param {string | null} value
*/
set(key, value) {
if (value == null) {
_filterParams.delete(key);
} else {
_filterParams.set(key, value);
}
},
}); });
// Update form inputs to reflect the state from URL. // Update form inputs to reflect the state from URL.
...@@ -171,11 +183,13 @@ function _initState() { ...@@ -171,11 +183,13 @@ function _initState() {
} }
// Update the state when the form changes. // Update the state when the form changes.
form.addEventListener('change', () => { function _updateStateFromForm() {
const modifiedForm = new FormData(form); const modifiedForm = new FormData(form);
_filterParams = new URLSearchParams(onlyChangedEntries(modifiedForm)); _filterParams = new URLSearchParams(onlyChangedEntries(modifiedForm));
history.replaceState(null, null, state.toString()); history.replaceState(null, null, state.toString());
}); }
form.addEventListener('change', _updateStateFromForm);
return state; return state;
} }
...@@ -277,7 +291,7 @@ function _makeIconTemplateGetter() { ...@@ -277,7 +291,7 @@ function _makeIconTemplateGetter() {
* @returns {SVGSVGElement} * @returns {SVGSVGElement}
*/ */
function getIconTemplate(type, readonly = false) { function getIconTemplate(type, readonly = false) {
const iconTemplate = symbolIcons[type] || symbolIcons.o; const iconTemplate = symbolIcons[type] || symbolIcons[_OTHER_SYMBOL_TYPE];
return readonly ? iconTemplate : iconTemplate.cloneNode(true); return readonly ? iconTemplate : iconTemplate.cloneNode(true);
} }
...@@ -302,7 +316,86 @@ function _makeIconTemplateGetter() { ...@@ -302,7 +316,86 @@ function _makeIconTemplateGetter() {
return {getIconTemplate, getIconStyle}; return {getIconTemplate, getIconStyle};
} }
function _makeSizeTextGetter() {
const _SIZE_CHANGE_CUTOFF = 50000;
/**
* Create the contents for the size element of a tree node.
* The unit to use is selected from the current state.
*
* If in method count mode, size instead represents the amount of methods in
* the node. Otherwise, the original number of bytes will be displayed.
*
* @param {TreeNode} node Node whose size is the number of bytes to use for
* the size text
* @returns {GetSizeResult} Object with hover text title and
* size element body. Can be consumed by `_applySizeFunc()`
*/
function getSizeContents(node) {
if (state.has('method_count')) {
const {count: methodCount = 0} =
node.childStats[_DEX_METHOD_SYMBOL_TYPE] || {};
const methodStr = methodCount.toLocaleString(_LOCALE, {
useGrouping: true,
});
return {
element: document.createTextNode(methodStr),
description: `${methodStr} method${methodCount === 1 ? '' : 's'}`,
value: methodCount,
};
} else {
const bytes = node.size;
const unit = state.get('byteunit', {
default: 'MiB',
valid: _BYTE_UNITS_SET,
});
// Format the bytes as a number with 2 digits after the decimal point
const text = (bytes / _BYTE_UNITS[unit]).toLocaleString(_LOCALE, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const textNode = document.createTextNode(`${text} `);
// Display the suffix with a smaller font
const suffixElement = dom.textElement('small', unit);
const bytesGrouped = bytes.toLocaleString(_LOCALE, {useGrouping: true});
return {
element: dom.createFragment([textNode, suffixElement]),
description: `${bytesGrouped} bytes`,
value: bytes,
};
}
}
/**
* Set classes on an element based on the size it represents.
* @param {HTMLElement} sizeElement
* @param {number} value
*/
function setSizeClasses(sizeElement, value) {
const shouldHaveStyle =
state.has('diff_mode') && Math.abs(value) > _SIZE_CHANGE_CUTOFF;
if (shouldHaveStyle) {
if (value < 0) {
sizeElement.classList.add('shrunk');
sizeElement.classList.remove('grew');
} else {
sizeElement.classList.remove('shrunk');
sizeElement.classList.add('grew');
}
} else {
sizeElement.classList.remove('shrunk', 'grew');
}
}
return {getSizeContents, setSizeClasses};
}
/** Utilities for working with the state */ /** Utilities for working with the state */
const state = _initState(); const state = _initState();
const {getIconTemplate, getIconStyle} = _makeIconTemplateGetter(); const {getIconTemplate, getIconStyle} = _makeIconTemplateGetter();
const {getSizeContents, setSizeClasses} = _makeSizeTextGetter();
_startListeners(); _startListeners();
...@@ -44,81 +44,6 @@ ...@@ -44,81 +44,6 @@
*/ */
const _uiNodeData = new WeakMap(); const _uiNodeData = new WeakMap();
/**
* Create the contents for the size element of a tree node.
* If in method count mode, size instead represents the amount of methods in
* the node. In this case, don't append a unit at the end.
* @param {TreeNode} node Node whose childStats will be polled to find the
* number of methods to use for the count text
* @returns {GetSizeResult} Object with hover text title and
* size element body. Can be consumed by `_applySizeFunc()`
*/
function _getMethodCountContents(node) {
const {count: methodCount = 0} =
node.childStats[_DEX_METHOD_SYMBOL_TYPE] || {};
const methodStr = methodCount.toLocaleString(_LOCALE, {
useGrouping: true,
});
return {
element: document.createTextNode(methodStr),
description: `${methodStr} method${methodCount === 1 ? '' : 's'}`,
value: methodCount,
};
}
/**
* Create the contents for the size element of 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 {TreeNode} node Node whose size is the number of bytes to use for
* the size text
* @param {string} unit Byte unit to use, such as 'MiB'.
* @returns {GetSizeResult} Object with hover text title and
* size element body. Can be consumed by `_applySizeFunc()`
*/
function _getSizeContents(node, unit) {
const bytes = node.size;
// Format the bytes as a number with 2 digits after the decimal point
const text = (bytes / _BYTE_UNITS[unit]).toLocaleString(_LOCALE, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const textNode = document.createTextNode(`${text} `);
// Display the suffix with a smaller font
const suffixElement = dom.textElement('small', unit);
const bytesGrouped = bytes.toLocaleString(_LOCALE, {useGrouping: true});
return {
element: dom.createFragment([textNode, suffixElement]),
description: `${bytesGrouped} bytes`,
value: bytes,
};
}
/**
* Replace the contexts of the size element for a tree node, using a
* predefined function which returns a title string and DOM element.
* @param {GetSize} sizeFunc
* @param {HTMLElement} sizeElement Element that should display the size
* @param {TreeNode} node
*/
function _applySizeFunc(sizeFunc, sizeElement, node, unit) {
const {description, element, value} = sizeFunc(node, unit);
// Replace the contents of '.size' and change its title
dom.replace(sizeElement, element);
sizeElement.title = description;
if (value < 0) {
sizeElement.classList.add('negative');
} else {
sizeElement.classList.remove('negative');
}
}
/** /**
* 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
...@@ -302,6 +227,20 @@ ...@@ -302,6 +227,20 @@
} }
} }
/**
* Replace the contents of the size element for a tree node.
* @param {HTMLElement} sizeElement Element that should display the size
* @param {TreeNode} node
*/
function _setSize(sizeElement, node) {
const {description, element, value} = getSizeContents(node);
// Replace the contents of '.size' and change its title
dom.replace(sizeElement, element);
sizeElement.title = description;
setSizeClasses(sizeElement, value);
}
/** /**
* Inflate a template to create an element that represents one tree node. * 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 * The element will represent a tree or a leaf, depending on if the tree
...@@ -323,7 +262,7 @@ ...@@ -323,7 +262,7 @@
// Icons are predefined in the HTML through hidden SVG elements // Icons are predefined in the HTML through hidden SVG elements
const type = data.type[0]; const type = data.type[0];
const icon = getIconTemplate(type); const icon = getIconTemplate(type);
if (_CONTAINER_TYPE_SET.has(type)) { if (!isLeaf) {
const symbolStyle = getIconStyle(data.type[1]); const symbolStyle = getIconStyle(data.type[1]);
icon.setAttribute('fill', symbolStyle.color); icon.setAttribute('fill', symbolStyle.color);
} }
...@@ -339,19 +278,18 @@ ...@@ -339,19 +278,18 @@
); );
symbolName.title = data.idPath; 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');
}
}
// Set the byte size and hover text // Set the byte size and hover text
const _getSizeLabels = state.has('method_count') _setSize(element.querySelector('.size'), data);
? _getMethodCountContents
: _getSizeContents;
_applySizeFunc(
_getSizeLabels,
element.querySelector('.size'),
data,
state.get('byteunit', {default: 'MiB', valid: _BYTE_UNITS_SET})
);
link.addEventListener('mouseover', event => link.addEventListener('mouseover', event =>
displayInfocard(_uiNodeData.get(event.currentTarget), _getSizeLabels) displayInfocard(_uiNodeData.get(event.currentTarget))
); );
if (!isLeaf) { if (!isLeaf) {
link.addEventListener('click', _toggleTreeElement); link.addEventListener('click', _toggleTreeElement);
...@@ -362,20 +300,18 @@ ...@@ -362,20 +300,18 @@
// 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 => { form.elements.namedItem('byteunit').addEventListener('change', event => {
const unit = event.currentTarget.value; // Update state early for _setSize
state.set(event.currentTarget.name, event.currentTarget.value);
// Update existing size elements with the new unit // Update existing size elements with the new unit
for (const sizeElement of _liveSizeSpanList) { for (const sizeElement of _liveSizeSpanList) {
const data = _uiNodeData.get(sizeElement.parentElement); const data = _uiNodeData.get(sizeElement.parentElement);
_applySizeFunc(_getSizeContents, sizeElement, data, unit); if (data) _setSize(sizeElement, data);
} }
}); });
_symbolTree.addEventListener('keydown', _handleKeyNavigation); _symbolTree.addEventListener('keydown', _handleKeyNavigation);
_symbolTree.addEventListener('focusin', event => { _symbolTree.addEventListener('focusin', event => {
displayInfocard( displayInfocard(_uiNodeData.get(event.target));
_uiNodeData.get(event.target),
state.has('method_count') ? _getMethodCountContents : _getSizeContents
);
event.currentTarget.parentElement.classList.add('focused'); event.currentTarget.parentElement.classList.add('focused');
}); });
_symbolTree.addEventListener('focusout', event => _symbolTree.addEventListener('focusout', event =>
...@@ -406,6 +342,7 @@ ...@@ -406,6 +342,7 @@
link.click(); link.click();
link.tabIndex = 0; link.tabIndex = 0;
} }
state.set('diff_mode', diffMode ? 'on' : null);
requestAnimationFrame(() => { requestAnimationFrame(() => {
_progress.value = percent; _progress.value = percent;
......
...@@ -22,11 +22,13 @@ ...@@ -22,11 +22,13 @@
* @prop {string} n Name of the symbol. * @prop {string} n Name of the symbol.
* @prop {number} b Byte size of the symbol, divided by num_aliases. * @prop {number} b Byte size of the symbol, divided by num_aliases.
* @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
* represents. Negative value when removed in a diff.
*/ */
/** /**
* @typedef {object} FileEntry JSON object representing a single file and its * @typedef {object} FileEntry JSON object representing a single file and its
* symbols. * symbols.
* @prop {number} p Path to the file (source_path). * @prop {string} p Path to the file (source_path).
* @prop {number} c Index of the file's component in meta (component_index). * @prop {number} c Index of the file's component in meta (component_index).
* @prop {SymbolEntry[]} s - Symbols belonging to this node. Array of objects. * @prop {SymbolEntry[]} s - Symbols belonging to this node. Array of objects.
*/ */
...@@ -69,16 +71,6 @@ function _compareFunc(a, b) { ...@@ -69,16 +71,6 @@ function _compareFunc(a, b) {
return Math.abs(b.size) - Math.abs(a.size); return Math.abs(b.size) - Math.abs(a.size);
} }
/**
* 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(_compareFunc);
node.children.forEach(sortTree);
}
/** /**
* Make a node with some default arguments * Make a node with some default arguments
* @param {Partial<TreeNode> & {shortName:string}} options * @param {Partial<TreeNode> & {shortName:string}} options
...@@ -99,40 +91,6 @@ function createNode(options) { ...@@ -99,40 +91,6 @@ function createNode(options) {
}; };
} }
/**
* Formats a tree node by removing references to its desendants and ancestors.
*
* Only children up to `depth` will be kept, and deeper children will be
* replaced with `null` to indicate that there were children by they were
* removed.
*
* Leaves will no children will always have an empty children array.
* If a tree has only 1 child, it is kept as the UI will expand chain of single
* children in the tree.
* @param {TreeNode} node Node to format
* @param {number} depth How many levels of children to keep.
* @returns {TreeNode}
*/
function formatNode(node, depth = 1) {
const childDepth = depth - 1;
// `null` represents that the children have not been loaded yet
let children = null;
if (depth > 0 || node.children.length <= 1) {
// If depth is larger than 0, include the children.
// If there are 0 children, include the empty array to indicate the node is
// a leaf.
// 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 => formatNode(n, childDepth));
}
return {
...node,
children,
parent: null,
};
}
/** /**
* Class used to build a tree from a list of symbol objects. * Class used to build a tree from a list of symbol objects.
* Add each file node using `addFileEntry()`, then call `build()` to finalize * Add each file node using `addFileEntry()`, then call `build()` to finalize
...@@ -195,8 +153,10 @@ class TreeBuilder { ...@@ -195,8 +153,10 @@ class TreeBuilder {
parentStat.count += stat.count; parentStat.count += stat.count;
node.parent.childStats[type] = parentStat; node.parent.childStats[type] = parentStat;
if (parentStat.size > lastBiggestSize) { const absSize = Math.abs(parentStat.size);
if (absSize > lastBiggestSize) {
node.parent.type = `${containerType}${type}`; node.parent.type = `${containerType}${type}`;
lastBiggestSize = absSize;
} }
} }
...@@ -206,12 +166,13 @@ class TreeBuilder { ...@@ -206,12 +166,13 @@ class TreeBuilder {
} }
/** /**
* * Merges dex method symbols such as "Controller#get" and "Controller#set"
* into containers, based on the class of the dex methods.
* @param {TreeNode} node * @param {TreeNode} node
*/ */
static _joinDexMethodClasses(node) { static _joinDexMethodClasses(node) {
const hasDexMethods = node.childStats[_DEX_METHOD_SYMBOL_TYPE] != null; const hasDexMethods = node.childStats[_DEX_METHOD_SYMBOL_TYPE] != null;
if (!hasDexMethods) return; if (!hasDexMethods || node.children == null) return node;
if (node.type[0] === _CONTAINER_TYPES.FILE) { if (node.type[0] === _CONTAINER_TYPES.FILE) {
/** @type {Map<string, TreeNode>} */ /** @type {Map<string, TreeNode>} */
...@@ -260,6 +221,47 @@ class TreeBuilder { ...@@ -260,6 +221,47 @@ class TreeBuilder {
} else { } else {
node.children.forEach(TreeBuilder._joinDexMethodClasses); node.children.forEach(TreeBuilder._joinDexMethodClasses);
} }
return node;
}
/**
* Formats a tree node by removing references to its desendants and ancestors.
* This reduces how much data is sent to the UI thread at once. For large
* trees, serialization and deserialization of the entire tree can take ~7s.
*
* Only children up to `depth` will be kept, and deeper children will be
* replaced with `null` to indicate that there were children by they were
* removed.
*
* Leaves with no children will always have an empty children array.
* If a tree has only 1 child, it is kept as the UI will expand chains of
* single children in the tree.
*
* Additionally sorts the formatted portion of the tree.
* @param {TreeNode} node Node to format
* @param {number} depth How many levels of children to keep.
* @returns {TreeNode}
*/
static formatNode(node, depth = 1) {
const childDepth = depth - 1;
// `null` represents that the children have not been loaded yet
let children = null;
if (depth > 0 || node.children.length <= 1) {
// If depth is larger than 0, include the children.
// If there are 0 children, include the empty array to indicate the node
// is a leaf.
// 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);
}
return TreeBuilder._joinDexMethodClasses({
...node,
children,
parent: null,
});
} }
/** /**
...@@ -336,13 +338,14 @@ class TreeBuilder { ...@@ -336,13 +338,14 @@ class TreeBuilder {
for (const symbol of fileEntry[_KEYS.FILE_SYMBOLS]) { for (const symbol of fileEntry[_KEYS.FILE_SYMBOLS]) {
const size = symbol[_KEYS.SIZE]; const size = symbol[_KEYS.SIZE];
const type = symbol[_KEYS.TYPE]; const type = symbol[_KEYS.TYPE];
const count = symbol[_KEYS.COUNT] || 1;
const symbolNode = createNode({ const symbolNode = createNode({
// Join file path to symbol name with a ":" // Join file path to symbol name with a ":"
idPath: `${idPath}:${symbol[_KEYS.SYMBOL_NAME]}`, idPath: `${idPath}:${symbol[_KEYS.SYMBOL_NAME]}`,
shortName: symbol[_KEYS.SYMBOL_NAME], shortName: symbol[_KEYS.SYMBOL_NAME],
size, size,
type: symbol[_KEYS.TYPE], type: symbol[_KEYS.TYPE],
childStats: {[type]: {size, count: 1}}, childStats: {[type]: {size, count}},
}); });
if (this._filterTest(symbolNode)) { if (this._filterTest(symbolNode)) {
...@@ -363,10 +366,6 @@ class TreeBuilder { ...@@ -363,10 +366,6 @@ class TreeBuilder {
* Finalize the creation of the tree and return the root node. * Finalize the creation of the tree and return the root node.
*/ */
build() { build() {
TreeBuilder._joinDexMethodClasses(this.rootNode);
// Sort the tree so that larger items are higher.
sortTree(this.rootNode);
return this.rootNode; return this.rootNode;
} }
...@@ -559,6 +558,7 @@ async function buildTree(options, onProgress) { ...@@ -559,6 +558,7 @@ async function buildTree(options, onProgress) {
/** @type {Meta | null} Object from the first line of the data file */ /** @type {Meta | null} Object from the first line of the data file */
let meta = null; let meta = null;
/** @type {{ [gropyBy: string]: (fileEntry: FileEntry) => string }} */
const getPathMap = { const getPathMap = {
component(fileEntry) { component(fileEntry) {
const component = meta.components[fileEntry[_KEYS.COMPONENT_INDEX]]; const component = meta.components[fileEntry[_KEYS.COMPONENT_INDEX]];
...@@ -580,12 +580,12 @@ async function buildTree(options, onProgress) { ...@@ -580,12 +580,12 @@ async function buildTree(options, onProgress) {
}); });
/** /**
* Post data to the UI thread. Defaults will be used for the root and percent * Creates data to post to the UI thread. Defaults will be used for the root
* values if not specified. * and percent values if not specified.
* @param {{root?:TreeNode,percent?:number,error?:Error}} data Default data * @param {{root?:TreeNode,percent?:number,error?:Error}} data Default data
* values to post. * values to post.
*/ */
function postToUi(data = {}) { function createProgressMessage(data = {}) {
let {percent} = data; let {percent} = data;
if (percent == null) { if (percent == null) {
if (meta == null) { if (meta == null) {
...@@ -596,15 +596,23 @@ async function buildTree(options, onProgress) { ...@@ -596,15 +596,23 @@ async function buildTree(options, onProgress) {
} }
const message = { const message = {
id: 0, root: TreeBuilder.formatNode(data.root || builder.rootNode),
root: formatNode(data.root || builder.rootNode),
percent, percent,
diffMode: meta && meta.diff_mode, diffMode: meta && meta.diff_mode,
}; };
if (data.error) { if (data.error) {
message.error = data.error.message; message.error = data.error.message;
} }
return message;
}
/**
* Post data to the UI thread. Defaults will be used for the root and percent
* values if not specified.
*/
function postToUi() {
const message = createProgressMessage();
message.id = 0;
onProgress(message); onProgress(message);
} }
...@@ -615,26 +623,26 @@ async function buildTree(options, onProgress) { ...@@ -615,26 +623,26 @@ async function buildTree(options, onProgress) {
interval = setInterval(postToUi, 1000); interval = setInterval(postToUi, 1000);
for await (const dataObj of fetcher.newlineDelimtedJsonStream()) { for await (const dataObj of fetcher.newlineDelimtedJsonStream()) {
if (meta == null) { if (meta == null) {
meta = dataObj; // First line of data is used to store meta information.
meta = /** @type {Meta} */ (dataObj);
postToUi(); postToUi();
} else { } else {
builder.addFileEntry(dataObj); builder.addFileEntry(/** @type {FileEntry} */ (dataObj));
} }
} }
clearInterval(interval); clearInterval(interval);
return { return createProgressMessage({
root: builder.build(), root: builder.build(),
percent: 1, percent: 1,
diffMode: meta && meta.diff_mode, });
};
} catch (error) { } catch (error) {
if (interval != null) clearInterval(interval); if (interval != null) clearInterval(interval);
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
console.info(error.message); console.info(error.message);
} else { } else {
console.error(error); console.error(error);
throw error; return createProgressMessage({error});
} }
} }
} }
...@@ -650,7 +658,7 @@ const actions = { ...@@ -650,7 +658,7 @@ const actions = {
async open(path) { async open(path) {
if (!builder) throw new Error('Called open before load'); if (!builder) throw new Error('Called open before load');
const node = builder.find(path); const node = builder.find(path);
return formatNode(node); return TreeBuilder.formatNode(node);
}, },
}; };
......
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