naiveproxy/tools/binary_size/libsupersize/static/infocard-ui.js
2018-12-09 21:59:24 -05:00

377 lines
12 KiB
JavaScript

// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @ts-check
'use strict';
/**
* @fileoverview
* UI classes and methods for the info cards that display informations about
* symbols as the user hovers or focuses on them.
*/
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
*/
constructor(id) {
this._infocard = document.getElementById(id);
/** @type {HTMLHeadingElement} */
this._sizeInfo = this._infocard.querySelector('.size-info');
/** @type {HTMLParagraphElement} */
this._pathInfo = this._infocard.querySelector('.path-info');
/** @type {HTMLDivElement} */
this._iconInfo = this._infocard.querySelector('.icon-info');
/** @type {HTMLSpanElement} */
this._typeInfo = this._infocard.querySelector('.type-info');
/** @type {HTMLSpanElement} */
this._flagsInfo = this._infocard.querySelector('.flags-info');
/**
* Last symbol type displayed.
* Tracked to avoid re-cloning the same icon.
* @type {string}
*/
this._lastType = '';
}
/**
* Updates the size header, which normally displayed the byte size of the
* node followed by an abbreviated version.
*
* Example: "1,234 bytes (1.23 KiB)"
* @param {TreeNode} node
*/
_updateSize(node) {
const {description, element, value} = getSizeContents(node);
const sizeFragment = dom.createFragment([
document.createTextNode(`${description} (`),
element,
document.createTextNode(')'),
]);
// Update DOM
setSizeClasses(this._sizeInfo, value);
dom.replace(this._sizeInfo, sizeFragment);
}
/**
* Updates the path text, which shows the idPath of the node but highlights
* the symbol name portion using bold text.
* @param {TreeNode} node
*/
_updatePath(node) {
const path = node.idPath.slice(0, node.shortNameIndex);
const boldShortName = dom.textElement(
'span',
shortName(node),
'symbol-name-info'
);
const pathFragment = dom.createFragment([
document.createTextNode(path),
boldShortName,
]);
// Update DOM
dom.replace(this._pathInfo, pathFragment);
}
/**
* Updates the icon and type text. The type label is pulled from the
* title of the icon supplied.
* @param {SVGSVGElement} icon Icon to display
*/
_setTypeContent(icon) {
const typeDescription = icon.querySelector('title').textContent;
icon.setAttribute('fill', '#fff');
this._typeInfo.textContent = typeDescription;
this._iconInfo.removeChild(this._iconInfo.lastElementChild);
this._iconInfo.appendChild(icon);
}
/**
* Returns a string representing the flags in the node.
* @param {TreeNode} node
*/
_flagsString(node) {
if (!node.flags) {
return '';
}
const flagsString = Array.from(_FLAG_LABELS)
.filter(([flag]) => hasFlag(flag, node))
.map(([, part]) => part)
.join(',');
return `{${flagsString}}`;
}
/**
* Toggle wheter or not the card is visible.
* @param {boolean} isHidden
*/
setHidden(isHidden) {
if (isHidden) {
this._infocard.setAttribute('hidden', '');
} else {
this._infocard.removeAttribute('hidden');
}
}
/**
* Updates the DOM for the info card.
* @param {TreeNode} node
*/
_updateInfocard(node) {
const type = node.type[0];
// Update DOM
this._updateSize(node);
this._updatePath(node);
if (type !== this._lastType) {
// No need to create a new icon if it is identical.
const icon = getIconTemplate(type);
this._setTypeContent(icon);
this._lastType = type;
}
this._flagsInfo.textContent = this._flagsString(node);
}
/**
* Updates the card on the next animation frame.
* @param {TreeNode} node
*/
updateInfocard(node) {
cancelAnimationFrame(Infocard._pendingFrame);
Infocard._pendingFrame = requestAnimationFrame(() =>
this._updateInfocard(node)
);
}
}
class SymbolInfocard extends Infocard {
/**
* @param {SVGSVGElement} icon Icon to display
*/
_setTypeContent(icon) {
const color = icon.getAttribute('fill');
super._setTypeContent(icon);
this._iconInfo.style.backgroundColor = color;
}
}
class ContainerInfocard extends Infocard {
constructor(id) {
super(id);
this._tableBody = this._infocard.querySelector('tbody');
this._ctx = this._infocard.querySelector('canvas').getContext('2d');
/**
* @type {{[type:string]: HTMLTableRowElement}} Rows in the container
* infocard that represent a particular symbol type.
*/
this._infoRows = {
b: this._tableBody.querySelector('.bss-info'),
d: this._tableBody.querySelector('.data-info'),
r: this._tableBody.querySelector('.rodata-info'),
t: this._tableBody.querySelector('.text-info'),
R: this._tableBody.querySelector('.relro-info'),
x: this._tableBody.querySelector('.dexnon-info'),
m: this._tableBody.querySelector('.dex-info'),
p: this._tableBody.querySelector('.pak-info'),
P: this._tableBody.querySelector('.paknon-info'),
o: this._tableBody.querySelector('.other-info'),
};
/**
* Update the DPI of the canvas for zoomed in and high density screens.
*/
const _updateCanvasDpi = () => {
this._ctx.canvas.height = _CANVAS_RADIUS * 2 * devicePixelRatio;
this._ctx.canvas.width = _CANVAS_RADIUS * 2 * devicePixelRatio;
this._ctx.scale(devicePixelRatio, devicePixelRatio);
};
_updateCanvasDpi();
window.addEventListener('resize', _updateCanvasDpi);
}
/**
* @param {SVGSVGElement} icon Icon to display
*/
_setTypeContent(icon) {
super._setTypeContent(icon);
icon.classList.add('canvas-overlay');
}
_flagsString(containerNode) {
const flags = super._flagsString(containerNode);
return flags ? `- contains ${flags}` : '';
}
/**
* Draw a border around part of a pie chart.
* @param {number} angleStart Starting angle, in radians.
* @param {number} angleEnd Ending angle, in radians.
* @param {string} strokeColor Color of the pie slice border.
* @param {number} lineWidth Width of the border.
*/
_drawBorder(angleStart, angleEnd, strokeColor, lineWidth) {
this._ctx.strokeStyle = strokeColor;
this._ctx.lineWidth = lineWidth;
this._ctx.beginPath();
this._ctx.arc(40, 40, _CANVAS_RADIUS, angleStart, angleEnd);
this._ctx.stroke();
}
/**
* Draw a slice of a pie chart.
* @param {number} angleStart Starting angle, in radians.
* @param {number} angleEnd Ending angle, in radians.
* @param {string} fillColor Color of the pie slice.
*/
_drawSlice(angleStart, angleEnd, fillColor) {
// Update DOM
this._ctx.fillStyle = fillColor;
// Move cursor to center, where line will start
this._ctx.beginPath();
this._ctx.moveTo(40, 40);
// Move cursor to start of arc then draw arc
this._ctx.arc(40, 40, _CANVAS_RADIUS, angleStart, angleEnd);
// Move cursor back to center
this._ctx.closePath();
this._ctx.fill();
}
/**
* Update a row in the breakdown table with the given values.
* @param {HTMLTableRowElement} row
* @param {{size:number,count:number} | null} stats Total size of the
* symbols of a given type in a container.
* @param {number} percentage How much the size represents in relation to
* the total size of the symbols in the container.
*/
_updateBreakdownRow(row, stats, percentage) {
if (stats == null || stats.size === 0) {
if (row.parentElement != null) {
this._tableBody.removeChild(row);
}
return;
}
const countColumn = row.querySelector('.count');
const sizeColumn = row.querySelector('.size');
const percentColumn = row.querySelector('.percent');
const countString = stats.count.toLocaleString(_LOCALE, {
useGrouping: true,
});
const sizeString = stats.size.toLocaleString(_LOCALE, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: true,
});
const percentString = percentage.toLocaleString(_LOCALE, {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
// Update DOM
countColumn.textContent = countString;
sizeColumn.textContent = sizeString;
percentColumn.textContent = percentString;
this._tableBody.appendChild(row);
}
/**
* Update DOM for the container infocard
* @param {TreeNode} containerNode
*/
_updateInfocard(containerNode) {
const extraRows = Object.assign({}, this._infoRows);
const statsEntries = Object.entries(containerNode.childStats).sort(
(a, b) => b[1].size - a[1].size
);
const diffMode = state.has('diff_mode');
const highlightMode = state.has('highlight');
let totalSize = 0;
for (const [, stats] of statsEntries) {
totalSize += Math.abs(stats.size);
}
// Update DOM
super._updateInfocard(containerNode);
let angleStart = 0;
for (const [type, stats] of statsEntries) {
delete extraRows[type];
const {color} = getIconStyle(type);
const percentage = stats.size / totalSize;
this._updateBreakdownRow(this._infoRows[type], stats, percentage);
const arcLength = Math.abs(percentage) * 2 * Math.PI;
if (arcLength > 0) {
const angleEnd = angleStart + arcLength;
this._drawSlice(angleStart, angleEnd, color);
if (highlightMode) {
const highlightPercent = stats.highlight / totalSize;
const highlightArcLength = Math.abs(highlightPercent) * 2 * Math.PI;
const highlightEnd = (angleStart + highlightArcLength);
this._drawBorder(angleStart, highlightEnd, '#feefc3', 32);
}
if (diffMode) {
const strokeColor = stats.size > 0 ? '#ea4335' : '#34a853';
this._drawBorder(angleStart, angleEnd, strokeColor, 16);
}
angleStart = angleEnd;
}
}
// Hide unused types
for (const row of Object.values(extraRows)) {
this._updateBreakdownRow(row, null, 0);
}
}
}
const _containerInfo = new ContainerInfocard('infocard-container');
const _symbolInfo = new SymbolInfocard('infocard-symbol');
/**
* Displays an infocard for the given symbol on the next frame.
* @param {TreeNode} node
*/
function displayInfocard(node) {
if (_CONTAINER_TYPE_SET.has(node.type[0])) {
_containerInfo.updateInfocard(node);
_containerInfo.setHidden(false);
_symbolInfo.setHidden(true);
} else {
_symbolInfo.updateInfocard(node);
_symbolInfo.setHidden(false);
_containerInfo.setHidden(true);
}
}
return displayInfocard;
})();