/*
* 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;
};