/* * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @fileoverview Profiler processor is used to process log file produced * by V8 and produce an internal profile representation which is used * for building profile views in "Profiles" tab. */ /** * Creates a Profile View builder object compatible with WebKit Profiler UI. * * @param {number} samplingRate Number of ms between profiler ticks. * @constructor */ devtools.profiler.WebKitViewBuilder = function(samplingRate) { devtools.profiler.ViewBuilder.call(this, samplingRate); }; devtools.profiler.WebKitViewBuilder.prototype.__proto__ = devtools.profiler.ViewBuilder.prototype; /** * @override */ devtools.profiler.WebKitViewBuilder.prototype.createViewNode = function(funcName, totalTime, selfTime, head) { return new devtools.profiler.WebKitViewNode(funcName, totalTime, selfTime, head); }; /** * Constructs a Profile View node object for displaying in WebKit Profiler UI. * * @param {string} internalFuncName A fully qualified function name. * @param {number} totalTime Amount of time that application spent in the * corresponding function and its descendants (not that depending on * profile they can be either callees or callers.) * @param {number} selfTime Amount of time that application spent in the * corresponding function only. * @param {devtools.profiler.ProfileView.Node} head Profile view head. * @constructor */ devtools.profiler.WebKitViewNode = function(internalFuncName, totalTime, selfTime, head) { devtools.profiler.ProfileView.Node.call(this, internalFuncName, totalTime, selfTime, head); this.initFuncInfo_(); this.callUID = internalFuncName; }; devtools.profiler.WebKitViewNode.prototype.__proto__ = devtools.profiler.ProfileView.Node.prototype; /** * RegEx for stripping V8's prefixes of compiled functions. */ devtools.profiler.WebKitViewNode.FUNC_NAME_STRIP_RE = /^(?:LazyCompile|Function|Callback): (.*)$/; /** * RegEx for extracting script source URL and line number. */ devtools.profiler.WebKitViewNode.FUNC_NAME_PARSE_RE = /^((?:get | set )?[^ ]+) (.*):(\d+)( \{\d+\})?$/; /** * Inits "functionName", "url", and "lineNumber" fields using "internalFuncName" * field. * @private */ devtools.profiler.WebKitViewNode.prototype.initFuncInfo_ = function() { var nodeAlias = devtools.profiler.WebKitViewNode; this.functionName = this.internalFuncName; var strippedName = nodeAlias.FUNC_NAME_STRIP_RE.exec(this.functionName); if (strippedName) this.functionName = strippedName[1]; var parsedName = nodeAlias.FUNC_NAME_PARSE_RE.exec(this.functionName); if (parsedName) { this.functionName = parsedName[1]; if (parsedName[4]) this.functionName += parsedName[4]; this.url = parsedName[2]; this.lineNumber = parsedName[3]; } else { this.url = ''; this.lineNumber = 0; } }; /** * Ancestor of a profile object that leaves out only JS-related functions. * @constructor */ devtools.profiler.JsProfile = function() { devtools.profiler.Profile.call(this); }; devtools.profiler.JsProfile.prototype.__proto__ = devtools.profiler.Profile.prototype; /** * RegExp that leaves only non-native JS functions. * @type {RegExp} */ devtools.profiler.JsProfile.JS_NON_NATIVE_RE = new RegExp( "^" + "(?:Callback:)|" + "(?:Script: (?!native))|" + "(?:(?:LazyCompile|Function): [^ ]*(?: (?!native )[^ ]+:\\d+)?$)"); /** * @override */ devtools.profiler.JsProfile.prototype.skipThisFunction = function(name) { return !devtools.profiler.JsProfile.JS_NON_NATIVE_RE.test(name); }; /** * Profiler processor. Consumes profiler log and builds profile views. * FIXME: change field naming style to use trailing underscore. * * @param {function(devtools.profiler.ProfileView)} newProfileCallback Callback * that receives a new processed profile. * @constructor */ devtools.profiler.Processor = function() { var dispatches = { "code-creation": { parsers: [null, this.createAddressParser("code"), parseInt, null], processor: this.processCodeCreation_, backrefs: true, needsProfile: true }, "code-move": { parsers: [this.createAddressParser("code"), this.createAddressParser("code-move-to")], processor: this.processCodeMove_, backrefs: true, needsProfile: true }, "code-delete": { parsers: [this.createAddressParser("code")], processor: this.processCodeDelete_, backrefs: true, needsProfile: true }, "function-creation": { parsers: [this.createAddressParser("code"), this.createAddressParser("function-obj")], processor: this.processFunctionCreation_, backrefs: true }, "function-move": { parsers: [this.createAddressParser("code"), this.createAddressParser("code-move-to")], processor: this.processFunctionMove_, backrefs: true }, "function-delete": { parsers: [this.createAddressParser("code")], processor: this.processFunctionDelete_, backrefs: true }, "tick": { parsers: [this.createAddressParser("code"), this.createAddressParser("stack"), parseInt, "var-args"], processor: this.processTick_, backrefs: true, needProfile: true }, "profiler": { parsers: [null, "var-args"], processor: this.processProfiler_, needsProfile: false }, "heap-sample-begin": { parsers: [null, null, parseInt], processor: this.processHeapSampleBegin_ }, "heap-sample-stats": { parsers: [null, null, parseInt, parseInt], processor: this.processHeapSampleStats_ }, "heap-sample-item": { parsers: [null, parseInt, parseInt], processor: this.processHeapSampleItem_ }, "heap-js-cons-item": { parsers: [null, parseInt, parseInt], processor: this.processHeapJsConsItem_ }, "heap-js-ret-item": { parsers: [null, "var-args"], processor: this.processHeapJsRetItem_ }, "heap-sample-end": { parsers: [null, null], processor: this.processHeapSampleEnd_ }, // Not used in DevTools Profiler. "shared-library": null, // Obsolete row types. "code-allocate": null, "begin-code-region": null, "end-code-region": null}; if (devtools.profiler.Profile.VERSION === 2) { dispatches["tick"] = { parsers: [this.createAddressParser("code"), this.createAddressParser("stack"), this.createAddressParser("func"), parseInt, "var-args"], processor: this.processTickV2_, backrefs: true }; } devtools.profiler.LogReader.call(this, dispatches); /** * Callback that is called when a new profile is encountered in the log. * @type {function()} */ this.startedProfileProcessing_ = null; /** * Callback that is called periodically to display processing status. * @type {function()} */ this.profileProcessingStatus_ = null; /** * Callback that is called when a profile has been processed and is ready * to be shown. * @type {function(devtools.profiler.ProfileView)} */ this.finishedProfileProcessing_ = null; /** * The current profile. * @type {devtools.profiler.JsProfile} */ this.currentProfile_ = null; /** * Builder of profile views. Created during "profiler,begin" event processing. * @type {devtools.profiler.WebKitViewBuilder} */ this.viewBuilder_ = null; /** * Next profile id. * @type {number} */ this.profileId_ = 1; /** * Counter for processed ticks. * @type {number} */ this.ticksCount_ = 0; /** * Interval id for updating processing status. * @type {number} */ this.processingInterval_ = null; /** * The current heap snapshot. * @type {string} */ this.currentHeapSnapshot_ = null; /** * Next heap snapshot id. * @type {number} */ this.heapSnapshotId_ = 1; }; devtools.profiler.Processor.prototype.__proto__ = devtools.profiler.LogReader.prototype; /** * @override */ devtools.profiler.Processor.prototype.printError = function(str) { debugPrint(str); }; /** * @override */ devtools.profiler.Processor.prototype.skipDispatch = function(dispatch) { return dispatch.needsProfile && this.currentProfile_ === null; }; /** * Sets profile processing callbacks. * * @param {function()} started Started processing callback. * @param {function(devtools.profiler.ProfileView)} finished Finished * processing callback. */ devtools.profiler.Processor.prototype.setCallbacks = function(started, processing, finished) { this.startedProfileProcessing_ = started; this.profileProcessingStatus_ = processing; this.finishedProfileProcessing_ = finished; }; /** * An address for the fake "(program)" entry. WebKit's visualisation * has assumptions on how the top of the call tree should look like, * and we need to add a fake entry as the topmost function. This * address is chosen because it's the end address of the first memory * page, which is never used for code or data, but only as a guard * page for catching AV errors. * * @type {number} */ devtools.profiler.Processor.PROGRAM_ENTRY = 0xffff; /** * @type {string} */ devtools.profiler.Processor.PROGRAM_ENTRY_STR = "0xffff"; /** * Sets new profile callback. * @param {function(devtools.profiler.ProfileView)} callback Callback function. */ devtools.profiler.Processor.prototype.setNewProfileCallback = function(callback) { this.newProfileCallback_ = callback; }; devtools.profiler.Processor.prototype.processProfiler_ = function(state, params) { switch (state) { case "resume": if (this.currentProfile_ === null) { this.currentProfile_ = new devtools.profiler.JsProfile(); // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY this.currentProfile_.addCode("Function", "(program)", devtools.profiler.Processor.PROGRAM_ENTRY, 1); if (this.startedProfileProcessing_) this.startedProfileProcessing_(); this.ticksCount_ = 0; var self = this; if (this.profileProcessingStatus_) { this.processingInterval_ = window.setInterval( function() { self.profileProcessingStatus_(self.ticksCount_); }, 1000); } } break; case "pause": if (this.currentProfile_ !== null) { window.clearInterval(this.processingInterval_); this.processingInterval_ = null; if (this.finishedProfileProcessing_) this.finishedProfileProcessing_(this.createProfileForView()); this.currentProfile_ = null; } break; case "begin": var samplingRate = NaN; if (params.length > 0) samplingRate = parseInt(params[0]); if (isNaN(samplingRate)) samplingRate = 1; this.viewBuilder_ = new devtools.profiler.WebKitViewBuilder(samplingRate); break; // These events are valid but aren't used. case "compression": case "end": break; default: throw new Error("unknown profiler state: " + state); } }; devtools.profiler.Processor.prototype.processCodeCreation_ = function(type, start, size, name) { this.currentProfile_.addCode(this.expandAlias(type), name, start, size); }; devtools.profiler.Processor.prototype.processCodeMove_ = function(from, to) { this.currentProfile_.moveCode(from, to); }; devtools.profiler.Processor.prototype.processCodeDelete_ = function(start) { this.currentProfile_.deleteCode(start); }; devtools.profiler.Processor.prototype.processFunctionCreation_ = function(functionAddr, codeAddr) { this.currentProfile_.addCodeAlias(functionAddr, codeAddr); }; devtools.profiler.Processor.prototype.processFunctionMove_ = function(from, to) { this.currentProfile_.safeMoveDynamicCode(from, to); }; devtools.profiler.Processor.prototype.processFunctionDelete_ = function(start) { this.currentProfile_.safeDeleteDynamicCode(start); }; // TODO(mnaganov): Remove after next V8 roll. devtools.profiler.Processor.prototype.processTick_ = function(pc, sp, vmState, stack) { // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY stack.push(devtools.profiler.Processor.PROGRAM_ENTRY_STR); this.currentProfile_.recordTick(this.processStack(pc, stack)); this.ticksCount_++; }; devtools.profiler.Processor.prototype.processTickV2_ = function(pc, sp, func, vmState, stack) { // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY stack.push(devtools.profiler.Processor.PROGRAM_ENTRY_STR); if (func) { var funcEntry = this.currentProfile_.findEntry(func); if (!funcEntry || !funcEntry.isJSFunction || !funcEntry.isJSFunction()) func = 0; else { var currEntry = this.currentProfile_.findEntry(pc); if (!currEntry || !currEntry.isJSFunction || currEntry.isJSFunction()) { func = 0; } } } this.currentProfile_.recordTick(this.processStack(pc, func, stack)); this.ticksCount_++; }; devtools.profiler.Processor.prototype.processHeapSampleBegin_ = function(space, state, ticks) { if (space !== "Heap") return; this.currentHeapSnapshot_ = { number: this.heapSnapshotId_++, entries: {}, clusters: {}, lowlevels: {}, ticks: ticks }; }; devtools.profiler.Processor.prototype.processHeapSampleStats_ = function(space, state, capacity, used) { if (space !== "Heap") return; }; devtools.profiler.Processor.prototype.processHeapSampleItem_ = function(item, number, size) { if (!this.currentHeapSnapshot_) return; this.currentHeapSnapshot_.lowlevels[item] = { type: item, count: number, size: size }; }; devtools.profiler.Processor.prototype.processHeapJsConsItem_ = function(item, number, size) { if (!this.currentHeapSnapshot_) return; this.currentHeapSnapshot_.entries[item] = { cons: item, count: number, size: size, retainers: {} }; }; devtools.profiler.Processor.prototype.processHeapJsRetItem_ = function(item, retainersArray) { if (!this.currentHeapSnapshot_) return; var rawRetainers = {}; for (var i = 0, n = retainersArray.length; i < n; ++i) { var entry = retainersArray[i].split(";"); rawRetainers[entry[0]] = parseInt(entry[1], 10); } function mergeRetainers(entry) { for (var rawRetainer in rawRetainers) { var consName = rawRetainer.indexOf(":") !== -1 ? rawRetainer.split(":")[0] : rawRetainer; if (!(consName in entry.retainers)) entry.retainers[consName] = { cons: consName, count: 0, clusters: {} }; var retainer = entry.retainers[consName]; retainer.count += rawRetainers[rawRetainer]; if (consName !== rawRetainer) retainer.clusters[rawRetainer] = true; } } if (item.indexOf(":") !== -1) { // Array, Function, or Object instances cluster case. if (!(item in this.currentHeapSnapshot_.clusters)) { this.currentHeapSnapshot_.clusters[item] = { cons: item, retainers: {} }; } mergeRetainers(this.currentHeapSnapshot_.clusters[item]); item = item.split(":")[0]; } mergeRetainers(this.currentHeapSnapshot_.entries[item]); }; devtools.profiler.Processor.prototype.processHeapSampleEnd_ = function(space, state) { if (space !== "Heap") return; var snapshot = this.currentHeapSnapshot_; this.currentHeapSnapshot_ = null; WebInspector.panels.profiles.addSnapshot(snapshot); }; /** * Creates a profile for further displaying in ProfileView. */ devtools.profiler.Processor.prototype.createProfileForView = function() { var profile = this.viewBuilder_.buildView(this.currentProfile_.getTopDownProfile()); profile.uid = this.profileId_++; profile.title = UserInitiatedProfileName + "." + profile.uid; return profile; };