Skip to content

Commit

Permalink
[SuperSize] Viewer: Reactor UI state code.
Browse files Browse the repository at this point in the history
SuperSize Viewer UI states are assigned using query params or UI
elements, and has complex interaction, e.g.:
* Each state has a default value. These are specified in the HTML and
  are represented by absence in query params.
* Some elements (e.g., Method Count Mode) can disable others (e.g.,
  Symbol Types to Show).
* Values need to be serialized when passed to tree-worker-wasm.js.
* Validation is needed.
* Some states (e.g., Symbol Types to Show) are represented as a short
  string in query param.

Some issues:
* |state| provides centralized access of UI states, but sometimes direct
  access to query params or controls are spotted.
* Query param is passed as serialized UI states to tree-worker-wasm.js,
  requiring redundant parsing and default assignment.
* |state|'s usage of URLSearchParam as main storage is awkward, and
  leads to some churn (e.g., needing to merge Symbol Types to Show
  &type= values).
* There are scattered and repeated code to access UI elements.

This CL refactors the above. Key changes:
* Use smart UI variables to manage:
  * Value read / write.
  * Default values (read from UI elements).
  * Read query param to assign initial value and update UI elements.
  * Write to query param.
  * Sync from UI elements on change (triggered centrally by #frm-options
    change event).
* Centralize UI element access.
* Pass exported UI states as object to tree-worker-wasm.js.
* Clean up HTML to improve consistency; fix validation errors.

New file:
* dom.js: For |dom| utility and MainElement with |g_el|.

New classes:
* UiState -> QueryParamUiState -> ElementUiState for smart UI variables.
* MainState for singleton |state| as central storage of UI variables,
  along with "diff mode" get / set.
* MainElement for singleton |g_el| to centralize DOM access.

Widescale changes:
* Rename element ids to include type prefix, with matching CSS changes.
* Centralize document.{querySelector{All},getElementById}() to dom.js.
  * Direct access to DOM elements goes through |g_el| variables.
  * Accessing sub-elements via |elt.querySelector*()| is allowed.

Additional fixes:
* ArtifactInfocard._updateInfocard(): Array out of bound if no diff.
* _makeIconTemplateGetter: |symbolIcons|: Noted that .generatedicon
  does not exist, so just stub with null (for key "*").
* Disassembly "Close" button missing style from redundant class.

Bug: 881319, 1186921
Change-Id: Icf0ec13bacbd2546fea5ca82f61ccb29ab631a6d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4232264
Reviewed-by: Andrew Grieve <agrieve@chromium.org>
Commit-Queue: Samuel Huang <huangs@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1103864}
  • Loading branch information
samuelhuang authored and Chromium LUCI CQ committed Feb 10, 2023
1 parent 99008c5 commit cbb9fd0
Show file tree
Hide file tree
Showing 12 changed files with 949 additions and 611 deletions.
7 changes: 3 additions & 4 deletions tools/binary_size/libsupersize/viewer/static/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ async function doAuthFetch() {

// Show a "Please sign in" dialog.
toggleSigninModal(true /*show*/);
let signinButton = document.querySelector('#signin-modal button.signin');
signinButton.addEventListener('click', getToken);
const btnSignin = g_el.divSigninModal.querySelector('button.signin');
btnSignin.addEventListener('click', getToken);

// Only hide the dialog once we have the token.
await g_authTokenPromise;
Expand All @@ -73,8 +73,7 @@ async function fetchAccessToken() {

/** @param {boolean} show */
function toggleSigninModal(show) {
const modal = document.getElementById('signin-modal');
modal.style.display = show ? '': 'none';
g_el.divSigninModal.style.display = show ? '' : 'none';
}

/**
Expand Down
206 changes: 206 additions & 0 deletions tools/binary_size/libsupersize/viewer/static/dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/** Utilities for working with the DOM */
const dom = {
/**
* Creates a document fragment from the given nodes.
* @param {Iterable<Node>} nodes
* @return {DocumentFragment}
*/
createFragment(nodes) {
const fragment = document.createDocumentFragment();
for (const node of nodes)
fragment.appendChild(node);
return fragment;
},
/**
* Removes all the existing children of `parent` and inserts `newChild` in
* their place.
* @param {!Element} parent
* @param {Node | null} newChild
*/
replace(parent, newChild) {
parent.innerHTML = '';
if (newChild)
parent.appendChild(newChild);
},
/**
* Builds a text element in a single statement.
* @param {string} tagName Type of the element, such as "span".
* @param {string} text Text content for the element.
* @param {string} [className] Class to apply to the element.
*/
textElement(tagName, text, className) {
const element = document.createElement(tagName);
element.textContent = text;
if (className)
element.className = className;
return element;
},
};

/** Centralized object for element access. */
class MainElements {
constructor() {
/** @public {!NodeList} Elements that toggle body.show-options on click. */
this.nlShowOptions =
/** @type {!NodeList} */ (document.querySelectorAll('.toggle-options'));

/** @public {!HTMLDivElement} */
this.divReviewInfo =
/** @type {!HTMLDivElement} */ (this.query('#div-review-info'));

/** @type {!HTMLAnchorElement} */
this.linkReviewText =
/** @type {!HTMLAnchorElement} */ (this.query('#link-review-text'));

/** @type {!HTMLAnchorElement} */
this.linkDownloadBefore =
/** @type {!HTMLAnchorElement} */ (this.query('#link-download-before'));

/** @type {!HTMLAnchorElement} */
this.linkDownloadLoad =
/** @type {!HTMLAnchorElement} */ (this.query('#link-download-load'));

/** @type {!HTMLInputElement} */
this.fileUpload =
/** @type {!HTMLInputElement} */ (this.query('#file-upload'));

/** @type {!HTMLAnchorElement} */
this.linkFaq =
/** @type {!HTMLAnchorElement} */ (this.query('#link-faq'));

/** @type {!HTMLProgressElement} */
this.progAppbar =
/** @type {!HTMLProgressElement} */ (this.query('#prog-appbar'));

/** @public {!HTMLFormElement} Form with options and filters. */
this.frmOptions =
/** @type {!HTMLFormElement} */ (this.query('#frm-options'));

/** @public {!HTMLInputElement} */
this.cbMethodCount =
/** @type {!HTMLInputElement} */ (this.query('#cb-method-count'));

/** @public {!HTMLSelectElement} */
this.selByteUnit =
/** @type {!HTMLSelectElement} */ (this.query('#sel-byte-unit'));

/** @public {!HTMLInputElement} */
this.nbMinSize =
/** @type {!HTMLInputElement} */ (this.query('#nb-min-size'));

/** @public {!RadioNodeList} */
this.rnlGroupBy = /** @type {!RadioNodeList} */ (
this.frmOptions.elements.namedItem(STATE_KEY.GROUP_BY));
assert(this.rnlGroupBy.length > 0);

/** @public {!HTMLInputElement} */
this.tbIncludeRegex =
/** @type {!HTMLInputElement} */ (this.query('#tb-include-regex'));

/** @public {!HTMLInputElement} */
this.tbExcludeRegex =
/** @type {!HTMLInputElement} */ (this.query('#tb-exclude-regex'));

/** @public {!RadioNodeList} */
this.rnlType = /** @type {!RadioNodeList} */ (
this.frmOptions.elements.namedItem(STATE_KEY.TYPE));
assert(this.rnlType.length > 0);

/** @type {!HTMLFieldSetElement} */
this.fsTypesFilter =
/** @type {!HTMLFieldSetElement} */ (this.query('#fs-types-filter'));

/** @type {!HTMLButtonElement} */
this.btnTypeAll =
/** @type {!HTMLButtonElement} */ (this.query('#btn-type-all'));

/** @type {!HTMLButtonElement} */
this.btnTypeNone =
/** @type {!HTMLButtonElement} */ (this.query('#btn-type-none'));

/** @public {!RadioNodeList} */
this.rnlFlagFilter = /** @type {!RadioNodeList} */ (
this.frmOptions.elements.namedItem(STATE_KEY.FLAG_FILTER));
assert(this.rnlFlagFilter.length > 0);

/** @public {!HTMLDivElement} */
this.divIcons =
/** @type {!HTMLDivElement} */ (this.query('#div-icons'));

/** @public {!HTMLDivElement} */
this.divDiffStatusIcons =
/** @type {!HTMLDivElement} */ (this.query('#div-diff-status-icons'));

/** @type {!HTMLTemplateElement} Template for groups in the tree. */
this.tmplSymbolTreeGroup = /** @type {!HTMLTemplateElement} */ (
this.query('#tmpl-symbol-tree-group'));

/** @type {!HTMLTemplateElement} Template for leaves in the tree. */
this.tmplSymbolTreeLeaf = /** @type {!HTMLTemplateElement} */ (
this.query('#tmpl-symbol-tree-leaf'));

/** @type {!HTMLSpanElement} */
this.spanSizeHeader =
/** @type {!HTMLSpanElement} */ (this.query('#span-size-header'));

/** @type {!HTMLUListElement} */
this.ulSymbolTree =
/** @type {!HTMLUListElement} */ (this.query('#ul-symbol-tree'));

/** @public {!HTMLDivElement} */
this.divNoSymbolsMsg =
/** @type {!HTMLDivElement} */ (this.query('#div-no-symbols-msg'));

/** @public {!HTMLDivElement} */
this.divMetadataView =
/** @type {!HTMLDivElement} */ (this.query('#div-metadata-view'));

/** @public {!HTMLPreElement} */
this.preMetadataContent =
/** @type {!HTMLPreElement} */ (this.query('#pre-metadata-content'));

/** @public {!HTMLDivElement} */
this.divInfocardArtifact =
/** @type {!HTMLDivElement} */ (this.query('#div-infocard-artifact'));

/** @public {!HTMLDivElement} */
this.divInfocardSymbol =
/** @type {!HTMLDivElement} */ (this.query('#div-infocard-symbol'));

/** @public {!HTMLDivElement} */
this.divSigninModal =
/** @type {!HTMLDivElement} */ (this.query('#div-signin-modal'));

/** @public {!HTMLDivElement} */
this.divDisassemblyModal =
/** @type {!HTMLDivElement} */ (this.query('#div-disassembly-modal'));
}

/**
* @param {string} q Query string.
* @return {!Element}
* @private
*/
query(q) {
return /** @type {!Element} */ (assertNotNull(document.querySelector(q)));
}

/**
* @param {!Element} elt
* @return {!Element}
* @public
*/
getAriaDescribedBy(elt) {
const id = assertNotNull(elt.getAttribute('aria-describedby'));
return assertNotNull(document.getElementById(id));
}
}

/** @const {!MainElements} */
const g_el = new MainElements();
45 changes: 23 additions & 22 deletions tools/binary_size/libsupersize/viewer/static/infocard-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ const displayInfocard = (() => {
]);

class Infocard {
/**
* @param {string} id
*/
constructor(id) {
this._infocard = document.getElementById(id);
/** @param {!Element} infocardElt */
constructor(infocardElt) {
this._infocard = infocardElt;
/** @type {HTMLSpanElement} */
this._sizeInfo = this._infocard.querySelector('.size-info');
/** @type {HTMLSpanElement} */
Expand Down Expand Up @@ -60,25 +58,26 @@ const displayInfocard = (() => {
* @param {string} disassembly
*/
_showDisassemblyOverlay(disassembly) {
const eltModal = document.getElementById('disassembly-modal');
const eltCode = document.getElementById('disassembly-code');
const eltDownload = /** @type {!HTMLAnchorElement} */ (
document.getElementById('disassembly-download'));
const eltClose = document.getElementById('disassembly-close');
const divModal = g_el.divDisassemblyModal;
const divCode = divModal.querySelector('.div-code');
const linkDownload = /** @type {!HTMLAnchorElement} */ (
divModal.querySelector('.link-download'));
const btnClose = /** @type {!HTMLButtonElement} */ (
divModal.querySelector('.btn-close'));
const diffHtml = Diff2Html.html(disassembly, {
drawFileList: false,
matching: 'lines',
outputFormat: 'side-by-side',
});
eltCode.innerHTML = diffHtml;
eltModal.style.display = '';
divCode.innerHTML = diffHtml;
divModal.style.display = '';
const blob = new Blob([disassembly], {type: 'text/plain'});
const objectUrl = URL.createObjectURL(blob);
eltDownload.href = objectUrl;
eltClose.onclick = function() {
linkDownload.href = objectUrl;
btnClose.onclick = () => {
URL.revokeObjectURL(objectUrl);
eltModal.style.display = 'none';
}
divModal.style.display = 'none';
};
}

/**
Expand Down Expand Up @@ -261,8 +260,9 @@ const displayInfocard = (() => {
}

class ArtifactInfocard extends Infocard {
constructor(id) {
super(id);
/** @param {!Element} infocardElt */
constructor(infocardElt) {
super(infocardElt);
this._tableBody = this._infocard.querySelector('tbody');
this._tableHeader = this._infocard.querySelector('thead');
this._ctx = this._infocard.querySelector('canvas').getContext('2d');
Expand Down Expand Up @@ -304,7 +304,7 @@ const displayInfocard = (() => {
_getTypeDescription(node, icon) {
const depth = node.idPath.replace(/[^/]/g, '').length;
if (depth === 0) {
const t = state.get('group_by');
const t = /** @type {string} */ (state.stGroupBy.get());
if (t) {
// Format, e.g., "generated_type" to "Generated type".
return (t[0].toUpperCase() + t.slice(1)).replace(/_/g, ' ');
Expand Down Expand Up @@ -437,7 +437,8 @@ const displayInfocard = (() => {
// so displaying count isn't useful.
// In non-diff view, we don't have added/removed/changed information, so
// we just display a count.
if (diffMode && statsEntries[0][1].added !== undefined) {
if (diffMode && statsEntries.length > 0 &&
statsEntries[0][1].added !== undefined) {
addedColumn.removeAttribute('hidden');
removedColumn.removeAttribute('hidden');
changedColumn.removeAttribute('hidden');
Expand Down Expand Up @@ -478,8 +479,8 @@ const displayInfocard = (() => {
}
}

const _artifactInfo = new ArtifactInfocard('infocard-artifact');
const _symbolInfo = new SymbolInfocard('infocard-symbol');
const _artifactInfo = new ArtifactInfocard(g_el.divInfocardArtifact);
const _symbolInfo = new SymbolInfocard(g_el.divInfocardSymbol);

/**
* Displays an infocard for the given symbol on the next frame.
Expand Down
4 changes: 2 additions & 2 deletions tools/binary_size/libsupersize/viewer/static/infocard.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
display: grid;
padding: 16px;
}
.infocard-artifact {
#div-infocard-artifact {
grid-template-areas: 'header icon' 'type type';
grid-template-columns: 1fr 80px;
grid-column-gap: 16px;
grid-row-gap: 8px;
}
.infocard-symbol {
#div-infocard-symbol {
grid-template-areas: 'icon header' 'type type';
grid-template-columns: 40px auto;
grid-column-gap: 16px;
Expand Down

0 comments on commit cbb9fd0

Please sign in to comment.