| // Copyright (c) 2017 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. |
| |
| /** @typedef {{startOffset: number, endOffset: number, count: number}} */ |
| Coverage.RangeUseCount; |
| |
| /** @typedef {{end: number, count: (number|undefined)}} */ |
| Coverage.CoverageSegment; |
| |
| /** |
| * @enum {number} |
| */ |
| export const CoverageType = { |
| CSS: (1 << 0), |
| JavaScript: (1 << 1), |
| JavaScriptPerFunction: (1 << 2), |
| }; |
| |
| /** @enum {symbol} */ |
| export const SuspensionState = { |
| Active: Symbol('Active'), |
| Suspending: Symbol('Suspending'), |
| Suspended: Symbol('Suspended') |
| }; |
| |
| /** @enum {symbol} */ |
| export const Events = { |
| CoverageUpdated: Symbol('CoverageUpdated'), |
| CoverageReset: Symbol('CoverageReset'), |
| }; |
| |
| /** @type {number} */ |
| const _coveragePollingPeriodMs = 200; |
| |
| export default class CoverageModel extends SDK.SDKModel { |
| /** |
| * @param {!SDK.Target} target |
| */ |
| constructor(target) { |
| super(target); |
| 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(); |
| /** @type {!Map<!Common.ContentProvider, !Coverage.CoverageInfo>} */ |
| this._coverageByContentProvider = new Map(); |
| |
| /** @type {!Coverage.SuspensionState} */ |
| this._suspensionState = SuspensionState.Active; |
| /** @type {?number} */ |
| this._pollTimer = null; |
| /** @type {?Promise} */ |
| 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; |
| } |
| |
| /** |
| * @param {boolean} jsCoveragePerBlock - Collect per Block coverage if `true`, per function coverage otherwise. |
| * @return {!Promise<boolean>} |
| */ |
| async start(jsCoveragePerBlock) { |
| if (this._suspensionState !== SuspensionState.Active) { |
| throw Error('Cannot start CoverageModel while it is not active.'); |
| } |
| const promises = []; |
| if (this._cssModel) { |
| // Note there's no JS coverage since JS won't ever return |
| // coverage twice, even after it's restarted. |
| this._clearCSS(); |
| |
| this._cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetAdded, this._handleStyleSheetAdded, this); |
| promises.push(this._cssModel.startCoverage()); |
| } |
| if (this._cpuProfilerModel) { |
| promises.push(this._cpuProfilerModel.startPreciseCoverage(jsCoveragePerBlock)); |
| } |
| |
| await Promise.all(promises); |
| return !!(this._cssModel || this._cpuProfilerModel); |
| } |
| |
| /** |
| * @return {!Promise} |
| */ |
| async stop() { |
| await this.stopPolling(); |
| const promises = []; |
| if (this._cpuProfilerModel) { |
| promises.push(this._cpuProfilerModel.stopPreciseCoverage()); |
| } |
| if (this._cssModel) { |
| promises.push(this._cssModel.stopCoverage()); |
| this._cssModel.removeEventListener(SDK.CSSModel.Events.StyleSheetAdded, this._handleStyleSheetAdded, this); |
| } |
| await Promise.all(promises); |
| } |
| |
| reset() { |
| this._coverageByURL = new Map(); |
| this._coverageByContentProvider = new Map(); |
| this.dispatchEventToListeners(CoverageModel.Events.CoverageReset); |
| } |
| |
| /** |
| * @return {!Promise} |
| */ |
| async startPolling() { |
| if (this._currentPollPromise || this._suspensionState !== SuspensionState.Active) { |
| return; |
| } |
| await this._pollLoop(); |
| } |
| |
| /** |
| * @return {!Promise} |
| */ |
| async _pollLoop() { |
| this._clearTimer(); |
| this._currentPollPromise = this._pollAndCallback(); |
| await this._currentPollPromise; |
| if (this._suspensionState === SuspensionState.Active || this._performanceTraceRecording) { |
| this._pollTimer = setTimeout(() => this._pollLoop(), _coveragePollingPeriodMs); |
| } |
| } |
| |
| async stopPolling() { |
| this._clearTimer(); |
| await this._currentPollPromise; |
| this._currentPollPromise = null; |
| // Do one last poll to get the final data. |
| await this._pollAndCallback(); |
| } |
| |
| /** |
| * @return {!Promise<undefined>} |
| */ |
| async _pollAndCallback() { |
| if (this._suspensionState === 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 !== SuspensionState.Suspended || this._performanceTraceRecording, |
| 'CoverageModel was suspended while polling.'); |
| if (updates.length) { |
| this.dispatchEventToListeners(Events.CoverageUpdated, updates); |
| } |
| } |
| |
| _clearTimer() { |
| if (this._pollTimer) { |
| clearTimeout(this._pollTimer); |
| this._pollTimer = null; |
| } |
| } |
| |
| /** |
| * 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(reason) { |
| if (this._suspensionState !== SuspensionState.Active) { |
| return; |
| } |
| this._suspensionState = 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; |
| } |
| } |
| |
| /** |
| * @override |
| * @param {string=} reason - optionally provide a reason, so the model can respond accordingly |
| * @return {!Promise<undefined>} |
| */ |
| async suspendModel(reason) { |
| this._suspensionState = SuspensionState.Suspended; |
| } |
| |
| /** |
| * @override |
| * @return {!Promise<undefined>} |
| */ |
| async resumeModel() { |
| } |
| |
| /** |
| * Restarts polling after suspension. Note that the function is idempotent |
| * because starting polling is idempotent. |
| * @override |
| * @return {!Promise<undefined>} |
| */ |
| async postResumeModel() { |
| this._suspensionState = SuspensionState.Active; |
| this._performanceTraceRecording = false; |
| if (this._shouldResumePollingOnResume) { |
| this._shouldResumePollingOnResume = false; |
| await this.startPolling(); |
| } |
| } |
| |
| /** |
| * @return {!Array<!Coverage.URLCoverageInfo>} |
| */ |
| entries() { |
| return Array.from(this._coverageByURL.values()); |
| } |
| |
| /** |
| * |
| * @param {string} url |
| * @return {?Coverage.URLCoverageInfo} |
| */ |
| getCoverageForUrl(url) { |
| return this._coverageByURL.get(url); |
| } |
| |
| /** |
| * @param {!Common.ContentProvider} contentProvider |
| * @param {number} startOffset |
| * @param {number} endOffset |
| * @return {boolean|undefined} |
| */ |
| usageForRange(contentProvider, startOffset, endOffset) { |
| const coverageInfo = this._coverageByContentProvider.get(contentProvider); |
| return coverageInfo && coverageInfo.usageForRange(startOffset, endOffset); |
| } |
| |
| _clearCSS() { |
| for (const entry of this._coverageByContentProvider.values()) { |
| if (entry.type() !== CoverageType.CSS) { |
| continue; |
| } |
| const contentProvider = /** @type {!SDK.CSSStyleSheetHeader} */ (entry.contentProvider()); |
| this._coverageByContentProvider.delete(contentProvider); |
| const key = `${contentProvider.startLine}:${contentProvider.startColumn}`; |
| const urlEntry = this._coverageByURL.get(entry.url()); |
| if (!urlEntry || !urlEntry._coverageInfoByLocation.delete(key)) { |
| continue; |
| } |
| urlEntry._addToSizes(-entry._usedSize, -entry._size); |
| if (!urlEntry._coverageInfoByLocation.size) { |
| this._coverageByURL.delete(entry.url()); |
| } |
| } |
| |
| for (const styleSheetHeader of this._cssModel.getAllStyleSheetHeaders()) { |
| this._addStyleSheetToCSSCoverage(styleSheetHeader); |
| } |
| } |
| |
| /** |
| * @return {!Promise<!Array<!Coverage.CoverageInfo>>} |
| */ |
| async _takeAllCoverage() { |
| const [updatesCSS, updatesJS] = await Promise.all([this._takeCSSCoverage(), this._takeJSCoverage()]); |
| return [...updatesCSS, ...updatesJS]; |
| } |
| |
| /** |
| * @return {!Promise<!Array<!Coverage.CoverageInfo>>} |
| */ |
| async _takeJSCoverage() { |
| if (!this._cpuProfilerModel) { |
| return []; |
| } |
| const now = Date.now(); |
| const freshRawCoverageData = await this._cpuProfilerModel.takePreciseCoverage(); |
| if (this._suspensionState !== 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, stamp) { |
| const updatedEntries = []; |
| for (const entry of scriptsCoverage) { |
| const script = this._debuggerModel.scriptForId(entry.scriptId); |
| if (!script) { |
| continue; |
| } |
| |
| const ranges = []; |
| let type = CoverageType.JavaScript; |
| for (const func of entry.functions) { |
| // Do not coerce undefined to false, i.e. only consider blockLevel to be false |
| // if back-end explicitly provides blockLevel field, otherwise presume blockLevel |
| // coverage is not available. Also, ignore non-block level functions that weren't |
| // ever called. |
| if (func.isBlockCoverage === false && !(func.ranges.length === 1 && !func.ranges[0].count)) { |
| type |= CoverageType.JavaScriptPerFunction; |
| } |
| for (const range of func.ranges) { |
| ranges.push(range); |
| } |
| } |
| const subentry = this._addCoverage( |
| script, script.contentLength, script.lineOffset, script.columnOffset, ranges, |
| /** @type {!Coverage.CoverageType} */ (type), stamp); |
| if (subentry) { |
| updatedEntries.push(subentry); |
| } |
| } |
| return updatedEntries; |
| } |
| |
| _handleStyleSheetAdded(event) { |
| const styleSheetHeader = /** @type {!SDK.CSSStyleSheetHeader} */ (event.data); |
| |
| this._addStyleSheetToCSSCoverage(styleSheetHeader); |
| } |
| |
| /** |
| * @return {!Promise<!Array<!Coverage.CoverageInfo>>} |
| */ |
| async _takeCSSCoverage() { |
| if (!this._cssModel) { |
| return []; |
| } |
| const now = Date.now(); |
| const freshRawCoverageData = await this._cssModel.takeCoverageDelta(); |
| if (this._suspensionState !== 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, stamp) { |
| const updatedEntries = []; |
| /** @type {!Map<!SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>} */ |
| const rulesByStyleSheet = new Map(); |
| for (const rule of ruleUsageList) { |
| const styleSheetHeader = this._cssModel.styleSheetHeaderForId(rule.styleSheetId); |
| if (!styleSheetHeader) { |
| continue; |
| } |
| let ranges = rulesByStyleSheet.get(styleSheetHeader); |
| if (!ranges) { |
| ranges = []; |
| rulesByStyleSheet.set(styleSheetHeader, ranges); |
| } |
| ranges.push({startOffset: rule.startOffset, endOffset: rule.endOffset, count: Number(rule.used)}); |
| } |
| for (const entry of rulesByStyleSheet) { |
| const styleSheetHeader = /** @type {!SDK.CSSStyleSheetHeader} */ (entry[0]); |
| const ranges = /** @type {!Array<!Coverage.RangeUseCount>} */ (entry[1]); |
| const subentry = this._addCoverage( |
| styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn, |
| ranges, CoverageType.CSS, stamp); |
| if (subentry) { |
| updatedEntries.push(subentry); |
| } |
| } |
| return updatedEntries; |
| } |
| |
| /** |
| * @param {!Array<!Coverage.RangeUseCount>} ranges |
| * @return {!Array<!Coverage.CoverageSegment>} |
| */ |
| static _convertToDisjointSegments(ranges, stamp) { |
| ranges.sort((a, b) => a.startOffset - b.startOffset); |
| |
| const result = []; |
| const stack = []; |
| for (const entry of ranges) { |
| let top = stack.peekLast(); |
| while (top && top.endOffset <= entry.startOffset) { |
| append(top.endOffset, top.count); |
| stack.pop(); |
| top = stack.peekLast(); |
| } |
| append(entry.startOffset, top ? top.count : undefined); |
| stack.push(entry); |
| } |
| |
| while (stack.length) { |
| const top = stack.pop(); |
| append(top.endOffset, top.count); |
| } |
| |
| /** |
| * @param {number} end |
| * @param {number} count |
| */ |
| function append(end, count) { |
| const last = result.peekLast(); |
| if (last) { |
| if (last.end === end) { |
| return; |
| } |
| if (last.count === count) { |
| last.end = end; |
| return; |
| } |
| } |
| result.push({end: end, count: count, stamp: stamp}); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * @param {!SDK.CSSStyleSheetHeader} styleSheetHeader |
| */ |
| _addStyleSheetToCSSCoverage(styleSheetHeader) { |
| this._addCoverage( |
| styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn, [], |
| CoverageType.CSS, Date.now()); |
| } |
| |
| /** |
| * @param {!Common.ContentProvider} contentProvider |
| * @param {number} contentLength |
| * @param {number} startLine |
| * @param {number} startColumn |
| * @param {!Array<!Coverage.RangeUseCount>} ranges |
| * @param {!Coverage.CoverageType} type |
| * @return {?Coverage.CoverageInfo} |
| */ |
| _addCoverage(contentProvider, contentLength, startLine, startColumn, ranges, type, stamp) { |
| const url = contentProvider.contentURL(); |
| if (!url) { |
| return null; |
| } |
| let urlCoverage = this._coverageByURL.get(url); |
| let isNewUrlCoverage = false; |
| if (!urlCoverage) { |
| isNewUrlCoverage = true; |
| urlCoverage = new Coverage.URLCoverageInfo(url); |
| this._coverageByURL.set(url, urlCoverage); |
| } |
| |
| const coverageInfo = urlCoverage._ensureEntry(contentProvider, contentLength, startLine, startColumn, type); |
| this._coverageByContentProvider.set(contentProvider, coverageInfo); |
| const segments = Coverage.CoverageModel._convertToDisjointSegments(ranges, stamp); |
| if (segments.length && segments.peekLast().end < contentLength) { |
| segments.push({end: contentLength, stamp: stamp}); |
| } |
| const oldUsedSize = coverageInfo._usedSize; |
| coverageInfo.mergeCoverage(segments); |
| if (!isNewUrlCoverage && coverageInfo._usedSize === oldUsedSize) { |
| return null; |
| } |
| urlCoverage._addToSizes(coverageInfo._usedSize - oldUsedSize, 0); |
| return coverageInfo; |
| } |
| |
| /** |
| * @param {!Bindings.FileOutputStream} fos |
| */ |
| async exportReport(fos) { |
| const result = []; |
| function locationCompare(a, b) { |
| const [aLine, aPos] = a.split(':'); |
| const [bLine, bPos] = b.split(':'); |
| return aLine - bLine || aPos - bPos; |
| } |
| const coverageByUrlKeys = Array.from(this._coverageByURL.keys()).sort(); |
| for (const urlInfoKey of coverageByUrlKeys) { |
| const urlInfo = this._coverageByURL.get(urlInfoKey); |
| const url = urlInfo.url(); |
| if (url.startsWith('extensions::') || url.startsWith('chrome-extension://')) { |
| continue; |
| } |
| |
| // For .html resources, multiple scripts share URL, but have different offsets. |
| let useFullText = false; |
| for (const info of urlInfo._coverageInfoByLocation.values()) { |
| if (info._lineOffset || info._columnOffset) { |
| useFullText = !!url; |
| break; |
| } |
| } |
| |
| let fullText = null; |
| if (useFullText) { |
| const resource = SDK.ResourceTreeModel.resourceForURL(url); |
| const content = (await resource.requestContent()).content; |
| fullText = resource ? new TextUtils.Text(content || '') : null; |
| } |
| |
| const coverageByLocationKeys = Array.from(urlInfo._coverageInfoByLocation.keys()).sort(locationCompare); |
| |
| // We have full text for this resource, resolve the offsets using the text line endings. |
| if (fullText) { |
| const entry = {url, ranges: [], text: fullText.value()}; |
| for (const infoKey of coverageByLocationKeys) { |
| const info = urlInfo._coverageInfoByLocation.get(infoKey); |
| const offset = fullText ? fullText.offsetFromPosition(info._lineOffset, info._columnOffset) : 0; |
| let start = 0; |
| for (const segment of info._segments) { |
| if (segment.count) { |
| entry.ranges.push({start: start + offset, end: segment.end + offset}); |
| } else { |
| start = segment.end; |
| } |
| } |
| } |
| result.push(entry); |
| continue; |
| } |
| |
| // Fall back to the per-script operation. |
| for (const infoKey of coverageByLocationKeys) { |
| const info = urlInfo._coverageInfoByLocation.get(infoKey); |
| const entry = {url, ranges: [], text: (await info.contentProvider().requestContent()).content}; |
| let start = 0; |
| for (const segment of info._segments) { |
| if (segment.count) { |
| entry.ranges.push({start: start, end: segment.end}); |
| } else { |
| start = segment.end; |
| } |
| } |
| result.push(entry); |
| } |
| } |
| await fos.write(JSON.stringify(result, undefined, 2)); |
| fos.close(); |
| } |
| } |
| |
| SDK.SDKModel.register(CoverageModel, SDK.Target.Capability.None, false); |
| |
| /** |
| * @unrestricted |
| */ |
| export class URLCoverageInfo extends Common.Object { |
| /** |
| * @param {string} url |
| */ |
| constructor(url) { |
| super(); |
| |
| this._url = url; |
| /** @type {!Map<string, !Coverage.CoverageInfo>} */ |
| this._coverageInfoByLocation = new Map(); |
| this._size = 0; |
| this._usedSize = 0; |
| /** @type {!Coverage.CoverageType} */ |
| this._type; |
| this._isContentScript = false; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| url() { |
| return this._url; |
| } |
| |
| /** |
| * @return {!Coverage.CoverageType} |
| */ |
| type() { |
| return this._type; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| size() { |
| return this._size; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| usedSize() { |
| return this._usedSize; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| unusedSize() { |
| return this._size - this._usedSize; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| usedPercentage() { |
| // Per convention, empty files are reported as 100 % uncovered |
| if (this._size === 0) { |
| return 0; |
| } |
| return this.usedSize() / this.size() * 100; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| unusedPercentage() { |
| // Per convention, empty files are reported as 100 % uncovered |
| if (this._size === 0) { |
| return 100; |
| } |
| return this.unusedSize() / this.size() * 100; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isContentScript() { |
| return this._isContentScript; |
| } |
| |
| entries() { |
| return this._coverageInfoByLocation.values(); |
| } |
| |
| _addToSizes(usedSize, size) { |
| this._usedSize += usedSize; |
| this._size += size; |
| |
| if (usedSize !== 0 || size !== 0) { |
| this.dispatchEventToListeners(Coverage.URLCoverageInfo.Events.SizesChanged); |
| } |
| } |
| |
| /** |
| * @param {!Common.ContentProvider} contentProvider |
| * @param {number} contentLength |
| * @param {number} lineOffset |
| * @param {number} columnOffset |
| * @param {!Coverage.CoverageType} type |
| * @return {!Coverage.CoverageInfo} |
| */ |
| _ensureEntry(contentProvider, contentLength, lineOffset, columnOffset, type) { |
| const key = `${lineOffset}:${columnOffset}`; |
| let entry = this._coverageInfoByLocation.get(key); |
| |
| if ((type & CoverageType.JavaScript) && !this._coverageInfoByLocation.size) { |
| this._isContentScript = /** @type {!SDK.Script} */ (contentProvider).isContentScript(); |
| } |
| this._type |= type; |
| |
| if (entry) { |
| entry._coverageType |= type; |
| return entry; |
| } |
| |
| if ((type & CoverageType.JavaScript) && !this._coverageInfoByLocation.size) { |
| this._isContentScript = /** @type {!SDK.Script} */ (contentProvider).isContentScript(); |
| } |
| |
| entry = new Coverage.CoverageInfo(contentProvider, contentLength, lineOffset, columnOffset, type); |
| this._coverageInfoByLocation.set(key, entry); |
| this._addToSizes(0, contentLength); |
| |
| return entry; |
| } |
| } |
| |
| /** @enum {symbol} */ |
| URLCoverageInfo.Events = { |
| SizesChanged: Symbol('SizesChanged') |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| export class CoverageInfo { |
| /** |
| * @param {!Common.ContentProvider} contentProvider |
| * @param {number} size |
| * @param {number} lineOffset |
| * @param {number} columnOffset |
| * @param {!Coverage.CoverageType} type |
| */ |
| constructor(contentProvider, size, lineOffset, columnOffset, type) { |
| this._contentProvider = contentProvider; |
| this._size = size; |
| this._usedSize = 0; |
| this._statsByTimestamp = new Map(); |
| this._lineOffset = lineOffset; |
| this._columnOffset = columnOffset; |
| this._coverageType = type; |
| |
| /** !Array<!Coverage.CoverageSegment> */ |
| this._segments = []; |
| } |
| |
| /** |
| * @return {!Common.ContentProvider} |
| */ |
| contentProvider() { |
| return this._contentProvider; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| url() { |
| return this._contentProvider.contentURL(); |
| } |
| |
| /** |
| * @return {!Coverage.CoverageType} |
| */ |
| type() { |
| return this._coverageType; |
| } |
| |
| /** |
| * @param {!Array<!Coverage.CoverageSegment>} segments |
| */ |
| mergeCoverage(segments) { |
| this._segments = Coverage.CoverageInfo._mergeCoverage(this._segments, segments); |
| this._updateStats(); |
| } |
| |
| usedByTimestamp() { |
| return this._statsByTimestamp; |
| } |
| |
| size() { |
| return this._size; |
| } |
| |
| /** |
| * @param {number} start |
| * @param {number} end |
| * @return {boolean} |
| */ |
| usageForRange(start, end) { |
| let index = this._segments.upperBound(start, (position, segment) => position - segment.end); |
| for (; index < this._segments.length && this._segments[index].end < end; ++index) { |
| if (this._segments[index].count) { |
| return true; |
| } |
| } |
| return index < this._segments.length && !!this._segments[index].count; |
| } |
| |
| /** |
| * @param {!Array<!Coverage.CoverageSegment>} segmentsA |
| * @param {!Array<!Coverage.CoverageSegment>} segmentsB |
| */ |
| static _mergeCoverage(segmentsA, segmentsB) { |
| const result = []; |
| |
| let indexA = 0; |
| let indexB = 0; |
| while (indexA < segmentsA.length && indexB < segmentsB.length) { |
| const a = segmentsA[indexA]; |
| const b = segmentsB[indexB]; |
| const count = |
| 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(); |
| 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) { |
| 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; |
| } |
| |
| _updateStats() { |
| this._statsByTimestamp = new Map(); |
| this._usedSize = 0; |
| |
| let last = 0; |
| for (const segment of this._segments) { |
| 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; |
| } |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.Coverage = self.Coverage || {}; |
| |
| /* Legacy exported object */ |
| Coverage = Coverage || {}; |
| |
| /** |
| * @constructor |
| */ |
| Coverage.CoverageModel = CoverageModel; |
| |
| /** @enum {symbol} */ |
| Coverage.CoverageModel.Events = Events; |
| |
| /** |
| * @enum {number} |
| */ |
| Coverage.CoverageType = CoverageType; |
| |
| /** @enum {symbol} */ |
| Coverage.SuspensionState = SuspensionState; |
| |
| /** |
| * @constructor |
| */ |
| Coverage.URLCoverageInfo = URLCoverageInfo; |
| |
| /** |
| * @constructor |
| */ |
| Coverage.CoverageInfo = CoverageInfo; |