Blink Reformat | 4c46d09 | 2018-04-07 15:32:37 | [diff] [blame] | 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 |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /** @typedef {{startOffset: number, endOffset: number, count: number}} */ |
| 6 | Coverage.RangeUseCount; |
| 7 | |
| 8 | /** @typedef {{end: number, count: (number|undefined)}} */ |
| 9 | Coverage.CoverageSegment; |
| 10 | |
| 11 | /** |
| 12 | * @enum {number} |
| 13 | */ |
| 14 | Coverage.CoverageType = { |
| 15 | CSS: (1 << 0), |
| 16 | JavaScript: (1 << 1), |
| 17 | JavaScriptCoarse: (1 << 2), |
| 18 | }; |
| 19 | |
| 20 | Coverage.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 | |
| 279 | Coverage.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 | |
| 368 | Coverage.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 | }; |