From dfe8af02a61a19e73ec548fcad8679f148350f63 Mon Sep 17 00:00:00 2001 From: "mikhail.naganov@gmail.com" Date: Fri, 17 Apr 2009 17:40:52 +0000 Subject: [PATCH] Implemented Profile object that processes profiling events and calculates profiling data. Review URL: http://codereview.chromium.org/77014 git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@1739 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- test/mjsunit/tools/profile.js | 283 +++++++++++++++++++++++++ tools/codemap.js | 5 + tools/profile.js | 468 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 756 insertions(+) create mode 100644 test/mjsunit/tools/profile.js create mode 100644 tools/profile.js diff --git a/test/mjsunit/tools/profile.js b/test/mjsunit/tools/profile.js new file mode 100644 index 0000000..87ec8fa --- /dev/null +++ b/test/mjsunit/tools/profile.js @@ -0,0 +1,283 @@ +// Copyright 2009 the V8 project authors. 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. + +// Load source code files from /tools. +// Files: tools/splaytree.js tools/codemap.js tools/profile.js + + +function stackToString(stack) { + return stack.join(' -> '); +}; + + +function assertPathExists(root, path, opt_message) { + var message = opt_message ? ' (' + opt_message + ')' : ''; + assertNotNull(root.descendToChild(path, function(node, pos) { + assertNotNull(node, + stackToString(path.slice(0, pos)) + ' has no child ' + + path[pos] + message); + }), opt_message); +}; + + +function assertNoPathExists(root, path, opt_message) { + var message = opt_message ? ' (' + opt_message + ')' : ''; + assertNull(root.descendToChild(path), opt_message); +}; + + +function countNodes(profile, traverseFunc) { + var count = 0; + traverseFunc.call(profile, function () { count++; }); + return count; +}; + + +function ProfileTestDriver() { + this.profile = new devtools.profiler.Profile(); + this.stack_ = []; + this.addFunctions_(); +}; + + +// Addresses inside functions. +ProfileTestDriver.prototype.funcAddrs_ = { + 'lib1-f1': 0x11110, 'lib1-f2': 0x11210, + 'lib2-f1': 0x21110, 'lib2-f2': 0x21210, + 'T: F1': 0x50110, 'T: F2': 0x50210, 'T: F3': 0x50410 }; + + +ProfileTestDriver.prototype.addFunctions_ = function() { + this.profile.addStaticCode('lib1', 0x11000, 0x12000); + this.profile.addStaticCode('lib1-f1', 0x11100, 0x11900); + this.profile.addStaticCode('lib1-f2', 0x11200, 0x11500); + this.profile.addStaticCode('lib2', 0x21000, 0x22000); + this.profile.addStaticCode('lib2-f1', 0x21100, 0x21900); + this.profile.addStaticCode('lib2-f2', 0x21200, 0x21500); + this.profile.addCode('T', 'F1', 0x50100, 0x100); + this.profile.addCode('T', 'F2', 0x50200, 0x100); + this.profile.addCode('T', 'F3', 0x50400, 0x100); +}; + + +ProfileTestDriver.prototype.enter = function(funcName) { + // Stack looks like this: [pc, caller, ..., main]. + // Therefore, we are adding entries at the beginning. + this.stack_.unshift(this.funcAddrs_[funcName]); + this.profile.recordTick(this.stack_); +}; + + +ProfileTestDriver.prototype.stay = function() { + this.profile.recordTick(this.stack_); +}; + + +ProfileTestDriver.prototype.leave = function() { + this.stack_.shift(); +}; + + +ProfileTestDriver.prototype.execute = function() { + this.enter('lib1-f1'); + this.enter('lib1-f2'); + this.enter('T: F1'); + this.enter('T: F2'); + this.leave(); + this.stay(); + this.enter('lib2-f1'); + this.enter('lib2-f1'); + this.leave(); + this.stay(); + this.leave(); + this.enter('T: F3'); + this.enter('T: F3'); + this.enter('T: F3'); + this.leave(); + this.enter('T: F2'); + this.stay(); + this.leave(); + this.leave(); + this.leave(); + this.leave(); + this.stay(); + this.leave(); +}; + + +function Inherits(childCtor, parentCtor) { + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; + + +(function testCallTreeBuilding() { + function Driver() { + ProfileTestDriver.call(this); + this.namesTopDown = []; + this.namesBottomUp = []; + }; + Inherits(Driver, ProfileTestDriver); + + Driver.prototype.enter = function(func) { + this.namesTopDown.push(func); + this.namesBottomUp.unshift(func); + assertNoPathExists(this.profile.getTopDownTreeRoot(), this.namesTopDown, + 'pre enter/topDown'); + assertNoPathExists(this.profile.getBottomUpTreeRoot(), this.namesBottomUp, + 'pre enter/bottomUp'); + Driver.superClass_.enter.call(this, func); + assertPathExists(this.profile.getTopDownTreeRoot(), this.namesTopDown, + 'post enter/topDown'); + assertPathExists(this.profile.getBottomUpTreeRoot(), this.namesBottomUp, + 'post enter/bottomUp'); + }; + + Driver.prototype.stay = function() { + var preTopDownNodes = countNodes(this.profile, this.profile.traverseTopDownTree); + var preBottomUpNodes = countNodes(this.profile, this.profile.traverseBottomUpTree); + Driver.superClass_.stay.call(this); + var postTopDownNodes = countNodes(this.profile, this.profile.traverseTopDownTree); + var postBottomUpNodes = countNodes(this.profile, this.profile.traverseBottomUpTree); + // Must be no changes in tree layout. + assertEquals(preTopDownNodes, postTopDownNodes, 'stay/topDown'); + assertEquals(preBottomUpNodes, postBottomUpNodes, 'stay/bottomUp'); + }; + + Driver.prototype.leave = function() { + Driver.superClass_.leave.call(this); + this.namesTopDown.pop(); + this.namesBottomUp.shift(); + }; + + var testDriver = new Driver(); + testDriver.execute(); +})(); + + +function assertNodeWeights(root, path, selfTicks, totalTicks) { + var node = root.descendToChild(path); + var stack = stackToString(path); + assertNotNull(node, 'node not found: ' + stack); + assertEquals(selfTicks, node.selfWeight, 'self of ' + stack); + assertEquals(totalTicks, node.totalWeight, 'total of ' + stack); +}; + + +(function testTopDownRootProfileTicks() { + var testDriver = new ProfileTestDriver(); + testDriver.execute(); + + var pathWeights = [ + [['lib1-f1'], 1, 14], + [['lib1-f1', 'lib1-f2'], 2, 13], + [['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], + [['lib1-f1', 'lib1-f2', 'T: F1', 'lib2-f1', 'lib2-f1'], 1, 1], + [['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] + ]; + + var root = testDriver.profile.getTopDownTreeRoot(); + for (var i = 0; i < pathWeights.length; ++i) { + var data = pathWeights[i]; + assertNodeWeights(root, data[0], data[1], data[2]); + } +})(); + + +(function testRootFlatProfileTicks() { + function Driver() { + ProfileTestDriver.call(this); + this.namesTopDown = ['']; + this.counters = {}; + }; + Inherits(Driver, ProfileTestDriver); + + Driver.prototype.increment = function(func, self, total) { + if (!(func in this.counters)) { + this.counters[func] = { self: 0, total: 0 }; + } + this.counters[func].self += self; + this.counters[func].total += total; + }; + + Driver.prototype.incrementTotals = function() { + // Only count each function in the stack once. + var met = {}; + for (var i = 0; i < this.namesTopDown.length; ++i) { + var name = this.namesTopDown[i]; + if (!(name in met)) { + this.increment(name, 0, 1); + } + met[name] = true; + } + }; + + Driver.prototype.enter = function(func) { + Driver.superClass_.enter.call(this, func); + this.namesTopDown.push(func); + this.increment(func, 1, 0); + this.incrementTotals(); + }; + + Driver.prototype.stay = function() { + Driver.superClass_.stay.call(this); + this.increment(this.namesTopDown[this.namesTopDown.length - 1], 1, 0); + this.incrementTotals(); + }; + + Driver.prototype.leave = function() { + Driver.superClass_.leave.call(this); + this.namesTopDown.pop(); + }; + + var testDriver = new Driver(); + testDriver.execute(); + + var counted = 0; + for (var c in testDriver.counters) { + counted++; + } + + var flatProfile = testDriver.profile.getFlatProfile(); + assertEquals(counted, flatProfile.length, 'counted vs. flatProfile'); + for (var i = 0; i < flatProfile.length; ++i) { + var rec = flatProfile[i]; + assertTrue(rec.label in testDriver.counters, 'uncounted: ' + rec.label); + var reference = testDriver.counters[rec.label]; + assertEquals(reference.self, rec.selfWeight, 'self of ' + rec.label); + assertEquals(reference.total, rec.totalWeight, 'total of ' + rec.label); + } + +})(); diff --git a/tools/codemap.js b/tools/codemap.js index f91d525..5149cfc 100644 --- a/tools/codemap.js +++ b/tools/codemap.js @@ -200,6 +200,11 @@ devtools.profiler.CodeMap.CodeEntry = function(size, opt_name) { }; +devtools.profiler.CodeMap.CodeEntry.prototype.getName = function() { + return this.name; +}; + + devtools.profiler.CodeMap.CodeEntry.prototype.toString = function() { return this.name + ': ' + this.size.toString(16); }; diff --git a/tools/profile.js b/tools/profile.js new file mode 100644 index 0000000..e70d244 --- /dev/null +++ b/tools/profile.js @@ -0,0 +1,468 @@ +// Copyright 2009 the V8 project authors. 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. + + +// Initlialize namespaces +var devtools = devtools || {}; +devtools.profiler = devtools.profiler || {}; + + +/** + * Creates a profile object for processing profiling-related events + * and calculating function execution times. + * + * @constructor + */ +devtools.profiler.Profile = function() { + this.codeMap_ = new devtools.profiler.CodeMap(); + this.topDownTree_ = new devtools.profiler.CallTree(); + this.bottomUpTree_ = new devtools.profiler.CallTree(); +}; + + +/** + * Returns whether a function with the specified name must be skipped. + * Should be overriden by subclasses. + * + * @param {string} name Function name. + */ +devtools.profiler.Profile.prototype.skipThisFunction = function(name) { + return false; +}; + + +/** + * Called whenever the specified operation has failed finding a function + * containing the specified address. Should be overriden by subclasses. + * Operation is one of the following: 'move', 'delete', 'tick'. + * + * @param {string} operation Operation name. + * @param {number} addr Address of the unknown code. + */ +devtools.profiler.Profile.prototype.handleUnknownCode = function( + operation, addr) { +}; + + +/** + * Registers static (library) code entry. + * + * @param {string} name Code entry name. + * @param {number} startAddr Starting address. + * @param {number} endAddr Ending address. + */ +devtools.profiler.Profile.prototype.addStaticCode = function( + name, startAddr, endAddr) { + this.codeMap_.addStaticCode(startAddr, + new devtools.profiler.CodeMap.CodeEntry(endAddr - startAddr, name)); +}; + + +/** + * Registers dynamic (JIT-compiled) code entry. + * + * @param {string} type Code entry type. + * @param {string} name Code entry name. + * @param {number} start Starting address. + * @param {number} size Code entry size. + */ +devtools.profiler.Profile.prototype.addCode = function( + type, name, start, size) { + this.codeMap_.addCode(start, + new devtools.profiler.Profile.DynamicCodeEntry(size, type, name)); +}; + + +/** + * Reports about moving of a dynamic code entry. + * + * @param {number} from Current code entry address. + * @param {number} to New code entry address. + */ +devtools.profiler.Profile.prototype.moveCode = function(from, to) { + try { + this.codeMap_.moveCode(from, to); + } catch (e) { + this.handleUnknownCode('move', from); + } +}; + + +/** + * Reports about deletion of a dynamic code entry. + * + * @param {number} start Starting address. + */ +devtools.profiler.Profile.prototype.deleteCode = function(start) { + try { + this.codeMap_.deleteCode(start); + } catch (e) { + this.handleUnknownCode('delete', start); + } +}; + + +/** + * Records a tick event. Stack must contain a sequence of + * addresses starting with the program counter value. + * + * @param {Array} stack Stack sample. + */ +devtools.profiler.Profile.prototype.recordTick = function(stack) { + var processedStack = this.resolveAndFilterFuncs_(stack); + this.bottomUpTree_.addPath(processedStack); + processedStack.reverse(); + this.topDownTree_.addPath(processedStack); +}; + + +/** + * Translates addresses into function names and filters unneeded + * functions. + * + * @param {Array} stack Stack sample. + */ +devtools.profiler.Profile.prototype.resolveAndFilterFuncs_ = function(stack) { + var result = []; + for (var i = 0; i < stack.length; ++i) { + var entry = this.codeMap_.findEntry(stack[i]); + if (entry) { + var name = entry.getName(); + if (!this.skipThisFunction(name)) { + result.push(name); + } + } else { + this.handleUnknownCode('tick', stack[i]); + } + } + return result; +}; + + +/** + * Returns the root of the top down call graph. + */ +devtools.profiler.Profile.prototype.getTopDownTreeRoot = function() { + this.topDownTree_.computeTotalWeights(); + return this.topDownTree_.root_; +}; + + +/** + * Returns the root of the bottom up call graph. + */ +devtools.profiler.Profile.prototype.getBottomUpTreeRoot = function() { + this.bottomUpTree_.computeTotalWeights(); + return this.bottomUpTree_.root_; +}; + + +/** + * Traverses the top down call graph in preorder. + * + * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. + */ +devtools.profiler.Profile.prototype.traverseTopDownTree = function(f) { + this.topDownTree_.traverse(f); +}; + + +/** + * Traverses the bottom up call graph in preorder. + * + * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. + */ +devtools.profiler.Profile.prototype.traverseBottomUpTree = function(f) { + this.bottomUpTree_.traverse(f); +}; + + +/** + * Calculates a flat profile of callees starting from the specified node. + * + * @param {devtools.profiler.CallTree.Node} opt_root Starting node. + */ +devtools.profiler.Profile.prototype.getFlatProfile = function(opt_root) { + var counters = new devtools.profiler.CallTree.Node(''); + var precs = {}; + 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; + } + precs[node.label]++; + }, + function onExit(node) { + precs[node.label]--; + }, + opt_root); + return counters.exportChildren(); +}; + + +/** + * Creates a dynamic code entry. + * + * @param {number} size Code size. + * @param {string} type Code type. + * @param {string} name Function name. + * @constructor + */ +devtools.profiler.Profile.DynamicCodeEntry = function(size, type, name) { + devtools.profiler.CodeMap.CodeEntry.call(this, size, name); + this.type = type; +}; + + +/** + * Returns node name. + */ +devtools.profiler.Profile.DynamicCodeEntry.prototype.getName = function() { + var name = this.name; + if (name.length == 0) { + name = ''; + } else if (name.charAt(0) == ' ') { + // An anonymous function with location: " aaa.js:10". + name = '' + name; + } + return this.type + ': ' + name; +}; + + +/** + * Constructs a call graph. + * + * @constructor + */ +devtools.profiler.CallTree = function() { + this.root_ = new devtools.profiler.CallTree.Node(''); +}; + + +/** + * @private + */ +devtools.profiler.CallTree.prototype.totalsComputed_ = false; + + +/** + * Adds the specified call path, constructing nodes as necessary. + * + * @param {Array} path Call path. + */ +devtools.profiler.CallTree.prototype.addPath = function(path) { + if (path.length == 0) { + return; + } + var curr = this.root_; + for (var i = 0; i < path.length; ++i) { + curr = curr.findOrAddChild(path[i]); + } + curr.selfWeight++; + this.totalsComputed_ = false; +}; + + +/** + * Computes total weights in the call graph. + */ +devtools.profiler.CallTree.prototype.computeTotalWeights = function() { + if (this.totalsComputed_) { + return; + } + this.root_.computeTotalWeight(); + this.totalsComputed_ = true; +}; + + +/** + * Traverses the call graph in preorder. + * + * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. + * @param {devtools.profiler.CallTree.Node} opt_start Starting node. + */ +devtools.profiler.CallTree.prototype.traverse = function(f, opt_start) { + var nodesToVisit = [opt_start || this.root_]; + while (nodesToVisit.length > 0) { + var node = nodesToVisit.shift(); + f(node); + nodesToVisit = nodesToVisit.concat(node.exportChildren()); + } +}; + + +/** + * Performs an indepth call graph traversal. + * + * @param {function(devtools.profiler.CallTree.Node)} enter A function called + * 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) { + function traverse(node) { + enter(node); + node.forEachChild(traverse); + exit(node); + } + traverse(opt_start || this.root_); +}; + + +/** + * Constructs a call graph node. + * + * @param {string} label Node label. + * @param {devtools.profiler.CallTree.Node} opt_parent Node parent. + */ +devtools.profiler.CallTree.Node = function(label, opt_parent) { + this.label = label; + this.parent = opt_parent; + this.children = {}; +}; + + +/** + * Node self weight (how many times this node was the last node in + * a call path). + * @type {number} + */ +devtools.profiler.CallTree.Node.prototype.selfWeight = 0; + + +/** + * Node total weight (includes weights of all children). + * @type {number} + */ +devtools.profiler.CallTree.Node.prototype.totalWeight = 0; + + +/** + * Adds a child node. + * + * @param {string} label Child node label. + */ +devtools.profiler.CallTree.Node.prototype.addChild = function(label) { + var child = new devtools.profiler.CallTree.Node(label, this); + this.children[label] = child; + return child; +}; + + +/** + * Computes node's total weight. + */ +devtools.profiler.CallTree.Node.prototype.computeTotalWeight = + function() { + var totalWeight = this.selfWeight; + this.forEachChild(function(child) { + totalWeight += child.computeTotalWeight(); }); + return this.totalWeight = totalWeight; +}; + + +/** + * Returns all node's children as an array. + */ +devtools.profiler.CallTree.Node.prototype.exportChildren = function() { + var result = []; + this.forEachChild(function (node) { result.push(node); }); + return result; +}; + + +/** + * Finds an immediate child with the specified label. + * + * @param {string} label Child node label. + */ +devtools.profiler.CallTree.Node.prototype.findChild = function(label) { + return this.children[label] || null; +}; + + +/** + * Finds an immediate child with the specified label, creates a child + * node if necessary. + * + * @param {string} label Child node label. + */ +devtools.profiler.CallTree.Node.prototype.findOrAddChild = function( + label) { + return this.findChild(label) || this.addChild(label); +}; + + +/** + * Calls the specified function for every child. + * + * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. + */ +devtools.profiler.CallTree.Node.prototype.forEachChild = function(f) { + for (var c in this.children) { + f(this.children[c]); + } +}; + + +/** + * Walks up from the current node up to the call tree root. + * + * @param {function(devtools.profiler.CallTree.Node)} f Visitor function. + */ +devtools.profiler.CallTree.Node.prototype.walkUpToRoot = function(f) { + for (var curr = this; curr != null; curr = curr.parent) { + f(curr); + } +}; + + +/** + * Tries to find a node with the specified path. + * + * @param {Array} labels The path. + * @param {function(devtools.profiler.CallTree.Node)} opt_f Visitor function. + */ +devtools.profiler.CallTree.Node.prototype.descendToChild = function( + labels, opt_f) { + for (var pos = 0, curr = this; pos < labels.length && curr != null; pos++) { + var child = curr.findChild(labels[pos]); + if (opt_f) { + opt_f(child, pos); + } + curr = child; + } + return curr; +}; -- 2.7.4