// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';
import type {Time} from '//resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';
import {BrowserProxy} from './browser_proxy.js';
import {getCss} from './event_log.css.js';
import {getHtml} from './event_log.html.js';
/**
* Converts a mojo time to a JS time.
*/
function convertMojoTimeToJs(mojoTime: Time): Date {
// The JS Date() is based off of the number of milliseconds since the
// UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
// base::Time (represented in mojom.Time) represents the number of
// microseconds since the Windows FILETIME epoch (1601-01-01 00:00:00 UTC).
// This computes the final JS time by computing the epoch delta and the
// conversion from microseconds to milliseconds.
const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
// |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
const epochDeltaInMs = unixEpoch - windowsEpoch;
const timeInMs = Number(mojoTime.internalValue) / 1000;
return new Date(timeInMs - epochDeltaInMs);
}
export class EventLogMessage {
eventTime: Date;
sourceLinkText: string;
sourceLinkURL: string;
message: string;
constructor(
eventTime: Time, sourceFile: string, sourceLine: number,
message: string) {
this.eventTime = convertMojoTimeToJs(eventTime);
this.message = message;
this.setSourceLink(sourceFile, sourceLine);
}
setSourceLink(sourceFile: string, sourceLine: number) {
if (!sourceFile.startsWith('../../')) {
this.sourceLinkText = `${sourceFile}(${sourceLine})`;
return;
}
const fileName = sourceFile.slice(sourceFile.lastIndexOf('/') + 1);
if (fileName.length === 0) {
this.sourceLinkText = `${sourceFile}(${sourceLine})`;
return;
}
this.sourceLinkText = `${fileName}(${sourceLine})`;
this.sourceLinkURL =
`https://source.chromium.org/chromium/chromium/src/+/main:${
sourceFile.slice(6)};l=${sourceLine}`;
}
/**
* Returns a string for dumping the message to logs.
*/
toLogDump() {
return `${this.eventTime} ${this.sourceLinkText} ${this.message}`;
}
}
export class OnDeviceInternalsEventLogElement extends CrLitElement {
static get is() {
return 'on-device-internals-event-log';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
eventLogMessages_: {type: Array},
};
}
protected eventLogMessages_: EventLogMessage[] = [];
override connectedCallback() {
super.connectedCallback();
BrowserProxy.getInstance().callbackRouter.onLogMessageAdded.addListener(
this.onLogMessageAdded_.bind(this));
}
/**
* The callback to save the logs to a file.
*/
protected onEventLogsDumpClick_() {
const data =
this.eventLogMessages_.map(message => message.toLogDump()).join('\r\n');
const blob = new Blob([data], {'type': 'text/json'});
const url = URL.createObjectURL(blob);
const filename = 'optimization_guide_internals_logs_dump.json';
const a = document.createElement('a');
a.setAttribute('href', url);
a.setAttribute('download', filename);
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
});
a.dispatchEvent(event);
}
private onLogMessageAdded_(
eventTime: Time, sourceFile: string, sourceLine: number,
message: string) {
this.eventLogMessages_.push(
new EventLogMessage(eventTime, sourceFile, sourceLine, message));
this.requestUpdate();
}
}
declare global {
interface HTMLElementTagNameMap {
'on-device-internals-event-log': OnDeviceInternalsEventLogElement;
}
}
customElements.define(
OnDeviceInternalsEventLogElement.is, OnDeviceInternalsEventLogElement);