DevTools: introduce WebInspector.Throttler

This patch introduces WebInspector.Throttler and uses it for throttling
calls to PageAgent in OverridesSupport, as well as throttling calls in StylesSourceMapping.

Throttler is a helper object that throttles execution of processes
(possibly asynchronous).
Throttler is created with a single parameter - throttle interval T.
Throttler satisfies to the following contract: for every two consecutive
runs performed by throttler, time between the end of the first run and
start of the second run is always greater or equal to T.

Throttler.schedule has additional second parameter "asSoonAsPossible" which essentially
sets throttler timeout into 0 for the next process run.

Review URL: https://codereview.chromium.org/319143002

git-svn-id: svn://svn.chromium.org/blink/trunk@175956 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent ea7d5af7
......@@ -321,7 +321,7 @@ InspectorTest.runTestSuite = function(testSuite)
var nextTest = testSuiteTests.shift();
InspectorTest.addResult("");
InspectorTest.addResult("Running: " + /function\s([^(]*)/.exec(nextTest)[1]);
InspectorTest.safeWrap(nextTest)(runner, runner);
InspectorTest.safeWrap(nextTest)(runner);
}
runner();
}
......@@ -569,6 +569,42 @@ InspectorTest.dumpLoadedModules = function(next)
next();
}
InspectorTest.TimeoutMock = function()
{
this._timeoutId = 0;
this._timeoutIdToProcess = {};
this._timeoutIdToMillis = {};
this.setTimeout = this.setTimeout.bind(this);
this.clearTimeout = this.clearTimeout.bind(this);
}
InspectorTest.TimeoutMock.prototype = {
setTimeout: function(operation, timeout)
{
this._timeoutIdToProcess[++this._timeoutId] = operation;
this._timeoutIdToMillis[this._timeoutId] = timeout;
return this._timeoutId;
},
clearTimeout: function(timeoutId)
{
delete this._timeoutIdToProcess[timeoutId];
delete this._timeoutIdToMillis[timeoutId];
},
activeTimersTimeouts: function()
{
return Object.values(this._timeoutIdToMillis);
},
fireAllTimers: function()
{
for (var timeoutId in this._timeoutIdToProcess)
this._timeoutIdToProcess[timeoutId].call(window);
this._timeoutIdToProcess = {};
this._timeoutIdToMillis = {};
}
}
WebInspector.TempFile = InspectorTest.TempFileMock;
};
......
......@@ -27,15 +27,20 @@ InspectorTest.clickOnURL = function()
InspectorTest.waitForStyleSheetChangedEvent = function(reply)
{
var oldUpdateTimeout = WebInspector.StyleFile.updateTimeout;
WebInspector.StyleFile.updateTimeout = 0;
var originalSetTimeout = WebInspector.Throttler.prototype._setTimeout;
WebInspector.Throttler.prototype._setTimeout = innerSetTimeout;
InspectorTest.addSniffer(WebInspector.CSSStyleModel.prototype, "_fireStyleSheetChanged", onStyleSheetChanged);
function onStyleSheetChanged()
{
WebInspector.StyleFile.updateTimeout = oldUpdateTimeout;
WebInspector.Throttler.prototype._setTimeout = originalSetTimeout;
reply();
}
function innerSetTimeout(operation, timeout)
{
return originalSetTimeout.call(this, operation, 0);
}
}
}
......
This test verifies throttler behavior.
Running: testSimpleSchedule
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #1' asSoonAsPossible: false
SCHEDULED: 'operation #2' asSoonAsPossible: undefined
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'operation #2' STARTED.
Process 'operation #2' FINISHED.
Running: testAsSoonAsPossibleOverrideTimeout
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #1' asSoonAsPossible: undefined
SCHEDULED: 'operation #2' asSoonAsPossible: true
Throttler is in TIMEOUT state. Scheduled timers timeouts: [0]
Process 'operation #2' STARTED.
Process 'operation #2' FINISHED.
Running: testAlwaysExecuteLastScheduled
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #0' asSoonAsPossible: true
SCHEDULED: 'operation #1' asSoonAsPossible: false
SCHEDULED: 'operation #2' asSoonAsPossible: true
SCHEDULED: 'operation #3' asSoonAsPossible: false
Throttler is in TIMEOUT state. Scheduled timers timeouts: [0]
Process 'operation #3' STARTED.
Process 'operation #3' FINISHED.
Running: testSimpleScheduleDuringProcess
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'long operation' asSoonAsPossible: undefined
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'long operation' STARTED.
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #1' asSoonAsPossible: false
SCHEDULED: 'operation #2' asSoonAsPossible: undefined
Process 'long operation' FINISHED.
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'operation #2' STARTED.
Process 'operation #2' FINISHED.
Running: testAsSoonAsPossibleOverrideDuringProcess
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'long operation' asSoonAsPossible: undefined
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'long operation' STARTED.
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #1' asSoonAsPossible: undefined
SCHEDULED: 'operation #2' asSoonAsPossible: true
Process 'long operation' FINISHED.
Throttler is in TIMEOUT state. Scheduled timers timeouts: [0]
Process 'operation #2' STARTED.
Process 'operation #2' FINISHED.
Running: testAlwaysExecuteLastScheduledDuringProcess
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'long operation' asSoonAsPossible: undefined
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'long operation' STARTED.
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #0' asSoonAsPossible: true
SCHEDULED: 'operation #1' asSoonAsPossible: false
SCHEDULED: 'operation #2' asSoonAsPossible: true
SCHEDULED: 'operation #3' asSoonAsPossible: false
Process 'long operation' FINISHED.
Throttler is in TIMEOUT state. Scheduled timers timeouts: [0]
Process 'operation #3' STARTED.
Process 'operation #3' FINISHED.
Running: testScheduleFromProcess
Throttler is in IDLE state (doesn't have any timers set up)
SCHEDULED: 'operation #1' asSoonAsPossible: undefined
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'operation #1' STARTED.
SCHEDULED: 'operation #2' asSoonAsPossible: false
Process 'operation #1' FINISHED.
Throttler is in TIMEOUT state. Scheduled timers timeouts: [1989]
Process 'operation #2' STARTED.
Process 'operation #2' FINISHED.
<html>
<head>
<script src="../http/tests/inspector/inspector-test.js"></script>
<script>
function test()
{
var ProcessMock = function(name, runnable)
{
this._runnable = runnable;
this._processName = name;
this._call = this._call.bind(this);
this._call.finish = this._finish.bind(this);
this._call.processName = name;
}
ProcessMock.create = function(name, runnable)
{
var processMock = new ProcessMock(name, runnable);
return processMock._call;
}
ProcessMock.prototype = {
_call: function(finishCallback)
{
InspectorTest.addResult("Process '" + this._processName + "' STARTED.");
this._finishCallback = finishCallback;
if (this._runnable)
this._runnable.call(null);
},
_finish: function()
{
InspectorTest.addResult("Process '" + this._processName + "' FINISHED.");
this._finishCallback();
delete this._finishCallback();
},
}
var throttler = new WebInspector.Throttler(1989);
var timeoutMock = new InspectorTest.TimeoutMock();
throttler._setTimeout = timeoutMock.setTimeout;
throttler._clearTimeout = timeoutMock.clearTimeout;
InspectorTest.addSniffer(throttler, "schedule", logSchedule, true);
function testSimpleSchedule(next, runningProcess)
{
assertThrottlerIdle();
throttler.schedule(ProcessMock.create("operation #1"), false);
var process = ProcessMock.create("operation #2");
throttler.schedule(process);
if (runningProcess)
runningProcess.finish();
assertThrottlerTimeout();
timeoutMock.fireAllTimers();
process.finish();
next();
}
function testAsSoonAsPossibleOverrideTimeout(next, runningProcess)
{
assertThrottlerIdle();
throttler.schedule(ProcessMock.create("operation #1"));
var process = ProcessMock.create("operation #2");
throttler.schedule(process, true);
if (runningProcess)
runningProcess.finish();
assertThrottlerTimeout();
timeoutMock.fireAllTimers();
process.finish();
next();
}
function testAlwaysExecuteLastScheduled(next, runningProcess)
{
assertThrottlerIdle();
var process = null;
for (var i = 0; i < 4; ++i) {
process = ProcessMock.create("operation #" + i);
throttler.schedule(process, i % 2 === 0);
}
if (runningProcess)
runningProcess.finish();
assertThrottlerTimeout();
timeoutMock.fireAllTimers();
process.finish();
next();
}
InspectorTest.runTestSuite([
testSimpleSchedule,
testAsSoonAsPossibleOverrideTimeout,
testAlwaysExecuteLastScheduled,
function testSimpleScheduleDuringProcess(next)
{
var runningProcess = throttlerToRunningState();
testSimpleSchedule(next, runningProcess);
},
function testAsSoonAsPossibleOverrideDuringProcess(next)
{
var runningProcess = throttlerToRunningState();
testAsSoonAsPossibleOverrideTimeout(next, runningProcess);
},
function testAlwaysExecuteLastScheduledDuringProcess(next)
{
var runningProcess = throttlerToRunningState();
testAlwaysExecuteLastScheduled(next, runningProcess);
},
function testScheduleFromProcess(next)
{
var nextProcess;
assertThrottlerIdle();
var process = ProcessMock.create("operation #1", processBody);
throttler.schedule(process);
assertThrottlerTimeout();
timeoutMock.fireAllTimers();
process.finish();
assertThrottlerTimeout();
timeoutMock.fireAllTimers();
nextProcess.finish();
next();
function processBody()
{
nextProcess = ProcessMock.create("operation #2");
throttler.schedule(nextProcess, false);
}
},
]);
function throttlerToRunningState()
{
assertThrottlerIdle();
var process = ProcessMock.create("long operation");
throttler.schedule(process);
assertThrottlerTimeout();
timeoutMock.fireAllTimers();
return process;
}
function assertThrottlerIdle()
{
var timeouts = timeoutMock.activeTimersTimeouts();
if (timeouts.length !== 0) {
InspectorTest.addResult("ERROR: throttler is not in idle state. Scheduled timers timeouts: [" + timeouts.sort().join(", ") + "]");
InspectorTest.completeTest();
return;
}
InspectorTest.addResult("Throttler is in IDLE state (doesn't have any timers set up)");
}
function assertThrottlerTimeout()
{
var timeouts = timeoutMock.activeTimersTimeouts();
if (timeouts.length === 0) {
InspectorTest.addResult("ERROR: throttler is not in timeout state. Scheduled timers timeouts are empty!");
InspectorTest.completeTest();
return;
}
InspectorTest.addResult("Throttler is in TIMEOUT state. Scheduled timers timeouts: [" + timeouts.sort().join(", ") + "]");
}
function logSchedule(operation, asSoonAsPossible)
{
InspectorTest.addResult("SCHEDULED: '" + operation.processName + "' asSoonAsPossible: " + asSoonAsPossible);
}
}
</script>
</head>
<body onload="runTest()">
<p>This test verifies throttler behavior.</p>
</body>
</html>
......@@ -73,6 +73,7 @@
'front_end/common/Progress.js',
'front_end/common/Settings.js',
'front_end/common/TextRange.js',
'front_end/common/Throttler.js',
'front_end/common/UIString.js',
'front_end/common/UserMetrics.js',
'front_end/common/utilities.js',
......
// Copyright 2014 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.
/**
* @constructor
* @param {number} timeout
*/
WebInspector.Throttler = function(timeout)
{
this._timeout = timeout;
this._isRunningProcess = false;
this._asSoonAsPossible = false;
/** @type {?function(!WebInspector.Throttler.FinishCallback)} */
this._process = null;
}
WebInspector.Throttler.prototype = {
_processCompleted: function()
{
this._isRunningProcess = false;
if (this._process)
this._innerSchedule(false);
},
_onTimeout: function()
{
delete this._processTimeout;
this._asSoonAsPossible = false;
this._isRunningProcess = true;
// Process might issue synchronous calls to this throttler.
var process = this._process;
this._process = null;
process(this._processCompleted.bind(this));
},
/**
* @param {function(!WebInspector.Throttler.FinishCallback)} process
* @param {boolean=} asSoonAsPossible
*/
schedule: function(process, asSoonAsPossible)
{
// Deliberately skip previous process.
this._process = process;
var force = !!asSoonAsPossible && !this._asSoonAsPossible;
this._asSoonAsPossible = this._asSoonAsPossible || !!asSoonAsPossible;
this._innerSchedule(force);
},
/**
* @param {boolean} force
*/
_innerSchedule: function(force)
{
if (this._isRunningProcess)
return;
if (this._processTimeout && !force)
return;
if (this._processTimeout)
this._clearTimeout(this._processTimeout);
var timeout = this._asSoonAsPossible ? 0 : this._timeout;
this._processTimeout = this._setTimeout(this._onTimeout.bind(this), timeout);
},
/**
* @param {number} timeoutId
*/
_clearTimeout: function(timeoutId)
{
clearTimeout(timeoutId);
},
/**
* @param {function()} operation
* @param {number} timeout
* @return {number}
*/
_setTimeout: function(operation, timeout)
{
return setTimeout(operation, timeout);
}
}
/** @typedef {function()} */
WebInspector.Throttler.FinishCallback;
......@@ -46,6 +46,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<script type="text/javascript" src="common/ParsedURL.js"></script>
<script type="text/javascript" src="common/Color.js"></script>
<script type="text/javascript" src="common/TextRange.js"></script>
<script type="text/javascript" src="common/Throttler.js"></script>
<script type="text/javascript" src="common/Platform.js"></script>
<script type="text/javascript" src="common/Geometry.js"></script>
<script type="text/javascript" src="common/Settings.js"></script>
......
......@@ -42,6 +42,7 @@ WebInspector.OverridesSupport = function(responsiveDesignAvailable)
this._userAgent = "";
this._pageResizer = null;
this._initialized = false;
this._deviceMetricsThrottler = new WebInspector.Throttler(0);
WebInspector.targetManager.observeTargets(this);
this._responsiveDesignAvailable = responsiveDesignAvailable;
}
......@@ -623,10 +624,9 @@ WebInspector.OverridesSupport.prototype = {
var overrideDeviceResolution = this.settings.overrideDeviceResolution.get();
var emulationEnabled = overrideDeviceResolution || this.settings.emulateViewport.get();
if (responsiveDesignAvailableAndDisabled || !emulationEnabled) {
PageAgent.clearDeviceMetricsOverride(apiCallback.bind(this));
this._deviceMetricsThrottler.schedule(clearDeviceMetricsOverride.bind(this));
if (this._pageResizer && !emulationEnabled)
this._pageResizer.update(0, 0, 0);
this.maybeHasActiveOverridesChanged();
return;
}
......@@ -651,47 +651,41 @@ WebInspector.OverridesSupport.prototype = {
}
}
// Do not emulate resolution more often than 10Hz.
this._setDeviceMetricsTimers = (this._setDeviceMetricsTimers || 0) + 1;
if (overrideWidth || overrideHeight)
setTimeout(setDeviceMetricsOverride.bind(this), 100);
else
setDeviceMetricsOverride.call(this);
this._deviceMetricsThrottler.schedule(setDeviceMetricsOverride.bind(this));
/**
* @param {!WebInspector.Throttler.FinishCallback} finishCallback
* @this {WebInspector.OverridesSupport}
*/
function setDeviceMetricsOverride()
function setDeviceMetricsOverride(finishCallback)
{
// Drop heavy intermediate commands.
this._setDeviceMetricsTimers--;
var isExpensive = overrideWidth || overrideHeight;
if (isExpensive && this._setDeviceMetricsTimers) {
var commandThreshold = 100;
var time = window.performance.now();
if (time - this._lastExpensivePageAgentCommandTime < commandThreshold)
return;
this._lastExpensivePageAgentCommandTime = time;
}
PageAgent.setDeviceMetricsOverride(
overrideWidth, overrideHeight, this.settings.deviceScaleFactor.get(),
this.settings.emulateViewport.get(), this._pageResizer ? false : this.settings.deviceFitWindow.get(),
this.settings.deviceTextAutosizing.get(), this._fontScaleFactor(overrideWidth || dipWidth, overrideHeight || dipHeight),
apiCallback.bind(this));
apiCallback.bind(this, finishCallback));
}
this.maybeHasActiveOverridesChanged();
/**
* @param {!WebInspector.Throttler.FinishCallback} finishCallback
* @this {WebInspector.OverridesSupport}
*/
function clearDeviceMetricsOverride(finishCallback)
{
PageAgent.clearDeviceMetricsOverride(apiCallback.bind(this, finishCallback));
}
/**
* @param {!WebInspector.Throttler.FinishCallback} finishCallback
* @param {?Protocol.Error} error
* @this {WebInspector.OverridesSupport}
*/
function apiCallback(error)
function apiCallback(finishCallback, error)
{
if (error) {
this._updateDeviceMetricsWarningMessage(WebInspector.UIString("Screen emulation is not available on this page."));
this._deviceMetricsOverrideAppliedForTest();
finishCallback();
return;
}
......@@ -702,6 +696,8 @@ WebInspector.OverridesSupport.prototype = {
this._overrideDeviceResolution = overrideDeviceResolution;
this._emulateViewportEnabled = viewportEnabled;
this._deviceMetricsOverrideAppliedForTest();
this.maybeHasActiveOverridesChanged();
finishCallback();
}
},
......
......@@ -323,18 +323,11 @@ WebInspector.StyleFile = function(uiSourceCode, mapping)
this._mapping = mapping;
this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
this._commitThrottler = new WebInspector.Throttler(WebInspector.StyleFile.updateTimeout);
}
WebInspector.StyleFile.updateTimeout = 200;
/**
* @enum {string}
*/
WebInspector.StyleFile.PendingChangeType = {
Major: "Major",
Minor: "Minor"
}
WebInspector.StyleFile.prototype = {
/**
* @param {!WebInspector.Event} event
......@@ -344,8 +337,8 @@ WebInspector.StyleFile.prototype = {
if (this._isAddingRevision)
return;
this._pendingChangeType = WebInspector.StyleFile.PendingChangeType.Major;
this._maybeProcessChange();
this._isMajorChangePending = true;
this._commitThrottler.schedule(this._commitIncrementalEdit.bind(this), true);
},
/**
......@@ -356,59 +349,27 @@ WebInspector.StyleFile.prototype = {
if (this._isAddingRevision)
return;
if (this._pendingChangeType === WebInspector.StyleFile.PendingChangeType.Major)
return;
this._pendingChangeType = WebInspector.StyleFile.PendingChangeType.Minor;
this._maybeProcessChange();
},
_maybeProcessChange: function()
{
if (this._isSettingContent)
return;
if (!this._pendingChangeType)
return;
if (this._pendingChangeType === WebInspector.StyleFile.PendingChangeType.Major) {
this._clearIncrementalUpdateTimer();
delete this._pendingChangeType;
this._commitIncrementalEdit(true);
return;
}
if (this._incrementalUpdateTimer)
return;
this._incrementalUpdateTimer = setTimeout(this._commitIncrementalEdit.bind(this, false), WebInspector.StyleFile.updateTimeout);
this._commitThrottler.schedule(this._commitIncrementalEdit.bind(this), false);
},
/**
* @param {boolean} majorChange
* @param {!WebInspector.Throttler.FinishCallback} finishCallback
*/
_commitIncrementalEdit: function(majorChange)
_commitIncrementalEdit: function(finishCallback)
{
this._clearIncrementalUpdateTimer();
delete this._pendingChangeType;
this._isSettingContent = true;
this._mapping._setStyleContent(this._uiSourceCode, this._uiSourceCode.workingCopy(), majorChange, this._styleContentSet.bind(this));
this._mapping._setStyleContent(this._uiSourceCode, this._uiSourceCode.workingCopy(), this._isMajorChangePending, this._styleContentSet.bind(this, finishCallback));
this._isMajorChangePending = false;
},
/**
* @param {!WebInspector.Throttler.FinishCallback} finishCallback
* @param {?string} error
*/
_styleContentSet: function(error)
_styleContentSet: function(finishCallback, error)
{
if (error)
this._mapping._cssModel.target().consoleModel.showErrorMessage(error);
delete this._isSettingContent;
this._maybeProcessChange();
},
_clearIncrementalUpdateTimer: function()
{
if (!this._incrementalUpdateTimer)
return;
clearTimeout(this._incrementalUpdateTimer);
delete this._incrementalUpdateTimer;
finishCallback();
},
/**
......
......@@ -16,6 +16,7 @@
"common/Progress.js",
"common/Settings.js",
"common/TextRange.js",
"common/Throttler.js",
"common/UIString.js",
"common/UserMetrics.js",
"common/utilities.js",
......
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