| // 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 {!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); |
| 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); |
| })(); |