| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js'; |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {addWebUiListener} from 'chrome://resources/js/cr.js'; |
| import {CustomElement} from 'chrome://resources/js/custom_element.js'; |
| |
| import {getTemplate} from './app.html.js'; |
| import {KeyValue, Log, LogData, MetricsInternalsBrowserProxy, MetricsInternalsBrowserProxyImpl} from './browser_proxy.js'; |
| import {getEventsPeekString, logEventToString, sizeToString, timestampToString, umaLogTypeToString} from './log_utils.js'; |
| |
| /** |
| * An empty log. It is appended to a logs table when there are no logs (for |
| * purely aesthetic reasons). |
| */ |
| const EMPTY_LOG: Log = { |
| type: 'N/A', |
| hash: 'N/A', |
| timestamp: '', |
| size: -1, |
| events: [], |
| }; |
| |
| export class MetricsInternalsAppElement extends CustomElement { |
| static get is(): string { |
| return 'metrics-internals-app'; |
| } |
| |
| static override get template() { |
| return getTemplate(); |
| } |
| |
| /** |
| * Resolves once the component has finished loading. |
| */ |
| initPromise: Promise<void>; |
| |
| private browserProxy_: MetricsInternalsBrowserProxy = |
| MetricsInternalsBrowserProxyImpl.getInstance(); |
| |
| /** |
| * Previous summary tables data. Used to prevent re-renderings of the tables |
| * when the data has not changed. |
| */ |
| private previousVariationsSummaryData_: string = ''; |
| private previousUmaSummaryData_: string = ''; |
| |
| constructor() { |
| super(); |
| this.initPromise = this.init_(); |
| } |
| |
| /** |
| * Returns UMA logs data (with their proto) as a JSON string. Used when |
| * exporting UMA logs data. Returns a promise. |
| */ |
| getUmaLogsExportContent(): Promise<string> { |
| return this.browserProxy_.getUmaLogData(/*includeLogProtoData*/ true); |
| } |
| |
| private async init_(): Promise<void> { |
| // Fetch variations summary data and set up a recurring timer. |
| await this.updateVariationsSummary_(); |
| setInterval(() => this.updateVariationsSummary_(), 3000); |
| |
| // Fetch UMA summary data and set up a recurring timer. |
| await this.updateUmaSummary_(); |
| setInterval(() => this.updateUmaSummary_(), 3000); |
| |
| // Set up the UMA table caption. |
| const umaTableCaption = this.$('#uma-table-caption') as HTMLElement; |
| const isUsingMetricsServiceObserver = |
| await this.browserProxy_.isUsingMetricsServiceObserver(); |
| umaTableCaption.textContent = isUsingMetricsServiceObserver ? |
| 'List of all UMA logs closed since browser startup.' : |
| 'List of UMA logs closed since opening this page. Starting the browser \ |
| with the --export-uma-logs-to-file command line flag will instead show \ |
| all logs closed since browser startup.'; |
| |
| // Set up a listener for UMA logs. Also update UMA log data immediately in |
| // case there are logs that we already have data on. |
| addWebUiListener( |
| 'uma-log-created-or-event', () => this.updateUmaLogsData_()); |
| await this.updateUmaLogsData_(); |
| |
| // Set up the UMA "Export logs" button. |
| const exportUmaLogsButton = this.$('#export-uma-logs') as HTMLElement; |
| exportUmaLogsButton.addEventListener('click', () => this.exportUmaLogs_()); |
| } |
| |
| /** |
| * Callback function to expand/collapse an element on click. |
| * @param e The click event. |
| */ |
| private toggleEventsExpand_(e: MouseEvent): void { |
| let umaLogEventsDiv = e.target as HTMLElement; |
| |
| // It is possible we have clicked a descendant. Keep checking the parent |
| // until we are the the root div of the events. |
| while (!umaLogEventsDiv.classList.contains('uma-log-events')) { |
| umaLogEventsDiv = umaLogEventsDiv.parentElement as HTMLElement; |
| } |
| umaLogEventsDiv.classList.toggle('uma-log-events-expanded'); |
| } |
| |
| /** |
| * Fills the passed table element with the given summary. |
| */ |
| private updateSummaryTable_(tableBody: HTMLElement, summary: KeyValue[]): |
| void { |
| // Clear the table first. |
| tableBody.replaceChildren(); |
| |
| const template = this.$('#summary-row-template') as HTMLTemplateElement; |
| for (const info of summary) { |
| const row = template.content.cloneNode(true) as HTMLElement; |
| const [key, value] = row.querySelectorAll('td'); |
| |
| assert(key); |
| key.textContent = info.key; |
| |
| assert(value); |
| value.textContent = info.value; |
| |
| tableBody.appendChild(row); |
| } |
| } |
| |
| /** |
| * Fetches variations summary data and updates the view. |
| */ |
| private async updateVariationsSummary_(): Promise<void> { |
| const summary: KeyValue[] = |
| await this.browserProxy_.fetchVariationsSummary(); |
| const variationsSummaryTableBody = |
| this.$('#variations-summary-body') as HTMLElement; |
| |
| // Don't re-render the table if the data has not changed. |
| const newDataString = summary.toString(); |
| if (newDataString === this.previousVariationsSummaryData_) { |
| return; |
| } |
| |
| this.previousVariationsSummaryData_ = newDataString; |
| this.updateSummaryTable_(variationsSummaryTableBody, summary); |
| } |
| |
| /** |
| * Fetches UMA summary data and updates the view. |
| */ |
| private async updateUmaSummary_(): Promise<void> { |
| const summary: KeyValue[] = await this.browserProxy_.fetchUmaSummary(); |
| const umaSummaryTableBody = this.$('#uma-summary-body') as HTMLElement; |
| |
| // Don't re-render the table if the data has not changed. |
| const newDataString = summary.toString(); |
| if (newDataString === this.previousUmaSummaryData_) { |
| return; |
| } |
| |
| this.previousUmaSummaryData_ = newDataString; |
| this.updateSummaryTable_(umaSummaryTableBody, summary); |
| } |
| |
| /** |
| * Fills the passed table element with the given logs. |
| */ |
| private updateLogsTable_(tableBody: HTMLElement, logs: Log[]): void { |
| // Clear the table first. |
| tableBody.replaceChildren(); |
| |
| const template = this.$('#uma-log-row-template') as HTMLTemplateElement; |
| |
| // Iterate through the logs in reverse order so that the most recent log |
| // shows up first. |
| for (const log of logs.slice(0).reverse()) { |
| const row = template.content.cloneNode(true) as HTMLElement; |
| const [type, hash, timestamp, size, events] = row.querySelectorAll('td'); |
| |
| assert(type); |
| type.textContent = umaLogTypeToString(log.type); |
| |
| assert(hash); |
| hash.textContent = log.hash; |
| |
| assert(timestamp); |
| timestamp.textContent = timestampToString(log.timestamp); |
| |
| assert(size); |
| size.textContent = sizeToString(log.size); |
| |
| assert(events); |
| const eventsPeekDiv = |
| events.querySelector<HTMLElement>('.uma-log-events-peek'); |
| assert(eventsPeekDiv); |
| eventsPeekDiv.addEventListener('click', this.toggleEventsExpand_, false); |
| const eventsPeekText = |
| events.querySelector<HTMLElement>('.uma-log-events-peek-text'); |
| assert(eventsPeekText); |
| eventsPeekText.textContent = getEventsPeekString(log.events); |
| const eventsText = |
| events.querySelector<HTMLElement>('.uma-log-events-text'); |
| assert(eventsText); |
| // Iterate through the events in reverse order so that the most recent |
| // event shows up first. |
| for (const event of log.events.slice(0).reverse()) { |
| const div = document.createElement('div'); |
| div.textContent = logEventToString(event); |
| eventsText.appendChild(div); |
| } |
| |
| tableBody.appendChild(row); |
| } |
| } |
| |
| /** |
| * Fetches the latest UMA logs and renders them. This is called when the page |
| * is loaded and whenever there is a log that created or changed. |
| */ |
| private async updateUmaLogsData_(): Promise<void> { |
| const logsData: string = |
| await this.browserProxy_.getUmaLogData(/*includeLogProtoData=*/ false); |
| const logs: LogData = JSON.parse(logsData); |
| // If there are no logs, append an empty log. This is purely for aesthetic |
| // reasons. Otherwise, the table may look confusing. |
| if (!logs.logs.length) { |
| logs.logs = [EMPTY_LOG]; |
| } |
| |
| // We don't compare the new data with the old data to prevent re-renderings |
| // because this should only be called when there is an actual change. |
| |
| const umaLogsTableBody = this.$('#uma-logs-body') as HTMLElement; |
| this.updateLogsTable_(umaLogsTableBody, logs.logs); |
| } |
| |
| /** |
| * Exports the accumulated UMA logs, including their proto data, as a JSON |
| * file. This will initiate a download. |
| */ |
| private async exportUmaLogs_(): Promise<void> { |
| const logsData: string = await this.getUmaLogsExportContent(); |
| const file = new Blob([logsData], {type: 'text/plain'}); |
| const a = document.createElement('a'); |
| a.href = URL.createObjectURL(file); |
| a.download = `uma_logs_${new Date().getTime()}.json`; |
| a.click(); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'metrics-internals-app': MetricsInternalsAppElement; |
| } |
| } |
| |
| customElements.define( |
| MetricsInternalsAppElement.is, MetricsInternalsAppElement); |