Commit dd4a550e authored by Sigurd Schneider's avatar Sigurd Schneider Committed by Commit Bot

[devtools] Prevent second tracing from starting

Devtools would allow starting timeline recording when another tracing
session was currently running, and entered an unexpected state when the
second tracing was requested to stop.

This CL prevents a second tracing session from being started and displays
an error message instead.

Bug: chromium:919798
Change-Id: I006953a544a7293446656fff002e68b88de44eaf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1715083
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: default avatarYang Guo <yangguo@chromium.org>
Reviewed-by: default avatarAlexei Filippov <alph@chromium.org>
Cr-Commit-Position: refs/heads/master@{#688434}
parent 9df3a635
......@@ -764,7 +764,8 @@ void TracingHandler::Start(Maybe<std::string> categories,
gzip_compression_ = gzip_compression;
proto_format_ = proto_format;
}
callback->sendFailure(Response::Error("Tracing is already started"));
callback->sendFailure(Response::Error(
"Tracing has already been started (possibly in another tab)."));
return;
}
......
......@@ -80,15 +80,23 @@ SDK.TracingManager = class extends SDK.SDKModel {
* @param {!SDK.TracingManagerClient} client
* @param {string} categoryFilter
* @param {string} options
* @return {!Promise}
* @return {!Promise<!Object>}
*/
start(client, categoryFilter, options) {
async start(client, categoryFilter, options) {
if (this._activeClient)
throw new Error('Tracing is already started');
const bufferUsageReportingIntervalMs = 500;
this._activeClient = client;
return this._tracingAgent.start(
categoryFilter, options, bufferUsageReportingIntervalMs, SDK.TracingManager.TransferMode.ReportEvents);
const args = {
bufferUsageReportingInterval: bufferUsageReportingIntervalMs,
categories: categoryFilter,
options: options,
transferMode: SDK.TracingManager.TransferMode.ReportEvents
};
const response = await this._tracingAgent.invoke_start(args);
if (response[Protocol.Error])
this._activeClient = null;
return response;
}
stop() {
......
......@@ -41,9 +41,9 @@ Timeline.TimelineController = class {
/**
* @param {!Timeline.TimelineController.RecordingOptions} options
* @param {!Array<!Extensions.ExtensionTraceProvider>} providers
* @return {!Promise}
* @return {!Promise<!Object>}
*/
startRecording(options, providers) {
async startRecording(options, providers) {
this._extensionTraceProviders = Extensions.extensionServer.traceProviders().slice();
/**
......@@ -83,32 +83,43 @@ Timeline.TimelineController = class {
this._extensionSessions =
providers.map(provider => new Timeline.ExtensionTracingSession(provider, this._performanceModel));
this._extensionSessions.forEach(session => session.start());
const startPromise = this._startRecordingWithCategories(categoriesArray.join(','), options.enableJSSampling);
this._performanceModel.setRecordStartTime(Date.now());
return startPromise;
const response = await this._startRecordingWithCategories(categoriesArray.join(','), options.enableJSSampling);
if (response[Protocol.Error])
await this._waitForTracingToStop(false);
return response;
}
/**
* @return {!Promise<!Timeline.PerformanceModel>}
*/
async stopRecording() {
const tracingStoppedPromises = [];
if (this._tracingManager)
tracingStoppedPromises.push(new Promise(resolve => this._tracingCompleteCallback = resolve));
tracingStoppedPromises.push(this._stopProfilingOnAllModels());
if (this._tracingManager)
this._tracingManager.stop();
this._client.loadingStarted();
await this._waitForTracingToStop(true);
this._allSourcesFinished();
return this._performanceModel;
}
/**
* @param {boolean} awaitTracingCompleteCallback - Whether to wait for the _tracingCompleteCallback to happen
* @return {!Promise}
*/
_waitForTracingToStop(awaitTracingCompleteCallback) {
const tracingStoppedPromises = [];
if (this._tracingManager && awaitTracingCompleteCallback)
tracingStoppedPromises.push(new Promise(resolve => this._tracingCompleteCallback = resolve));
tracingStoppedPromises.push(this._stopProfilingOnAllModels());
const extensionCompletionPromises = this._extensionSessions.map(session => session.stop());
if (extensionCompletionPromises.length) {
tracingStoppedPromises.push(
Promise.race([Promise.all(extensionCompletionPromises), new Promise(r => setTimeout(r, 5000))]));
}
await Promise.all(tracingStoppedPromises);
this._allSourcesFinished();
return this._performanceModel;
return Promise.all(tracingStoppedPromises);
}
/**
......@@ -170,7 +181,7 @@ Timeline.TimelineController = class {
/**
* @param {string} categories
* @param {boolean=} enableJSSampling
* @return {!Promise}
* @return {!Promise<!Object>}
*/
async _startRecordingWithCategories(categories, enableJSSampling) {
SDK.targetManager.suspendAllTargets();
......
......@@ -464,9 +464,6 @@ Timeline.TimelinePanel = class extends UI.Panel {
this._recordingOptionUIControls.forEach(control => control.setEnabled(enabled));
}
/**
* @return {!Promise}
*/
async _startRecording() {
console.assert(!this._statusPane, 'Status pane is already opened.');
this._setState(Timeline.TimelinePanel.State.StartPending);
......@@ -485,8 +482,11 @@ Timeline.TimelinePanel = class extends UI.Panel {
this._controller = new Timeline.TimelineController(mainTarget, this);
this._setUIControlsEnabled(false);
this._hideLandingPage();
await this._controller.startRecording(recordingOptions, enabledTraceProviders);
this._recordingStarted();
const response = await this._controller.startRecording(recordingOptions, enabledTraceProviders);
if (response[Protocol.Error])
this._recordingFailed(response[Protocol.Error]);
else
this._recordingStarted();
}
async _stopRecording() {
......@@ -496,12 +496,31 @@ Timeline.TimelinePanel = class extends UI.Panel {
this._statusPane.updateProgressBar(Common.UIString('Received'), 0);
}
this._setState(Timeline.TimelinePanel.State.StopPending);
this._performanceModel = await this._controller.stopRecording();
const model = await this._controller.stopRecording();
this._performanceModel = model;
this._setUIControlsEnabled(true);
this._controller.dispose();
this._controller = null;
}
/**
* @param {string} error The error message to display
*/
_recordingFailed(error) {
if (this._statusPane)
this._statusPane.hide();
this._statusPane = new Timeline.TimelinePanel.StatusPane({description: error}, () => this.loadingComplete(null));
this._statusPane.showPane(this._statusPaneContainer);
this._statusPane.updateStatus(ls`Recording failed`);
this._statusPane.updateButton(ls`Close`);
this._setState(Timeline.TimelinePanel.State.RecordingFailed);
this._performanceModel = null;
this._setUIControlsEnabled(false);
this._controller.dispose();
this._controller = null;
}
_onSuspendStateChanged() {
this._updateTimelineControls();
}
......@@ -677,7 +696,7 @@ Timeline.TimelinePanel = class extends UI.Panel {
if (this._statusPane)
this._statusPane.hide();
this._statusPane = new Timeline.TimelinePanel.StatusPane(false, this._cancelLoading.bind(this));
this._statusPane = new Timeline.TimelinePanel.StatusPane({showProgress: true}, this._cancelLoading.bind(this));
this._statusPane.showPane(this._statusPaneContainer);
this._statusPane.updateStatus(Common.UIString('Loading profile\u2026'));
// FIXME: make loading from backend cancelable as well.
......@@ -729,7 +748,8 @@ Timeline.TimelinePanel = class extends UI.Panel {
_showRecordingStarted() {
if (this._statusPane)
return;
this._statusPane = new Timeline.TimelinePanel.StatusPane(true, this._stopRecording.bind(this));
this._statusPane =
new Timeline.TimelinePanel.StatusPane({showTimer: true, showProgress: true}, this._stopRecording.bind(this));
this._statusPane.showPane(this._statusPaneContainer);
this._statusPane.updateStatus(Common.UIString('Initializing profiler\u2026'));
}
......@@ -887,7 +907,8 @@ Timeline.TimelinePanel.State = {
StartPending: Symbol('StartPending'),
Recording: Symbol('Recording'),
StopPending: Symbol('StopPending'),
Loading: Symbol('Loading')
Loading: Symbol('Loading'),
RecordingFailed: Symbol('RecordingFailed')
};
/**
......@@ -1024,10 +1045,14 @@ Timeline.TimelineModeViewDelegate.prototype = {
*/
Timeline.TimelinePanel.StatusPane = class extends UI.VBox {
/**
* @param {boolean} showTimer
* @param {!{showTimer: (boolean|undefined), showProgress: (boolean|undefined), description: (string|undefined)}} options - a collection of options controlling the appearance of the pane.
* The options object can have the following properties:
* - **showTimer** - `{boolean}` - Display seconds since dialog opened
* - **showProgress** - `{boolean}` - Display a progress bar
* - **description** - `{string}` - Display this string in a description line
* @param {function()} stopCallback
*/
constructor(showTimer, stopCallback) {
constructor(options, stopCallback) {
super(true);
this.registerRequiredCSS('timeline/timelineStatusDialog.css');
this.contentElement.classList.add('timeline-status-dialog');
......@@ -1037,15 +1062,25 @@ Timeline.TimelinePanel.StatusPane = class extends UI.VBox {
this._status = statusLine.createChild('div', 'content');
UI.ARIAUtils.markAsStatus(this._status);
if (showTimer) {
if (options.showTimer) {
const timeLine = this.contentElement.createChild('div', 'status-dialog-line time');
timeLine.createChild('div', 'label').textContent = Common.UIString('Time');
this._time = timeLine.createChild('div', 'content');
}
const progressLine = this.contentElement.createChild('div', 'status-dialog-line progress');
this._progressLabel = progressLine.createChild('div', 'label');
this._progressBar = progressLine.createChild('div', 'indicator-container').createChild('div', 'indicator');
UI.ARIAUtils.markAsProgressBar(this._progressBar);
if (options.showProgress) {
const progressLine = this.contentElement.createChild('div', 'status-dialog-line progress');
this._progressLabel = progressLine.createChild('div', 'label');
this._progressBar = progressLine.createChild('div', 'indicator-container').createChild('div', 'indicator');
UI.ARIAUtils.markAsProgressBar(this._progressBar);
}
if (typeof options.description === 'string') {
const descriptionLine = this.contentElement.createChild('div', 'status-dialog-line description');
descriptionLine.createChild('div', 'label').textContent = ls`Description`;
this._description = descriptionLine.createChild('div', 'content');
this._description.innerText = options.description;
}
this._stopButton = UI.createTextButton(Common.UIString('Stop'), stopCallback, '', true);
this.contentElement.createChild('div', 'stop-button').appendChild(this._stopButton);
......@@ -1088,6 +1123,13 @@ Timeline.TimelinePanel.StatusPane = class extends UI.VBox {
this._updateTimer();
}
/**
* @param {string} caption
*/
updateButton(caption) {
this._stopButton.innerText = caption;
}
startTimer() {
this._startTime = Date.now();
this._timeUpdateTimer = setInterval(this._updateTimer.bind(this, false), 1000);
......
......@@ -493,6 +493,9 @@ Click the reload button <ph name="RELOADBUTTON">$3s<ex>reload</ex></ph> or hit <
<message name="IDS_DEVTOOLS_80a4b8755d836d50c19e6eefb976bc2e" desc="Text in Timeline UIUtils of the Performance panel">
<ph name="SELFCATEGORY_TITLE">$1s<ex>blink.console</ex></ph> (self)
</message>
<message name="IDS_DEVTOOLS_812f4cf4e8c262a256350332191a52b2" desc="">
Recording failed
</message>
<message name="IDS_DEVTOOLS_81e91be8db96be69e372f0c715ec7444" desc="Text in Timeline UIUtils of the Performance panel">
Collected
</message>
......
......@@ -10,7 +10,7 @@ Starting tracing in session2
{
error : {
code : -32000
message : Tracing is already started
message : Tracing has already been started (possibly in another tab).
}
id : <number>
sessionId : <string>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment