chrome-devtools-frontend
Version:
Chrome DevTools UI
288 lines (253 loc) • 10.9 kB
text/typescript
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Trace from '../../models/trace/trace.js';
import type {Client} from './TimelineController.js';
const UIStrings = {
/**
*@description Text in Timeline Loader of the Performance panel
*@example {Unknown JSON format} PH1
*/
malformedTimelineDataS: 'Malformed timeline data: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineLoader.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
/**
* This class handles loading traces from file and URL, and from the Lighthouse panel
* It also handles loading cpuprofiles from file, url and console.profileEnd()
*
* Meanwhile, the normal trace recording flow bypasses TimelineLoader entirely,
* as it's handled from TracingManager => TimelineController.
*/
export class TimelineLoader implements Common.StringOutputStream.OutputStream {
private client: Client|null;
private canceledCallback: (() => void)|null;
private buffer: string;
private firstRawChunk: boolean;
private totalSize!: number;
private filter: Trace.Extras.TraceFilter.TraceFilter|null;
#traceIsCPUProfile: boolean;
#collectedEvents: Trace.Types.Events.Event[] = [];
#metadata: Trace.Types.File.MetaData|null;
#traceFinalizedCallbackForTest?: () => void;
#traceFinalizedPromiseForTest: Promise<void>;
constructor(client: Client) {
this.client = client;
this.canceledCallback = null;
this.buffer = '';
this.firstRawChunk = true;
this.filter = null;
this.#traceIsCPUProfile = false;
this.#metadata = null;
this.#traceFinalizedPromiseForTest = new Promise<void>(resolve => {
this.#traceFinalizedCallbackForTest = resolve;
});
}
static async loadFromFile(file: File, client: Client): Promise<TimelineLoader> {
const loader = new TimelineLoader(client);
const fileReader = new Bindings.FileUtils.ChunkedFileReader(file);
loader.canceledCallback = fileReader.cancel.bind(fileReader);
loader.totalSize = file.size;
// We'll resolve and return the loader instance before finalizing the trace.
setTimeout(async () => {
const success = await fileReader.read(loader);
if (!success && fileReader.error()) {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
loader.reportErrorAndCancelLoading((fileReader.error() as any).message);
}
});
return loader;
}
static loadFromEvents(events: Trace.Types.Events.Event[], client: Client): TimelineLoader {
const loader = new TimelineLoader(client);
window.setTimeout(async () => {
void loader.addEvents(events, null);
});
return loader;
}
static loadFromTraceFile(traceFile: Trace.Types.File.TraceFile, client: Client): TimelineLoader {
const loader = new TimelineLoader(client);
window.setTimeout(async () => {
void loader.addEvents(traceFile.traceEvents, traceFile.metadata);
});
return loader;
}
static loadFromCpuProfile(profile: Protocol.Profiler.Profile, client: Client): TimelineLoader {
const loader = new TimelineLoader(client);
loader.#traceIsCPUProfile = true;
try {
const contents = Trace.Helpers.SamplesIntegrator.SamplesIntegrator.createFakeTraceFromCpuProfile(
profile, Trace.Types.Events.ThreadID(1));
window.setTimeout(async () => {
void loader.addEvents(contents.traceEvents, null);
});
} catch (e) {
console.error(e.stack);
}
return loader;
}
static async loadFromURL(url: Platform.DevToolsPath.UrlString, client: Client): Promise<TimelineLoader> {
const loader = new TimelineLoader(client);
const stream = new Common.StringOutputStream.StringOutputStream();
client.loadingStarted();
const allowRemoteFilePaths =
Common.Settings.Settings.instance().moduleSetting('network.enable-remote-file-loading').get();
Host.ResourceLoader.loadAsStream(url, null, stream, finishedCallback, allowRemoteFilePaths);
async function finishedCallback(
success: boolean, _headers: Record<string, string>,
errorDescription: Host.ResourceLoader.LoadErrorDescription): Promise<void> {
if (!success) {
return loader.reportErrorAndCancelLoading(errorDescription.message);
}
try {
const txt = stream.data();
const trace = JSON.parse(txt);
loader.#processParsedFile(trace);
await loader.close();
} catch (e: unknown) {
await loader.close();
const message = e instanceof Error ? e.message : '';
return loader.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineDataS, {PH1: message}));
}
}
return loader;
}
#processParsedFile(trace: ParsedJSONFile): void {
if ('traceEvents' in trace || Array.isArray(trace)) {
// We know that this is NOT a raw CPU Profile because it has traceEvents
// (either at the top level, or nested under the traceEvents key)
const items = Array.isArray(trace) ? trace : trace.traceEvents;
this.#collectEvents(items);
} else if (trace.nodes) {
// We know it's a raw Protocol CPU Profile.
this.#parseCPUProfileFormatFromFile(trace);
this.#traceIsCPUProfile = true;
} else {
this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineDataS));
return;
}
if ('metadata' in trace) {
this.#metadata = trace.metadata;
// Older traces set these fields even when throttling is not active, while newer traces do not.
// Clear them out on load to simplify usage.
if (this.#metadata.cpuThrottling === 1) {
this.#metadata.cpuThrottling = undefined;
}
// This string is translated, so this only covers the english case and the current locale.
// Due to this, older traces in other locales will end up displaying "No throttling" in the trace history selector.
const noThrottlingString = typeof SDK.NetworkManager.NoThrottlingConditions.title === 'string' ?
SDK.NetworkManager.NoThrottlingConditions.title :
SDK.NetworkManager.NoThrottlingConditions.title();
if (this.#metadata.networkThrottling === 'No throttling' ||
this.#metadata.networkThrottling === noThrottlingString) {
this.#metadata.networkThrottling = undefined;
}
}
}
async addEvents(events: readonly Trace.Types.Events.Event[], metadata: Trace.Types.File.MetaData|null):
Promise<void> {
this.#metadata = metadata;
this.client?.loadingStarted();
/**
* See the `eventsPerChunk` comment in `models/trace/types/Configuration.ts`.
*
* This value is different though. Why? `The addEvents()` work below is different
* (and much faster!) than running `handleEvent()` on all handlers.
*/
const eventsPerChunk = 150_000;
for (let i = 0; i < events.length; i += eventsPerChunk) {
const chunk = events.slice(i, i + eventsPerChunk);
this.#collectEvents(chunk as unknown as Trace.Types.Events.Event[]);
this.client?.loadingProgress((i + chunk.length) / events.length);
await new Promise(r => window.setTimeout(r, 0)); // Yield event loop to paint.
}
void this.close();
}
async cancel(): Promise<void> {
if (this.client) {
await this.client.loadingComplete(
/* collectedEvents */[], /* exclusiveFilter= */ null, /* metadata= */ null);
this.client = null;
}
if (this.canceledCallback) {
this.canceledCallback();
}
}
/**
* As TimelineLoader implements `Common.StringOutputStream.OutputStream`, `write()` is called when a
* Common.StringOutputStream.StringOutputStream instance has decoded a chunk. This path is only used
* by `loadFromFile()`; it's NOT used by `loadFromEvents` or `loadFromURL`.
*/
async write(chunk: string, endOfFile: boolean): Promise<void> {
if (!this.client) {
return await Promise.resolve();
}
this.buffer += chunk;
if (this.firstRawChunk) {
this.client.loadingStarted();
// Ensure we paint the loading dialog before continuing
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
this.firstRawChunk = false;
} else {
let progress = undefined;
progress = this.buffer.length / this.totalSize;
// For compressed traces, we can't provide a definite progress percentage. So, just keep it moving.
// For other traces, calculate a loaded part.
progress = progress > 1 ? progress - Math.floor(progress) : progress;
this.client.loadingProgress(progress);
}
if (endOfFile) {
let trace;
try {
trace = JSON.parse(this.buffer) as ParsedJSONFile;
this.#processParsedFile(trace);
} catch (e) {
this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineDataS, {PH1: e.toString()}));
}
return;
}
}
private reportErrorAndCancelLoading(message?: string): void {
if (message) {
Common.Console.Console.instance().error(message);
}
void this.cancel();
}
async close(): Promise<void> {
if (!this.client) {
return;
}
this.client.processingStarted();
await this.finalizeTrace();
}
private async finalizeTrace(): Promise<void> {
if (!this.#metadata && this.#traceIsCPUProfile) {
this.#metadata = {dataOrigin: Trace.Types.File.DataOrigin.CPU_PROFILE};
}
await (this.client as Client).loadingComplete(this.#collectedEvents, this.filter, this.#metadata);
this.#traceFinalizedCallbackForTest?.();
}
traceFinalizedForTest(): Promise<void> {
return this.#traceFinalizedPromiseForTest;
}
#parseCPUProfileFormatFromFile(parsedTrace: Protocol.Profiler.Profile): void {
const traceFile = Trace.Helpers.SamplesIntegrator.SamplesIntegrator.createFakeTraceFromCpuProfile(
parsedTrace, Trace.Types.Events.ThreadID(1));
this.#collectEvents(traceFile.traceEvents);
}
#collectEvents(events: readonly Trace.Types.Events.Event[]): void {
this.#collectedEvents = this.#collectedEvents.concat(events);
}
}
/**
* Used when we parse the input, but do not yet know if it is a raw CPU Profile or a Trace
**/
type ParsedJSONFile = Trace.Types.File.Contents|Protocol.Profiler.Profile;