blob: ff58087041f89433dd5843bddd291e03b7f40d89 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright (c) 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/** @typedef {{startOffset: number, endOffset: number, count: number}} */
6Coverage.RangeUseCount;
7
8/** @typedef {{end: number, count: (number|undefined)}} */
9Coverage.CoverageSegment;
10
11/**
12 * @enum {number}
13 */
14Coverage.CoverageType = {
15 CSS: (1 << 0),
16 JavaScript: (1 << 1),
17 JavaScriptCoarse: (1 << 2),
18};
19
20Coverage.CoverageModel = class extends SDK.SDKModel {
21 /**
22 * @param {!SDK.Target} target
23 */
24 constructor(target) {
25 super(target);
26 this._cpuProfilerModel = target.model(SDK.CPUProfilerModel);
27 this._cssModel = target.model(SDK.CSSModel);
28 this._debuggerModel = target.model(SDK.DebuggerModel);
29
30 /** @type {!Map<string, !Coverage.URLCoverageInfo>} */
31 this._coverageByURL = new Map();
32 /** @type {!Map<!Common.ContentProvider, !Coverage.CoverageInfo>} */
33 this._coverageByContentProvider = new Map();
34 /** @type {?Promise<!Array<!Protocol.Profiler.ScriptCoverage>>} */
35 this._bestEffortCoveragePromise = null;
36 }
37
38 /**
39 * @return {boolean}
40 */
41 start() {
42 if (this._cssModel) {
43 // Note there's no JS coverage since JS won't ever return
44 // coverage twice, even after it's restarted.
45 this._clearCSS();
46 this._cssModel.startCoverage();
47 }
48 if (this._cpuProfilerModel) {
49 this._bestEffortCoveragePromise = this._cpuProfilerModel.bestEffortCoverage();
50 this._cpuProfilerModel.startPreciseCoverage();
51 }
52 return !!(this._cssModel || this._cpuProfilerModel);
53 }
54
55 /**
56 * @return {!Promise<!Array<!Coverage.CoverageInfo>>}
57 */
58 stop() {
59 const pollPromise = this.poll();
60 if (this._cpuProfilerModel)
61 this._cpuProfilerModel.stopPreciseCoverage();
62 if (this._cssModel)
63 this._cssModel.stopCoverage();
64 return pollPromise;
65 }
66
67 reset() {
68 this._coverageByURL = new Map();
69 this._coverageByContentProvider = new Map();
70 }
71
72 /**
73 * @return {!Promise<!Array<!Coverage.CoverageInfo>>}
74 */
75 async poll() {
76 const updates = await Promise.all([this._takeCSSCoverage(), this._takeJSCoverage()]);
77 return updates[0].concat(updates[1]);
78 }
79
80 /**
81 * @return {!Array<!Coverage.URLCoverageInfo>}
82 */
83 entries() {
84 return Array.from(this._coverageByURL.values());
85 }
86
87 /**
88 * @param {!Common.ContentProvider} contentProvider
89 * @param {number} startOffset
90 * @param {number} endOffset
91 * @return {boolean|undefined}
92 */
93 usageForRange(contentProvider, startOffset, endOffset) {
94 const coverageInfo = this._coverageByContentProvider.get(contentProvider);
95 return coverageInfo && coverageInfo.usageForRange(startOffset, endOffset);
96 }
97
98 _clearCSS() {
99 for (const entry of this._coverageByContentProvider.values()) {
100 if (entry.type() !== Coverage.CoverageType.CSS)
101 continue;
102 const contentProvider = /** @type {!SDK.CSSStyleSheetHeader} */ (entry.contentProvider());
103 this._coverageByContentProvider.delete(contentProvider);
104 const key = `${contentProvider.startLine}:${contentProvider.startColumn}`;
105 const urlEntry = this._coverageByURL.get(entry.url());
106 if (!urlEntry || !urlEntry._coverageInfoByLocation.delete(key))
107 continue;
108 urlEntry._size -= entry._size;
109 urlEntry._usedSize -= entry._usedSize;
110 if (!urlEntry._coverageInfoByLocation.size)
111 this._coverageByURL.delete(entry.url());
112 }
113 }
114
115 /**
116 * @return {!Promise<!Array<!Coverage.CoverageInfo>>}
117 */
118 async _takeJSCoverage() {
119 if (!this._cpuProfilerModel)
120 return [];
121 let rawCoverageData = await this._cpuProfilerModel.takePreciseCoverage();
122 if (this._bestEffortCoveragePromise) {
123 const bestEffortCoverage = await this._bestEffortCoveragePromise;
124 this._bestEffortCoveragePromise = null;
125 rawCoverageData = bestEffortCoverage.concat(rawCoverageData);
126 }
127 return this._processJSCoverage(rawCoverageData);
128 }
129
130 /**
131 * @param {!Array<!Protocol.Profiler.ScriptCoverage>} scriptsCoverage
132 * @return {!Array<!Coverage.CoverageInfo>}
133 */
134 _processJSCoverage(scriptsCoverage) {
135 const updatedEntries = [];
136 for (const entry of scriptsCoverage) {
137 const script = this._debuggerModel.scriptForId(entry.scriptId);
138 if (!script)
139 continue;
140 const ranges = [];
141 let type = Coverage.CoverageType.JavaScript;
142 for (const func of entry.functions) {
143 // Do not coerce undefined to false, i.e. only consider blockLevel to be false
144 // if back-end explicitly provides blockLevel field, otherwise presume blockLevel
145 // coverage is not available. Also, ignore non-block level functions that weren't
146 // ever called.
147 if (func.isBlockCoverage === false && !(func.ranges.length === 1 && !func.ranges[0].count))
148 type |= Coverage.CoverageType.JavaScriptCoarse;
149 for (const range of func.ranges)
150 ranges.push(range);
151 }
152 const subentry =
153 this._addCoverage(script, script.contentLength, script.lineOffset, script.columnOffset, ranges, type);
154 if (subentry)
155 updatedEntries.push(subentry);
156 }
157 return updatedEntries;
158 }
159
160 /**
161 * @return {!Promise<!Array<!Coverage.CoverageInfo>>}
162 */
163 async _takeCSSCoverage() {
164 if (!this._cssModel)
165 return [];
166 const rawCoverageData = await this._cssModel.takeCoverageDelta();
167 return this._processCSSCoverage(rawCoverageData);
168 }
169
170 /**
171 * @param {!Array<!Protocol.CSS.RuleUsage>} ruleUsageList
172 * @return {!Array<!Coverage.CoverageInfo>}
173 */
174 _processCSSCoverage(ruleUsageList) {
175 const updatedEntries = [];
176 /** @type {!Map<!SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>} */
177 const rulesByStyleSheet = new Map();
178 for (const rule of ruleUsageList) {
179 const styleSheetHeader = this._cssModel.styleSheetHeaderForId(rule.styleSheetId);
180 if (!styleSheetHeader)
181 continue;
182 let ranges = rulesByStyleSheet.get(styleSheetHeader);
183 if (!ranges) {
184 ranges = [];
185 rulesByStyleSheet.set(styleSheetHeader, ranges);
186 }
187 ranges.push({startOffset: rule.startOffset, endOffset: rule.endOffset, count: Number(rule.used)});
188 }
189 for (const entry of rulesByStyleSheet) {
190 const styleSheetHeader = /** @type {!SDK.CSSStyleSheetHeader} */ (entry[0]);
191 const ranges = /** @type {!Array<!Coverage.RangeUseCount>} */ (entry[1]);
192 const subentry = this._addCoverage(
193 styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn,
194 ranges, Coverage.CoverageType.CSS);
195 if (subentry)
196 updatedEntries.push(subentry);
197 }
198 return updatedEntries;
199 }
200
201 /**
202 * @param {!Array<!Coverage.RangeUseCount>} ranges
203 * @return {!Array<!Coverage.CoverageSegment>}
204 */
205 static _convertToDisjointSegments(ranges) {
206 ranges.sort((a, b) => a.startOffset - b.startOffset);
207
208 const result = [];
209 const stack = [];
210 for (const entry of ranges) {
211 let top = stack.peekLast();
212 while (top && top.endOffset <= entry.startOffset) {
213 append(top.endOffset, top.count);
214 stack.pop();
215 top = stack.peekLast();
216 }
217 append(entry.startOffset, top ? top.count : undefined);
218 stack.push(entry);
219 }
220
221 while (stack.length) {
222 const top = stack.pop();
223 append(top.endOffset, top.count);
224 }
225
226 /**
227 * @param {number} end
228 * @param {number} count
229 */
230 function append(end, count) {
231 const last = result.peekLast();
232 if (last) {
233 if (last.end === end)
234 return;
235 if (last.count === count) {
236 last.end = end;
237 return;
238 }
239 }
240 result.push({end: end, count: count});
241 }
242
243 return result;
244 }
245
246 /**
247 * @param {!Common.ContentProvider} contentProvider
248 * @param {number} contentLength
249 * @param {number} startLine
250 * @param {number} startColumn
251 * @param {!Array<!Coverage.RangeUseCount>} ranges
252 * @param {!Coverage.CoverageType} type
253 * @return {?Coverage.CoverageInfo}
254 */
255 _addCoverage(contentProvider, contentLength, startLine, startColumn, ranges, type) {
256 const url = contentProvider.contentURL();
257 if (!url)
258 return null;
259 let urlCoverage = this._coverageByURL.get(url);
260 if (!urlCoverage) {
261 urlCoverage = new Coverage.URLCoverageInfo(url);
262 this._coverageByURL.set(url, urlCoverage);
263 }
264
265 const coverageInfo = urlCoverage._ensureEntry(contentProvider, contentLength, startLine, startColumn, type);
266 this._coverageByContentProvider.set(contentProvider, coverageInfo);
267 const segments = Coverage.CoverageModel._convertToDisjointSegments(ranges);
268 if (segments.length && segments.peekLast().end < contentLength)
269 segments.push({end: contentLength});
270 const oldUsedSize = coverageInfo._usedSize;
271 coverageInfo.mergeCoverage(segments);
272 if (coverageInfo._usedSize === oldUsedSize)
273 return null;
274 urlCoverage._usedSize += coverageInfo._usedSize - oldUsedSize;
275 return coverageInfo;
276 }
277};
278
279Coverage.URLCoverageInfo = class {
280 /**
281 * @param {string} url
282 */
283 constructor(url) {
284 this._url = url;
285 /** @type {!Map<string, !Coverage.CoverageInfo>} */
286 this._coverageInfoByLocation = new Map();
287 this._size = 0;
288 this._usedSize = 0;
289 /** @type {!Coverage.CoverageType} */
290 this._type;
291 this._isContentScript = false;
292 }
293
294 /**
295 * @return {string}
296 */
297 url() {
298 return this._url;
299 }
300
301 /**
302 * @return {!Coverage.CoverageType}
303 */
304 type() {
305 return this._type;
306 }
307
308 /**
309 * @return {number}
310 */
311 size() {
312 return this._size;
313 }
314
315 /**
316 * @return {number}
317 */
318 usedSize() {
319 return this._usedSize;
320 }
321
322 /**
323 * @return {number}
324 */
325 unusedSize() {
326 return this._size - this._usedSize;
327 }
328
329 /**
330 * @return {boolean}
331 */
332 isContentScript() {
333 return this._isContentScript;
334 }
335
336 /**
337 * @param {!Common.ContentProvider} contentProvider
338 * @param {number} contentLength
339 * @param {number} lineOffset
340 * @param {number} columnOffset
341 * @param {!Coverage.CoverageType} type
342 * @return {!Coverage.CoverageInfo}
343 */
344 _ensureEntry(contentProvider, contentLength, lineOffset, columnOffset, type) {
345 const key = `${lineOffset}:${columnOffset}`;
346 let entry = this._coverageInfoByLocation.get(key);
347
348 if ((type & Coverage.CoverageType.JavaScript) && !this._coverageInfoByLocation.size)
349 this._isContentScript = /** @type {!SDK.Script} */ (contentProvider).isContentScript();
350 this._type |= type;
351
352 if (entry) {
353 entry._coverageType |= type;
354 return entry;
355 }
356
357 if ((type & Coverage.CoverageType.JavaScript) && !this._coverageInfoByLocation.size)
358 this._isContentScript = /** @type {!SDK.Script} */ (contentProvider).isContentScript();
359
360 entry = new Coverage.CoverageInfo(contentProvider, contentLength, type);
361 this._coverageInfoByLocation.set(key, entry);
362 this._size += contentLength;
363
364 return entry;
365 }
366};
367
368Coverage.CoverageInfo = class {
369 /**
370 * @param {!Common.ContentProvider} contentProvider
371 * @param {number} size
372 * @param {!Coverage.CoverageType} type
373 */
374 constructor(contentProvider, size, type) {
375 this._contentProvider = contentProvider;
376 this._size = size;
377 this._usedSize = 0;
378 this._coverageType = type;
379
380 /** !Array<!Coverage.CoverageSegment> */
381 this._segments = [];
382 }
383
384 /**
385 * @return {!Common.ContentProvider}
386 */
387 contentProvider() {
388 return this._contentProvider;
389 }
390
391 /**
392 * @return {string}
393 */
394 url() {
395 return this._contentProvider.contentURL();
396 }
397
398 /**
399 * @return {!Coverage.CoverageType}
400 */
401 type() {
402 return this._coverageType;
403 }
404
405 /**
406 * @param {!Array<!Coverage.CoverageSegment>} segments
407 */
408 mergeCoverage(segments) {
409 this._segments = Coverage.CoverageInfo._mergeCoverage(this._segments, segments);
410 this._updateStats();
411 }
412
413 /**
414 * @param {number} start
415 * @param {number} end
416 * @return {boolean}
417 */
418 usageForRange(start, end) {
419 let index = this._segments.upperBound(start, (position, segment) => position - segment.end);
420 for (; index < this._segments.length && this._segments[index].end < end; ++index) {
421 if (this._segments[index].count)
422 return true;
423 }
424 return index < this._segments.length && !!this._segments[index].count;
425 }
426
427 /**
428 * @param {!Array<!Coverage.CoverageSegment>} segmentsA
429 * @param {!Array<!Coverage.CoverageSegment>} segmentsB
430 */
431 static _mergeCoverage(segmentsA, segmentsB) {
432 const result = [];
433
434 let indexA = 0;
435 let indexB = 0;
436 while (indexA < segmentsA.length && indexB < segmentsB.length) {
437 const a = segmentsA[indexA];
438 const b = segmentsB[indexB];
439 const count =
440 typeof a.count === 'number' || typeof b.count === 'number' ? (a.count || 0) + (b.count || 0) : undefined;
441 const end = Math.min(a.end, b.end);
442 const last = result.peekLast();
443 if (!last || last.count !== count)
444 result.push({end: end, count: count});
445 else
446 last.end = end;
447 if (a.end <= b.end)
448 indexA++;
449 if (a.end >= b.end)
450 indexB++;
451 }
452
453 for (; indexA < segmentsA.length; indexA++)
454 result.push(segmentsA[indexA]);
455 for (; indexB < segmentsB.length; indexB++)
456 result.push(segmentsB[indexB]);
457 return result;
458 }
459
460 _updateStats() {
461 this._usedSize = 0;
462
463 let last = 0;
464 for (const segment of this._segments) {
465 if (segment.count)
466 this._usedSize += segment.end - last;
467 last = segment.end;
468 }
469 }
470};