chromium/tools/binary_size/libsupersize/viewer/static/ui-main.js

// 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';

/**
 * @fileoverview
 * UI glue code and instances (including singletons).
 */

(function() {
  /** @type {ProgressBar} */
  const _progress = new ProgressBar(g_el.progAppbar);

  /** @type {!SymbolTreeUi} */
  const _symbolTreeUi = new SymbolTreeUi();
  _symbolTreeUi.init();

  /** @type {!MetricsTreeModel} */
  const _metricsTreeModel = new MetricsTreeModel();

  /** @type {!MetricsTreeUi} */
  const _metricsTreeUi = new MetricsTreeUi(_metricsTreeModel);
  _metricsTreeUi.init();

  /** @type {!MetadataTreeModel} */
  const _metadataTreeModel = new MetadataTreeModel();

  /** @type {!MetadataTreeUi} */
  const _metadataTreeUi = new MetadataTreeUi(_metadataTreeModel);
  _metadataTreeUi.init();

  /** @param {TreeProgress} message */
  function onProgressMessage(message) {
    const {error, percent} = message;
    _progress.setValue(percent);
    document.body.classList.toggle('error', Boolean(error));
  }

  /**
   * Processes response of an initial load / upload.
   * @param {BuildTreeResults} message
   */
  function processLoadTreeResponse(message) {
    const {diffMode} = message;
    const {beforeBlobUrl, loadBlobUrl, isMultiContainer, metadata} =
        message.loadResults;
    console.log(
        '%cPro Tip: %cawait supersize.worker.openNode("$FILE_PATH")',
        'font-weight:bold;color:red;', '')

    window.supersize.metadata = metadata;
    for (const key of ['size_file', 'before_size_file']) {
      if (metadata.hasOwnProperty(key))
        preprocessSizeFileInPlace(metadata[key]);
    }

    displayOrHideDownloadButton(beforeBlobUrl, loadBlobUrl);

    state.setDiffMode(diffMode);
    document.body.classList.toggle('diff', Boolean(diffMode));

    processBuildTreeResponse(message);
    renderAndShowMetricsTree(metadata);
    renderAndShowMetadataTree(metadata);
    setReviewInfo(metadata);
  }

  /**
   * Sets the review URL and title from message to the HTML element.
   * @param {MetadataType} metadata
   */
  function setReviewInfo(metadata) {
    const processReviewInfo = (field) => {
      const urlExists = Boolean(
          field?.hasOwnProperty('url') && field?.hasOwnProperty('title'));
      if (urlExists) {
        g_el.linkReviewText.href = field['url'];
        g_el.linkReviewText.textContent = field['title'];
      }
      g_el.divReviewInfo.style.display = urlExists ? '' : 'none';
    };
    const sizeFile = metadata['size_file'];
    if (sizeFile?.hasOwnProperty('build_config')) {
      processReviewInfo(sizeFile['build_config'])
    }
  }

  /**
   * Processes the result of a buildTree() message.
   * @param {BuildTreeResults} message
   */
  function processBuildTreeResponse(message) {
    const {root} = message;
    _progress.setValue(1);

    const noSymbols = (Object.keys(root.childStats).length === 0);
    _symbolTreeUi.toggleNoSymbolsMessage(noSymbols);

    /** @type {?DocumentFragment} */
    let rootElement = null;
    if (root) {
      rootElement = _symbolTreeUi.makeNodeElement(root);
      /** @type {!HTMLAnchorElement} */
      const link = rootElement.querySelector('.node');
      // Expand the root UI node.
      link.click();
      link.tabIndex = 0;
    }

    // Double requestAnimationFrame ensures that the code inside executes in a
    // different frame than the above tree element creation.
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        dom.replace(g_el.ulSymbolTree, rootElement);
      });
    });
  }

  /**
   * Displays/hides download buttons for loadUrl.size and beforeUrl.size.
   * @param {?string=} beforeUrl
   * @param {?string=} loadUrl
   */
  function displayOrHideDownloadButton(beforeUrl = null, loadUrl = null) {
    const updateAnchor = (anchor, url) => {
      anchor.style.display = url ? '' : 'none';
      if (anchor.href && anchor.href.startsWith('blob:')) {
        URL.revokeObjectURL(anchor.href);
      }
      anchor.href = url;
    };
    updateAnchor(g_el.linkDownloadBefore, beforeUrl);
    updateAnchor(g_el.linkDownloadLoad, loadUrl);

    if (/** @type {string} */ (state.stLoadUrl.get()).includes('.sizediff')) {
      g_el.linkDownloadLoad.title = 'Download .sizediff file';
      g_el.linkDownloadLoad.download = 'load_size.sizediff';
    }
  }

  /**
   * Extracts metrics data and renders the Metrics Tree.
   * @param {?Object} metadata Used to compute Metrics Tree data. If null, reuse
   *     cached data. Otherwise new data is saved to cache.
   */
  function renderAndShowMetricsTree(metadata) {
    _metricsTreeModel.updateFilter();
    _metricsTreeModel.extractAndStoreRoot(metadata);

    /** @type {?DocumentFragment} */
    let rootElement = null;
    if (_metricsTreeModel.rootNode) {
      rootElement = _metricsTreeUi.makeNodeElement(_metricsTreeModel.rootNode);
      /** @type {!HTMLAnchorElement} */
      const link = rootElement.querySelector('.node');
      // Expand the root UI node.
      link.click();
      link.tabIndex = 0;
    }

    dom.replace(g_el.ulMetricsTree, rootElement);
    g_el.divMetricsView.classList.toggle('active', true);
  }

  /**
   * Modifies per-container metadata in-place so they render better.
   * @param {Object} subMetadata
   */
  function formatMetadataInPlace(subMetadata) {
    if (subMetadata?.hasOwnProperty('elf_mtime')) {
      const date = new Date(subMetadata['elf_mtime'] * 1000);
      subMetadata['elf_mtime'] = date.toString();
    }
  }

  /**
   * Modifies size_file / before_size_file in-place so they render better.
   * @param {MetadataType} sizeFile
   */
  function preprocessSizeFileInPlace(sizeFile) {
    const processContainer = (container) => {
      if (container?.hasOwnProperty('metadata')) {
        formatMetadataInPlace(container['metadata']);
      }
      // Strip section_sizes because it is already shown in tree.
      if (container?.hasOwnProperty('section_sizes')) {
        delete container['section_sizes'];
      }
    };
    if (sizeFile?.hasOwnProperty('containers')) {
      for (const container of sizeFile['containers']) {
        processContainer(container);
      }
    } else {
      // Covers the case if the metadata is in old schema.
      processContainer(sizeFile);
    }
  }

  /**
   * Renders the Metadata Tree.
   * @param {?Object} metadata
   */
  function renderAndShowMetadataTree(metadata) {
    _metadataTreeModel.extractAndStoreRoot(metadata);
    /** @type {?DocumentFragment} */
    let rootElement = null;
    if (_metricsTreeModel.rootNode) {
      rootElement =
          _metadataTreeUi.makeNodeElement(_metadataTreeModel.rootNode);
    }

    dom.replace(g_el.ulMetadataTree, rootElement);
    g_el.divMetadataView.classList.toggle('active', true);
  }

  /**
   * @param {!TreeWorker} worker
   * @return {!Promise}
   */
  async function planSymbolTreeFocusPathExpansionIfRequired(worker) {
    const focusStr = state.stFocus.get();
    if (focusStr) {
      const focus = parseInt(focusStr, 10);
      if (!isNaN(focus)) {
        const ancestryResults = await worker.queryAncestryById(focus);
        if (ancestryResults.ancestorIds?.length) {
          _symbolTreeUi.planPathExpansion(ancestryResults.ancestorIds);
          _symbolTreeUi.focus();
          return;
        }
      }
      state.stFocus.set('');  // Clear invalid value.
    }
  }

  /** @param {!Array<!URL>} urlsToLoad */
  async function performInitialLoad(urlsToLoad) {
    let accessToken = null;
    _progress.setValue(0.1);
    if (requiresAuthentication(urlsToLoad)) {
      accessToken = await fetchAccessToken();
      _progress.setValue(0.2);
    }
    const worker = restartWorker(onProgressMessage);
    _progress.setValue(0.3);
    const message = await worker.loadAndBuildTree('from-url://', accessToken);
    await planSymbolTreeFocusPathExpansionIfRequired(worker);
    processLoadTreeResponse(message);
  }

  async function rebuildTree() {
    _progress.setValue(0);
    const message = await window.supersize.worker.buildTree();
    processBuildTreeResponse(message);
    renderAndShowMetricsTree(null);
  }

  g_el.fileUpload.addEventListener('change', async (event) => {
    _progress.setValue(0.1);
    const input = /** @type {HTMLInputElement} */ (event.currentTarget);
    const file = input.files.item(0);
    const fileUrl = URL.createObjectURL(file);

    state.stLoadUrl.set(fileUrl);

    const worker = restartWorker(onProgressMessage);
    _progress.setValue(0.3);
    const message = await worker.loadAndBuildTree(fileUrl);
    URL.revokeObjectURL(fileUrl);
    processLoadTreeResponse(message);
    // Clean up afterwards so new files trigger event.
    input.value = '';
  });

  g_el.frmOptions.addEventListener('change', (event) => {
    // Update the tree when options change.
    // Some options update the tree themselves, don't regenerate when those
    // options (marked by "data-dynamic") are changed.
    if (!/** @type {HTMLElement} */ (event.target)
             .dataset.hasOwnProperty('dynamic')) {
      rebuildTree();
    }
  });
  g_el.frmOptions.addEventListener('submit', (event) => {
    event.preventDefault();
    rebuildTree();
  });

  const urlsToLoad = [];
  for (const url of [state.stBeforeUrl.get(), state.stLoadUrl.get()]) {
    if (url)
      urlsToLoad.push(new URL(/** @type {string} */ (url), document.baseURI));
  }
  if (urlsToLoad.length > 0)
    performInitialLoad(urlsToLoad);

  // By default, updating the hash portion of the URL via UI does not cause page
  // refresh. We can intercept this for interesting navigation feature, but for
  // now just refresh the page to be consistent with other changes to the URL.
  window.addEventListener('hashchange', (event) => {
    window.location.reload();
  });
})();