1 /* 2 * Copyright (C) 2010 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @fileoverview Profiler processor is used to process log file produced 33 * by V8 and produce an internal profile representation which is used 34 * for building profile views in "Profiles" tab. 35 */ 36 37 38 /** 39 * Creates a Profile View builder object compatible with WebKit Profiler UI. 40 * 41 * @param {number} samplingRate Number of ms between profiler ticks. 42 * @constructor 43 */ 44 devtools.profiler.WebKitViewBuilder = function(samplingRate) 45 { 46 devtools.profiler.ViewBuilder.call(this, samplingRate); 47 }; 48 devtools.profiler.WebKitViewBuilder.prototype.__proto__ = devtools.profiler.ViewBuilder.prototype; 49 50 51 /** 52 * @override 53 */ 54 devtools.profiler.WebKitViewBuilder.prototype.createViewNode = function(funcName, totalTime, selfTime, head) 55 { 56 return new devtools.profiler.WebKitViewNode(funcName, totalTime, selfTime, head); 57 }; 58 59 60 /** 61 * Constructs a Profile View node object for displaying in WebKit Profiler UI. 62 * 63 * @param {string} internalFuncName A fully qualified function name. 64 * @param {number} totalTime Amount of time that application spent in the 65 * corresponding function and its descendants (not that depending on 66 * profile they can be either callees or callers.) 67 * @param {number} selfTime Amount of time that application spent in the 68 * corresponding function only. 69 * @param {devtools.profiler.ProfileView.Node} head Profile view head. 70 * @constructor 71 */ 72 devtools.profiler.WebKitViewNode = function(internalFuncName, totalTime, selfTime, head) 73 { 74 devtools.profiler.ProfileView.Node.call(this, internalFuncName, totalTime, selfTime, head); 75 this.initFuncInfo_(); 76 this.callUID = internalFuncName; 77 }; 78 devtools.profiler.WebKitViewNode.prototype.__proto__ = devtools.profiler.ProfileView.Node.prototype; 79 80 81 /** 82 * RegEx for stripping V8's prefixes of compiled functions. 83 */ 84 devtools.profiler.WebKitViewNode.FUNC_NAME_STRIP_RE = /^(?:LazyCompile|Function|Callback): (.*)$/; 85 86 87 /** 88 * RegEx for extracting script source URL and line number. 89 */ 90 devtools.profiler.WebKitViewNode.FUNC_NAME_PARSE_RE = /^((?:get | set )?[^ ]+) (.*):(\d+)( \{\d+\})?$/; 91 92 93 /** 94 * Inits "functionName", "url", and "lineNumber" fields using "internalFuncName" 95 * field. 96 * @private 97 */ 98 devtools.profiler.WebKitViewNode.prototype.initFuncInfo_ = function() 99 { 100 var nodeAlias = devtools.profiler.WebKitViewNode; 101 this.functionName = this.internalFuncName; 102 103 var strippedName = nodeAlias.FUNC_NAME_STRIP_RE.exec(this.functionName); 104 if (strippedName) 105 this.functionName = strippedName[1]; 106 107 var parsedName = nodeAlias.FUNC_NAME_PARSE_RE.exec(this.functionName); 108 if (parsedName) { 109 this.functionName = parsedName[1]; 110 if (parsedName[4]) 111 this.functionName += parsedName[4]; 112 this.url = parsedName[2]; 113 this.lineNumber = parsedName[3]; 114 } else { 115 this.url = ''; 116 this.lineNumber = 0; 117 } 118 }; 119 120 121 /** 122 * Ancestor of a profile object that leaves out only JS-related functions. 123 * @constructor 124 */ 125 devtools.profiler.JsProfile = function() 126 { 127 devtools.profiler.Profile.call(this); 128 }; 129 devtools.profiler.JsProfile.prototype.__proto__ = devtools.profiler.Profile.prototype; 130 131 132 /** 133 * RegExp that leaves only non-native JS functions. 134 * @type {RegExp} 135 */ 136 devtools.profiler.JsProfile.JS_NON_NATIVE_RE = new RegExp( 137 "^" + 138 "(?:Callback:)|" + 139 "(?:Script: (?!native))|" + 140 "(?:(?:LazyCompile|Function): [^ ]*(?: (?!native )[^ ]+:\\d+)?$)"); 141 142 143 /** 144 * @override 145 */ 146 devtools.profiler.JsProfile.prototype.skipThisFunction = function(name) 147 { 148 return !devtools.profiler.JsProfile.JS_NON_NATIVE_RE.test(name); 149 }; 150 151 152 /** 153 * Profiler processor. Consumes profiler log and builds profile views. 154 * FIXME: change field naming style to use trailing underscore. 155 * 156 * @param {function(devtools.profiler.ProfileView)} newProfileCallback Callback 157 * that receives a new processed profile. 158 * @constructor 159 */ 160 devtools.profiler.Processor = function() 161 { 162 var dispatches = { 163 "code-creation": { 164 parsers: [null, this.createAddressParser("code"), parseInt, null], 165 processor: this.processCodeCreation_, backrefs: true, 166 needsProfile: true }, 167 "code-move": { parsers: [this.createAddressParser("code"), 168 this.createAddressParser("code-move-to")], 169 processor: this.processCodeMove_, backrefs: true, 170 needsProfile: true }, 171 "code-delete": { parsers: [this.createAddressParser("code")], 172 processor: this.processCodeDelete_, backrefs: true, 173 needsProfile: true }, 174 "function-creation": { parsers: [this.createAddressParser("code"), 175 this.createAddressParser("function-obj")], 176 processor: this.processFunctionCreation_, backrefs: true }, 177 "function-move": { parsers: [this.createAddressParser("code"), 178 this.createAddressParser("code-move-to")], 179 processor: this.processFunctionMove_, backrefs: true }, 180 "function-delete": { parsers: [this.createAddressParser("code")], 181 processor: this.processFunctionDelete_, backrefs: true }, 182 "tick": { parsers: [this.createAddressParser("code"), 183 this.createAddressParser("stack"), parseInt, "var-args"], 184 processor: this.processTick_, backrefs: true, needProfile: true }, 185 "profiler": { parsers: [null, "var-args"], 186 processor: this.processProfiler_, needsProfile: false }, 187 "heap-sample-begin": { parsers: [null, null, parseInt], 188 processor: this.processHeapSampleBegin_ }, 189 "heap-sample-stats": { parsers: [null, null, parseInt, parseInt], 190 processor: this.processHeapSampleStats_ }, 191 "heap-sample-item": { parsers: [null, parseInt, parseInt], 192 processor: this.processHeapSampleItem_ }, 193 "heap-js-cons-item": { parsers: [null, parseInt, parseInt], 194 processor: this.processHeapJsConsItem_ }, 195 "heap-js-ret-item": { parsers: [null, "var-args"], 196 processor: this.processHeapJsRetItem_ }, 197 "heap-sample-end": { parsers: [null, null], 198 processor: this.processHeapSampleEnd_ }, 199 // Not used in DevTools Profiler. 200 "shared-library": null, 201 // Obsolete row types. 202 "code-allocate": null, 203 "begin-code-region": null, 204 "end-code-region": null}; 205 206 if (devtools.profiler.Profile.VERSION === 2) { 207 dispatches["tick"] = { parsers: [this.createAddressParser("code"), 208 this.createAddressParser("stack"), 209 this.createAddressParser("func"), parseInt, "var-args"], 210 processor: this.processTickV2_, backrefs: true }; 211 } 212 213 devtools.profiler.LogReader.call(this, dispatches); 214 215 /** 216 * Callback that is called when a new profile is encountered in the log. 217 * @type {function()} 218 */ 219 this.startedProfileProcessing_ = null; 220 221 /** 222 * Callback that is called periodically to display processing status. 223 * @type {function()} 224 */ 225 this.profileProcessingStatus_ = null; 226 227 /** 228 * Callback that is called when a profile has been processed and is ready 229 * to be shown. 230 * @type {function(devtools.profiler.ProfileView)} 231 */ 232 this.finishedProfileProcessing_ = null; 233 234 /** 235 * The current profile. 236 * @type {devtools.profiler.JsProfile} 237 */ 238 this.currentProfile_ = null; 239 240 /** 241 * Builder of profile views. Created during "profiler,begin" event processing. 242 * @type {devtools.profiler.WebKitViewBuilder} 243 */ 244 this.viewBuilder_ = null; 245 246 /** 247 * Next profile id. 248 * @type {number} 249 */ 250 this.profileId_ = 1; 251 252 /** 253 * Counter for processed ticks. 254 * @type {number} 255 */ 256 this.ticksCount_ = 0; 257 258 /** 259 * Interval id for updating processing status. 260 * @type {number} 261 */ 262 this.processingInterval_ = null; 263 264 /** 265 * The current heap snapshot. 266 * @type {string} 267 */ 268 this.currentHeapSnapshot_ = null; 269 270 /** 271 * Next heap snapshot id. 272 * @type {number} 273 */ 274 this.heapSnapshotId_ = 1; 275 }; 276 devtools.profiler.Processor.prototype.__proto__ = devtools.profiler.LogReader.prototype; 277 278 279 /** 280 * @override 281 */ 282 devtools.profiler.Processor.prototype.printError = function(str) 283 { 284 debugPrint(str); 285 }; 286 287 288 /** 289 * @override 290 */ 291 devtools.profiler.Processor.prototype.skipDispatch = function(dispatch) 292 { 293 return dispatch.needsProfile && this.currentProfile_ === null; 294 }; 295 296 297 /** 298 * Sets profile processing callbacks. 299 * 300 * @param {function()} started Started processing callback. 301 * @param {function(devtools.profiler.ProfileView)} finished Finished 302 * processing callback. 303 */ 304 devtools.profiler.Processor.prototype.setCallbacks = function(started, processing, finished) 305 { 306 this.startedProfileProcessing_ = started; 307 this.profileProcessingStatus_ = processing; 308 this.finishedProfileProcessing_ = finished; 309 }; 310 311 312 /** 313 * An address for the fake "(program)" entry. WebKit's visualisation 314 * has assumptions on how the top of the call tree should look like, 315 * and we need to add a fake entry as the topmost function. This 316 * address is chosen because it's the end address of the first memory 317 * page, which is never used for code or data, but only as a guard 318 * page for catching AV errors. 319 * 320 * @type {number} 321 */ 322 devtools.profiler.Processor.PROGRAM_ENTRY = 0xffff; 323 /** 324 * @type {string} 325 */ 326 devtools.profiler.Processor.PROGRAM_ENTRY_STR = "0xffff"; 327 328 329 /** 330 * Sets new profile callback. 331 * @param {function(devtools.profiler.ProfileView)} callback Callback function. 332 */ 333 devtools.profiler.Processor.prototype.setNewProfileCallback = function(callback) 334 { 335 this.newProfileCallback_ = callback; 336 }; 337 338 339 devtools.profiler.Processor.prototype.processProfiler_ = function(state, params) 340 { 341 switch (state) { 342 case "resume": 343 if (this.currentProfile_ === null) { 344 this.currentProfile_ = new devtools.profiler.JsProfile(); 345 // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY 346 this.currentProfile_.addCode("Function", "(program)", devtools.profiler.Processor.PROGRAM_ENTRY, 1); 347 if (this.startedProfileProcessing_) 348 this.startedProfileProcessing_(); 349 this.ticksCount_ = 0; 350 var self = this; 351 if (this.profileProcessingStatus_) { 352 this.processingInterval_ = window.setInterval( 353 function() { self.profileProcessingStatus_(self.ticksCount_); }, 354 1000); 355 } 356 } 357 break; 358 case "pause": 359 if (this.currentProfile_ !== null) { 360 window.clearInterval(this.processingInterval_); 361 this.processingInterval_ = null; 362 if (this.finishedProfileProcessing_) 363 this.finishedProfileProcessing_(this.createProfileForView()); 364 this.currentProfile_ = null; 365 } 366 break; 367 case "begin": 368 var samplingRate = NaN; 369 if (params.length > 0) 370 samplingRate = parseInt(params[0]); 371 if (isNaN(samplingRate)) 372 samplingRate = 1; 373 this.viewBuilder_ = new devtools.profiler.WebKitViewBuilder(samplingRate); 374 break; 375 // These events are valid but aren't used. 376 case "compression": 377 case "end": break; 378 default: 379 throw new Error("unknown profiler state: " + state); 380 } 381 }; 382 383 384 devtools.profiler.Processor.prototype.processCodeCreation_ = function(type, start, size, name) 385 { 386 this.currentProfile_.addCode(this.expandAlias(type), name, start, size); 387 }; 388 389 390 devtools.profiler.Processor.prototype.processCodeMove_ = function(from, to) 391 { 392 this.currentProfile_.moveCode(from, to); 393 }; 394 395 396 devtools.profiler.Processor.prototype.processCodeDelete_ = function(start) 397 { 398 this.currentProfile_.deleteCode(start); 399 }; 400 401 402 devtools.profiler.Processor.prototype.processFunctionCreation_ = function(functionAddr, codeAddr) 403 { 404 this.currentProfile_.addCodeAlias(functionAddr, codeAddr); 405 }; 406 407 408 devtools.profiler.Processor.prototype.processFunctionMove_ = function(from, to) 409 { 410 this.currentProfile_.safeMoveDynamicCode(from, to); 411 }; 412 413 414 devtools.profiler.Processor.prototype.processFunctionDelete_ = function(start) 415 { 416 this.currentProfile_.safeDeleteDynamicCode(start); 417 }; 418 419 420 // TODO(mnaganov): Remove after next V8 roll. 421 devtools.profiler.Processor.prototype.processTick_ = function(pc, sp, vmState, stack) 422 { 423 // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY 424 stack.push(devtools.profiler.Processor.PROGRAM_ENTRY_STR); 425 this.currentProfile_.recordTick(this.processStack(pc, stack)); 426 this.ticksCount_++; 427 }; 428 429 430 devtools.profiler.Processor.prototype.processTickV2_ = function(pc, sp, func, vmState, stack) 431 { 432 // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY 433 stack.push(devtools.profiler.Processor.PROGRAM_ENTRY_STR); 434 435 436 if (func) { 437 var funcEntry = this.currentProfile_.findEntry(func); 438 if (!funcEntry || !funcEntry.isJSFunction || !funcEntry.isJSFunction()) 439 func = 0; 440 else { 441 var currEntry = this.currentProfile_.findEntry(pc); 442 if (!currEntry || !currEntry.isJSFunction || currEntry.isJSFunction()) { 443 func = 0; 444 } 445 } 446 } 447 448 this.currentProfile_.recordTick(this.processStack(pc, func, stack)); 449 this.ticksCount_++; 450 }; 451 452 453 devtools.profiler.Processor.prototype.processHeapSampleBegin_ = function(space, state, ticks) 454 { 455 if (space !== "Heap") return; 456 this.currentHeapSnapshot_ = { 457 number: this.heapSnapshotId_++, 458 entries: {}, 459 clusters: {}, 460 lowlevels: {}, 461 ticks: ticks 462 }; 463 }; 464 465 466 devtools.profiler.Processor.prototype.processHeapSampleStats_ = function(space, state, capacity, used) 467 { 468 if (space !== "Heap") return; 469 }; 470 471 472 devtools.profiler.Processor.prototype.processHeapSampleItem_ = function(item, number, size) 473 { 474 if (!this.currentHeapSnapshot_) return; 475 this.currentHeapSnapshot_.lowlevels[item] = { 476 type: item, count: number, size: size 477 }; 478 }; 479 480 481 devtools.profiler.Processor.prototype.processHeapJsConsItem_ = function(item, number, size) 482 { 483 if (!this.currentHeapSnapshot_) return; 484 this.currentHeapSnapshot_.entries[item] = { 485 cons: item, count: number, size: size, retainers: {} 486 }; 487 }; 488 489 490 devtools.profiler.Processor.prototype.processHeapJsRetItem_ = function(item, retainersArray) 491 { 492 if (!this.currentHeapSnapshot_) return; 493 var rawRetainers = {}; 494 for (var i = 0, n = retainersArray.length; i < n; ++i) { 495 var entry = retainersArray[i].split(";"); 496 rawRetainers[entry[0]] = parseInt(entry[1], 10); 497 } 498 499 function mergeRetainers(entry) { 500 for (var rawRetainer in rawRetainers) { 501 var consName = rawRetainer.indexOf(":") !== -1 ? rawRetainer.split(":")[0] : rawRetainer; 502 if (!(consName in entry.retainers)) 503 entry.retainers[consName] = { cons: consName, count: 0, clusters: {} }; 504 var retainer = entry.retainers[consName]; 505 retainer.count += rawRetainers[rawRetainer]; 506 if (consName !== rawRetainer) 507 retainer.clusters[rawRetainer] = true; 508 } 509 } 510 511 if (item.indexOf(":") !== -1) { 512 // Array, Function, or Object instances cluster case. 513 if (!(item in this.currentHeapSnapshot_.clusters)) { 514 this.currentHeapSnapshot_.clusters[item] = { 515 cons: item, retainers: {} 516 }; 517 } 518 mergeRetainers(this.currentHeapSnapshot_.clusters[item]); 519 item = item.split(":")[0]; 520 } 521 mergeRetainers(this.currentHeapSnapshot_.entries[item]); 522 }; 523 524 525 devtools.profiler.Processor.prototype.processHeapSampleEnd_ = function(space, state) 526 { 527 if (space !== "Heap") return; 528 var snapshot = this.currentHeapSnapshot_; 529 this.currentHeapSnapshot_ = null; 530 WebInspector.panels.profiles.addSnapshot(snapshot); 531 }; 532 533 534 /** 535 * Creates a profile for further displaying in ProfileView. 536 */ 537 devtools.profiler.Processor.prototype.createProfileForView = function() 538 { 539 var profile = this.viewBuilder_.buildView(this.currentProfile_.getTopDownProfile()); 540 profile.uid = this.profileId_++; 541 profile.title = UserInitiatedProfileName + "." + profile.uid; 542 return profile; 543 }; 544