xref: /aosp_15_r20/external/cronet/components/metrics/debug/app.ts (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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