• 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';
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