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