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

[devtools] Keep polling for coverage during performance tracing

This CL enables the coverage model to keep polling the back-end for
coverage data while performance tracing is enabled.

Bug: chromium:1004203
Change-Id: I187afa45dadae3aac0d5012fa8a293eb26d438fd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1814825Reviewed-by: default avatarYang Guo <yangguo@chromium.org>
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Cr-Commit-Position: refs/heads/master@{#699718}
parent 2f563ea8
......@@ -49,6 +49,12 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
this._currentPollPromise = null;
/** @type {?boolean} */
this._shouldResumePollingOnResume = false;
/** @type {!Array<!{rawCoverageData:!Array<!Protocol.Profiler.ScriptCoverage>,stamp:number}>} */
this._jsBacklog = [];
/** @type {!Array<!{rawCoverageData:!Array<!Protocol.CSS.RuleUsage>,stamp:number}>} */
this._cssBacklog = [];
/** @type {?boolean} */
this._performanceTraceRecording = false;
}
/**
......@@ -103,12 +109,11 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* @return {!Promise}
*/
async _pollLoop() {
const coveragePollingPeriodMs = 700;
this._clearTimer();
this._currentPollPromise = this._pollAndCallback();
await this._currentPollPromise;
if (this._suspensionState === Coverage.SuspensionState.Active)
this._pollTimer = setTimeout(() => this._pollLoop(), coveragePollingPeriodMs);
if (this._suspensionState === Coverage.SuspensionState.Active || this._performanceTraceRecording)
this._pollTimer = setTimeout(() => this._pollLoop(), Coverage.CoverageModel._coveragePollingPeriodMs);
}
async stopPolling() {
......@@ -123,13 +128,14 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* @return {!Promise<undefined>}
*/
async _pollAndCallback() {
if (this._suspensionState === Coverage.SuspensionState.Suspended)
if (this._suspensionState === Coverage.SuspensionState.Suspended && !this._performanceTraceRecording)
return;
const updates = await this._takeAllCoverage();
// This conditional should never trigger, as all intended ways to stop
// polling are awaiting the `_currentPollPromise` before suspending.
console.assert(
this._suspensionState !== Coverage.SuspensionState.Suspended, 'CoverageModel was suspended while polling.');
this._suspensionState !== Coverage.SuspensionState.Suspended || this._performanceTraceRecording,
'CoverageModel was suspended while polling.');
if (updates.length)
this.dispatchEventToListeners(Coverage.CoverageModel.Events.CoverageUpdated, updates);
}
......@@ -145,12 +151,18 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* Stops polling as preparation for suspension. This function is idempotent
* due because it changes the state to suspending.
* @override
* @param {string=} reason - optionally provide a reason, so the model can respond accordingly
* @return {!Promise<undefined>}
*/
async preSuspendModel() {
async preSuspendModel(reason) {
if (this._suspensionState !== Coverage.SuspensionState.Active)
return;
this._suspensionState = Coverage.SuspensionState.Suspending;
if (reason === 'performance-timeline') {
this._performanceTraceRecording = true;
// Keep polling to the backlog if a performance trace is recorded.
return;
}
if (this._currentPollPromise) {
await this.stopPolling();
this._shouldResumePollingOnResume = true;
......@@ -159,9 +171,10 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
/**
* @override
* @param {string=} reason - optionally provide a reason, so the model can respond accordingly
* @return {!Promise<undefined>}
*/
async suspendModel() {
async suspendModel(reason) {
this._suspensionState = Coverage.SuspensionState.Suspended;
}
......@@ -170,7 +183,6 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* @return {!Promise<undefined>}
*/
async resumeModel() {
this._suspensionState = Coverage.SuspensionState.Active;
}
/**
......@@ -180,9 +192,11 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* @return {!Promise<undefined>}
*/
async postResumeModel() {
this._suspensionState = Coverage.SuspensionState.Active;
this._performanceTraceRecording = false;
if (this._shouldResumePollingOnResume) {
await this.startPolling();
this._shouldResumePollingOnResume = false;
await this.startPolling();
}
}
......@@ -235,25 +249,40 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
async _takeJSCoverage() {
if (!this._cpuProfilerModel)
return [];
let rawCoverageData = await this._cpuProfilerModel.takePreciseCoverage();
const now = Date.now();
let freshRawCoverageData = await this._cpuProfilerModel.takePreciseCoverage();
if (this._bestEffortCoveragePromise) {
const bestEffortCoverage = await this._bestEffortCoveragePromise;
this._bestEffortCoveragePromise = null;
rawCoverageData = bestEffortCoverage.concat(rawCoverageData);
freshRawCoverageData = bestEffortCoverage.concat(freshRawCoverageData);
}
return this._processJSCoverage(rawCoverageData);
if (this._suspensionState !== Coverage.SuspensionState.Active) {
if (freshRawCoverageData.length > 0)
this._jsBacklog.push({rawCoverageData: freshRawCoverageData, stamp: now});
return [];
}
const results = [];
for (const {rawCoverageData, stamp} of this._jsBacklog)
results.push(this._processJSCoverage(rawCoverageData, stamp));
this._jsBacklog = [];
if (freshRawCoverageData.length > 0)
results.push(this._processJSCoverage(freshRawCoverageData, now));
return results.flat();
}
/**
* @param {!Array<!Protocol.Profiler.ScriptCoverage>} scriptsCoverage
* @return {!Array<!Coverage.CoverageInfo>}
*/
_processJSCoverage(scriptsCoverage) {
_processJSCoverage(scriptsCoverage, stamp) {
const updatedEntries = [];
for (const entry of scriptsCoverage) {
const script = this._debuggerModel.scriptForId(entry.scriptId);
if (!script)
continue;
const ranges = [];
let type = Coverage.CoverageType.JavaScript;
for (const func of entry.functions) {
......@@ -268,7 +297,7 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
}
const subentry = this._addCoverage(
script, script.contentLength, script.lineOffset, script.columnOffset, ranges,
/** @type {!Coverage.CoverageType} */ (type));
/** @type {!Coverage.CoverageType} */ (type), stamp);
if (subentry)
updatedEntries.push(subentry);
}
......@@ -281,15 +310,29 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
async _takeCSSCoverage() {
if (!this._cssModel)
return [];
const rawCoverageData = await this._cssModel.takeCoverageDelta();
return this._processCSSCoverage(rawCoverageData);
const now = Date.now();
const freshRawCoverageData = await this._cssModel.takeCoverageDelta();
if (this._suspensionState !== Coverage.SuspensionState.Active) {
if (freshRawCoverageData.length > 0)
this._cssBacklog.push({rawCoverageData: freshRawCoverageData, stamp: now});
return [];
}
const results = [];
for (const {rawCoverageData, stamp} of this._cssBacklog)
results.push(this._processCSSCoverage(rawCoverageData, stamp));
this._cssBacklog = [];
if (freshRawCoverageData.length > 0)
results.push(this._processCSSCoverage(freshRawCoverageData, now));
return results.flat();
}
/**
* @param {!Array<!Protocol.CSS.RuleUsage>} ruleUsageList
* @return {!Array<!Coverage.CoverageInfo>}
*/
_processCSSCoverage(ruleUsageList) {
_processCSSCoverage(ruleUsageList, stamp) {
const updatedEntries = [];
/** @type {!Map<!SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>} */
const rulesByStyleSheet = new Map();
......@@ -309,7 +352,7 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
const ranges = /** @type {!Array<!Coverage.RangeUseCount>} */ (entry[1]);
const subentry = this._addCoverage(
styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn,
ranges, Coverage.CoverageType.CSS);
ranges, Coverage.CoverageType.CSS, stamp);
if (subentry)
updatedEntries.push(subentry);
}
......@@ -320,7 +363,7 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* @param {!Array<!Coverage.RangeUseCount>} ranges
* @return {!Array<!Coverage.CoverageSegment>}
*/
static _convertToDisjointSegments(ranges) {
static _convertToDisjointSegments(ranges, stamp) {
ranges.sort((a, b) => a.startOffset - b.startOffset);
const result = [];
......@@ -355,7 +398,7 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
return;
}
}
result.push({end: end, count: count});
result.push({end: end, count: count, stamp: stamp});
}
return result;
......@@ -370,7 +413,7 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
* @param {!Coverage.CoverageType} type
* @return {?Coverage.CoverageInfo}
*/
_addCoverage(contentProvider, contentLength, startLine, startColumn, ranges, type) {
_addCoverage(contentProvider, contentLength, startLine, startColumn, ranges, type, stamp) {
const url = contentProvider.contentURL();
if (!url)
return null;
......@@ -382,9 +425,9 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
const coverageInfo = urlCoverage._ensureEntry(contentProvider, contentLength, startLine, startColumn, type);
this._coverageByContentProvider.set(contentProvider, coverageInfo);
const segments = Coverage.CoverageModel._convertToDisjointSegments(ranges);
const segments = Coverage.CoverageModel._convertToDisjointSegments(ranges, stamp);
if (segments.length && segments.peekLast().end < contentLength)
segments.push({end: contentLength});
segments.push({end: contentLength, stamp: stamp});
const oldUsedSize = coverageInfo._usedSize;
coverageInfo.mergeCoverage(segments);
if (coverageInfo._usedSize === oldUsedSize)
......@@ -464,13 +507,16 @@ Coverage.CoverageModel = class extends SDK.SDKModel {
}
};
SDK.SDKModel.register(Coverage.CoverageModel, SDK.Target.Capability.None, false);
/** @enum {symbol} */
Coverage.CoverageModel.Events = {
CoverageUpdated: Symbol('CoverageUpdated')
};
/** @type {number} */
Coverage.CoverageModel._coveragePollingPeriodMs = 200;
SDK.SDKModel.register(Coverage.CoverageModel, SDK.Target.Capability.None, false);
Coverage.URLCoverageInfo = class {
/**
* @param {string} url
......@@ -528,6 +574,10 @@ Coverage.URLCoverageInfo = class {
return this._isContentScript;
}
entries() {
return this._coverageInfoByLocation.values();
}
/**
* @param {!Common.ContentProvider} contentProvider
* @param {number} contentLength
......@@ -572,6 +622,7 @@ Coverage.CoverageInfo = class {
this._contentProvider = contentProvider;
this._size = size;
this._usedSize = 0;
this._statsByTimestamp = new Map();
this._lineOffset = lineOffset;
this._columnOffset = columnOffset;
this._coverageType = type;
......@@ -609,6 +660,14 @@ Coverage.CoverageInfo = class {
this._updateStats();
}
usedByTimestamp() {
return this._statsByTimestamp;
}
size() {
return this._size;
}
/**
* @param {number} start
* @param {number} end
......@@ -639,8 +698,9 @@ Coverage.CoverageInfo = class {
typeof a.count === 'number' || typeof b.count === 'number' ? (a.count || 0) + (b.count || 0) : undefined;
const end = Math.min(a.end, b.end);
const last = result.peekLast();
if (!last || last.count !== count)
result.push({end: end, count: count});
const stamp = Math.min(a.stamp, b.stamp);
if (!last || last.count !== count || last.stamp !== stamp)
result.push({end: end, count: count, stamp: stamp});
else
last.end = end;
if (a.end <= b.end)
......@@ -657,12 +717,19 @@ Coverage.CoverageInfo = class {
}
_updateStats() {
this._statsByTimestamp = new Map();
this._usedSize = 0;
let last = 0;
for (const segment of this._segments) {
if (segment.count)
this._usedSize += segment.end - last;
if (!this._statsByTimestamp.has(segment.stamp))
this._statsByTimestamp.set(segment.stamp, 0);
if (segment.count) {
const used = segment.end - last;
this._usedSize += used;
this._statsByTimestamp.set(segment.stamp, this._statsByTimestamp.get(segment.stamp) + used);
}
last = segment.end;
}
}
......
......@@ -198,15 +198,16 @@ SDK.Target = class extends Protocol.TargetBase {
}
/**
* @param {string=} reason - optionally provide a reason, so models can respond accordingly
* @return {!Promise}
*/
async suspend() {
async suspend(reason) {
if (this._isSuspended)
return Promise.resolve();
this._isSuspended = true;
await Promise.all(Array.from(this.models().values(), m => m.preSuspendModel()));
await Promise.all(Array.from(this.models().values(), m => m.suspendModel()));
await Promise.all(Array.from(this.models().values(), m => m.preSuspendModel(reason)));
await Promise.all(Array.from(this.models().values(), m => m.suspendModel(reason)));
}
/**
......@@ -285,16 +286,18 @@ SDK.SDKModel = class extends Common.Object {
/**
* Override this method to perform tasks that are required to suspend the
* model and that still need other models in an unsuspended state.
* @param {string=} reason - optionally provide a reason, the model can respond accordingly
* @return {!Promise}
*/
preSuspendModel() {
preSuspendModel(reason) {
return Promise.resolve();
}
/**
* @param {string=} reason - optionally provide a reason, the model can respond accordingly
* @return {!Promise}
*/
suspendModel() {
suspendModel(reason) {
return Promise.resolve();
}
......
......@@ -19,14 +19,15 @@ SDK.TargetManager = class extends Common.Object {
}
/**
* @param {string=} reason - optionally provide a reason, so targets can respond accordingly
* @return {!Promise}
*/
suspendAllTargets() {
suspendAllTargets(reason) {
if (this._isSuspended)
return Promise.resolve();
this._isSuspended = true;
this.dispatchEventToListeners(SDK.TargetManager.Events.SuspendStateChanged);
return Promise.all(this._targets.map(target => target.suspend()));
return Promise.all(this._targets.map(target => target.suspend(reason)));
}
/**
......
......@@ -187,7 +187,7 @@ Timeline.TimelineController = class {
// There might be a significant delay in the beginning of timeline recording
// caused by starting CPU profiler, that needs to traverse JS heap to collect
// all the functions data.
await SDK.targetManager.suspendAllTargets();
await SDK.targetManager.suspendAllTargets('performance-timeline');
if (enableJSSampling && Runtime.queryParam('timelineTracingJSProfileDisabled'))
await this._startProfilingOnAllModels();
if (!this._tracingManager)
......
......@@ -3,22 +3,35 @@ Tests the merge of disjoint segment lists in CoverageModel.
A: []
B: []
merged: []
A: [{"end":10,"count":1}]
A: [{"end":10,"count":1,"stamp":100}]
B: []
merged: [{"end":10,"count":1}]
A: [{"end":10,"count":1}]
B: [{"end":10,"count":1}]
merged: [{"end":10,"count":2}]
A: [{"end":10,"count":1}]
B: [{"end":20,"count":1}]
merged: [{"end":10,"count":2},{"end":20,"count":1}]
A: [{"end":10,"count":1},{"end":20,"count":1}]
merged: [{"end":10,"count":1,"stamp":100}]
A: [{"end":10,"count":1,"stamp":100}]
B: [{"end":10,"count":1,"stamp":100}]
merged: [{"end":10,"count":2,"stamp":100}]
A: [{"end":10,"count":1,"stamp":100}]
B: [{"end":20,"count":1,"stamp":100}]
merged: [{"end":10,"count":2,"stamp":100},{"end":20,"count":1,"stamp":100}]
A: [{"end":10,"count":1,"stamp":100},{"end":20,"count":1,"stamp":100}]
B: []
merged: [{"end":10,"count":1},{"end":20,"count":1}]
A: [{"end":30,"count":1}]
B: [{"end":10},{"end":20,"count":2}]
merged: [{"end":10,"count":1},{"end":20,"count":3},{"end":30,"count":1}]
A: [{"end":30}]
B: [{"end":10},{"end":20,"count":2}]
merged: [{"end":10},{"end":20,"count":2},{"end":30}]
merged: [{"end":10,"count":1,"stamp":100},{"end":20,"count":1,"stamp":100}]
A: [{"end":30,"count":1,"stamp":100}]
B: [{"end":10,"stamp":100},{"end":20,"count":2,"stamp":100}]
merged: [{"end":10,"count":1,"stamp":100},{"end":20,"count":3,"stamp":100},{"end":30,"count":1,"stamp":100}]
A: [{"end":30,"stamp":100}]
B: [{"end":10,"stamp":100},{"end":20,"count":2,"stamp":100}]
merged: [{"end":10,"stamp":100},{"end":20,"count":2,"stamp":100},{"end":30,"stamp":100}]
Merging different stamps should result in the minimum timestamp
A: [{"end":10,"count":1,"stamp":100}]
B: [{"end":10,"count":1,"stamp":200}]
merged: [{"end":10,"count":2,"stamp":100}]
A: [{"end":10,"count":1,"stamp":100}]
B: [{"end":20,"count":1,"stamp":200}]
merged: [{"end":10,"count":2,"stamp":100},{"end":20,"count":1,"stamp":200}]
A: [{"end":10,"count":1,"stamp":100},{"end":20,"count":1,"stamp":200}]
B: []
merged: [{"end":10,"count":1,"stamp":100},{"end":20,"count":1,"stamp":200}]
A: [{"end":30,"count":1,"stamp":100}]
B: [{"end":10,"stamp":100},{"end":20,"count":2,"stamp":200}]
merged: [{"end":10,"count":1,"stamp":100},{"end":20,"count":3,"stamp":100},{"end":30,"count":1,"stamp":100}]
......@@ -7,12 +7,18 @@
await TestRunner.loadModule('coverage');
testAndDump([], []);
testAndDump([{end: 10, count: 1}], []);
testAndDump([{end: 10, count: 1}], [{end: 10, count: 1}]);
testAndDump([{end: 10, count: 1}], [{end: 20, count: 1}]);
testAndDump([{end: 10, count: 1}, {end: 20, count: 1}], []);
testAndDump([{end: 30, count: 1}], [{end: 10, count: undefined}, {end: 20, count: 2}]);
testAndDump([{end: 30, count: undefined}], [{end: 10, count: undefined}, {end: 20, count: 2}]);
testAndDump([{end: 10, count: 1, stamp: 100}], []);
testAndDump([{end: 10, count: 1, stamp: 100}], [{end: 10, count: 1, stamp: 100}]);
testAndDump([{end: 10, count: 1, stamp: 100}], [{end: 20, count: 1, stamp: 100}]);
testAndDump([{end: 10, count: 1, stamp: 100}, {end: 20, count: 1, stamp: 100}], []);
testAndDump([{end: 30, count: 1, stamp: 100}], [{end: 10, count: undefined, stamp: 100}, {end: 20, count: 2, stamp: 100}]);
testAndDump([{end: 30, count: undefined, stamp: 100}], [{end: 10, count: undefined, stamp: 100}, {end: 20, count: 2, stamp: 100}]);
TestRunner.addResult(`Merging different stamps should result in the minimum timestamp`);
testAndDump([{end: 10, count: 1, stamp: 100}], [{end: 10, count: 1, stamp: 200}]);
testAndDump([{end: 10, count: 1, stamp: 100}], [{end: 20, count: 1, stamp: 200}]);
testAndDump([{end: 10, count: 1, stamp: 100}, {end: 20, count: 1, stamp: 200}], []);
testAndDump([{end: 30, count: 1, stamp: 100}], [{end: 10, count: undefined, stamp: 100}, {end: 20, count: 2, stamp: 200}]);
TestRunner.completeTest();
......
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