blob: c6e03c137340537cd4af3a56f681273a8725ddd5 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js';
import {assert} from 'chrome://resources/js/assert.js';
import {addWebUiListener} from 'chrome://resources/js/cr.js';
import {CustomElement} from 'chrome://resources/js/custom_element.js';
import {getTemplate} from './app.html.js';
import {KeyValue, Log, LogData, MetricsInternalsBrowserProxy, MetricsInternalsBrowserProxyImpl} from './browser_proxy.js';
import {getEventsPeekString, logEventToString, sizeToString, timestampToString, umaLogTypeToString} from './log_utils.js';
/**
* An empty log. It is appended to a logs table when there are no logs (for
* purely aesthetic reasons).
*/
const EMPTY_LOG: Log = {
type: 'N/A',
hash: 'N/A',
timestamp: '',
size: -1,
events: [],
};
export class MetricsInternalsAppElement extends CustomElement {
static get is(): string {
return 'metrics-internals-app';
}
static override get template() {
return getTemplate();
}
/**
* Resolves once the component has finished loading.
*/
initPromise: Promise<void>;
private browserProxy_: MetricsInternalsBrowserProxy =
MetricsInternalsBrowserProxyImpl.getInstance();
/**
* Previous summary tables data. Used to prevent re-renderings of the tables
* when the data has not changed.
*/
private previousVariationsSummaryData_: string = '';
private previousUmaSummaryData_: string = '';
constructor() {
super();
this.initPromise = this.init_();
}
/**
* Returns UMA logs data (with their proto) as a JSON string. Used when
* exporting UMA logs data. Returns a promise.
*/
getUmaLogsExportContent(): Promise<string> {
return this.browserProxy_.getUmaLogData(/*includeLogProtoData*/ true);
}
private async init_(): Promise<void> {
// Fetch variations summary data and set up a recurring timer.
await this.updateVariationsSummary_();
setInterval(() => this.updateVariationsSummary_(), 3000);
// Fetch UMA summary data and set up a recurring timer.
await this.updateUmaSummary_();
setInterval(() => this.updateUmaSummary_(), 3000);
// Set up the UMA table caption.
const umaTableCaption = this.$('#uma-table-caption') as HTMLElement;
const isUsingMetricsServiceObserver =
await this.browserProxy_.isUsingMetricsServiceObserver();
umaTableCaption.textContent = isUsingMetricsServiceObserver ?
'List of all UMA logs closed since browser startup.' :
'List of UMA logs closed since opening this page. Starting the browser \
with the --export-uma-logs-to-file command line flag will instead show \
all logs closed since browser startup.';
// Set up a listener for UMA logs. Also update UMA log data immediately in
// case there are logs that we already have data on.
addWebUiListener(
'uma-log-created-or-event', () => this.updateUmaLogsData_());
await this.updateUmaLogsData_();
// Set up the UMA "Export logs" button.
const exportUmaLogsButton = this.$('#export-uma-logs') as HTMLElement;
exportUmaLogsButton.addEventListener('click', () => this.exportUmaLogs_());
}
/**
* Callback function to expand/collapse an element on click.
* @param e The click event.
*/
private toggleEventsExpand_(e: MouseEvent): void {
let umaLogEventsDiv = e.target as HTMLElement;
// It is possible we have clicked a descendant. Keep checking the parent
// until we are the the root div of the events.
while (!umaLogEventsDiv.classList.contains('uma-log-events')) {
umaLogEventsDiv = umaLogEventsDiv.parentElement as HTMLElement;
}
umaLogEventsDiv.classList.toggle('uma-log-events-expanded');
}
/**
* Fills the passed table element with the given summary.
*/
private updateSummaryTable_(tableBody: HTMLElement, summary: KeyValue[]):
void {
// Clear the table first.
tableBody.replaceChildren();
const template = this.$('#summary-row-template') as HTMLTemplateElement;
for (const info of summary) {
const row = template.content.cloneNode(true) as HTMLElement;
const [key, value] = row.querySelectorAll('td');
assert(key);
key.textContent = info.key;
assert(value);
value.textContent = info.value;
tableBody.appendChild(row);
}
}
/**
* Fetches variations summary data and updates the view.
*/
private async updateVariationsSummary_(): Promise<void> {
const summary: KeyValue[] =
await this.browserProxy_.fetchVariationsSummary();
const variationsSummaryTableBody =
this.$('#variations-summary-body') as HTMLElement;
// Don't re-render the table if the data has not changed.
const newDataString = summary.toString();
if (newDataString === this.previousVariationsSummaryData_) {
return;
}
this.previousVariationsSummaryData_ = newDataString;
this.updateSummaryTable_(variationsSummaryTableBody, summary);
}
/**
* Fetches UMA summary data and updates the view.
*/
private async updateUmaSummary_(): Promise<void> {
const summary: KeyValue[] = await this.browserProxy_.fetchUmaSummary();
const umaSummaryTableBody = this.$('#uma-summary-body') as HTMLElement;
// Don't re-render the table if the data has not changed.
const newDataString = summary.toString();
if (newDataString === this.previousUmaSummaryData_) {
return;
}
this.previousUmaSummaryData_ = newDataString;
this.updateSummaryTable_(umaSummaryTableBody, summary);
}
/**
* Fills the passed table element with the given logs.
*/
private updateLogsTable_(tableBody: HTMLElement, logs: Log[]): void {
// Clear the table first.
tableBody.replaceChildren();
const template = this.$('#uma-log-row-template') as HTMLTemplateElement;
// Iterate through the logs in reverse order so that the most recent log
// shows up first.
for (const log of logs.slice(0).reverse()) {
const row = template.content.cloneNode(true) as HTMLElement;
const [type, hash, timestamp, size, events] = row.querySelectorAll('td');
assert(type);
type.textContent = umaLogTypeToString(log.type);
assert(hash);
hash.textContent = log.hash;
assert(timestamp);
timestamp.textContent = timestampToString(log.timestamp);
assert(size);
size.textContent = sizeToString(log.size);
assert(events);
const eventsPeekDiv =
events.querySelector<HTMLElement>('.uma-log-events-peek');
assert(eventsPeekDiv);
eventsPeekDiv.addEventListener('click', this.toggleEventsExpand_, false);
const eventsPeekText =
events.querySelector<HTMLElement>('.uma-log-events-peek-text');
assert(eventsPeekText);
eventsPeekText.textContent = getEventsPeekString(log.events);
const eventsText =
events.querySelector<HTMLElement>('.uma-log-events-text');
assert(eventsText);
// Iterate through the events in reverse order so that the most recent
// event shows up first.
for (const event of log.events.slice(0).reverse()) {
const div = document.createElement('div');
div.textContent = logEventToString(event);
eventsText.appendChild(div);
}
tableBody.appendChild(row);
}
}
/**
* Fetches the latest UMA logs and renders them. This is called when the page
* is loaded and whenever there is a log that created or changed.
*/
private async updateUmaLogsData_(): Promise<void> {
const logsData: string =
await this.browserProxy_.getUmaLogData(/*includeLogProtoData=*/ false);
const logs: LogData = JSON.parse(logsData);
// If there are no logs, append an empty log. This is purely for aesthetic
// reasons. Otherwise, the table may look confusing.
if (!logs.logs.length) {
logs.logs = [EMPTY_LOG];
}
// We don't compare the new data with the old data to prevent re-renderings
// because this should only be called when there is an actual change.
const umaLogsTableBody = this.$('#uma-logs-body') as HTMLElement;
this.updateLogsTable_(umaLogsTableBody, logs.logs);
}
/**
* Exports the accumulated UMA logs, including their proto data, as a JSON
* file. This will initiate a download.
*/
private async exportUmaLogs_(): Promise<void> {
const logsData: string = await this.getUmaLogsExportContent();
const file = new Blob([logsData], {type: 'text/plain'});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = `uma_logs_${new Date().getTime()}.json`;
a.click();
}
}
declare global {
interface HTMLElementTagNameMap {
'metrics-internals-app': MetricsInternalsAppElement;
}
}
customElements.define(
MetricsInternalsAppElement.is, MetricsInternalsAppElement);