UNPKG

chrome-devtools-frontend

Version:
408 lines (343 loc) • 13 kB
// Copyright (c) 2019 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 i18n from '../../core/i18n/i18n.js'; import * 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 UI from '../../ui/legacy/legacy.js'; import * as Timeline from '../timeline/timeline.js'; import inputTimelineStyles from './inputTimeline.css.js'; import {InputModel} from './InputModel.js'; const UIStrings = { /** *@description Text to clear everything */ clearAll: 'Clear all', /** *@description Tooltip text that appears when hovering over the largeicon load button */ loadProfile: 'Load profile…', /** *@description Tooltip text that appears when hovering over the largeicon download button */ saveProfile: 'Save profile…', }; const str_ = i18n.i18n.registerUIStrings('panels/input//InputTimeline.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let inputTimelineInstance: InputTimeline; export class InputTimeline extends UI.Widget.VBox implements Timeline.TimelineLoader.Client { private tracingClient: TracingClient|null; private tracingModel: SDK.TracingModel.TracingModel|null; private inputModel: InputModel|null; private state: State; private readonly toggleRecordAction: UI.ActionRegistration.Action; private readonly startReplayAction: UI.ActionRegistration.Action; private readonly togglePauseAction: UI.ActionRegistration.Action; private readonly panelToolbar: UI.Toolbar.Toolbar; private readonly clearButton: UI.Toolbar.ToolbarButton; private readonly loadButton: UI.Toolbar.ToolbarButton; private readonly saveButton: UI.Toolbar.ToolbarButton; private fileSelectorElement?: HTMLInputElement; private loader?: Timeline.TimelineLoader.TimelineLoader; constructor() { super(true); this.element.classList.add('inputs-timeline'); this.tracingClient = null; this.tracingModel = null; this.inputModel = null; this.state = State.Idle; this.toggleRecordAction = UI.ActionRegistry.ActionRegistry.instance().action('input.toggle-recording') as UI.ActionRegistration.Action; this.startReplayAction = UI.ActionRegistry.ActionRegistry.instance().action('input.start-replaying') as UI.ActionRegistration.Action; this.togglePauseAction = UI.ActionRegistry.ActionRegistry.instance().action('input.toggle-pause') as UI.ActionRegistration.Action; const toolbarContainer = this.contentElement.createChild('div', 'input-timeline-toolbar-container'); this.panelToolbar = new UI.Toolbar.Toolbar('input-timeline-toolbar', toolbarContainer); this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.toggleRecordAction)); this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.startReplayAction)); this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.togglePauseAction)); this.clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'largeicon-clear'); this.clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.reset.bind(this)); this.panelToolbar.appendToolbarItem(this.clearButton); this.panelToolbar.appendSeparator(); // Load / Save this.loadButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.loadProfile), 'largeicon-load'); this.loadButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => this.selectFileToLoad()); this.saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.saveProfile), 'largeicon-download'); this.saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, _event => { void this.saveToFile(); }); this.panelToolbar.appendSeparator(); this.panelToolbar.appendToolbarItem(this.loadButton); this.panelToolbar.appendToolbarItem(this.saveButton); this.panelToolbar.appendSeparator(); this.createFileSelector(); this.updateControls(); } static instance(opts: {forceNew: boolean} = {forceNew: false}): InputTimeline { const {forceNew} = opts; if (!inputTimelineInstance || forceNew) { inputTimelineInstance = new InputTimeline(); } return inputTimelineInstance; } private reset(): void { this.tracingClient = null; this.tracingModel = null; this.inputModel = null; this.setState(State.Idle); } private createFileSelector(): void { if (this.fileSelectorElement) { this.fileSelectorElement.remove(); } this.fileSelectorElement = UI.UIUtils.createFileSelectorElement(this.loadFromFile.bind(this)); this.element.appendChild(this.fileSelectorElement); } wasShown(): void { super.wasShown(); this.registerCSSFiles([inputTimelineStyles]); } willHide(): void { } private setState(state: State): void { this.state = state; this.updateControls(); } private isAvailableState(): boolean { return this.state === State.Idle || this.state === State.ReplayPaused; } private updateControls(): void { this.toggleRecordAction.setToggled(this.state === State.Recording); this.toggleRecordAction.setEnabled(this.isAvailableState() || this.state === State.Recording); this.startReplayAction.setEnabled(this.isAvailableState() && Boolean(this.tracingModel)); this.togglePauseAction.setEnabled(this.state === State.Replaying || this.state === State.ReplayPaused); this.togglePauseAction.setToggled(this.state === State.ReplayPaused); this.clearButton.setEnabled(this.isAvailableState()); this.loadButton.setEnabled(this.isAvailableState()); this.saveButton.setEnabled(this.isAvailableState() && Boolean(this.tracingModel)); } toggleRecording(): void { switch (this.state) { case State.Recording: { void this.stopRecording(); break; } case State.Idle: { void this.startRecording(); break; } } } startReplay(): void { void this.replayEvents(); } toggleReplayPause(): void { switch (this.state) { case State.Replaying: { this.pauseReplay(); break; } case State.ReplayPaused: { this.resumeReplay(); break; } } } /** * Saves all current events in a file (JSON format). */ private async saveToFile(): Promise<void> { console.assert(this.state === State.Idle); if (!this.tracingModel) { return; } const fileName = `InputProfile-${Platform.DateUtilities.toISO8601Compact(new Date())}.json` as Platform.DevToolsPath.RawPathString; const stream = new Bindings.FileUtils.FileOutputStream(); const accepted = await stream.open(fileName); if (!accepted) { return; } const backingStorage = this.tracingModel.backingStorage() as Bindings.TempFile.TempFileBackingStorage; await backingStorage.writeToStream(stream); void stream.close(); } private selectFileToLoad(): void { if (this.fileSelectorElement) { this.fileSelectorElement.click(); } } private loadFromFile(file: File): void { console.assert(this.isAvailableState()); this.setState(State.Loading); this.loader = Timeline.TimelineLoader.TimelineLoader.loadFromFile(file, this); this.createFileSelector(); } private async startRecording(): Promise<void> { this.setState(State.StartPending); this.tracingClient = new TracingClient(SDK.TargetManager.TargetManager.instance().mainTarget() as SDK.Target.Target, this); const response = await this.tracingClient.startRecording(); if (!response || response.getError()) { this.recordingFailed(); } else { this.setState(State.Recording); } } private async stopRecording(): Promise<void> { if (!this.tracingClient) { return; } this.setState(State.StopPending); await this.tracingClient.stopRecording(); this.tracingClient = null; } private async replayEvents(): Promise<void> { if (!this.inputModel) { return; } this.setState(State.Replaying); await this.inputModel.startReplay(this.replayStopped.bind(this)); } private pauseReplay(): void { if (!this.inputModel) { return; } this.inputModel.pause(); this.setState(State.ReplayPaused); } private resumeReplay(): void { if (!this.inputModel) { return; } this.inputModel.resume(); this.setState(State.Replaying); } loadingStarted(): void { } loadingProgress(_progress?: number): void { } processingStarted(): void { } loadingComplete(tracingModel: SDK.TracingModel.TracingModel|null): void { if (!tracingModel) { this.reset(); return; } this.inputModel = new InputModel(SDK.TargetManager.TargetManager.instance().mainTarget() as SDK.Target.Target); this.tracingModel = tracingModel; this.inputModel.setEvents(tracingModel); this.setState(State.Idle); } private recordingFailed(): void { this.tracingClient = null; this.setState(State.Idle); } replayStopped(): void { this.setState(State.Idle); } } export const enum State { Idle = 'Idle', StartPending = 'StartPending', Recording = 'Recording', StopPending = 'StopPending', Replaying = 'Replaying', ReplayPaused = 'ReplayPaused', Loading = 'Loading', } let actionDelegateInstance: ActionDelegate; export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { static instance(opts: {forceNew: boolean|null} = {forceNew: null}): ActionDelegate { const {forceNew} = opts; if (!actionDelegateInstance || forceNew) { actionDelegateInstance = new ActionDelegate(); } return actionDelegateInstance; } handleAction(context: UI.Context.Context, actionId: string): boolean { const inputViewId = 'Inputs'; void UI.ViewManager.ViewManager.instance() .showView(inputViewId) .then(() => UI.ViewManager.ViewManager.instance().view(inputViewId).widget()) .then(widget => this.innerHandleAction(widget as InputTimeline, actionId)); return true; } private innerHandleAction(inputTimeline: InputTimeline, actionId: string): void { switch (actionId) { case 'input.toggle-recording': inputTimeline.toggleRecording(); break; case 'input.start-replaying': inputTimeline.startReplay(); break; case 'input.toggle-pause': inputTimeline.toggleReplayPause(); break; default: console.assert(false, `Unknown action: ${actionId}`); } } } export class TracingClient implements SDK.TracingManager.TracingManagerClient { private readonly target: SDK.Target.Target; private tracingManager: SDK.TracingManager.TracingManager|null; private readonly client: InputTimeline; private readonly tracingModel: SDK.TracingModel.TracingModel; private tracingCompleteCallback: (() => void)|null; constructor(target: SDK.Target.Target, client: InputTimeline) { this.target = target; this.tracingManager = target.model(SDK.TracingManager.TracingManager); this.client = client; const backingStorage = new Bindings.TempFile.TempFileBackingStorage(); this.tracingModel = new SDK.TracingModel.TracingModel(backingStorage); this.tracingCompleteCallback = null; } async startRecording(): Promise<Protocol.ProtocolResponseWithError|undefined> { if (!this.tracingManager) { return; } const categoriesArray = ['devtools.timeline', 'disabled-by-default-devtools.timeline.inputs']; const categories = categoriesArray.join(','); const response = await this.tracingManager.start(this, categories, ''); if (response.getError()) { await this.waitForTracingToStop(false); } return response; } async stopRecording(): Promise<void> { if (this.tracingManager) { this.tracingManager.stop(); } await this.waitForTracingToStop(true); await SDK.TargetManager.TargetManager.instance().resumeAllTargets(); this.tracingModel.tracingComplete(); this.client.loadingComplete(this.tracingModel); } traceEventsCollected(events: SDK.TracingManager.EventPayload[]): void { this.tracingModel.addEvents(events); } tracingComplete(): void { if (this.tracingCompleteCallback) { this.tracingCompleteCallback(); } this.tracingCompleteCallback = null; } tracingBufferUsage(_usage: number): void { } eventsRetrievalProgress(_progress: number): void { } private waitForTracingToStop(awaitTracingCompleteCallback: boolean): Promise<void> { return new Promise(resolve => { if (this.tracingManager && awaitTracingCompleteCallback) { this.tracingCompleteCallback = resolve; } else { resolve(); } }); } }