 Chromium Code Reviews
 Chromium Code Reviews Issue 2745283002:
  DevTools: merge coverage segments from different instances of same URL  (Closed)
    
  
    Issue 2745283002:
  DevTools: merge coverage segments from different instances of same URL  (Closed) 
  | Index: third_party/WebKit/Source/devtools/front_end/coverage/CoverageModel.js | 
| diff --git a/third_party/WebKit/Source/devtools/front_end/coverage/CoverageModel.js b/third_party/WebKit/Source/devtools/front_end/coverage/CoverageModel.js | 
| index 61d9482b40daf348f5a0937b65f88a3eda4c482a..3ed7d351aacad7c787e8e1018eefd26af0bae5c7 100644 | 
| --- a/third_party/WebKit/Source/devtools/front_end/coverage/CoverageModel.js | 
| +++ b/third_party/WebKit/Source/devtools/front_end/coverage/CoverageModel.js | 
| @@ -8,19 +8,6 @@ Coverage.RangeUseCount; | 
| /** @typedef {{end: number, count: (number|undefined), depth: number}} */ | 
| Coverage.CoverageSegment; | 
| -/** @typedef {{ | 
| - * contentProvider: !Common.ContentProvider, | 
| - * size: number, | 
| - * unusedSize: number, | 
| - * usedSize: number, | 
| - * type: !Coverage.CoverageType, | 
| - * lineOffset: number, | 
| - * columnOffset: number, | 
| - * segments: !Array<!Coverage.CoverageSegment> | 
| - * }} | 
| - */ | 
| -Coverage.CoverageInfo; | 
| - | 
| /** | 
| * @enum {number} | 
| */ | 
| @@ -35,15 +22,19 @@ Coverage.CoverageModel = class extends SDK.SDKModel { | 
| */ | 
| constructor(target) { | 
| super(target); | 
| - this._target = target; | 
| - this._cpuProfilerModel = this._target.model(SDK.CPUProfilerModel); | 
| - this._cssModel = this._target.model(SDK.CSSModel); | 
| + this._cpuProfilerModel = target.model(SDK.CPUProfilerModel); | 
| + this._cssModel = target.model(SDK.CSSModel); | 
| + this._debuggerModel = target.model(SDK.DebuggerModel); | 
| + | 
| + /** @type {!Map<string, !Coverage.URLCoverageInfo>} */ | 
| + this._coverageByURL = new Map(); | 
| } | 
| /** | 
| * @return {boolean} | 
| */ | 
| start() { | 
| + this._coverageByURL.clear(); | 
| if (this._cssModel) | 
| this._cssModel.startRuleUsageTracking(); | 
| if (this._cpuProfilerModel) | 
| @@ -52,62 +43,28 @@ Coverage.CoverageModel = class extends SDK.SDKModel { | 
| } | 
| /** | 
| - * @return {!Promise<!Array<!Coverage.CoverageInfo>>} | 
| + * @return {!Promise<!Array<!Coverage.URLCoverageInfo>>} | 
| */ | 
| async stop() { | 
| - var cssCoverageInfoPromise = this._stopCSSCoverage(); | 
| - var jsCoverageInfoPromise = this._stopJSCoverage(); | 
| - var cssCoverageInfo = await cssCoverageInfoPromise; | 
| - var jsCoverageInfo = await jsCoverageInfoPromise; | 
| - return Coverage.CoverageModel._coalesceByURL(cssCoverageInfo.concat(jsCoverageInfo)); | 
| + await Promise.all([this._stopCSSCoverage(), this._stopJSCoverage()]); | 
| + return Array.from(this._coverageByURL.values()); | 
| } | 
| - /** | 
| - * @param {!Array<!Coverage.CoverageInfo>} coverageInfo | 
| - * @return {!Array<!Coverage.CoverageInfo>} | 
| - */ | 
| - static _coalesceByURL(coverageInfo) { | 
| - coverageInfo.sort((a, b) => (a.contentProvider.contentURL() || '').localeCompare(b.contentProvider.contentURL())); | 
| - var result = []; | 
| - for (var entry of coverageInfo) { | 
| - var url = entry.contentProvider.contentURL(); | 
| - if (!url) | 
| - continue; | 
| - if (result.length && result.peekLast().contentProvider.contentURL() === url) { | 
| - var lastEntry = result.peekLast(); | 
| - lastEntry.size += entry.size; | 
| - lastEntry.usedSize += entry.usedSize; | 
| - lastEntry.unusedSize += entry.unusedSize; | 
| - lastEntry.type |= entry.type; | 
| - } else { | 
| - result.push(entry); | 
| - } | 
| - } | 
| - return result; | 
| - } | 
| - | 
| - /** | 
| - * @return {!Promise<!Array<!Coverage.CoverageInfo>>} | 
| - */ | 
| async _stopJSCoverage() { | 
| if (!this._cpuProfilerModel) | 
| return []; | 
| var coveragePromise = this._cpuProfilerModel.takePreciseCoverage(); | 
| this._cpuProfilerModel.stopPreciseCoverage(); | 
| var rawCoverageData = await coveragePromise; | 
| - return Coverage.CoverageModel._processJSCoverage( | 
| - /** @type !SDK.DebuggerModel */ (SDK.DebuggerModel.fromTarget(this.target())), rawCoverageData); | 
| + this._processJSCoverage(rawCoverageData); | 
| } | 
| /** | 
| - * @param {!SDK.DebuggerModel} debuggerModel | 
| * @param {!Array<!Protocol.Profiler.ScriptCoverage>} scriptsCoverage | 
| - * @return {!Array<!Coverage.CoverageInfo>} | 
| */ | 
| - static _processJSCoverage(debuggerModel, scriptsCoverage) { | 
| - var result = []; | 
| + _processJSCoverage(scriptsCoverage) { | 
| for (var entry of scriptsCoverage) { | 
| - var script = debuggerModel.scriptForId(entry.scriptId); | 
| + var script = this._debuggerModel.scriptForId(entry.scriptId); | 
| if (!script) | 
| continue; | 
| var ranges = []; | 
| @@ -116,10 +73,8 @@ Coverage.CoverageModel = class extends SDK.SDKModel { | 
| ranges.push(range); | 
| } | 
| ranges.sort((a, b) => a.startOffset - b.startOffset); | 
| - result.push(Coverage.CoverageModel._buildCoverageInfo( | 
| - script, script.contentLength, script.lineOffset, script.columnOffset, ranges)); | 
| + this._addCoverage(script, script.contentLength, script.lineOffset, script.columnOffset, ranges); | 
| } | 
| - return result; | 
| } | 
| /** | 
| @@ -168,28 +123,24 @@ Coverage.CoverageModel = class extends SDK.SDKModel { | 
| return result; | 
| } | 
| - /** | 
| - * @return {!Promise<!Array<!Coverage.CoverageInfo>>} | 
| - */ | 
| async _stopCSSCoverage() { | 
| if (!this._cssModel) | 
| return []; | 
| 
alph
2017/03/13 20:59:07
return
 | 
| var rawCoverageData = await this._cssModel.ruleListPromise(); | 
| - return Coverage.CoverageModel._processCSSCoverage( | 
| - /** @type !SDK.CSSModel */ (this._cssModel), rawCoverageData); | 
| + this._processCSSCoverage(rawCoverageData); | 
| } | 
| /** | 
| - * @param {!SDK.CSSModel} cssModel | 
| * @param {!Array<!Protocol.CSS.RuleUsage>} ruleUsageList | 
| - * @return {!Array<!Coverage.CoverageInfo>} | 
| */ | 
| - static _processCSSCoverage(cssModel, ruleUsageList) { | 
| - /** @type {!Map<?SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>} */ | 
| + _processCSSCoverage(ruleUsageList) { | 
| + /** @type {!Map<!SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>} */ | 
| var rulesByStyleSheet = new Map(); | 
| for (var rule of ruleUsageList) { | 
| - var styleSheetHeader = cssModel.styleSheetHeaderForId(rule.styleSheetId); | 
| + var styleSheetHeader = this._cssModel.styleSheetHeaderForId(rule.styleSheetId); | 
| + if (!styleSheetHeader) | 
| + continue; | 
| var ranges = rulesByStyleSheet.get(styleSheetHeader); | 
| if (!ranges) { | 
| ranges = []; | 
| @@ -197,10 +148,13 @@ Coverage.CoverageModel = class extends SDK.SDKModel { | 
| } | 
| ranges.push({startOffset: rule.startOffset, endOffset: rule.endOffset, count: Number(rule.used)}); | 
| } | 
| - return Array.from( | 
| - rulesByStyleSheet.entries(), | 
| - entry => Coverage.CoverageModel._buildCoverageInfo( | 
| - entry[0], entry[0].contentLength, entry[0].startLine, entry[0].startColumn, entry[1])); | 
| + for (var entry of rulesByStyleSheet) { | 
| + var styleSheetHeader = /** @type {!SDK.CSSStyleSheetHeader} */ (entry[0]); | 
| + var ranges = /** @type {!Array<!Coverage.RangeUseCount>} */ (entry[1]); | 
| + this._addCoverage( | 
| + styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn, | 
| + ranges); | 
| + } | 
| } | 
| /** | 
| @@ -209,42 +163,238 @@ Coverage.CoverageModel = class extends SDK.SDKModel { | 
| * @param {number} startLine | 
| * @param {number} startColumn | 
| * @param {!Array<!Coverage.RangeUseCount>} ranges | 
| - * @return {!Coverage.CoverageInfo} | 
| */ | 
| - static _buildCoverageInfo(contentProvider, contentLength, startLine, startColumn, ranges) { | 
| - /** @type Coverage.CoverageType */ | 
| - var coverageType; | 
| + _addCoverage(contentProvider, contentLength, startLine, startColumn, ranges) { | 
| var url = contentProvider.contentURL(); | 
| - if (contentProvider.contentType().isScript()) | 
| - coverageType = Coverage.CoverageType.JavaScript; | 
| - else if (contentProvider.contentType().isStyleSheet()) | 
| - coverageType = Coverage.CoverageType.CSS; | 
| - else | 
| - console.assert(false, `Unexpected resource type ${contentProvider.contentType().name} for ${url}`); | 
| - | 
| + if (!url) | 
| + return; | 
| + var entry = this._coverageByURL.get(url); | 
| + if (!entry) { | 
| + entry = new Coverage.URLCoverageInfo(url); | 
| + this._coverageByURL.set(url, entry); | 
| + } | 
| var segments = Coverage.CoverageModel._convertToDisjointSegments(ranges); | 
| - var usedSize = 0; | 
| - var unusedSize = 0; | 
| + entry.update(contentProvider, contentLength, startLine, startColumn, segments); | 
| + } | 
| +}; | 
| + | 
| +Coverage.URLCoverageInfo = class { | 
| + /** | 
| + * @param {string} url | 
| + */ | 
| + constructor(url) { | 
| + this._url = url; | 
| + /** @type {!Map<string, !Coverage.CoverageInfo>} */ | 
| + this._coverageInfoByLocation = new Map(); | 
| + this._size = 0; | 
| + this._unusedSize = 0; | 
| + this._usedSize = 0; | 
| + /** @type {!Coverage.CoverageType} */ | 
| + this._type; | 
| + } | 
| + | 
| + /** | 
| + * @param {!Common.ContentProvider} contentProvider | 
| + * @param {number} contentLength | 
| + * @param {number} lineOffset | 
| + * @param {number} columnOffset | 
| + * @param {!Array<!Coverage.CoverageSegment>} segments | 
| + */ | 
| + update(contentProvider, contentLength, lineOffset, columnOffset, segments) { | 
| + var key = `${lineOffset}:${columnOffset}`; | 
| + var entry = this._coverageInfoByLocation.get(key); | 
| + | 
| + if (!entry) { | 
| + entry = new Coverage.CoverageInfo(contentProvider, lineOffset, columnOffset); | 
| + this._coverageInfoByLocation.set(key, entry); | 
| + this._size += contentLength; | 
| + this._type |= entry.type(); | 
| + } | 
| + this._usedSize -= entry._usedSize; | 
| + this._unusedSize -= entry._unusedSize; | 
| + entry.mergeCoverage(segments); | 
| + this._usedSize += entry._usedSize; | 
| + this._unusedSize += entry._unusedSize; | 
| + } | 
| + | 
| + /** | 
| + * @return {string} | 
| + */ | 
| + url() { | 
| + return this._url; | 
| + } | 
| + | 
| + /** | 
| + * @return {!Coverage.CoverageType} | 
| + */ | 
| + type() { | 
| + return this._type; | 
| + } | 
| + | 
| + /** | 
| + * @return {number} | 
| + */ | 
| + size() { | 
| + return this._size; | 
| + } | 
| + | 
| + /** | 
| + * @return {number} | 
| + */ | 
| + unusedSize() { | 
| + return this._unusedSize; | 
| + } | 
| + | 
| + /** | 
| + * @return {number} | 
| + */ | 
| + usedSize() { | 
| + return this._usedSize; | 
| + } | 
| + | 
| + /** | 
| + * @return {!Promise<!Array<!{range: !Common.TextRange, count: number}>>} | 
| + */ | 
| + async buildTextRanges() { | 
| + var textRangePromises = []; | 
| + for (var coverageInfo of this._coverageInfoByLocation.values()) | 
| + textRangePromises.push(coverageInfo.buildTextRanges()); | 
| + var allTextRanges = await Promise.all(textRangePromises); | 
| + return [].concat(...allTextRanges); | 
| + } | 
| +}; | 
| + | 
| +Coverage.CoverageInfo = class { | 
| + /** | 
| + * @param {!Common.ContentProvider} contentProvider | 
| + * @param {number} lineOffset | 
| + * @param {number} columnOffset | 
| + */ | 
| + constructor(contentProvider, lineOffset, columnOffset) { | 
| + this._contentProvider = contentProvider; | 
| + this._lineOffset = lineOffset; | 
| + this._columnOffset = columnOffset; | 
| + this._usedSize = 0; | 
| + this._unusedSize = 0; | 
| + | 
| + if (contentProvider.contentType().isScript()) { | 
| + this._coverageType = Coverage.CoverageType.JavaScript; | 
| + } else if (contentProvider.contentType().isStyleSheet()) { | 
| + this._coverageType = Coverage.CoverageType.CSS; | 
| + } else { | 
| + console.assert( | 
| + false, `Unexpected resource type ${contentProvider.contentType().name} for ${contentProvider.contentURL()}`); | 
| + } | 
| + /** !Array<!Coverage.CoverageSegment> */ | 
| + this._segments = []; | 
| + } | 
| + | 
| + /** | 
| + * @return {!Coverage.CoverageType} | 
| + */ | 
| + type() { | 
| + return this._coverageType; | 
| + } | 
| + | 
| + /** | 
| + * @param {!Array<!Coverage.CoverageSegment>} segments | 
| + */ | 
| + mergeCoverage(segments) { | 
| + this._segments = Coverage.CoverageInfo._mergeCoverage(this._segments, segments); | 
| + this._updateStats(); | 
| + } | 
| + | 
| + /** | 
| + * @param {!Array<!Coverage.CoverageSegment>} segmentsA | 
| + * @param {!Array<!Coverage.CoverageSegment>} segmentsB | 
| + */ | 
| + static _mergeCoverage(segmentsA, segmentsB) { | 
| + var result = []; | 
| + | 
| + var indexA = 0; | 
| + var indexB = 0; | 
| + while (indexA < segmentsA.length && indexB < segmentsB.length) { | 
| + var a = segmentsA[indexA]; | 
| + var b = segmentsB[indexB]; | 
| + var count = | 
| + typeof a.count === 'number' || typeof b.count === 'number' ? (a.count || 0) + (b.count || 0) : undefined; | 
| + var depth = Math.max(a.depth, b.depth); | 
| + var end = Math.min(a.end, b.end); | 
| + var last = result.peekLast(); | 
| + if (!last || last.count !== count || last.depth !== depth) | 
| + result.push({end: end, count: count, depth: depth}); | 
| + else | 
| + last.end = end; | 
| + if (a.end <= b.end) | 
| + indexA++; | 
| + if (a.end >= b.end) | 
| + indexB++; | 
| + } | 
| + | 
| + for (; indexA < segmentsA.length; indexA++) | 
| + result.push(segmentsA[indexA]); | 
| + for (; indexB < segmentsB.length; indexB++) | 
| + result.push(segmentsB[indexB]); | 
| + return result; | 
| + } | 
| + | 
| + /** | 
| + * @return {!Promise<!Array<!{range: !Common.TextRange, count: number}>>} | 
| + */ | 
| + async buildTextRanges() { | 
| + var contents = await this._contentProvider.requestContent(); | 
| + if (!contents) | 
| + return; | 
| 
alph
2017/03/13 20:59:07
return [];
 | 
| + var text = new Common.Text(contents); | 
| + var lastOffset = 0; | 
| + var rangesByDepth = []; | 
| + for (var segment of this._segments) { | 
| + if (typeof segment.count !== 'number') { | 
| + lastOffset = segment.end; | 
| + continue; | 
| + } | 
| + var startPosition = text.positionFromOffset(lastOffset); | 
| + var endPosition = text.positionFromOffset(segment.end); | 
| + if (!startPosition.lineNumber) | 
| + startPosition.columnNumber += this._columnOffset; | 
| + startPosition.lineNumber += this._lineOffset; | 
| + if (!endPosition.lineNumber) | 
| + endPosition.columnNumber += this._columnOffset; | 
| + endPosition.lineNumber += this._lineOffset; | 
| + | 
| + var ranges = rangesByDepth[segment.depth - 1]; // depth === 0 => count === undefined | 
| + if (!ranges) { | 
| + ranges = []; | 
| + rangesByDepth[segment.depth - 1] = ranges; | 
| + } | 
| + ranges.push({ | 
| + count: segment.count, | 
| + range: new Common.TextRange( | 
| + startPosition.lineNumber, startPosition.columnNumber, endPosition.lineNumber, endPosition.columnNumber) | 
| + }); | 
| + lastOffset = segment.end; | 
| + } | 
| + var result = []; | 
| + for (var ranges of rangesByDepth) { | 
| + for (var r of ranges) | 
| + result.push({count: r.count, range: r.range}); | 
| + } | 
| + return result; | 
| + } | 
| + | 
| + _updateStats() { | 
| + this._usedSize = 0; | 
| + this._unusedSize = 0; | 
| + | 
| var last = 0; | 
| - for (var segment of segments) { | 
| + for (var segment of this._segments) { | 
| if (typeof segment.count === 'number') { | 
| if (segment.count) | 
| - usedSize += segment.end - last; | 
| + this._usedSize += segment.end - last; | 
| else | 
| - unusedSize += segment.end - last; | 
| + this._unusedSize += segment.end - last; | 
| } | 
| last = segment.end; | 
| } | 
| - var coverageInfo = { | 
| - contentProvider: contentProvider, | 
| - segments: segments, | 
| - type: coverageType, | 
| - size: contentLength, | 
| - usedSize: usedSize, | 
| - unusedSize: unusedSize, | 
| - lineOffset: startLine, | 
| - columnOffset: startColumn | 
| - }; | 
| - return coverageInfo; | 
| } | 
| }; |