// 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'), v: this._tableBody.querySelector('.vtable-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; })();