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