chrome-devtools-frontend
Version:
Chrome DevTools UI
846 lines (739 loc) • 28.1 kB
text/typescript
// Copyright (c) 2020 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.
import type * as Bindings from '../../models/bindings/bindings.js';
import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import type * as Protocol from '../../generated/protocol.js';
export const enum CoverageType {
CSS = (1 << 0),
JavaScript = (1 << 1),
JavaScriptPerFunction = (1 << 2),
}
export const enum SuspensionState {
Active = 'Active',
Suspending = 'Suspending',
Suspended = 'Suspended',
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
CoverageUpdated = 'CoverageUpdated',
CoverageReset = 'CoverageReset',
}
export type EventTypes = {
[Events.CoverageUpdated]: CoverageInfo[],
[Events.CoverageReset]: void,
};
const COVERAGE_POLLING_PERIOD_MS: number = 200;
interface BacklogItem<T> {
rawCoverageData: Array<T>;
stamp: number;
}
export class CoverageModel extends SDK.SDKModel.SDKModel<EventTypes> {
private cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel|null;
private cssModel: SDK.CSSModel.CSSModel|null;
private debuggerModel: SDK.DebuggerModel.DebuggerModel|null;
private coverageByURL: Map<Platform.DevToolsPath.UrlString, URLCoverageInfo>;
private coverageByContentProvider: Map<TextUtils.ContentProvider.ContentProvider, CoverageInfo>;
private coverageUpdateTimes: Set<number>;
private suspensionState: SuspensionState;
private pollTimer: number|null;
private currentPollPromise: Promise<void>|null;
private shouldResumePollingOnResume: boolean|null;
private jsBacklog: BacklogItem<Protocol.Profiler.ScriptCoverage>[];
private cssBacklog: BacklogItem<Protocol.CSS.RuleUsage>[];
private performanceTraceRecording: boolean|null;
constructor(target: SDK.Target.Target) {
super(target);
this.cpuProfilerModel = target.model(SDK.CPUProfilerModel.CPUProfilerModel);
this.cssModel = target.model(SDK.CSSModel.CSSModel);
this.debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
this.coverageByURL = new Map();
this.coverageByContentProvider = new Map();
// We keep track of the update times, because the other data-structures don't change if an
// update doesn't change the coverage. Some visualizations want to convey to the user that
// an update was received at a certain time, but did not result in a coverage change.
this.coverageUpdateTimes = new Set();
this.suspensionState = SuspensionState.Active;
this.pollTimer = null;
this.currentPollPromise = null;
this.shouldResumePollingOnResume = false;
this.jsBacklog = [];
this.cssBacklog = [];
this.performanceTraceRecording = false;
}
async start(jsCoveragePerBlock: boolean): Promise<boolean> {
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, this.preciseCoverageDeltaUpdate.bind(this)));
}
await Promise.all(promises);
return Boolean(this.cssModel || this.cpuProfilerModel);
}
preciseCoverageDeltaUpdate(timestamp: number, occasion: string, coverageData: Protocol.Profiler.ScriptCoverage[]):
void {
this.coverageUpdateTimes.add(timestamp);
void this.backlogOrProcessJSCoverage(coverageData, timestamp);
}
async stop(): Promise<void> {
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(): void {
this.coverageByURL = new Map();
this.coverageByContentProvider = new Map();
this.coverageUpdateTimes = new Set();
this.dispatchEventToListeners(Events.CoverageReset);
}
async startPolling(): Promise<void> {
if (this.currentPollPromise || this.suspensionState !== SuspensionState.Active) {
return;
}
await this.pollLoop();
}
private async pollLoop(): Promise<void> {
this.clearTimer();
this.currentPollPromise = this.pollAndCallback();
await this.currentPollPromise;
if (this.suspensionState === SuspensionState.Active || this.performanceTraceRecording) {
this.pollTimer = window.setTimeout(() => this.pollLoop(), COVERAGE_POLLING_PERIOD_MS);
}
}
async stopPolling(): Promise<void> {
this.clearTimer();
await this.currentPollPromise;
this.currentPollPromise = null;
// Do one last poll to get the final data.
await this.pollAndCallback();
}
private async pollAndCallback(): Promise<void> {
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 || Boolean(this.performanceTraceRecording),
'CoverageModel was suspended while polling.');
if (updates.length) {
this.dispatchEventToListeners(Events.CoverageUpdated, updates);
}
}
private clearTimer(): void {
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 async preSuspendModel(reason?: string): Promise<void> {
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 async suspendModel(_reason?: string): Promise<void> {
this.suspensionState = SuspensionState.Suspended;
}
override async resumeModel(): Promise<void> {
}
/**
* Restarts polling after suspension. Note that the function is idempotent
* because starting polling is idempotent.
*/
override async postResumeModel(): Promise<void> {
this.suspensionState = SuspensionState.Active;
this.performanceTraceRecording = false;
if (this.shouldResumePollingOnResume) {
this.shouldResumePollingOnResume = false;
await this.startPolling();
}
}
entries(): URLCoverageInfo[] {
return Array.from(this.coverageByURL.values());
}
getCoverageForUrl(url: Platform.DevToolsPath.UrlString): URLCoverageInfo|null {
return this.coverageByURL.get(url) || null;
}
usageForRange(contentProvider: TextUtils.ContentProvider.ContentProvider, startOffset: number, endOffset: number):
boolean|undefined {
const coverageInfo = this.coverageByContentProvider.get(contentProvider);
return coverageInfo && coverageInfo.usageForRange(startOffset, endOffset);
}
private clearCSS(): void {
for (const entry of this.coverageByContentProvider.values()) {
if (entry.type() !== CoverageType.CSS) {
continue;
}
const contentProvider = entry.getContentProvider() as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;
this.coverageByContentProvider.delete(contentProvider);
const urlEntry = this.coverageByURL.get(entry.url());
if (!urlEntry) {
continue;
}
const key = `${contentProvider.startLine}:${contentProvider.startColumn}`;
urlEntry.removeCoverageEntry(key, entry);
if (urlEntry.numberOfEntries() === 0) {
this.coverageByURL.delete(entry.url());
}
}
if (this.cssModel) {
for (const styleSheetHeader of this.cssModel.getAllStyleSheetHeaders()) {
this.addStyleSheetToCSSCoverage(styleSheetHeader);
}
}
}
private async takeAllCoverage(): Promise<CoverageInfo[]> {
const [updatesCSS, updatesJS] = await Promise.all([this.takeCSSCoverage(), this.takeJSCoverage()]);
return [...updatesCSS, ...updatesJS];
}
private async takeJSCoverage(): Promise<CoverageInfo[]> {
if (!this.cpuProfilerModel) {
return [];
}
const {coverage, timestamp} = await this.cpuProfilerModel.takePreciseCoverage();
this.coverageUpdateTimes.add(timestamp);
return this.backlogOrProcessJSCoverage(coverage, timestamp);
}
getCoverageUpdateTimes(): Set<number> {
return this.coverageUpdateTimes;
}
private async backlogOrProcessJSCoverage(
freshRawCoverageData: Protocol.Profiler.ScriptCoverage[], freshTimestamp: number): Promise<CoverageInfo[]> {
if (freshRawCoverageData.length > 0) {
this.jsBacklog.push({rawCoverageData: freshRawCoverageData, stamp: freshTimestamp});
}
if (this.suspensionState !== SuspensionState.Active) {
return [];
}
const ascendingByTimestamp = (x: {stamp: number}, y: {stamp: number}): number => x.stamp - y.stamp;
const results = [];
for (const {rawCoverageData, stamp} of this.jsBacklog.sort(ascendingByTimestamp)) {
results.push(this.processJSCoverage(rawCoverageData, stamp));
}
this.jsBacklog = [];
return results.flat();
}
async processJSBacklog(): Promise<void> {
void this.backlogOrProcessJSCoverage([], 0);
}
private processJSCoverage(scriptsCoverage: Protocol.Profiler.ScriptCoverage[], stamp: number): CoverageInfo[] {
if (!this.debuggerModel) {
return [];
}
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 as CoverageType, stamp);
if (subentry) {
updatedEntries.push(subentry);
}
}
return updatedEntries;
}
private handleStyleSheetAdded(
event: Common.EventTarget.EventTargetEvent<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>): void {
this.addStyleSheetToCSSCoverage(event.data);
}
private async takeCSSCoverage(): Promise<CoverageInfo[]> {
// Don't poll if we have no model, or are suspended.
if (!this.cssModel || this.suspensionState !== SuspensionState.Active) {
return [];
}
const {coverage, timestamp} = await this.cssModel.takeCoverageDelta();
this.coverageUpdateTimes.add(timestamp);
return this.backlogOrProcessCSSCoverage(coverage, timestamp);
}
private async backlogOrProcessCSSCoverage(freshRawCoverageData: Protocol.CSS.RuleUsage[], freshTimestamp: number):
Promise<CoverageInfo[]> {
if (freshRawCoverageData.length > 0) {
this.cssBacklog.push({rawCoverageData: freshRawCoverageData, stamp: freshTimestamp});
}
if (this.suspensionState !== SuspensionState.Active) {
return [];
}
const ascendingByTimestamp = (x: {stamp: number}, y: {stamp: number}): number => x.stamp - y.stamp;
const results = [];
for (const {rawCoverageData, stamp} of this.cssBacklog.sort(ascendingByTimestamp)) {
results.push(this.processCSSCoverage(rawCoverageData, stamp));
}
this.cssBacklog = [];
return results.flat();
}
private processCSSCoverage(ruleUsageList: Protocol.CSS.RuleUsage[], stamp: number): CoverageInfo[] {
if (!this.cssModel) {
return [];
}
const updatedEntries = [];
const rulesByStyleSheet = new Map<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, RangeUseCount[]>();
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 = entry[0] as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;
const ranges = entry[1] as RangeUseCount[];
const subentry = this.addCoverage(
styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn,
ranges, CoverageType.CSS, stamp);
if (subentry) {
updatedEntries.push(subentry);
}
}
return updatedEntries;
}
private static convertToDisjointSegments(ranges: RangeUseCount[], stamp: number): CoverageSegment[] {
ranges.sort((a, b) => a.startOffset - b.startOffset);
const result: CoverageSegment[] = [];
const stack = [];
for (const entry of ranges) {
let top: RangeUseCount = stack[stack.length - 1];
while (top && top.endOffset <= entry.startOffset) {
append(top.endOffset, top.count);
stack.pop();
top = stack[stack.length - 1];
}
append(entry.startOffset, top ? top.count : 0);
stack.push(entry);
}
for (let top = stack.pop(); top; top = stack.pop()) {
append(top.endOffset, top.count);
}
function append(end: number, count: number): void {
const last = result[result.length - 1];
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;
}
private addStyleSheetToCSSCoverage(styleSheetHeader: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader): void {
this.addCoverage(
styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn, [],
CoverageType.CSS, Date.now());
}
private addCoverage(
contentProvider: TextUtils.ContentProvider.ContentProvider, contentLength: number, startLine: number,
startColumn: number, ranges: RangeUseCount[], type: CoverageType, stamp: number): CoverageInfo|null {
const url = contentProvider.contentURL();
if (!url) {
return null;
}
let urlCoverage = this.coverageByURL.get(url);
let isNewUrlCoverage = false;
if (!urlCoverage) {
isNewUrlCoverage = true;
urlCoverage = new URLCoverageInfo(url);
this.coverageByURL.set(url, urlCoverage);
}
const coverageInfo = urlCoverage.ensureEntry(contentProvider, contentLength, startLine, startColumn, type);
this.coverageByContentProvider.set(contentProvider, coverageInfo);
const segments = CoverageModel.convertToDisjointSegments(ranges, stamp);
const last = segments[segments.length - 1];
if (last && last.end < contentLength) {
segments.push({end: contentLength, stamp: stamp, count: 0});
}
const usedSizeDelta = coverageInfo.mergeCoverage(segments);
if (!isNewUrlCoverage && usedSizeDelta === 0) {
return null;
}
urlCoverage.addToSizes(usedSizeDelta, 0);
return coverageInfo;
}
async exportReport(fos: Bindings.FileUtils.FileOutputStream): Promise<void> {
const result: {url: string, ranges: {start: number, end: number}[], text: string|null}[] = [];
const coverageByUrlKeys = Array.from(this.coverageByURL.keys()).sort();
for (const urlInfoKey of coverageByUrlKeys) {
const urlInfo = this.coverageByURL.get(urlInfoKey);
if (!urlInfo) {
continue;
}
const url = urlInfo.url();
if (url.startsWith('extensions::') || url.startsWith('chrome-extension://')) {
continue;
}
result.push(...await urlInfo.entriesForExport());
}
await fos.write(JSON.stringify(result, undefined, 2));
void fos.close();
}
}
SDK.SDKModel.SDKModel.register(CoverageModel, {capabilities: SDK.Target.Capability.None, autostart: false});
export interface EntryForExport {
url: Platform.DevToolsPath.UrlString;
ranges: {start: number, end: number}[];
text: string|null;
}
function locationCompare(a: string, b: string): number {
const [aLine, aPos] = a.split(':');
const [bLine, bPos] = b.split(':');
return Number.parseInt(aLine, 10) - Number.parseInt(bLine, 10) ||
Number.parseInt(aPos, 10) - Number.parseInt(bPos, 10);
}
export class URLCoverageInfo extends Common.ObjectWrapper.ObjectWrapper<URLCoverageInfo.EventTypes> {
private readonly urlInternal: Platform.DevToolsPath.UrlString;
private coverageInfoByLocation: Map<string, CoverageInfo>;
private sizeInternal: number;
private usedSizeInternal: number;
private typeInternal!: CoverageType;
private isContentScriptInternal: boolean;
constructor(url: Platform.DevToolsPath.UrlString) {
super();
this.urlInternal = url;
this.coverageInfoByLocation = new Map();
this.sizeInternal = 0;
this.usedSizeInternal = 0;
this.isContentScriptInternal = false;
}
url(): Platform.DevToolsPath.UrlString {
return this.urlInternal;
}
type(): CoverageType {
return this.typeInternal;
}
size(): number {
return this.sizeInternal;
}
usedSize(): number {
return this.usedSizeInternal;
}
unusedSize(): number {
return this.sizeInternal - this.usedSizeInternal;
}
usedPercentage(): number {
// Per convention, empty files are reported as 100 % uncovered
if (this.sizeInternal === 0) {
return 0;
}
return this.usedSize() / this.size();
}
unusedPercentage(): number {
// Per convention, empty files are reported as 100 % uncovered
if (this.sizeInternal === 0) {
return 100;
}
return this.unusedSize() / this.size();
}
isContentScript(): boolean {
return this.isContentScriptInternal;
}
entries(): IterableIterator<CoverageInfo> {
return this.coverageInfoByLocation.values();
}
numberOfEntries(): number {
return this.coverageInfoByLocation.size;
}
removeCoverageEntry(key: string, entry: CoverageInfo): void {
if (!this.coverageInfoByLocation.delete(key)) {
return;
}
this.addToSizes(-entry.getUsedSize(), -entry.getSize());
}
addToSizes(usedSize: number, size: number): void {
this.usedSizeInternal += usedSize;
this.sizeInternal += size;
if (usedSize !== 0 || size !== 0) {
this.dispatchEventToListeners(URLCoverageInfo.Events.SizesChanged);
}
}
ensureEntry(
contentProvider: TextUtils.ContentProvider.ContentProvider, contentLength: number, lineOffset: number,
columnOffset: number, type: CoverageType): CoverageInfo {
const key = `${lineOffset}:${columnOffset}`;
let entry = this.coverageInfoByLocation.get(key);
if ((type & CoverageType.JavaScript) && !this.coverageInfoByLocation.size) {
this.isContentScriptInternal = (contentProvider as SDK.Script.Script).isContentScript();
}
this.typeInternal |= type;
if (entry) {
entry.addCoverageType(type);
return entry;
}
if ((type & CoverageType.JavaScript) && !this.coverageInfoByLocation.size) {
this.isContentScriptInternal = (contentProvider as SDK.Script.Script).isContentScript();
}
entry = new CoverageInfo(contentProvider, contentLength, lineOffset, columnOffset, type);
this.coverageInfoByLocation.set(key, entry);
this.addToSizes(0, contentLength);
return entry;
}
async getFullText(): Promise<TextUtils.Text.Text|null> {
// For .html resources, multiple scripts share URL, but have different offsets.
let useFullText = false;
const url = this.url();
for (const info of this.coverageInfoByLocation.values()) {
const {lineOffset, columnOffset} = info.getOffsets();
if (lineOffset || columnOffset) {
useFullText = Boolean(url);
break;
}
}
if (!useFullText) {
return null;
}
const resource = SDK.ResourceTreeModel.ResourceTreeModel.resourceForURL(url);
if (!resource) {
return null;
}
const content = (await resource.requestContent()).content;
return new TextUtils.Text.Text(content || '');
}
entriesForExportBasedOnFullText(fullText: TextUtils.Text.Text): EntryForExport {
const coverageByLocationKeys = Array.from(this.coverageInfoByLocation.keys()).sort(locationCompare);
const entry: EntryForExport = {url: this.url(), ranges: [], text: fullText.value()};
for (const infoKey of coverageByLocationKeys) {
const info = this.coverageInfoByLocation.get(infoKey);
if (!info) {
continue;
}
const {lineOffset, columnOffset} = info.getOffsets();
const offset = fullText ? fullText.offsetFromPosition(lineOffset, columnOffset) : 0;
entry.ranges.push(...info.rangesForExport(offset));
}
return entry;
}
async entriesForExportBasedOnContent(): Promise<EntryForExport[]> {
const coverageByLocationKeys = Array.from(this.coverageInfoByLocation.keys()).sort(locationCompare);
const result = [];
for (const infoKey of coverageByLocationKeys) {
const info = this.coverageInfoByLocation.get(infoKey);
if (!info) {
continue;
}
const entry: EntryForExport = {
url: this.url(),
ranges: info.rangesForExport(),
text: (await info.getContentProvider().requestContent()).content,
};
result.push(entry);
}
return result;
}
async entriesForExport(): Promise<EntryForExport[]> {
const fullText = await this.getFullText();
// We have full text for this resource, resolve the offsets using the text line endings.
if (fullText) {
return [await this.entriesForExportBasedOnFullText(fullText)];
}
// Fall back to the per-script operation.
return this.entriesForExportBasedOnContent();
}
}
export namespace URLCoverageInfo {
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
SizesChanged = 'SizesChanged',
}
export type EventTypes = {
[Events.SizesChanged]: void,
};
}
export const mergeSegments = (segmentsA: CoverageSegment[], segmentsB: CoverageSegment[]): CoverageSegment[] => {
const result: CoverageSegment[] = [];
let indexA = 0;
let indexB = 0;
while (indexA < segmentsA.length && indexB < segmentsB.length) {
const a = segmentsA[indexA];
const b = segmentsB[indexB];
const count = (a.count || 0) + (b.count || 0);
const end = Math.min(a.end, b.end);
const last = result[result.length - 1];
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;
};
export class CoverageInfo {
private contentProvider: TextUtils.ContentProvider.ContentProvider;
private size: number;
private usedSize: number;
private statsByTimestamp: Map<number, number>;
private lineOffset: number;
private columnOffset: number;
private coverageType: CoverageType;
private segments: CoverageSegment[];
constructor(
contentProvider: TextUtils.ContentProvider.ContentProvider, size: number, lineOffset: number,
columnOffset: number, type: CoverageType) {
this.contentProvider = contentProvider;
this.size = size;
this.usedSize = 0;
this.statsByTimestamp = new Map();
this.lineOffset = lineOffset;
this.columnOffset = columnOffset;
this.coverageType = type;
this.segments = [];
}
getContentProvider(): TextUtils.ContentProvider.ContentProvider {
return this.contentProvider;
}
url(): Platform.DevToolsPath.UrlString {
return this.contentProvider.contentURL();
}
type(): CoverageType {
return this.coverageType;
}
addCoverageType(type: CoverageType): void {
this.coverageType |= type;
}
getOffsets(): {lineOffset: number, columnOffset: number} {
return {lineOffset: this.lineOffset, columnOffset: this.columnOffset};
}
/**
* Returns the delta by which usedSize increased.
*/
mergeCoverage(segments: CoverageSegment[]): number {
const oldUsedSize = this.usedSize;
this.segments = mergeSegments(this.segments, segments);
this.updateStats();
return this.usedSize - oldUsedSize;
}
usedByTimestamp(): Map<number, number> {
return this.statsByTimestamp;
}
getSize(): number {
return this.size;
}
getUsedSize(): number {
return this.usedSize;
}
usageForRange(start: number, end: number): boolean {
let index = Platform.ArrayUtilities.upperBound(this.segments, 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 && Boolean(this.segments[index].count);
}
private updateStats(): void {
this.statsByTimestamp = new Map();
this.usedSize = 0;
let last = 0;
for (const segment of this.segments) {
let previousCount = this.statsByTimestamp.get(segment.stamp);
if (previousCount === undefined) {
previousCount = 0;
}
if (segment.count) {
const used = segment.end - last;
this.usedSize += used;
this.statsByTimestamp.set(segment.stamp, previousCount + used);
}
last = segment.end;
}
}
rangesForExport(offset: number = 0): {start: number, end: number}[] {
const ranges = [];
let start = 0;
for (const segment of this.segments) {
if (segment.count) {
const last = ranges.length > 0 ? ranges[ranges.length - 1] : null;
if (last && last.end === start + offset) {
// We can extend the last segment.
last.end = segment.end + offset;
} else {
// There was a gap, add a new segment.
ranges.push({start: start + offset, end: segment.end + offset});
}
}
start = segment.end;
}
return ranges;
}
}
export interface RangeUseCount {
startOffset: number;
endOffset: number;
count: number;
}
export interface CoverageSegment {
end: number;
count: number;
stamp: number;
}