UNPKG

chrome-devtools-frontend

Version:
344 lines (306 loc) • 12.6 kB
// 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 TextUtils from '../../models/text_utils/text_utils.js'; import * as TimelineModel from '../../models/timeline_model/timeline_model.js'; const UIStrings = { /** *@description Text in Timeline Loader of the Performance panel */ malformedTimelineDataUnknownJson: 'Malformed timeline data: Unknown JSON format', /** *@description Text in Timeline Loader of the Performance panel */ malformedTimelineInputWrongJson: 'Malformed timeline input, wrong JSON brackets balance', /** *@description Text in Timeline Loader of the Performance panel *@example {Unknown JSON format} PH1 */ malformedTimelineDataS: 'Malformed timeline data: {PH1}', /** *@description Text in Timeline Loader of the Performance panel */ legacyTimelineFormatIsNot: 'Legacy Timeline format is not supported.', /** *@description Text in Timeline Loader of the Performance panel */ malformedCpuProfileFormat: 'Malformed CPU profile format', }; 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 tracingModel: SDK.TracingModel.TracingModel|null; private canceledCallback: (() => void)|null; private state: State; private buffer: string; private firstRawChunk: boolean; private firstChunk: boolean; private loadedBytes: number; private totalSize!: number; private readonly jsonTokenizer: TextUtils.TextUtils.BalancedJSONTokenizer; private filter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null; constructor(client: Client, title?: string) { this.client = client; this.tracingModel = new SDK.TracingModel.TracingModel(title); this.canceledCallback = null; this.state = State.Initial; this.buffer = ''; this.firstRawChunk = true; this.firstChunk = true; this.loadedBytes = 0; this.jsonTokenizer = new TextUtils.TextUtils.BalancedJSONTokenizer(this.writeBalancedJSON.bind(this), true); this.filter = null; } static async loadFromFile(file: File, client: Client): Promise<TimelineLoader> { const loader = new TimelineLoader(client); const fileReader = new Bindings.FileUtils.ChunkedFileReader(file, TransferChunkLengthBytes); loader.canceledCallback = fileReader.cancel.bind(fileReader); loader.totalSize = file.size; 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: SDK.TracingManager.EventPayload[], client: Client): TimelineLoader { const loader = new TimelineLoader(client); window.setTimeout(async () => { void loader.addEvents(events); }); return loader; } static getCpuProfileFilter(): TimelineModel.TimelineModelFilter.TimelineVisibleEventsFilter { const visibleTypes = []; visibleTypes.push(TimelineModel.TimelineModel.RecordType.JSFrame); visibleTypes.push(TimelineModel.TimelineModel.RecordType.JSIdleFrame); visibleTypes.push(TimelineModel.TimelineModel.RecordType.JSSystemFrame); return new TimelineModel.TimelineModelFilter.TimelineVisibleEventsFilter(visibleTypes); } static loadFromCpuProfile(profile: Protocol.Profiler.Profile|null, client: Client, title?: string): TimelineLoader { const loader = new TimelineLoader(client, title); try { const events = TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile( profile, /* tid */ 1, /* injectPageEvent */ true); loader.filter = TimelineLoader.getCpuProfileFilter(); window.setTimeout(async () => { void loader.addEvents(events); }); } 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(); await 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: {[x: string]: string}, errorDescription: Host.ResourceLoader.LoadErrorDescription): Promise<void> { if (!success) { return loader.reportErrorAndCancelLoading(errorDescription.message); } const txt = stream.data(); const trace = JSON.parse(txt); if (Array.isArray(trace.nodes)) { loader.state = State.LoadingCPUProfileFormat; loader.buffer = txt; await loader.close(); return; } const events = Array.isArray(trace.traceEvents) ? trace.traceEvents : trace; void loader.addEvents(events); } return loader; } async addEvents(events: SDK.TracingManager.EventPayload[]): Promise<void> { await this.client?.loadingStarted(); const eventsPerChunk = 15_000; for (let i = 0; i < events.length; i += eventsPerChunk) { const chunk = events.slice(i, i + eventsPerChunk); (this.tracingModel as SDK.TracingModel.TracingModel).addEvents(chunk); await this.client?.loadingProgress((i + chunk.length) / events.length); await new Promise(r => window.setTimeout(r)); // Yield event loop to paint. } void this.close(); } async cancel(): Promise<void> { this.tracingModel = null; if (this.client) { await this.client.loadingComplete(null, null); this.client = null; } if (this.canceledCallback) { this.canceledCallback(); } } async write(chunk: string): Promise<void> { if (!this.client) { return Promise.resolve(); } this.loadedBytes += chunk.length; if (this.firstRawChunk) { await this.client.loadingStarted(); // Ensure we paint the loading dialog before continuing await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); } else { let progress = undefined; if (this.totalSize) { progress = this.loadedBytes / this.totalSize; // For compressed traces, we can't provide a definite progress percentage. So, just keep it moving. progress = progress > 1 ? progress - Math.floor(progress) : progress; } await this.client.loadingProgress(progress); } this.firstRawChunk = false; if (this.state === State.Initial) { if (chunk.match(/^{(\s)*"nodes":(\s)*\[/)) { this.state = State.LoadingCPUProfileFormat; } else if (chunk[0] === '{') { this.state = State.LookingForEvents; } else if (chunk[0] === '[') { this.state = State.ReadingEvents; } else { this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineDataUnknownJson)); return Promise.resolve(); } } if (this.state === State.LoadingCPUProfileFormat) { this.buffer += chunk; return Promise.resolve(); } if (this.state === State.LookingForEvents) { const objectName = '"traceEvents":'; const startPos = this.buffer.length - objectName.length; this.buffer += chunk; const pos = this.buffer.indexOf(objectName, startPos); if (pos === -1) { return Promise.resolve(); } chunk = this.buffer.slice(pos + objectName.length); this.state = State.ReadingEvents; } if (this.state !== State.ReadingEvents) { return Promise.resolve(); } // This is where we actually do the loading of events from JSON: the JSON // Tokenizer writes the JSON to a buffer, and then as a callback the // writeBalancedJSON method below is invoked. It then parses this chunk // of JSON as a set of events, and adds them to the TracingModel via // addEvents() if (this.jsonTokenizer.write(chunk)) { return Promise.resolve(); } this.state = State.SkippingTail; if (this.firstChunk) { this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineInputWrongJson)); } return Promise.resolve(); } private writeBalancedJSON(data: string): void { let json: string = data + ']'; if (!this.firstChunk) { const commaIndex = json.indexOf(','); if (commaIndex !== -1) { json = json.slice(commaIndex + 1); } json = '[' + json; } let items; try { items = (JSON.parse(json) as SDK.TracingManager.EventPayload[]); } catch (e) { this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineDataS, {PH1: e.toString()})); return; } if (this.firstChunk) { this.firstChunk = false; if (this.looksLikeAppVersion(items[0])) { this.reportErrorAndCancelLoading(i18nString(UIStrings.legacyTimelineFormatIsNot)); return; } } try { (this.tracingModel as SDK.TracingModel.TracingModel).addEvents(items); } catch (e) { this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedTimelineDataS, {PH1: e.toString()})); } } private reportErrorAndCancelLoading(message?: string): void { if (message) { Common.Console.Console.instance().error(message); } void this.cancel(); } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private looksLikeAppVersion(item: any): boolean { return typeof item === 'string' && item.indexOf('Chrome') !== -1; } async close(): Promise<void> { if (!this.client) { return; } await this.client.processingStarted(); await this.finalizeTrace(); } private async finalizeTrace(): Promise<void> { if (this.state === State.LoadingCPUProfileFormat) { this.parseCPUProfileFormat(this.buffer); this.buffer = ''; } (this.tracingModel as SDK.TracingModel.TracingModel).tracingComplete(); await (this.client as Client).loadingComplete(this.tracingModel, this.filter); } private parseCPUProfileFormat(text: string): void { let traceEvents; try { const profile = JSON.parse(text); traceEvents = TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile( profile, /* tid */ 1, /* injectPageEvent */ true); } catch (e) { this.reportErrorAndCancelLoading(i18nString(UIStrings.malformedCpuProfileFormat)); return; } this.filter = TimelineLoader.getCpuProfileFilter(); (this.tracingModel as SDK.TracingModel.TracingModel).addEvents(traceEvents); } } export const TransferChunkLengthBytes = 5000000; export interface Client { loadingStarted(): Promise<void>; loadingProgress(progress?: number): Promise<void>; processingStarted(): Promise<void>; loadingComplete( tracingModel: SDK.TracingModel.TracingModel|null, exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null): Promise<void>; } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum State { Initial = 'Initial', LookingForEvents = 'LookingForEvents', ReadingEvents = 'ReadingEvents', SkippingTail = 'SkippingTail', LoadingCPUProfileFormat = 'LoadingCPUProfileFormat', }