mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2024-12-03 18:56:09 +03:00
377 lines
12 KiB
JavaScript
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;
|
|
})();
|