mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 05:08:28 +08:00
Bug: chromium:1152736 Change-Id: I8c1a64b049ce5142ac184e5f5121e7f7e289cb94 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2692770 Reviewed-by: Wolfgang Beyer <wolfi@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
409 lines
13 KiB
TypeScript
409 lines
13 KiB
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();
|
|
}
|
|
});
|
|
}
|
|
}
|