OLD | NEW |
1 // Copyright (c) 2017 The Chromium Authors. All rights reserved. | 1 // 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** @typedef {{startOffset: number, endOffset: number, count: number}} */ | 5 /** @typedef {{startOffset: number, endOffset: number, count: number}} */ |
6 Coverage.RangeUseCount; | 6 Coverage.RangeUseCount; |
7 | 7 |
8 /** @typedef {{end: number, count: (number|undefined), depth: number}} */ | 8 /** @typedef {{end: number, count: (number|undefined), depth: number}} */ |
9 Coverage.CoverageSegment; | 9 Coverage.CoverageSegment; |
10 | 10 |
11 /** @typedef {{ | |
12 * contentProvider: !Common.ContentProvider, | |
13 * size: number, | |
14 * unusedSize: number, | |
15 * usedSize: number, | |
16 * type: !Coverage.CoverageType, | |
17 * lineOffset: number, | |
18 * columnOffset: number, | |
19 * segments: !Array<!Coverage.CoverageSegment> | |
20 * }} | |
21 */ | |
22 Coverage.CoverageInfo; | |
23 | |
24 /** | 11 /** |
25 * @enum {number} | 12 * @enum {number} |
26 */ | 13 */ |
27 Coverage.CoverageType = { | 14 Coverage.CoverageType = { |
28 CSS: (1 << 0), | 15 CSS: (1 << 0), |
29 JavaScript: (1 << 1), | 16 JavaScript: (1 << 1), |
30 }; | 17 }; |
31 | 18 |
32 Coverage.CoverageModel = class extends SDK.SDKModel { | 19 Coverage.CoverageModel = class extends SDK.SDKModel { |
33 /** | 20 /** |
34 * @param {!SDK.Target} target | 21 * @param {!SDK.Target} target |
35 */ | 22 */ |
36 constructor(target) { | 23 constructor(target) { |
37 super(target); | 24 super(target); |
38 this._target = target; | 25 this._cpuProfilerModel = target.model(SDK.CPUProfilerModel); |
39 this._cpuProfilerModel = this._target.model(SDK.CPUProfilerModel); | 26 this._cssModel = target.model(SDK.CSSModel); |
40 this._cssModel = this._target.model(SDK.CSSModel); | 27 this._debuggerModel = target.model(SDK.DebuggerModel); |
| 28 |
| 29 /** @type {!Map<string, !Coverage.URLCoverageInfo>} */ |
| 30 this._coverageByURL = new Map(); |
41 } | 31 } |
42 | 32 |
43 /** | 33 /** |
44 * @return {boolean} | 34 * @return {boolean} |
45 */ | 35 */ |
46 start() { | 36 start() { |
| 37 this._coverageByURL.clear(); |
47 if (this._cssModel) | 38 if (this._cssModel) |
48 this._cssModel.startRuleUsageTracking(); | 39 this._cssModel.startRuleUsageTracking(); |
49 if (this._cpuProfilerModel) | 40 if (this._cpuProfilerModel) |
50 this._cpuProfilerModel.startPreciseCoverage(); | 41 this._cpuProfilerModel.startPreciseCoverage(); |
51 return !!(this._cssModel || this._cpuProfilerModel); | 42 return !!(this._cssModel || this._cpuProfilerModel); |
52 } | 43 } |
53 | 44 |
54 /** | 45 /** |
55 * @return {!Promise<!Array<!Coverage.CoverageInfo>>} | 46 * @return {!Promise<!Array<!Coverage.URLCoverageInfo>>} |
56 */ | 47 */ |
57 async stop() { | 48 async stop() { |
58 var cssCoverageInfoPromise = this._stopCSSCoverage(); | 49 await Promise.all([this._stopCSSCoverage(), this._stopJSCoverage()]); |
59 var jsCoverageInfoPromise = this._stopJSCoverage(); | 50 return Array.from(this._coverageByURL.values()); |
60 var cssCoverageInfo = await cssCoverageInfoPromise; | 51 } |
61 var jsCoverageInfo = await jsCoverageInfoPromise; | 52 |
62 return Coverage.CoverageModel._coalesceByURL(cssCoverageInfo.concat(jsCovera
geInfo)); | 53 async _stopJSCoverage() { |
| 54 if (!this._cpuProfilerModel) |
| 55 return; |
| 56 var coveragePromise = this._cpuProfilerModel.takePreciseCoverage(); |
| 57 this._cpuProfilerModel.stopPreciseCoverage(); |
| 58 var rawCoverageData = await coveragePromise; |
| 59 this._processJSCoverage(rawCoverageData); |
63 } | 60 } |
64 | 61 |
65 /** | 62 /** |
66 * @param {!Array<!Coverage.CoverageInfo>} coverageInfo | 63 * @param {!Array<!Protocol.Profiler.ScriptCoverage>} scriptsCoverage |
67 * @return {!Array<!Coverage.CoverageInfo>} | |
68 */ | 64 */ |
69 static _coalesceByURL(coverageInfo) { | 65 _processJSCoverage(scriptsCoverage) { |
70 coverageInfo.sort((a, b) => (a.contentProvider.contentURL() || '').localeCom
pare(b.contentProvider.contentURL())); | |
71 var result = []; | |
72 for (var entry of coverageInfo) { | |
73 var url = entry.contentProvider.contentURL(); | |
74 if (!url) | |
75 continue; | |
76 if (result.length && result.peekLast().contentProvider.contentURL() === ur
l) { | |
77 var lastEntry = result.peekLast(); | |
78 lastEntry.size += entry.size; | |
79 lastEntry.usedSize += entry.usedSize; | |
80 lastEntry.unusedSize += entry.unusedSize; | |
81 lastEntry.type |= entry.type; | |
82 } else { | |
83 result.push(entry); | |
84 } | |
85 } | |
86 return result; | |
87 } | |
88 | |
89 /** | |
90 * @return {!Promise<!Array<!Coverage.CoverageInfo>>} | |
91 */ | |
92 async _stopJSCoverage() { | |
93 if (!this._cpuProfilerModel) | |
94 return []; | |
95 var coveragePromise = this._cpuProfilerModel.takePreciseCoverage(); | |
96 this._cpuProfilerModel.stopPreciseCoverage(); | |
97 var rawCoverageData = await coveragePromise; | |
98 return Coverage.CoverageModel._processJSCoverage( | |
99 /** @type !SDK.DebuggerModel */ (SDK.DebuggerModel.fromTarget(this.targe
t())), rawCoverageData); | |
100 } | |
101 | |
102 /** | |
103 * @param {!SDK.DebuggerModel} debuggerModel | |
104 * @param {!Array<!Protocol.Profiler.ScriptCoverage>} scriptsCoverage | |
105 * @return {!Array<!Coverage.CoverageInfo>} | |
106 */ | |
107 static _processJSCoverage(debuggerModel, scriptsCoverage) { | |
108 var result = []; | |
109 for (var entry of scriptsCoverage) { | 66 for (var entry of scriptsCoverage) { |
110 var script = debuggerModel.scriptForId(entry.scriptId); | 67 var script = this._debuggerModel.scriptForId(entry.scriptId); |
111 if (!script) | 68 if (!script) |
112 continue; | 69 continue; |
113 var ranges = []; | 70 var ranges = []; |
114 for (var func of entry.functions) { | 71 for (var func of entry.functions) { |
115 for (var range of func.ranges) | 72 for (var range of func.ranges) |
116 ranges.push(range); | 73 ranges.push(range); |
117 } | 74 } |
118 ranges.sort((a, b) => a.startOffset - b.startOffset); | 75 ranges.sort((a, b) => a.startOffset - b.startOffset); |
119 result.push(Coverage.CoverageModel._buildCoverageInfo( | 76 this._addCoverage(script, script.contentLength, script.lineOffset, script.
columnOffset, ranges); |
120 script, script.contentLength, script.lineOffset, script.columnOffset,
ranges)); | |
121 } | 77 } |
122 return result; | |
123 } | 78 } |
124 | 79 |
125 /** | 80 /** |
126 * @param {!Array<!Coverage.RangeUseCount>} ranges | 81 * @param {!Array<!Coverage.RangeUseCount>} ranges |
127 * @return {!Array<!Coverage.CoverageSegment>} | 82 * @return {!Array<!Coverage.CoverageSegment>} |
128 */ | 83 */ |
129 static _convertToDisjointSegments(ranges) { | 84 static _convertToDisjointSegments(ranges) { |
130 var result = []; | 85 var result = []; |
131 | 86 |
132 var stack = []; | 87 var stack = []; |
(...skipping 28 matching lines...) Expand all Loading... |
161 last.end = end; | 116 last.end = end; |
162 return; | 117 return; |
163 } | 118 } |
164 } | 119 } |
165 result.push({end: end, count: count, depth: depth}); | 120 result.push({end: end, count: count, depth: depth}); |
166 } | 121 } |
167 | 122 |
168 return result; | 123 return result; |
169 } | 124 } |
170 | 125 |
171 /** | |
172 * @return {!Promise<!Array<!Coverage.CoverageInfo>>} | |
173 */ | |
174 async _stopCSSCoverage() { | 126 async _stopCSSCoverage() { |
175 if (!this._cssModel) | 127 if (!this._cssModel) |
176 return []; | 128 return []; |
177 | 129 |
178 var rawCoverageData = await this._cssModel.ruleListPromise(); | 130 var rawCoverageData = await this._cssModel.ruleListPromise(); |
179 return Coverage.CoverageModel._processCSSCoverage( | 131 this._processCSSCoverage(rawCoverageData); |
180 /** @type !SDK.CSSModel */ (this._cssModel), rawCoverageData); | |
181 } | 132 } |
182 | 133 |
183 /** | 134 /** |
184 * @param {!SDK.CSSModel} cssModel | |
185 * @param {!Array<!Protocol.CSS.RuleUsage>} ruleUsageList | 135 * @param {!Array<!Protocol.CSS.RuleUsage>} ruleUsageList |
186 * @return {!Array<!Coverage.CoverageInfo>} | |
187 */ | 136 */ |
188 static _processCSSCoverage(cssModel, ruleUsageList) { | 137 _processCSSCoverage(ruleUsageList) { |
189 /** @type {!Map<?SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>}
*/ | 138 /** @type {!Map<!SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUseCount>>}
*/ |
190 var rulesByStyleSheet = new Map(); | 139 var rulesByStyleSheet = new Map(); |
191 for (var rule of ruleUsageList) { | 140 for (var rule of ruleUsageList) { |
192 var styleSheetHeader = cssModel.styleSheetHeaderForId(rule.styleSheetId); | 141 var styleSheetHeader = this._cssModel.styleSheetHeaderForId(rule.styleShee
tId); |
| 142 if (!styleSheetHeader) |
| 143 continue; |
193 var ranges = rulesByStyleSheet.get(styleSheetHeader); | 144 var ranges = rulesByStyleSheet.get(styleSheetHeader); |
194 if (!ranges) { | 145 if (!ranges) { |
195 ranges = []; | 146 ranges = []; |
196 rulesByStyleSheet.set(styleSheetHeader, ranges); | 147 rulesByStyleSheet.set(styleSheetHeader, ranges); |
197 } | 148 } |
198 ranges.push({startOffset: rule.startOffset, endOffset: rule.endOffset, cou
nt: Number(rule.used)}); | 149 ranges.push({startOffset: rule.startOffset, endOffset: rule.endOffset, cou
nt: Number(rule.used)}); |
199 } | 150 } |
200 return Array.from( | 151 for (var entry of rulesByStyleSheet) { |
201 rulesByStyleSheet.entries(), | 152 var styleSheetHeader = /** @type {!SDK.CSSStyleSheetHeader} */ (entry[0]); |
202 entry => Coverage.CoverageModel._buildCoverageInfo( | 153 var ranges = /** @type {!Array<!Coverage.RangeUseCount>} */ (entry[1]); |
203 entry[0], entry[0].contentLength, entry[0].startLine, entry[0].start
Column, entry[1])); | 154 this._addCoverage( |
| 155 styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.sta
rtLine, styleSheetHeader.startColumn, |
| 156 ranges); |
| 157 } |
204 } | 158 } |
205 | 159 |
206 /** | 160 /** |
207 * @param {!Common.ContentProvider} contentProvider | 161 * @param {!Common.ContentProvider} contentProvider |
208 * @param {number} contentLength | 162 * @param {number} contentLength |
209 * @param {number} startLine | 163 * @param {number} startLine |
210 * @param {number} startColumn | 164 * @param {number} startColumn |
211 * @param {!Array<!Coverage.RangeUseCount>} ranges | 165 * @param {!Array<!Coverage.RangeUseCount>} ranges |
212 * @return {!Coverage.CoverageInfo} | 166 */ |
213 */ | 167 _addCoverage(contentProvider, contentLength, startLine, startColumn, ranges) { |
214 static _buildCoverageInfo(contentProvider, contentLength, startLine, startColu
mn, ranges) { | |
215 /** @type Coverage.CoverageType */ | |
216 var coverageType; | |
217 var url = contentProvider.contentURL(); | 168 var url = contentProvider.contentURL(); |
218 if (contentProvider.contentType().isScript()) | 169 if (!url) |
219 coverageType = Coverage.CoverageType.JavaScript; | 170 return; |
220 else if (contentProvider.contentType().isStyleSheet()) | 171 var entry = this._coverageByURL.get(url); |
221 coverageType = Coverage.CoverageType.CSS; | 172 if (!entry) { |
222 else | 173 entry = new Coverage.URLCoverageInfo(url); |
223 console.assert(false, `Unexpected resource type ${contentProvider.contentT
ype().name} for ${url}`); | 174 this._coverageByURL.set(url, entry); |
224 | 175 } |
225 var segments = Coverage.CoverageModel._convertToDisjointSegments(ranges); | 176 var segments = Coverage.CoverageModel._convertToDisjointSegments(ranges); |
226 var usedSize = 0; | 177 entry.update(contentProvider, contentLength, startLine, startColumn, segment
s); |
227 var unusedSize = 0; | 178 } |
| 179 }; |
| 180 |
| 181 Coverage.URLCoverageInfo = class { |
| 182 /** |
| 183 * @param {string} url |
| 184 */ |
| 185 constructor(url) { |
| 186 this._url = url; |
| 187 /** @type {!Map<string, !Coverage.CoverageInfo>} */ |
| 188 this._coverageInfoByLocation = new Map(); |
| 189 this._size = 0; |
| 190 this._unusedSize = 0; |
| 191 this._usedSize = 0; |
| 192 /** @type {!Coverage.CoverageType} */ |
| 193 this._type; |
| 194 } |
| 195 |
| 196 /** |
| 197 * @param {!Common.ContentProvider} contentProvider |
| 198 * @param {number} contentLength |
| 199 * @param {number} lineOffset |
| 200 * @param {number} columnOffset |
| 201 * @param {!Array<!Coverage.CoverageSegment>} segments |
| 202 */ |
| 203 update(contentProvider, contentLength, lineOffset, columnOffset, segments) { |
| 204 var key = `${lineOffset}:${columnOffset}`; |
| 205 var entry = this._coverageInfoByLocation.get(key); |
| 206 |
| 207 if (!entry) { |
| 208 entry = new Coverage.CoverageInfo(contentProvider, lineOffset, columnOffse
t); |
| 209 this._coverageInfoByLocation.set(key, entry); |
| 210 this._size += contentLength; |
| 211 this._type |= entry.type(); |
| 212 } |
| 213 this._usedSize -= entry._usedSize; |
| 214 this._unusedSize -= entry._unusedSize; |
| 215 entry.mergeCoverage(segments); |
| 216 this._usedSize += entry._usedSize; |
| 217 this._unusedSize += entry._unusedSize; |
| 218 } |
| 219 |
| 220 /** |
| 221 * @return {string} |
| 222 */ |
| 223 url() { |
| 224 return this._url; |
| 225 } |
| 226 |
| 227 /** |
| 228 * @return {!Coverage.CoverageType} |
| 229 */ |
| 230 type() { |
| 231 return this._type; |
| 232 } |
| 233 |
| 234 /** |
| 235 * @return {number} |
| 236 */ |
| 237 size() { |
| 238 return this._size; |
| 239 } |
| 240 |
| 241 /** |
| 242 * @return {number} |
| 243 */ |
| 244 unusedSize() { |
| 245 return this._unusedSize; |
| 246 } |
| 247 |
| 248 /** |
| 249 * @return {number} |
| 250 */ |
| 251 usedSize() { |
| 252 return this._usedSize; |
| 253 } |
| 254 |
| 255 /** |
| 256 * @return {!Promise<!Array<!{range: !Common.TextRange, count: number}>>} |
| 257 */ |
| 258 async buildTextRanges() { |
| 259 var textRangePromises = []; |
| 260 for (var coverageInfo of this._coverageInfoByLocation.values()) |
| 261 textRangePromises.push(coverageInfo.buildTextRanges()); |
| 262 var allTextRanges = await Promise.all(textRangePromises); |
| 263 return [].concat(...allTextRanges); |
| 264 } |
| 265 }; |
| 266 |
| 267 Coverage.CoverageInfo = class { |
| 268 /** |
| 269 * @param {!Common.ContentProvider} contentProvider |
| 270 * @param {number} lineOffset |
| 271 * @param {number} columnOffset |
| 272 */ |
| 273 constructor(contentProvider, lineOffset, columnOffset) { |
| 274 this._contentProvider = contentProvider; |
| 275 this._lineOffset = lineOffset; |
| 276 this._columnOffset = columnOffset; |
| 277 this._usedSize = 0; |
| 278 this._unusedSize = 0; |
| 279 |
| 280 if (contentProvider.contentType().isScript()) { |
| 281 this._coverageType = Coverage.CoverageType.JavaScript; |
| 282 } else if (contentProvider.contentType().isStyleSheet()) { |
| 283 this._coverageType = Coverage.CoverageType.CSS; |
| 284 } else { |
| 285 console.assert( |
| 286 false, `Unexpected resource type ${contentProvider.contentType().name}
for ${contentProvider.contentURL()}`); |
| 287 } |
| 288 /** !Array<!Coverage.CoverageSegment> */ |
| 289 this._segments = []; |
| 290 } |
| 291 |
| 292 /** |
| 293 * @return {!Coverage.CoverageType} |
| 294 */ |
| 295 type() { |
| 296 return this._coverageType; |
| 297 } |
| 298 |
| 299 /** |
| 300 * @param {!Array<!Coverage.CoverageSegment>} segments |
| 301 */ |
| 302 mergeCoverage(segments) { |
| 303 this._segments = Coverage.CoverageInfo._mergeCoverage(this._segments, segmen
ts); |
| 304 this._updateStats(); |
| 305 } |
| 306 |
| 307 /** |
| 308 * @param {!Array<!Coverage.CoverageSegment>} segmentsA |
| 309 * @param {!Array<!Coverage.CoverageSegment>} segmentsB |
| 310 */ |
| 311 static _mergeCoverage(segmentsA, segmentsB) { |
| 312 var result = []; |
| 313 |
| 314 var indexA = 0; |
| 315 var indexB = 0; |
| 316 while (indexA < segmentsA.length && indexB < segmentsB.length) { |
| 317 var a = segmentsA[indexA]; |
| 318 var b = segmentsB[indexB]; |
| 319 var count = |
| 320 typeof a.count === 'number' || typeof b.count === 'number' ? (a.count
|| 0) + (b.count || 0) : undefined; |
| 321 var depth = Math.max(a.depth, b.depth); |
| 322 var end = Math.min(a.end, b.end); |
| 323 var last = result.peekLast(); |
| 324 if (!last || last.count !== count || last.depth !== depth) |
| 325 result.push({end: end, count: count, depth: depth}); |
| 326 else |
| 327 last.end = end; |
| 328 if (a.end <= b.end) |
| 329 indexA++; |
| 330 if (a.end >= b.end) |
| 331 indexB++; |
| 332 } |
| 333 |
| 334 for (; indexA < segmentsA.length; indexA++) |
| 335 result.push(segmentsA[indexA]); |
| 336 for (; indexB < segmentsB.length; indexB++) |
| 337 result.push(segmentsB[indexB]); |
| 338 return result; |
| 339 } |
| 340 |
| 341 /** |
| 342 * @return {!Promise<!Array<!{range: !Common.TextRange, count: number}>>} |
| 343 */ |
| 344 async buildTextRanges() { |
| 345 var contents = await this._contentProvider.requestContent(); |
| 346 if (!contents) |
| 347 return []; |
| 348 var text = new Common.Text(contents); |
| 349 var lastOffset = 0; |
| 350 var rangesByDepth = []; |
| 351 for (var segment of this._segments) { |
| 352 if (typeof segment.count !== 'number') { |
| 353 lastOffset = segment.end; |
| 354 continue; |
| 355 } |
| 356 var startPosition = text.positionFromOffset(lastOffset); |
| 357 var endPosition = text.positionFromOffset(segment.end); |
| 358 if (!startPosition.lineNumber) |
| 359 startPosition.columnNumber += this._columnOffset; |
| 360 startPosition.lineNumber += this._lineOffset; |
| 361 if (!endPosition.lineNumber) |
| 362 endPosition.columnNumber += this._columnOffset; |
| 363 endPosition.lineNumber += this._lineOffset; |
| 364 |
| 365 var ranges = rangesByDepth[segment.depth - 1]; // depth === 0 => count ==
= undefined |
| 366 if (!ranges) { |
| 367 ranges = []; |
| 368 rangesByDepth[segment.depth - 1] = ranges; |
| 369 } |
| 370 ranges.push({ |
| 371 count: segment.count, |
| 372 range: new Common.TextRange( |
| 373 startPosition.lineNumber, startPosition.columnNumber, endPosition.li
neNumber, endPosition.columnNumber) |
| 374 }); |
| 375 lastOffset = segment.end; |
| 376 } |
| 377 var result = []; |
| 378 for (var ranges of rangesByDepth) { |
| 379 for (var r of ranges) |
| 380 result.push({count: r.count, range: r.range}); |
| 381 } |
| 382 return result; |
| 383 } |
| 384 |
| 385 _updateStats() { |
| 386 this._usedSize = 0; |
| 387 this._unusedSize = 0; |
| 388 |
228 var last = 0; | 389 var last = 0; |
229 for (var segment of segments) { | 390 for (var segment of this._segments) { |
230 if (typeof segment.count === 'number') { | 391 if (typeof segment.count === 'number') { |
231 if (segment.count) | 392 if (segment.count) |
232 usedSize += segment.end - last; | 393 this._usedSize += segment.end - last; |
233 else | 394 else |
234 unusedSize += segment.end - last; | 395 this._unusedSize += segment.end - last; |
235 } | 396 } |
236 last = segment.end; | 397 last = segment.end; |
237 } | 398 } |
238 var coverageInfo = { | |
239 contentProvider: contentProvider, | |
240 segments: segments, | |
241 type: coverageType, | |
242 size: contentLength, | |
243 usedSize: usedSize, | |
244 unusedSize: unusedSize, | |
245 lineOffset: startLine, | |
246 columnOffset: startColumn | |
247 }; | |
248 return coverageInfo; | |
249 } | 399 } |
250 }; | 400 }; |
OLD | NEW |