1// Copyright 2022 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js'; 6 7import {assert} from 'chrome://resources/js/assert.js'; 8import {addWebUiListener} from 'chrome://resources/js/cr.js'; 9import {CustomElement} from 'chrome://resources/js/custom_element.js'; 10 11import {getTemplate} from './app.html.js'; 12import type {KeyValue, Log, LogData, MetricsInternalsBrowserProxy} from './browser_proxy.js'; 13import {MetricsInternalsBrowserProxyImpl} from './browser_proxy.js'; 14import {getEventsPeekString, logEventToString, sizeToString, timestampToString, umaLogTypeToString} from './log_utils.js'; 15 16/** 17 * An empty log. It is appended to a logs table when there are no logs (for 18 * purely aesthetic reasons). 19 */ 20const EMPTY_LOG: Log = { 21 type: 'N/A', 22 hash: 'N/A', 23 timestamp: '', 24 size: -1, 25 events: [], 26}; 27 28export class MetricsInternalsAppElement extends CustomElement { 29 static get is(): string { 30 return 'metrics-internals-app'; 31 } 32 33 static override get template() { 34 return getTemplate(); 35 } 36 37 /** 38 * Resolves once the component has finished loading. 39 */ 40 initPromise: Promise<void>; 41 42 private browserProxy_: MetricsInternalsBrowserProxy = 43 MetricsInternalsBrowserProxyImpl.getInstance(); 44 45 /** 46 * Previous summary tables data. Used to prevent re-renderings of the tables 47 * when the data has not changed. 48 */ 49 private previousVariationsSummaryData_: string = ''; 50 private previousUmaSummaryData_: string = ''; 51 52 constructor() { 53 super(); 54 this.initPromise = this.init_(); 55 } 56 57 /** 58 * Returns UMA logs data (with their proto) as a JSON string. Used when 59 * exporting UMA logs data. Returns a promise. 60 */ 61 getUmaLogsExportContent(): Promise<string> { 62 return this.browserProxy_.getUmaLogData(/*includeLogProtoData*/ true); 63 } 64 65 private async init_(): Promise<void> { 66 // Fetch variations summary data and set up a recurring timer. 67 await this.updateVariationsSummary_(); 68 setInterval(() => this.updateVariationsSummary_(), 3000); 69 70 // Fetch UMA summary data and set up a recurring timer. 71 await this.updateUmaSummary_(); 72 setInterval(() => this.updateUmaSummary_(), 3000); 73 74 // Set up the UMA table caption. 75 const umaTableCaption = this.$('#uma-table-caption') as HTMLElement; 76 const isUsingMetricsServiceObserver = 77 await this.browserProxy_.isUsingMetricsServiceObserver(); 78 umaTableCaption.textContent = isUsingMetricsServiceObserver ? 79 'List of all UMA logs closed since browser startup.' : 80 'List of UMA logs closed since opening this page. Starting the browser \ 81 with the --export-uma-logs-to-file command line flag will instead show \ 82 all logs closed since browser startup.'; 83 84 // Set up a listener for UMA logs. Also update UMA log data immediately in 85 // case there are logs that we already have data on. 86 addWebUiListener( 87 'uma-log-created-or-event', () => this.updateUmaLogsData_()); 88 await this.updateUmaLogsData_(); 89 90 // Set up the UMA "Export logs" button. 91 const exportUmaLogsButton = this.$('#export-uma-logs') as HTMLElement; 92 exportUmaLogsButton.addEventListener('click', () => this.exportUmaLogs_()); 93 } 94 95 /** 96 * Callback function to expand/collapse an element on click. 97 * @param e The click event. 98 */ 99 private toggleEventsExpand_(e: MouseEvent): void { 100 let umaLogEventsDiv = e.target as HTMLElement; 101 102 // It is possible we have clicked a descendant. Keep checking the parent 103 // until we are the the root div of the events. 104 while (!umaLogEventsDiv.classList.contains('uma-log-events')) { 105 umaLogEventsDiv = umaLogEventsDiv.parentElement as HTMLElement; 106 } 107 umaLogEventsDiv.classList.toggle('uma-log-events-expanded'); 108 } 109 110 /** 111 * Fills the passed table element with the given summary. 112 */ 113 private updateSummaryTable_(tableBody: HTMLElement, summary: KeyValue[]): 114 void { 115 // Clear the table first. 116 tableBody.replaceChildren(); 117 118 const template = this.$('#summary-row-template') as HTMLTemplateElement; 119 for (const info of summary) { 120 const row = template.content.cloneNode(true) as HTMLElement; 121 const [key, value] = row.querySelectorAll('td'); 122 123 assert(key); 124 key.textContent = info.key; 125 126 assert(value); 127 value.textContent = info.value; 128 129 tableBody.appendChild(row); 130 } 131 } 132 133 /** 134 * Fetches variations summary data and updates the view. 135 */ 136 private async updateVariationsSummary_(): Promise<void> { 137 const summary: KeyValue[] = 138 await this.browserProxy_.fetchVariationsSummary(); 139 const variationsSummaryTableBody = 140 this.$('#variations-summary-body') as HTMLElement; 141 142 // Don't re-render the table if the data has not changed. 143 const newDataString = summary.toString(); 144 if (newDataString === this.previousVariationsSummaryData_) { 145 return; 146 } 147 148 this.previousVariationsSummaryData_ = newDataString; 149 this.updateSummaryTable_(variationsSummaryTableBody, summary); 150 } 151 152 /** 153 * Fetches UMA summary data and updates the view. 154 */ 155 private async updateUmaSummary_(): Promise<void> { 156 const summary: KeyValue[] = await this.browserProxy_.fetchUmaSummary(); 157 const umaSummaryTableBody = this.$('#uma-summary-body') as HTMLElement; 158 159 // Don't re-render the table if the data has not changed. 160 const newDataString = summary.toString(); 161 if (newDataString === this.previousUmaSummaryData_) { 162 return; 163 } 164 165 this.previousUmaSummaryData_ = newDataString; 166 this.updateSummaryTable_(umaSummaryTableBody, summary); 167 } 168 169 /** 170 * Fills the passed table element with the given logs. 171 */ 172 private updateLogsTable_(tableBody: HTMLElement, logs: Log[]): void { 173 // Clear the table first. 174 tableBody.replaceChildren(); 175 176 const template = this.$('#uma-log-row-template') as HTMLTemplateElement; 177 178 // Iterate through the logs in reverse order so that the most recent log 179 // shows up first. 180 for (const log of logs.slice(0).reverse()) { 181 const row = template.content.cloneNode(true) as HTMLElement; 182 const [type, hash, timestamp, size, events] = row.querySelectorAll('td'); 183 184 assert(type); 185 type.textContent = umaLogTypeToString(log.type); 186 187 assert(hash); 188 hash.textContent = log.hash; 189 190 assert(timestamp); 191 timestamp.textContent = timestampToString(log.timestamp); 192 193 assert(size); 194 size.textContent = sizeToString(log.size); 195 196 assert(events); 197 const eventsPeekDiv = 198 events.querySelector<HTMLElement>('.uma-log-events-peek'); 199 assert(eventsPeekDiv); 200 eventsPeekDiv.addEventListener('click', this.toggleEventsExpand_, false); 201 const eventsPeekText = 202 events.querySelector<HTMLElement>('.uma-log-events-peek-text'); 203 assert(eventsPeekText); 204 eventsPeekText.textContent = getEventsPeekString(log.events); 205 const eventsText = 206 events.querySelector<HTMLElement>('.uma-log-events-text'); 207 assert(eventsText); 208 // Iterate through the events in reverse order so that the most recent 209 // event shows up first. 210 for (const event of log.events.slice(0).reverse()) { 211 const div = document.createElement('div'); 212 div.textContent = logEventToString(event); 213 eventsText.appendChild(div); 214 } 215 216 tableBody.appendChild(row); 217 } 218 } 219 220 /** 221 * Fetches the latest UMA logs and renders them. This is called when the page 222 * is loaded and whenever there is a log that created or changed. 223 */ 224 private async updateUmaLogsData_(): Promise<void> { 225 const logsData: string = 226 await this.browserProxy_.getUmaLogData(/*includeLogProtoData=*/ false); 227 const logs: LogData = JSON.parse(logsData); 228 // If there are no logs, append an empty log. This is purely for aesthetic 229 // reasons. Otherwise, the table may look confusing. 230 if (!logs.logs.length) { 231 logs.logs = [EMPTY_LOG]; 232 } 233 234 // We don't compare the new data with the old data to prevent re-renderings 235 // because this should only be called when there is an actual change. 236 237 const umaLogsTableBody = this.$('#uma-logs-body') as HTMLElement; 238 this.updateLogsTable_(umaLogsTableBody, logs.logs); 239 } 240 241 /** 242 * Exports the accumulated UMA logs, including their proto data, as a JSON 243 * file. This will initiate a download. 244 */ 245 private async exportUmaLogs_(): Promise<void> { 246 const logsData: string = await this.getUmaLogsExportContent(); 247 const file = new Blob([logsData], {type: 'text/plain'}); 248 const a = document.createElement('a'); 249 a.href = URL.createObjectURL(file); 250 a.download = `uma_logs_${new Date().getTime()}.json`; 251 a.click(); 252 } 253} 254 255declare global { 256 interface HTMLElementTagNameMap { 257 'metrics-internals-app': MetricsInternalsAppElement; 258 } 259} 260 261customElements.define( 262 MetricsInternalsAppElement.is, MetricsInternalsAppElement); 263