From 178a656429475b77ea169fb8764c7e5873a75cdc Mon Sep 17 00:00:00 2001 From: "mikhail.naganov@gmail.com" Date: Thu, 30 Apr 2009 08:10:27 +0000 Subject: [PATCH] Enhancing profiling data processing code with functionality needed for the Dev Tools Profiler. Details: - added properties / functions in view objects needed for WebKit's ProfileView; - added ability to count profiles for specific functions. The tickprocessor functionality does not affected. Review URL: http://codereview.chromium.org/99181 git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@1823 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- test/mjsunit/tools/profile.js | 84 +++++++++++++++-- tools/profile.js | 169 ++++++++++++++++++++++------------ tools/profile_view.js | 138 +++++++++++++++++++++++++++ tools/splaytree.js | 3 +- tools/tickprocessor.js | 7 +- 5 files changed, 328 insertions(+), 73 deletions(-) diff --git a/test/mjsunit/tools/profile.js b/test/mjsunit/tools/profile.js index aaa9aa76c..49eef3b0a 100644 --- a/test/mjsunit/tools/profile.js +++ b/test/mjsunit/tools/profile.js @@ -124,6 +124,10 @@ ProfileTestDriver.prototype.execute = function() { this.leave(); this.leave(); this.leave(); + this.enter('lib2-f1'); + this.enter('lib1-f1'); + this.leave(); + this.leave(); this.stay(); this.leave(); }; @@ -149,14 +153,14 @@ function Inherits(childCtor, parentCtor) { Driver.prototype.enter = function(func) { this.namesTopDown.push(func); this.namesBottomUp.unshift(func); - assertNoPathExists(this.profile.getTopDownTreeRoot(), this.namesTopDown, + assertNoPathExists(this.profile.getTopDownProfile().getRoot(), this.namesTopDown, 'pre enter/topDown'); - assertNoPathExists(this.profile.getBottomUpTreeRoot(), this.namesBottomUp, + assertNoPathExists(this.profile.getBottomUpProfile().getRoot(), this.namesBottomUp, 'pre enter/bottomUp'); Driver.superClass_.enter.call(this, func); - assertPathExists(this.profile.getTopDownTreeRoot(), this.namesTopDown, + assertPathExists(this.profile.getTopDownProfile().getRoot(), this.namesTopDown, 'post enter/topDown'); - assertPathExists(this.profile.getBottomUpTreeRoot(), this.namesBottomUp, + assertPathExists(this.profile.getBottomUpProfile().getRoot(), this.namesBottomUp, 'post enter/bottomUp'); }; @@ -196,8 +200,8 @@ function assertNodeWeights(root, path, selfTicks, totalTicks) { testDriver.execute(); var pathWeights = [ - [['lib1-f1'], 1, 14], - [['lib1-f1', 'lib1-f2'], 2, 13], + [['lib1-f1'], 1, 16], + [['lib1-f1', 'lib1-f2'], 2, 15], [['lib1-f1', 'lib1-f2', 'T: F1'], 2, 11], [['lib1-f1', 'lib1-f2', 'T: F1', 'T: F2'], 1, 1], [['lib1-f1', 'lib1-f2', 'T: F1', 'lib2-f1'], 2, 3], @@ -205,10 +209,12 @@ function assertNodeWeights(root, path, selfTicks, totalTicks) { [['lib1-f1', 'lib1-f2', 'T: F1', 'T: F3'], 1, 5], [['lib1-f1', 'lib1-f2', 'T: F1', 'T: F3', 'T: F3'], 1, 4], [['lib1-f1', 'lib1-f2', 'T: F1', 'T: F3', 'T: F3', 'T: F3'], 1, 1], - [['lib1-f1', 'lib1-f2', 'T: F1', 'T: F3', 'T: F3', 'T: F2'], 2, 2] + [['lib1-f1', 'lib1-f2', 'T: F1', 'T: F3', 'T: F3', 'T: F2'], 2, 2], + [['lib1-f1', 'lib1-f2', 'lib2-f1'], 1, 2], + [['lib1-f1', 'lib1-f2', 'lib2-f1', 'lib1-f1'], 1, 1] ]; - var root = testDriver.profile.getTopDownTreeRoot(); + var root = testDriver.profile.getTopDownProfile().getRoot(); for (var i = 0; i < pathWeights.length; ++i) { var data = pathWeights[i]; assertNodeWeights(root, data[0], data[1], data[2]); @@ -221,6 +227,7 @@ function assertNodeWeights(root, path, selfTicks, totalTicks) { ProfileTestDriver.call(this); this.namesTopDown = ['']; this.counters = {}; + this.root = null; }; Inherits(Driver, ProfileTestDriver); @@ -262,16 +269,26 @@ function assertNodeWeights(root, path, selfTicks, totalTicks) { this.namesTopDown.pop(); }; + Driver.prototype.extractRoot = function() { + assertTrue('' in this.counters); + this.root = this.counters['']; + delete this.counters['']; + }; + var testDriver = new Driver(); testDriver.execute(); + testDriver.extractRoot(); var counted = 0; for (var c in testDriver.counters) { counted++; } - var flatProfile = - testDriver.profile.getFlatProfile().getRoot().exportChildren(); + var flatProfileRoot = testDriver.profile.getFlatProfile().getRoot(); + assertEquals(testDriver.root.self, flatProfileRoot.selfWeight); + assertEquals(testDriver.root.total, flatProfileRoot.totalWeight); + + var flatProfile = flatProfileRoot.exportChildren(); assertEquals(counted, flatProfile.length, 'counted vs. flatProfile'); for (var i = 0; i < flatProfile.length; ++i) { var rec = flatProfile[i]; @@ -282,3 +299,50 @@ function assertNodeWeights(root, path, selfTicks, totalTicks) { } })(); + + +(function testFunctionCalleesProfileTicks() { + var testDriver = new ProfileTestDriver(); + testDriver.execute(); + + var pathWeights = [ + [['lib2-f1'], 3, 5], + [['lib2-f1', 'lib2-f1'], 1, 1], + [['lib2-f1', 'lib1-f1'], 1, 1] + ]; + + var profile = testDriver.profile.getTopDownProfile('lib2-f1'); + var root = profile.getRoot(); + for (var i = 0; i < pathWeights.length; ++i) { + var data = pathWeights[i]; + assertNodeWeights(root, data[0], data[1], data[2]); + } +})(); + + +(function testFunctionFlatProfileTicks() { + var testDriver = new ProfileTestDriver(); + testDriver.execute(); + + var flatWeights = { + 'lib2-f1': [1, 1], + 'lib1-f1': [1, 1] + }; + + var flatProfileRoot = + testDriver.profile.getFlatProfile('lib2-f1').findOrAddChild('lib2-f1'); + assertEquals(3, flatProfileRoot.selfWeight); + assertEquals(5, flatProfileRoot.totalWeight); + + var flatProfile = flatProfileRoot.exportChildren(); + assertEquals(2, flatProfile.length, 'counted vs. flatProfile'); + for (var i = 0; i < flatProfile.length; ++i) { + var rec = flatProfile[i]; + assertTrue(rec.label in flatWeights, 'uncounted: ' + rec.label); + var reference = flatWeights[rec.label]; + assertEquals(reference[0], rec.selfWeight, 'self of ' + rec.label); + assertEquals(reference[1], rec.totalWeight, 'total of ' + rec.label); + } + +})(); + diff --git a/tools/profile.js b/tools/profile.js index 70e30846d..614c63557 100644 --- a/tools/profile.js +++ b/tools/profile.js @@ -185,25 +185,7 @@ devtools.profiler.Profile.prototype.resolveAndFilterFuncs_ = function(stack) { /** - * Returns the root of the top down call graph. - */ -devtools.profiler.Profile.prototype.getTopDownTreeRoot = function() { - this.topDownTree_.computeTotalWeights(); - return this.topDownTree_.getRoot(); -}; - - -/** - * Returns the root of the bottom up call graph. - */ -devtools.profiler.Profile.prototype.getBottomUpTreeRoot = function() { - this.bottomUpTree_.computeTotalWeights(); - return this.bottomUpTree_.getRoot(); -}; - - -/** - * Traverses the top down call graph in preorder. + * Performs a BF traversal of the top down call graph. * * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. */ @@ -213,7 +195,7 @@ devtools.profiler.Profile.prototype.traverseTopDownTree = function(f) { /** - * Traverses the bottom up call graph in preorder. + * Performs a BF traversal of the bottom up call graph. * * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. */ @@ -223,60 +205,96 @@ devtools.profiler.Profile.prototype.traverseBottomUpTree = function(f) { /** - * Calculates a top down profile starting from the specified node. + * Calculates a top down profile for a node with the specified label. + * If no name specified, returns the whole top down calls tree. * - * @param {devtools.profiler.CallTree.Node} opt_root Starting node. + * @param {string} opt_label Node label. */ -devtools.profiler.Profile.prototype.getTopDownProfile = function(opt_root) { - if (!opt_root) { - this.topDownTree_.computeTotalWeights(); - return this.topDownTree_; - } else { - throw Error('not implemented'); - } +devtools.profiler.Profile.prototype.getTopDownProfile = function(opt_label) { + return this.getTreeProfile_(this.topDownTree_, opt_label); +}; + + +/** + * Calculates a bottom up profile for a node with the specified label. + * If no name specified, returns the whole bottom up calls tree. + * + * @param {string} opt_label Node label. + */ +devtools.profiler.Profile.prototype.getBottomUpProfile = function(opt_label) { + return this.getTreeProfile_(this.bottomUpTree_, opt_label); }; /** - * Calculates a bottom up profile starting from the specified node. + * Helper function for calculating a tree profile. * - * @param {devtools.profiler.CallTree.Node} opt_root Starting node. + * @param {devtools.profiler.Profile.CallTree} tree Call tree. + * @param {string} opt_label Node label. */ -devtools.profiler.Profile.prototype.getBottomUpProfile = function(opt_root) { - if (!opt_root) { - this.bottomUpTree_.computeTotalWeights(); - return this.bottomUpTree_; +devtools.profiler.Profile.prototype.getTreeProfile_ = function(tree, opt_label) { + if (!opt_label) { + tree.computeTotalWeights(); + return tree; } else { - throw Error('not implemented'); + var subTree = tree.cloneSubtree(opt_label); + subTree.computeTotalWeights(); + return subTree; } }; /** - * Calculates a flat profile of callees starting from the specified node. + * Calculates a flat profile of callees starting from a node with + * the specified label. If no name specified, starts from the root. * - * @param {devtools.profiler.CallTree.Node} opt_root Starting node. + * @param {string} opt_label Starting node label. */ -devtools.profiler.Profile.prototype.getFlatProfile = function(opt_root) { +devtools.profiler.Profile.prototype.getFlatProfile = function(opt_label) { var counters = new devtools.profiler.CallTree(); + var rootLabel = opt_label || devtools.profiler.CallTree.ROOT_NODE_LABEL; var precs = {}; + precs[rootLabel] = 0; + var root = counters.findOrAddChild(rootLabel); + this.topDownTree_.computeTotalWeights(); this.topDownTree_.traverseInDepth( function onEnter(node) { if (!(node.label in precs)) { precs[node.label] = 0; } - var rec = counters.findOrAddChild(node.label); - rec.selfWeight += node.selfWeight; - if (precs[node.label] == 0) { - rec.totalWeight += node.totalWeight; + var nodeLabelIsRootLabel = node.label == rootLabel; + if (nodeLabelIsRootLabel || precs[rootLabel] > 0) { + if (precs[rootLabel] == 0) { + root.selfWeight += node.selfWeight; + root.totalWeight += node.totalWeight; + } else { + var rec = root.findOrAddChild(node.label); + rec.selfWeight += node.selfWeight; + if (nodeLabelIsRootLabel || precs[node.label] == 0) { + rec.totalWeight += node.totalWeight; + } + } + precs[node.label]++; } - precs[node.label]++; }, function onExit(node) { - precs[node.label]--; + if (node.label == rootLabel || precs[rootLabel] > 0) { + precs[node.label]--; + } }, - opt_root); + null); + + if (!opt_label) { + // If we have created a flat profile for the whole program, we don't + // need an explicit root in it. Thus, replace the counters tree + // root with the node corresponding to the whole program. + counters.root_ = root; + } else { + // Propagate weights so percents can be calculated correctly. + counters.getRoot().selfWeight = root.selfWeight; + counters.getRoot().totalWeight = root.totalWeight; + } return counters; }; @@ -316,10 +334,17 @@ devtools.profiler.Profile.DynamicCodeEntry.prototype.getName = function() { * @constructor */ devtools.profiler.CallTree = function() { - this.root_ = new devtools.profiler.CallTree.Node(''); + this.root_ = new devtools.profiler.CallTree.Node( + devtools.profiler.CallTree.ROOT_NODE_LABEL); }; +/** + * The label of the root node. + */ +devtools.profiler.CallTree.ROOT_NODE_LABEL = ''; + + /** * @private */ @@ -359,10 +384,38 @@ devtools.profiler.CallTree.prototype.addPath = function(path) { * * @param {string} label Child node label. */ -devtools.profiler.CallTree.prototype.findOrAddChild = function( - label, opt_parent) { - var parent = opt_parent || this.root_; - return parent.findOrAddChild(label); +devtools.profiler.CallTree.prototype.findOrAddChild = function(label) { + return this.root_.findOrAddChild(label); +}; + + +/** + * Creates a subtree by cloning and merging all subtrees rooted at nodes + * with a given label. E.g. cloning the following call tree on label 'A' + * will give the following result: + * + * -- + * / / + * == clone on 'A' ==> -- + * \ \ + * ---- + * + * And 's selfWeight will be the sum of selfWeights of 's from the + * source call tree. + * + * @param {string} label The label of the new root node. + */ +devtools.profiler.CallTree.prototype.cloneSubtree = function(label) { + var subTree = new devtools.profiler.CallTree(); + this.traverse(function(node, parent) { + if (!parent && node.label != label) { + return null; + } + var child = (parent ? parent : subTree).findOrAddChild(node.label); + child.selfWeight += node.selfWeight; + return child; + }); + return subTree; }; @@ -392,11 +445,10 @@ devtools.profiler.CallTree.prototype.computeTotalWeights = function() { * * @param {function(devtools.profiler.CallTree.Node, *)} f Visitor function. * The second parameter is the result of calling 'f' on the parent node. - * @param {devtools.profiler.CallTree.Node} opt_start Starting node. */ -devtools.profiler.CallTree.prototype.traverse = function(f, opt_start) { +devtools.profiler.CallTree.prototype.traverse = function(f) { var pairsToProcess = new ConsArray(); - pairsToProcess.concat([{node: opt_start || this.root_, param: null}]); + pairsToProcess.concat([{node: this.root_, param: null}]); while (!pairsToProcess.atEnd()) { var pair = pairsToProcess.next(); var node = pair.node; @@ -416,16 +468,14 @@ devtools.profiler.CallTree.prototype.traverse = function(f, opt_start) { * prior to visiting node's children. * @param {function(devtools.profiler.CallTree.Node)} exit A function called * after visiting node's children. - * @param {devtools.profiler.CallTree.Node} opt_start Starting node. */ -devtools.profiler.CallTree.prototype.traverseInDepth = function( - enter, exit, opt_start) { +devtools.profiler.CallTree.prototype.traverseInDepth = function(enter, exit) { function traverse(node) { enter(node); node.forEachChild(traverse); exit(node); } - traverse(opt_start || this.root_); + traverse(this.root_); }; @@ -507,8 +557,7 @@ devtools.profiler.CallTree.Node.prototype.findChild = function(label) { * * @param {string} label Child node label. */ -devtools.profiler.CallTree.Node.prototype.findOrAddChild = function( - label) { +devtools.profiler.CallTree.Node.prototype.findOrAddChild = function(label) { return this.findChild(label) || this.addChild(label); }; diff --git a/tools/profile_view.js b/tools/profile_view.js index 8cffccc7c..cd0511f6b 100644 --- a/tools/profile_view.js +++ b/tools/profile_view.js @@ -77,6 +77,26 @@ devtools.profiler.ViewBuilder.prototype.buildView = function( */ devtools.profiler.ProfileView = function(head) { this.head = head; + this.title = ''; + this.uid = ''; + this.heavyProfile = null; + this.treeProfile = null; + this.flatProfile = null; +}; + + +/** + * Updates references between profiles. This is needed for WebKit + * ProfileView. + */ +devtools.profiler.ProfileView.prototype.updateProfilesRefs = function() { + var profileNames = ["treeProfile", "heavyProfile", "flatProfile"]; + for (var i = 0; i < profileNames.length; ++i) { + var destProfile = this[profileNames[i]]; + for (var j = 0; j < profileNames.length; ++j) { + destProfile[profileNames[j]] = this[profileNames[j]]; + } + } }; @@ -94,6 +114,73 @@ devtools.profiler.ProfileView.prototype.sort = function(sortFunc) { }; +/** + * Sorts the profile view by self time, ascending. + */ +devtools.profiler.ProfileView.prototype.sortSelfTimeAscending = function() { + this.sort(function (node1, node2) { + return node1.selfTime - node2.selfTime; }); +}; + + +/** + * Sorts the profile view by self time, descending. + */ +devtools.profiler.ProfileView.prototype.sortSelfTimeDescending = function() { + this.sort(function (node1, node2) { + return node2.selfTime - node1.selfTime; }); +}; + + +/** + * Sorts the profile view by total time, ascending. + */ +devtools.profiler.ProfileView.prototype.sortTotalTimeAscending = function() { + this.sort(function (node1, node2) { + return node1.totalTime - node2.totalTime; }); +}; + + +/** + * Sorts the profile view by total time, descending. + */ +devtools.profiler.ProfileView.prototype.sortTotalTimeDescending = function() { + this.sort(function (node1, node2) { + return node2.totalTime - node1.totalTime; }); +}; + + +/** + * String comparator compatible with Array.sort requirements. + * + * @param {string} s1 First string. + * @param {string} s2 Second string. + */ +devtools.profiler.ProfileView.compareStrings = function(s1, s2) { + return s1 < s2 ? -1 : (s1 > s2 ? 1 : 0); +}; + + +/** + * Sorts the profile view by function name, ascending. + */ +devtools.profiler.ProfileView.prototype.sortFunctionNameAscending = function() { + this.sort(function (node1, node2) { + return devtools.profiler.ProfileView.compareStrings( + node1.functionName, node2.functionName); }); +}; + + +/** + * Sorts the profile view by function name, descending. + */ +devtools.profiler.ProfileView.prototype.sortFunctionNameDescending = function() { + this.sort(function (node1, node2) { + return devtools.profiler.ProfileView.compareStrings( + node2.functionName, node1.functionName); }); +}; + + /** * Traverses profile view nodes in preorder. * @@ -125,12 +212,63 @@ devtools.profiler.ProfileView.prototype.traverse = function(f) { */ devtools.profiler.ProfileView.Node = function( internalFuncName, totalTime, selfTime, head) { + this.callIdentifier = 0; this.internalFuncName = internalFuncName; + this.initFuncInfo(); this.totalTime = totalTime; this.selfTime = selfTime; this.head = head; this.parent = null; this.children = []; + this.visible = true; +}; + + +/** + * RegEx for stripping V8's prefixes of compiled functions. + */ +devtools.profiler.ProfileView.Node.FUNC_NAME_STRIP_RE = + /^(?:LazyCompile|Function): (.*)$/; + + +/** + * RegEx for extracting script source URL and line number. + */ +devtools.profiler.ProfileView.Node.FUNC_NAME_PARSE_RE = /^([^ ]+) (.*):(\d+)$/; + + +/** + * RegEx for removing protocol name from URL. + */ +devtools.profiler.ProfileView.Node.URL_PARSE_RE = /^(?:http:\/)?.*\/([^/]+)$/; + + +/** + * Inits 'functionName', 'url', and 'lineNumber' fields using 'internalFuncName' + * field. + */ +devtools.profiler.ProfileView.Node.prototype.initFuncInfo = function() { + var nodeAlias = devtools.profiler.ProfileView.Node; + 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.url = parsedName[2]; + var parsedUrl = nodeAlias.URL_PARSE_RE.exec(this.url); + if (parsedUrl) { + this.url = parsedUrl[1]; + } + this.functionName = parsedName[1]; + this.lineNumber = parsedName[3]; + } else { + this.url = ''; + this.lineNumber = 0; + } }; diff --git a/tools/splaytree.js b/tools/splaytree.js index f3beb164a..7b3af8b99 100644 --- a/tools/splaytree.js +++ b/tools/splaytree.js @@ -28,7 +28,8 @@ // A namespace stub. It will become more clear how to declare it properly // during integration of this script into Dev Tools. -goog = { structs: {} }; +var goog = goog || {}; +goog.structs = goog.structs || {}; /** diff --git a/tools/tickprocessor.js b/tools/tickprocessor.js index 9d1971971..64020cabb 100644 --- a/tools/tickprocessor.js +++ b/tools/tickprocessor.js @@ -273,6 +273,10 @@ TickProcessor.prototype.printStatistics = function() { this.printCounter(this.ticks_.unaccounted, this.ticks_.total); } + // Disable initialization of 'funcName', 'url', 'lineNumber' as + // we don't use it and it just wastes time. + devtools.profiler.ProfileView.Node.prototype.initFuncInfo = function() {}; + var flatProfile = this.profile_.getFlatProfile(); var flatView = this.viewBuilder_.buildView(flatProfile); // Sort by self time, desc, then by name, desc. @@ -361,8 +365,7 @@ TickProcessor.prototype.processProfile = function( profile, filterP, func) { for (var i = 0, n = profile.length; i < n; ++i) { var rec = profile[i]; - // An empty record corresponds to a tree root. - if (!rec.internalFuncName || !filterP(rec.internalFuncName)) { + if (!filterP(rec.internalFuncName)) { continue; } func(rec); -- 2.34.1