├── coverage.js ├── package.json └── readme.md /coverage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const URL = require('url').URL; 4 | const chromeLauncher = require('chrome-launcher'); 5 | const CDP = require('chrome-remote-interface'); 6 | const js_protocol = require('devtools-protocol/json/js_protocol.json'); 7 | 8 | const launchChrome = () => 9 | chromeLauncher.launch({ 10 | chromeFlags: ['--disable-gpu', '--headless'], 11 | logLevel: 'error' 12 | }); 13 | 14 | 15 | launchChrome() 16 | .then(async chrome => { 17 | const cdp = await CDP({port: chrome.port}); 18 | try { 19 | const {Page, Profiler} = cdp; 20 | 21 | setupDevToolsTarget(cdp); 22 | 23 | await Profiler.enable(); 24 | await Page.enable(); 25 | 26 | const covModel = new Coverage.CoverageModel(target); 27 | covModel.start(); 28 | 29 | Page.navigate({url: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy'}); 30 | await Page.loadEventFired(); 31 | 32 | await covModel.stop(); 33 | const coverage = covModel.entries(); 34 | 35 | coverage.sort((a, b) => b.unusedSize() - a.unusedSize()); 36 | 37 | console.log('Coverage data: (sorted by unused bytes, descending)'); 38 | for (const file of coverage) { 39 | for (const chunk of file._coverageInfoByLocation.values()) { 40 | console.log('--------------------------------------'); 41 | console.log(`${coverageTypeToString(chunk.type())} found in: ${chunk.url()}`); 42 | const unusedSize = chunk._size - chunk._usedSize; 43 | console.log(`${unusedSize.toLocaleString()}B total unused in chunk. (${(unusedSize / chunk._size).toLocaleString()}%).`); 44 | 45 | const unusedSegments = chunk._segments.filter(seg => seg.count === 0); 46 | console.log(`${unusedSegments.length} unused segments and ${chunk._segments.length - unusedSegments.length} used segments found.`); 47 | console.log(''); 48 | } 49 | } 50 | return coverage; 51 | 52 | } catch (err) { 53 | console.error(err); 54 | } finally { 55 | cdp.close(); 56 | chrome.kill(); 57 | } 58 | }) 59 | .catch(err => console.error(err)); 60 | 61 | // Let's setup devtools env 62 | global.Common = {}; 63 | global.SDK = {}; 64 | global.Coverage = {}; 65 | global.Protocol = {}; 66 | 67 | // DevTools Dependencies 68 | require('chrome-devtools-frontend/front_end/common/Object.js'); 69 | require('chrome-devtools-frontend/front_end/protocol/InspectorBackend.js'); 70 | require('chrome-devtools-frontend/front_end/sdk/Target.js'); 71 | require('chrome-devtools-frontend/front_end/sdk/DebuggerModel.js'); 72 | require('chrome-devtools-frontend/front_end/coverage/CoverageModel.js'); 73 | 74 | require('chrome-devtools-frontend/front_end/sdk/CPUProfilerModel.js'); 75 | require('chrome-devtools-frontend/front_end/sdk/RuntimeModel.js'); 76 | require('chrome-devtools-frontend/front_end/sdk/CSSModel.js'); 77 | 78 | global.Multimap = defineMultimap(); 79 | require('chrome-devtools-frontend/front_end/sdk/SourceMapManager.js'); // For debuggermodel 80 | require('chrome-devtools-frontend/front_end/sdk/TargetManager.js'); 81 | 82 | require('chrome-devtools-frontend/front_end/common/ParsedURL.js'); // for runtimemodel 83 | require('chrome-devtools-frontend/front_end/sdk/Script.js'); // for SDK.DebuggerModel._parsedScriptSource 84 | require('chrome-devtools-frontend/front_end/common/ResourceType.js'); // for SDK.Script.contentType 85 | 86 | function createTarget() { 87 | const targetManager = SDK.targetManager; 88 | 89 | const id = 'main'; 90 | const name = 'Main'; 91 | const capabilitiesMask = SDK.Target.Capability.JS; 92 | const connectionFactory = _ => {}; 93 | const parentTarget = null; 94 | 95 | const target = new SDK.Target(targetManager, id, name, capabilitiesMask, connectionFactory, parentTarget); 96 | return target; 97 | } 98 | 99 | function setupDevToolsTarget(cdp) { 100 | const profilerAgent = installProxies(cdp.Profiler, 'Profiler'); 101 | const debuggerAgent = installProxies(cdp.Debugger, 'Debugger'); 102 | const runtimeAgent = installProxies(cdp.Runtime, 'Runtime'); 103 | 104 | target.profilerAgent = _ => profilerAgent; 105 | target.debuggerAgent = _ => debuggerAgent; 106 | target.runtimeAgent = _ => runtimeAgent; 107 | 108 | target.registerProfilerDispatcher = dpcher => registerDispatcher(dpcher, 'Profiler'); 109 | target.registerDebuggerDispatcher = dpcher => registerDispatcher(dpcher, 'Debugger'); 110 | target.registerRuntimeDispatcher = dpcher => registerDispatcher(dpcher, 'Runtime'); 111 | 112 | function registerDispatcher(dispatcher, domain) { 113 | cdp.on('event', message => { 114 | if (!message.method.startsWith(`${domain}.`)) return; 115 | const evtName = message.method.split('.')[1]; 116 | dispatcher[evtName].apply(dispatcher, spreadArguments(message.method, message.params)); 117 | }); 118 | } 119 | 120 | // Install a proxy over every CDP method in each domain passed in 121 | function installProxies(cdpDomain, domainStr) { 122 | for (const fnName of Object.keys(cdpDomain)) { 123 | const method = `${domainStr}.${fnName}`; 124 | if (typeof cdpDomain[fnName] !== 'function') continue; 125 | 126 | // Install a proxy over the original method 127 | const proxyHandler = { 128 | apply(target, thisArg, args) { 129 | // Note: `method` from parent scope is trapped. 130 | const opts = unspreadArguments(method, args); 131 | 132 | return target.call(thisArg, opts).then(res => { 133 | // DevTools expects both error handling and unwrapping the {result} 134 | if (res.error) { 135 | console.error('Protocol error', res.error); 136 | return Promise.reject(new Error(res.error)); 137 | } 138 | return res.result; 139 | }); 140 | } 141 | }; 142 | cdpDomain[fnName] = new Proxy(cdpDomain[fnName], proxyHandler); 143 | } 144 | return cdpDomain; 145 | } 146 | 147 | // DevTools agents speak a language of ordered arguments, but CRI takes an object of named properties 148 | // Here we convert from the former to the latter 149 | function unspreadArguments(method, args) { 150 | if (args.length === 0) return {}; 151 | 152 | const domainStr = method.split('.')[0]; 153 | const commandStr = method.split('.')[1]; 154 | 155 | const domain = js_protocol.domains.find(d => d.domain === domainStr); 156 | const command = domain.commands.find(c => c.name === commandStr); 157 | const opts = {}; 158 | args.forEach((arg, i) => { 159 | opts[command.parameters[i].name] = arg; 160 | }); 161 | return opts; 162 | } 163 | 164 | // DevTools agents speak a language of ordered arguments, but CRI takes an object of named properties 165 | // Here we convert from the former to the latter 166 | function spreadArguments(method, args) { 167 | const domainStr = method.split('.')[0]; 168 | const eventStr = method.split('.')[1]; 169 | 170 | const paramsArr = []; 171 | if (Object.keys(args).length > 0) { 172 | const domain = js_protocol.domains.find(d => d.domain === domainStr); 173 | const parameters = domain.events.find(c => c.name === eventStr).parameters; 174 | parameters.forEach(param => { 175 | paramsArr.push(args[param.name]); 176 | }); 177 | } 178 | return paramsArr; 179 | } 180 | } 181 | 182 | const target = createTarget(); 183 | 184 | // 185 | // start of copypasted devtools polyfills 186 | // 187 | Common.moduleSetting = settingName => { 188 | return { 189 | addChangeListener: _ => true, 190 | get: _ => false 191 | }; 192 | }; 193 | 194 | /** 195 | * @param {!Coverage.CoverageType} type 196 | */ 197 | function coverageTypeToString(type) { 198 | const types = []; 199 | if (type & Coverage.CoverageType.CSS) types.push('CSS'); 200 | if (type & Coverage.CoverageType.JavaScript) types.push('JS'); 201 | return types.join('+'); 202 | } 203 | 204 | Object.defineProperty(Array.prototype, 'peekLast', { 205 | /** 206 | * @return {!T|undefined} 207 | * @this {Array.} 208 | * @template T 209 | */ 210 | value: function() { 211 | return this[this.length - 1]; 212 | } 213 | }); 214 | 215 | // From utilities 216 | /** 217 | * @return {!Array} 218 | */ 219 | Map.prototype.valuesArray = function() { 220 | return Array.from(this.values()); 221 | }; 222 | 223 | // From utilities 224 | function defineMultimap() { 225 | /** 226 | * @constructor 227 | * @template K, V 228 | */ 229 | let Multimap = function() { 230 | /** @type {!Map.>} */ 231 | this._map = new Map(); 232 | }; 233 | 234 | Multimap.prototype = { 235 | /** 236 | * @param {K} key 237 | * @param {V} value 238 | */ 239 | set(key, value) { 240 | var set = this._map.get(key); 241 | if (!set) { 242 | set = new Set(); 243 | this._map.set(key, set); 244 | } 245 | set.add(value); 246 | }, 247 | 248 | /** 249 | * @param {K} key 250 | * @return {!Set.} 251 | */ 252 | get(key) { 253 | var result = this._map.get(key); 254 | if (!result) result = new Set(); 255 | return result; 256 | }, 257 | 258 | /** 259 | * @param {K} key 260 | * @return {boolean} 261 | */ 262 | has(key) { 263 | return this._map.has(key); 264 | }, 265 | 266 | /** 267 | * @param {K} key 268 | * @param {V} value 269 | * @return {boolean} 270 | */ 271 | hasValue(key, value) { 272 | var set = this._map.get(key); 273 | if (!set) return false; 274 | return set.has(value); 275 | }, 276 | 277 | /** 278 | * @return {number} 279 | */ 280 | get size() { 281 | return this._map.size; 282 | }, 283 | 284 | /** 285 | * @param {K} key 286 | * @param {V} value 287 | * @return {boolean} 288 | */ 289 | delete(key, value) { 290 | var values = this.get(key); 291 | var result = values.delete(value); 292 | if (!values.size) this._map.delete(key); 293 | return result; 294 | }, 295 | 296 | /** 297 | * @param {K} key 298 | */ 299 | deleteAll(key) { 300 | this._map.delete(key); 301 | }, 302 | 303 | /** 304 | * @return {!Array.} 305 | */ 306 | keysArray() { 307 | return this._map.keysArray(); 308 | }, 309 | 310 | /** 311 | * @return {!Array.} 312 | */ 313 | valuesArray() { 314 | var result = []; 315 | var keys = this.keysArray(); 316 | for (var i = 0; i < keys.length; ++i) result.pushAll(this.get(keys[i]).valuesArray()); 317 | return result; 318 | }, 319 | 320 | clear() { 321 | this._map.clear(); 322 | } 323 | }; 324 | return Multimap; 325 | } 326 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "covfefe-coverage", 3 | "version": "1.0.0", 4 | "main": "coverage.js", 5 | "author": "Paul Irish ", 6 | "license": "MIT", 7 | "dependencies": { 8 | "chrome-devtools-frontend": "^1.0.485447", 9 | "chrome-launcher": "^0.3.1", 10 | "chrome-remote-interface": "^0.24.1", 11 | "devtools-protocol": "^0.0.485940" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # covfefe-coverage 2 | 3 | _(very early)_ 4 | 5 | Access the DevTools JavaScript Coverage Profiler data. 6 | 7 | ![image](https://user-images.githubusercontent.com/39191/28147058-c3b15b70-6732-11e7-8b82-477324db0699.png) 8 | 9 | Right now this script is a little proof of concept, but it does sit on top of: 10 | 11 | * `chrome-devtools-frontend` for calculation of metrics. 12 | * `chrome-remote-interface` because ya gotta 13 | * `chrome-launcher` for the launch 14 | * `devtools-protocol` for declarative protocol JSON 15 | 16 | Interestingly, this is the first time we've had devtools frontend's `Target` and `TargetManager` working well with a chrome-remote-interface target. There's aspects to be improved but it's quite exciting nonetheless. 17 | --------------------------------------------------------------------------------