chrome-devtools-frontend
Version:
Chrome DevTools UI
409 lines (343 loc) ⢠12.9 kB
text/typescript
// 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.
/* eslint-disable rulesdir/no_underscored_properties */
import * as Bindings from '../bindings/bindings.js';
import * as i18n from '../i18n/i18n.js';
import * as Platform from '../platform/platform.js';
import * as ProtocolClient from '../protocol_client/protocol_client.js';
import * as SDK from '../sdk/sdk.js';
import * as Timeline from '../timeline/timeline.js';
import * as UI from '../ui/ui.js';
import {InputModel} from './InputModel.js';
export 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('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 {
_tracingClient: TracingClient|null;
_tracingModel: SDK.TracingModel.TracingModel|null;
_inputModel: InputModel|null;
_state: State;
_toggleRecordAction: UI.ActionRegistration.Action;
_startReplayAction: UI.ActionRegistration.Action;
_togglePauseAction: UI.ActionRegistration.Action;
_panelToolbar: UI.Toolbar.Toolbar;
_clearButton: UI.Toolbar.ToolbarButton;
_loadButton: UI.Toolbar.ToolbarButton;
_saveButton: UI.Toolbar.ToolbarButton;
_fileSelectorElement?: HTMLInputElement;
_loader?: Timeline.TimelineLoader.TimelineLoader;
constructor() {
super(true);
this.registerRequiredCSS('input/inputTimeline.css', {enableLegacyPatching: false});
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 => {
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;
}
_reset(): void {
this._tracingClient = null;
this._tracingModel = null;
this._inputModel = null;
this._setState(State.Idle);
}
_createFileSelector(): void {
if (this._fileSelectorElement) {
this._fileSelectorElement.remove();
}
this._fileSelectorElement = UI.UIUtils.createFileSelectorElement(this._loadFromFile.bind(this));
this.element.appendChild(this._fileSelectorElement);
}
wasShown(): void {
}
willHide(): void {
}
_setState(state: State): void {
this._state = state;
this._updateControls();
}
_isAvailableState(): boolean {
return this._state === State.Idle || this._state === State.ReplayPaused;
}
_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: {
this._stopRecording();
break;
}
case State.Idle: {
this._startRecording();
break;
}
}
}
_startReplay(): 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).
*/
async _saveToFile(): Promise<void> {
console.assert(this._state === State.Idle);
if (!this._tracingModel) {
return;
}
const fileName = `InputProfile-${Platform.DateUtilities.toISO8601Compact(new Date())}.json`;
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);
stream.close();
}
_selectFileToLoad(): void {
if (this._fileSelectorElement) {
this._fileSelectorElement.click();
}
}
_loadFromFile(file: File): void {
console.assert(this._isAvailableState());
this._setState(State.Loading);
this._loader = Timeline.TimelineLoader.TimelineLoader.loadFromFile(file, this);
this._createFileSelector();
}
async _startRecording(): Promise<void> {
this._setState(State.StartPending);
this._tracingClient =
new TracingClient(SDK.SDKModel.TargetManager.instance().mainTarget() as SDK.SDKModel.Target, this);
const response = await this._tracingClient.startRecording();
// @ts-ignore crbug.com/1011811 Fix tracing manager type once Closure is gone
if (response[ProtocolClient.InspectorBackend.ProtocolError]) {
this._recordingFailed();
} else {
this._setState(State.Recording);
}
}
async _stopRecording(): Promise<void> {
if (!this._tracingClient) {
return;
}
this._setState(State.StopPending);
await this._tracingClient.stopRecording();
this._tracingClient = null;
}
async _replayEvents(): Promise<void> {
if (!this._inputModel) {
return;
}
this._setState(State.Replaying);
await this._inputModel.startReplay(this.replayStopped.bind(this));
}
_pauseReplay(): void {
if (!this._inputModel) {
return;
}
this._inputModel.pause();
this._setState(State.ReplayPaused);
}
_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.SDKModel.TargetManager.instance().mainTarget() as SDK.SDKModel.Target);
this._tracingModel = tracingModel;
this._inputModel.setEvents(tracingModel);
this._setState(State.Idle);
}
_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';
UI.ViewManager.ViewManager.instance()
.showView(inputViewId)
.then(() => (UI.ViewManager.ViewManager.instance().view(inputViewId) as UI.View.View).widget())
.then(widget => this._innerHandleAction(widget as InputTimeline, actionId));
return true;
}
_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 {
_target: SDK.SDKModel.Target;
_tracingManager: SDK.TracingManager.TracingManager|null;
_client: InputTimeline;
_tracingModel: SDK.TracingModel.TracingModel;
_tracingCompleteCallback: (() => void)|null;
constructor(target: SDK.SDKModel.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<Object> {
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, '');
// @ts-ignore crbug.com/1011811 Fix tracing manager type once Closure is gone
if (response['Protocol.Error']) {
await this._waitForTracingToStop(false);
}
return response;
}
async stopRecording(): Promise<void> {
if (this._tracingManager) {
this._tracingManager.stop();
}
await this._waitForTracingToStop(true);
await SDK.SDKModel.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 {
}
_waitForTracingToStop(awaitTracingCompleteCallback: boolean): Promise<void> {
return new Promise(resolve => {
if (this._tracingManager && awaitTracingCompleteCallback) {
this._tracingCompleteCallback = resolve;
} else {
resolve();
}
});
}
}