| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of sample_profiler; |
| |
| abstract class CallTreeNode<NodeT extends M.CallTreeNode> |
| implements M.CallTreeNode { |
| final List<NodeT> children; |
| int get count => _count; |
| int _count = 0; |
| int inclusiveNativeAllocations = 0; |
| int exclusiveNativeAllocations = 0; |
| double get percentage => _percentage; |
| double _percentage = 0.0; |
| final attributes = <String>{}; |
| |
| // Used for building timeline |
| int frameId = null; |
| int parentId = null; |
| |
| // Either a ProfileCode or a ProfileFunction. |
| Object get profileData; |
| String get name; |
| |
| CallTreeNode(this.children, |
| [this._count = 0, |
| this.inclusiveNativeAllocations = 0, |
| this.exclusiveNativeAllocations = 0]) {} |
| |
| NodeT getChild(int index); |
| |
| void sortChildren() { |
| children.sort((a, b) => b.count - a.count); |
| children.forEach((NodeT child) => child.sortChildren()); |
| } |
| |
| void tick(Map sample, {bool exclusive = false}) { |
| ++_count; |
| if (SampleProfile._isNativeAllocationSample(sample)) { |
| final allocationSize = sample[SampleProfile._kNativeAllocationSizeBytes]; |
| if (exclusive) { |
| exclusiveNativeAllocations += allocationSize; |
| } |
| inclusiveNativeAllocations += allocationSize; |
| } |
| } |
| } |
| |
| class CodeCallTreeNode extends CallTreeNode<CodeCallTreeNode> |
| implements M.CodeCallTreeNode { |
| final ProfileCode profileCode; |
| final SampleProfile profile; |
| |
| Object get profileData => profileCode; |
| |
| String get name => profileCode.code.name; |
| |
| final attributes = <String>{}; |
| CodeCallTreeNode(this.profileCode, int count, int inclusiveNativeAllocations, |
| int exclusiveNativeAllocations) |
| : profile = profileCode.profile, |
| super(<CodeCallTreeNode>[], count, inclusiveNativeAllocations, |
| exclusiveNativeAllocations) { |
| attributes.addAll(profileCode.attributes); |
| } |
| |
| CodeCallTreeNode.fromIndex(this.profile, int tableIndex) |
| : profileCode = profile.codes[tableIndex], |
| super(<CodeCallTreeNode>[]); |
| |
| CodeCallTreeNode getChild(int codeTableIndex) { |
| final length = children.length; |
| int i = 0; |
| while (i < length) { |
| final child = children[i]; |
| final childTableIndex = child.profileCode.tableIndex; |
| if (childTableIndex == codeTableIndex) { |
| return child; |
| } |
| if (childTableIndex > codeTableIndex) { |
| break; |
| } |
| ++i; |
| } |
| final child = CodeCallTreeNode.fromIndex(profile, codeTableIndex); |
| if (i < length) { |
| children.insert(i, child); |
| } else { |
| children.add(child); |
| } |
| return child; |
| } |
| } |
| |
| class CallTree<NodeT extends CallTreeNode> { |
| final bool inclusive; |
| final NodeT root; |
| |
| CallTree(this.inclusive, this.root); |
| } |
| |
| class CodeCallTree extends CallTree<CodeCallTreeNode> |
| implements M.CodeCallTree { |
| CodeCallTree(bool inclusive, CodeCallTreeNode root) : super(inclusive, root) { |
| if ((root.inclusiveNativeAllocations != null) && |
| (root.inclusiveNativeAllocations != 0)) { |
| _setCodeMemoryPercentage(null, root); |
| } else { |
| _setCodePercentage(null, root); |
| } |
| } |
| |
| CodeCallTree filtered(CallTreeNodeFilter filter) { |
| final treeFilter = _FilteredCodeCallTreeBuilder(filter, this); |
| treeFilter.build(); |
| if ((treeFilter.filtered.root.inclusiveNativeAllocations != null) && |
| (treeFilter.filtered.root.inclusiveNativeAllocations != 0)) { |
| _setCodeMemoryPercentage(null, treeFilter.filtered.root); |
| } else { |
| _setCodePercentage(null, treeFilter.filtered.root); |
| } |
| return treeFilter.filtered; |
| } |
| |
| _setCodePercentage(CodeCallTreeNode parent, CodeCallTreeNode node) { |
| assert(node != null); |
| var parentPercentage = 1.0; |
| var parentCount = node.count; |
| if (parent != null) { |
| parentPercentage = parent._percentage; |
| parentCount = parent.count; |
| } |
| if (inclusive) { |
| node._percentage = parentPercentage * (node.count / parentCount); |
| } else { |
| node._percentage = (node.count / parentCount); |
| } |
| for (var child in node.children) { |
| _setCodePercentage(node, child); |
| } |
| } |
| |
| _setCodeMemoryPercentage(CodeCallTreeNode parent, CodeCallTreeNode node) { |
| assert(node != null); |
| var parentPercentage = 1.0; |
| var parentMemory = node.inclusiveNativeAllocations; |
| if (parent != null) { |
| parentPercentage = parent._percentage; |
| parentMemory = parent.inclusiveNativeAllocations; |
| } |
| if (inclusive) { |
| node._percentage = |
| parentPercentage * (node.inclusiveNativeAllocations / parentMemory); |
| } else { |
| node._percentage = (node.inclusiveNativeAllocations / parentMemory); |
| } |
| for (var child in node.children) { |
| _setCodeMemoryPercentage(node, child); |
| } |
| node.children.sort((a, b) { |
| return b.inclusiveNativeAllocations - a.inclusiveNativeAllocations; |
| }); |
| } |
| |
| _recordCallerAndCalleesInner( |
| CodeCallTreeNode caller, CodeCallTreeNode callee) { |
| if (caller != null) { |
| caller.profileCode._recordCallee(callee.profileCode, callee.count); |
| callee.profileCode._recordCaller(caller.profileCode, caller.count); |
| } |
| |
| for (var child in callee.children) { |
| _recordCallerAndCalleesInner(callee, child); |
| } |
| } |
| |
| _recordCallerAndCallees() { |
| for (var child in root.children) { |
| _recordCallerAndCalleesInner(null, child); |
| } |
| } |
| } |
| |
| class FunctionCallTreeNodeCode { |
| final ProfileCode code; |
| final int ticks; |
| FunctionCallTreeNodeCode(this.code, this.ticks); |
| } |
| |
| class FunctionCallTreeNode extends CallTreeNode<FunctionCallTreeNode> |
| implements M.FunctionCallTreeNode { |
| final ProfileFunction profileFunction; |
| final SampleProfile profile; |
| final codes = <FunctionCallTreeNodeCode>[]; |
| int _totalCodeTicks = 0; |
| int get totalCodesTicks => _totalCodeTicks; |
| |
| String get name => M.getFunctionFullName(profileFunction.function); |
| Object get profileData => profileFunction; |
| |
| FunctionCallTreeNode(this.profileFunction, int count, |
| inclusiveNativeAllocations, exclusiveNativeAllocations) |
| : profile = profileFunction.profile, |
| super(<FunctionCallTreeNode>[], count, inclusiveNativeAllocations, |
| exclusiveNativeAllocations) { |
| profileFunction._addKindBasedAttributes(attributes); |
| } |
| |
| FunctionCallTreeNode.fromIndex(this.profile, int tableIndex) |
| : profileFunction = profile.functions[tableIndex], |
| super(<FunctionCallTreeNode>[]); |
| |
| FunctionCallTreeNode getChild(int functionTableIndex) { |
| final length = children.length; |
| int i = 0; |
| while (i < length) { |
| final child = children[i]; |
| final childTableIndex = child.profileFunction.tableIndex; |
| if (childTableIndex == functionTableIndex) { |
| return child; |
| } |
| if (childTableIndex > functionTableIndex) { |
| break; |
| } |
| ++i; |
| } |
| final child = FunctionCallTreeNode.fromIndex(profile, functionTableIndex); |
| if (i < length) { |
| children.insert(i, child); |
| } else { |
| children.add(child); |
| } |
| return child; |
| } |
| } |
| |
| /// Predicate filter function. Returns true if path from root to [node] and all |
| /// of [node]'s children should be added to the filtered tree. |
| typedef CallTreeNodeFilter = bool Function(CallTreeNode node); |
| |
| /// Build a filter version of a FunctionCallTree. |
| abstract class _FilteredCallTreeBuilder<NodeT extends CallTreeNode> { |
| /// The filter. |
| final CallTreeNodeFilter filter; |
| |
| /// The unfiltered tree. |
| final CallTree _unfilteredTree; |
| |
| /// The filtered tree (construct by [build]). |
| final CallTree filtered; |
| final List _currentPath = []; |
| |
| /// Construct a filtered tree builder using [filter] and [tree]. |
| _FilteredCallTreeBuilder(this.filter, CallTree tree, this.filtered) |
| : _unfilteredTree = tree; |
| |
| /// Build the filtered tree. |
| build() { |
| assert(filtered != null); |
| assert(filter != null); |
| assert(_unfilteredTree != null); |
| _descend(_unfilteredTree.root); |
| } |
| |
| CallTreeNode _findInChildren(CallTreeNode current, CallTreeNode needle) { |
| for (var child in current.children) { |
| if ((child as CallTreeNode).profileData == needle.profileData) { |
| return child; |
| } |
| } |
| return null; |
| } |
| |
| NodeT _copyNode(NodeT node); |
| |
| /// Add all nodes in [_currentPath]. |
| FunctionCallTreeNode _addCurrentPath() { |
| FunctionCallTreeNode current = filtered.root; |
| // Tree root is always the first element of the current path. |
| assert(_unfilteredTree.root == _currentPath[0]); |
| // Assert that unfiltered tree's root and filtered tree's root are different. |
| assert(_unfilteredTree.root != current); |
| for (var i = 1; i < _currentPath.length; i++) { |
| // toAdd is from the unfiltered tree. |
| var toAdd = _currentPath[i]; |
| // See if we already have a node for toAdd in the filtered tree. |
| var child = _findInChildren(current, toAdd); |
| if (child == null) { |
| // New node. |
| child = _copyNode(toAdd); |
| current.children.add(child); |
| } |
| current = child; |
| } |
| return current; |
| } |
| |
| /// Starting at [current] append [next] and all of [next]'s sub-trees |
| _appendTree(CallTreeNode current, CallTreeNode next) { |
| if (next == null) { |
| return; |
| } |
| var child = _findInChildren(current, next); |
| if (child == null) { |
| child = _copyNode(next); |
| current.children.add(child); |
| } |
| current = child; |
| for (var nextChild in next.children) { |
| _appendTree(current, nextChild); |
| } |
| } |
| |
| /// Add path from root to [child], [child], and all of [child]'s sub-trees |
| /// to filtered tree. |
| _addTree(CallTreeNode child) { |
| var current = _addCurrentPath(); |
| _appendTree(current, child); |
| } |
| |
| /// Descend further into the tree. [current] is from the unfiltered tree. |
| _descend(CallTreeNode current) { |
| if (current == null) { |
| return; |
| } |
| _currentPath.add(current); |
| |
| if (filter(current)) { |
| // Filter matched. |
| if (current.children.length == 0) { |
| // Have no children. Add this path. |
| _addTree(null); |
| } else { |
| // Add all child trees. |
| for (var child in current.children) { |
| _addTree(child); |
| } |
| } |
| } else { |
| // Did not match, descend to each child. |
| for (var child in current.children) { |
| _descend(child); |
| } |
| } |
| |
| var last = _currentPath.removeLast(); |
| assert(current == last); |
| } |
| } |
| |
| class _FilteredFunctionCallTreeBuilder |
| extends _FilteredCallTreeBuilder<FunctionCallTreeNode> { |
| _FilteredFunctionCallTreeBuilder( |
| CallTreeNodeFilter filter, FunctionCallTree tree) |
| : super( |
| filter, |
| tree, |
| FunctionCallTree( |
| tree.inclusive, |
| FunctionCallTreeNode( |
| tree.root.profileData, |
| tree.root.count, |
| tree.root.inclusiveNativeAllocations, |
| tree.root.exclusiveNativeAllocations))); |
| |
| _copyNode(FunctionCallTreeNode node) { |
| return FunctionCallTreeNode(node.profileData, node.count, |
| node.inclusiveNativeAllocations, node.exclusiveNativeAllocations); |
| } |
| } |
| |
| class _FilteredCodeCallTreeBuilder |
| extends _FilteredCallTreeBuilder<CodeCallTreeNode> { |
| _FilteredCodeCallTreeBuilder(CallTreeNodeFilter filter, CodeCallTree tree) |
| : super( |
| filter, |
| tree, |
| CodeCallTree( |
| tree.inclusive, |
| CodeCallTreeNode( |
| tree.root.profileData, |
| tree.root.count, |
| tree.root.inclusiveNativeAllocations, |
| tree.root.exclusiveNativeAllocations))); |
| |
| _copyNode(CodeCallTreeNode node) { |
| return CodeCallTreeNode(node.profileData, node.count, |
| node.inclusiveNativeAllocations, node.exclusiveNativeAllocations); |
| } |
| } |
| |
| class FunctionCallTree extends CallTree<FunctionCallTreeNode> |
| implements M.FunctionCallTree { |
| FunctionCallTree(bool inclusive, FunctionCallTreeNode root) |
| : super(inclusive, root) { |
| if ((root.inclusiveNativeAllocations != null) && |
| (root.inclusiveNativeAllocations != 0)) { |
| _setFunctionMemoryPercentage(null, root); |
| } else { |
| _setFunctionPercentage(null, root); |
| } |
| } |
| |
| FunctionCallTree filtered(CallTreeNodeFilter filter) { |
| final treeFilter = _FilteredFunctionCallTreeBuilder(filter, this); |
| treeFilter.build(); |
| if ((treeFilter.filtered.root.inclusiveNativeAllocations != null) && |
| (treeFilter.filtered.root.inclusiveNativeAllocations != 0)) { |
| _setFunctionMemoryPercentage(null, treeFilter.filtered.root); |
| } else { |
| _setFunctionPercentage(null, treeFilter.filtered.root); |
| } |
| return treeFilter.filtered; |
| } |
| |
| void _setFunctionPercentage( |
| FunctionCallTreeNode parent, FunctionCallTreeNode node) { |
| assert(node != null); |
| var parentPercentage = 1.0; |
| var parentCount = node.count; |
| if (parent != null) { |
| parentPercentage = parent._percentage; |
| parentCount = parent.count; |
| } |
| if (inclusive) { |
| node._percentage = parentPercentage * (node.count / parentCount); |
| } else { |
| node._percentage = (node.count / parentCount); |
| } |
| for (var child in node.children) { |
| _setFunctionPercentage(node, child); |
| } |
| } |
| |
| void _setFunctionMemoryPercentage( |
| FunctionCallTreeNode parent, FunctionCallTreeNode node) { |
| assert(node != null); |
| var parentPercentage = 1.0; |
| var parentMemory = node.inclusiveNativeAllocations; |
| if (parent != null) { |
| parentPercentage = parent._percentage; |
| parentMemory = parent.inclusiveNativeAllocations; |
| } |
| if (inclusive) { |
| node._percentage = |
| parentPercentage * (node.inclusiveNativeAllocations / parentMemory); |
| } else { |
| node._percentage = (node.inclusiveNativeAllocations / parentMemory); |
| } |
| for (var child in node.children) { |
| _setFunctionMemoryPercentage(node, child); |
| } |
| node.children.sort((a, b) { |
| return b.inclusiveNativeAllocations - a.inclusiveNativeAllocations; |
| }); |
| } |
| |
| _markFunctionCallsInner( |
| FunctionCallTreeNode caller, FunctionCallTreeNode callee) { |
| if (caller != null) { |
| caller.profileFunction |
| ._recordCallee(callee.profileFunction, callee.count); |
| callee.profileFunction |
| ._recordCaller(caller.profileFunction, caller.count); |
| } |
| for (var child in callee.children) { |
| _markFunctionCallsInner(callee, child); |
| } |
| } |
| |
| _markFunctionCalls() { |
| for (var child in root.children) { |
| _markFunctionCallsInner(null, child); |
| } |
| } |
| } |
| |
| class CodeTick { |
| final int exclusiveTicks; |
| final int inclusiveTicks; |
| CodeTick(this.exclusiveTicks, this.inclusiveTicks); |
| } |
| |
| class InlineIntervalTick { |
| final int startAddress; |
| int _inclusiveTicks = 0; |
| int get inclusiveTicks => _inclusiveTicks; |
| int _exclusiveTicks = 0; |
| int get exclusiveTicks => _exclusiveTicks; |
| InlineIntervalTick(this.startAddress); |
| } |
| |
| class ProfileCode implements M.ProfileCode { |
| final int tableIndex; |
| final SampleProfile profile; |
| final Code code; |
| int exclusiveTicks; |
| int inclusiveTicks; |
| double normalizedExclusiveTicks = 0.0; |
| double normalizedInclusiveTicks = 0.0; |
| final addressTicks = <int, CodeTick>{}; |
| final intervalTicks = <int, InlineIntervalTick>{}; |
| String formattedInclusiveTicks = ''; |
| String formattedExclusiveTicks = ''; |
| String formattedExclusivePercent = ''; |
| final Set<String> attributes = <String>{}; |
| final callers = <ProfileCode, int>{}; |
| final callees = <ProfileCode, int>{}; |
| |
| void _processTicks(List<dynamic> profileTicks) { |
| assert(profileTicks != null); |
| assert((profileTicks.length % 3) == 0); |
| for (var i = 0; i < profileTicks.length; i += 3) { |
| // TODO(observatory): Address is not necessarily representable as a JS |
| // integer. |
| var address = int.parse(profileTicks[i] as String, radix: 16); |
| var exclusive = profileTicks[i + 1] as int; |
| var inclusive = profileTicks[i + 2] as int; |
| var tick = CodeTick(exclusive, inclusive); |
| addressTicks[address] = tick; |
| |
| var interval = code.findInterval(address); |
| if (interval != null) { |
| var intervalTick = intervalTicks[interval.start]; |
| if (intervalTick == null) { |
| // Insert into map. |
| intervalTick = InlineIntervalTick(interval.start); |
| intervalTicks[interval.start] = intervalTick; |
| } |
| intervalTick._inclusiveTicks += inclusive; |
| intervalTick._exclusiveTicks += exclusive; |
| } |
| } |
| } |
| |
| void clearTicks() { |
| exclusiveTicks = 0; |
| inclusiveTicks = 0; |
| normalizedExclusiveTicks = 0; |
| normalizedInclusiveTicks = 0; |
| formattedInclusiveTicks = ''; |
| formattedExclusiveTicks = ''; |
| formattedExclusivePercent = ''; |
| } |
| |
| ProfileCode.fromMap(this.tableIndex, this.profile, this.code, Map data) { |
| assert(profile != null); |
| assert(code != null); |
| |
| code.profile = this; |
| |
| if (code.kind == M.CodeKind.stub) { |
| attributes.add('stub'); |
| } else if (code.kind == M.CodeKind.dart) { |
| if (code.isNative) { |
| attributes.add('ffi'); // Not to be confused with a C function. |
| } else { |
| attributes.add('dart'); |
| } |
| if (code.hasIntrinsic) { |
| attributes.add('intrinsic'); |
| } |
| if (code.isOptimized) { |
| attributes.add('optimized'); |
| } else { |
| attributes.add('unoptimized'); |
| } |
| } else if (code.kind == M.CodeKind.tag) { |
| attributes.add('tag'); |
| } else if (code.kind == M.CodeKind.native) { |
| attributes.add('native'); |
| } |
| inclusiveTicks = data['inclusiveTicks']; |
| exclusiveTicks = data['exclusiveTicks']; |
| |
| normalizedExclusiveTicks = exclusiveTicks / profile.sampleCount; |
| |
| normalizedInclusiveTicks = inclusiveTicks / profile.sampleCount; |
| |
| var ticks = data['ticks']; |
| if (ticks != null) { |
| _processTicks(ticks); |
| } |
| |
| formattedExclusivePercent = |
| Utils.formatPercent(exclusiveTicks, profile.sampleCount); |
| |
| formattedInclusiveTicks = |
| '${Utils.formatPercent(inclusiveTicks, profile.sampleCount)} ' |
| '($inclusiveTicks)'; |
| |
| formattedExclusiveTicks = |
| '${Utils.formatPercent(exclusiveTicks, profile.sampleCount)} ' |
| '($exclusiveTicks)'; |
| } |
| |
| _recordCaller(ProfileCode caller, int count) { |
| var r = callers[caller]; |
| if (r == null) { |
| r = 0; |
| } |
| callers[caller] = r + count; |
| } |
| |
| _recordCallee(ProfileCode callee, int count) { |
| var r = callees[callee]; |
| if (r == null) { |
| r = 0; |
| } |
| callees[callee] = r + count; |
| } |
| |
| void _normalizeTicks() { |
| normalizedExclusiveTicks = exclusiveTicks / profile.sampleCount; |
| normalizedInclusiveTicks = inclusiveTicks / profile.sampleCount; |
| |
| formattedExclusivePercent = |
| Utils.formatPercent(exclusiveTicks, profile.sampleCount); |
| |
| formattedInclusiveTicks = |
| '${Utils.formatPercent(inclusiveTicks, profile.sampleCount)} ' |
| '($inclusiveTicks)'; |
| |
| formattedExclusiveTicks = |
| '${Utils.formatPercent(exclusiveTicks, profile.sampleCount)} ' |
| '($exclusiveTicks)'; |
| } |
| |
| void tickTag() { |
| // All functions *except* those for tags are ticked in the VM while |
| // generating the CpuSamples response. |
| if (code.kind != M.CodeKind.tag) { |
| throw StateError('Only tags should be ticked. ' |
| 'Attempted to tick: ${code.name}'); |
| } |
| ++exclusiveTicks; |
| ++inclusiveTicks; |
| _normalizeTicks(); |
| } |
| } |
| |
| class ProfileFunction implements M.ProfileFunction { |
| final int tableIndex; |
| final SampleProfile profile; |
| final ServiceFunction function; |
| final String resolvedUrl; |
| final callers = <ProfileFunction, int>{}; |
| final callees = <ProfileFunction, int>{}; |
| |
| // Absolute ticks: |
| int exclusiveTicks = 0; |
| int inclusiveTicks = 0; |
| |
| // Global percentages: |
| double normalizedExclusiveTicks = 0.0; |
| double normalizedInclusiveTicks = 0.0; |
| |
| String formattedInclusiveTicks = ''; |
| String formattedExclusiveTicks = ''; |
| String formattedExclusivePercent = ''; |
| final attributes = <String>{}; |
| |
| void clearTicks() { |
| exclusiveTicks = 0; |
| inclusiveTicks = 0; |
| normalizedExclusiveTicks = 0; |
| normalizedInclusiveTicks = 0; |
| formattedInclusiveTicks = ''; |
| formattedExclusiveTicks = ''; |
| formattedExclusivePercent = ''; |
| } |
| |
| void _addKindBasedAttributes(Set<String> attribs) { |
| if (function.kind == M.FunctionKind.tag) { |
| attribs.add('tag'); |
| } else if (function.kind == M.FunctionKind.stub) { |
| attribs.add('stub'); |
| } else if (function.kind == M.FunctionKind.native) { |
| attribs.add('native'); |
| } else if (M.isSyntheticFunction(function.kind)) { |
| attribs.add('synthetic'); |
| } else if (function.isNative) { |
| attribs.add('ffi'); // Not to be confused with a C function. |
| } else { |
| attribs.add('dart'); |
| } |
| if (function.hasIntrinsic == true) { |
| attribs.add('intrinsic'); |
| } |
| } |
| |
| ProfileFunction.fromMap( |
| this.tableIndex, this.profile, this.function, Map data) |
| : resolvedUrl = data['resolvedUrl'] { |
| function.profile = this; |
| _addKindBasedAttributes(attributes); |
| exclusiveTicks = data['exclusiveTicks']; |
| inclusiveTicks = data['inclusiveTicks']; |
| _normalizeTicks(); |
| } |
| |
| _recordCaller(ProfileFunction caller, int count) { |
| var r = callers[caller]; |
| if (r == null) { |
| r = 0; |
| } |
| callers[caller] = r + count; |
| } |
| |
| _recordCallee(ProfileFunction callee, int count) { |
| var r = callees[callee]; |
| if (r == null) { |
| r = 0; |
| } |
| callees[callee] = r + count; |
| } |
| |
| void _normalizeTicks() { |
| normalizedExclusiveTicks = exclusiveTicks / profile.sampleCount; |
| normalizedInclusiveTicks = inclusiveTicks / profile.sampleCount; |
| |
| formattedExclusivePercent = |
| Utils.formatPercent(exclusiveTicks, profile.sampleCount); |
| |
| formattedInclusiveTicks = |
| '${Utils.formatPercent(inclusiveTicks, profile.sampleCount)} ' |
| '($inclusiveTicks)'; |
| |
| formattedExclusiveTicks = |
| '${Utils.formatPercent(exclusiveTicks, profile.sampleCount)} ' |
| '($exclusiveTicks)'; |
| } |
| |
| void tickTag() { |
| // All functions *except* those for functions are ticked in the VM while |
| // generating the CpuSamples response. |
| if (function.kind != M.FunctionKind.tag) { |
| throw StateError('Only tags should be ticked. ' |
| 'Attempted to tick: ${function.name}'); |
| } |
| ++exclusiveTicks; |
| ++inclusiveTicks; |
| _normalizeTicks(); |
| } |
| } |
| |
| class SampleProfile extends M.SampleProfile { |
| Isolate isolate; |
| |
| int sampleCount = 0; |
| int samplePeriod = 0; |
| double sampleRate = 0.0; |
| int pid = 0; |
| int maxStackDepth = 0; |
| |
| double timeSpan = 0.0; |
| |
| M.SampleProfileTag tagOrder = M.SampleProfileTag.none; |
| |
| final _functionTagMapping = <String, int>{}; |
| final _codeTagMapping = <String, int>{}; |
| |
| final List samples = []; |
| final codes = <ProfileCode>[]; |
| bool _builtCodeCalls = false; |
| final functions = <ProfileFunction>[]; |
| bool _builtFunctionCalls = false; |
| |
| static const String _kCode = 'code'; |
| static const String _kCodes = '_codes'; |
| static const String _kCodeStack = '_codeStack'; |
| static const String _kFunction = 'function'; |
| static const String _kFunctions = 'functions'; |
| static const String _kNativeAllocationSizeBytes = |
| '_nativeAllocationSizeBytes'; |
| static const String _kPid = 'pid'; |
| static const String _kSampleCount = 'sampleCount'; |
| static const String _kSamplePeriod = 'samplePeriod'; |
| static const String _kSamples = 'samples'; |
| static const String _kStack = 'stack'; |
| static const String _kMaxStackDepth = 'maxStackDepth'; |
| static const String _kTimeSpan = 'timespan'; |
| static const String _kUserTag = 'userTag'; |
| static const String _kVmTag = 'vmTag'; |
| |
| // Names of special tag types. |
| static const String kNativeTag = 'Native'; |
| static const String kRootTag = 'Root'; |
| static const String kRuntimeTag = 'Runtime'; |
| static const String kTruncatedTag = '[Truncated]'; |
| |
| // Used to stash trie information in samples for timeline processing. |
| static const String kTimelineFunctionTrie = 'timelineFunctionTrie'; |
| |
| CodeCallTree loadCodeTree(M.ProfileTreeDirection direction) { |
| switch (direction) { |
| case M.ProfileTreeDirection.inclusive: |
| return _loadCodeTree(true); |
| case M.ProfileTreeDirection.exclusive: |
| return _loadCodeTree(false); |
| } |
| throw Exception('Unknown ProfileTreeDirection'); |
| } |
| |
| FunctionCallTree loadFunctionTree(M.ProfileTreeDirection direction) { |
| switch (direction) { |
| case M.ProfileTreeDirection.inclusive: |
| return _loadFunctionTree(true); |
| case M.ProfileTreeDirection.exclusive: |
| return _loadFunctionTree(false); |
| } |
| throw Exception('Unknown ProfileTreeDirection'); |
| } |
| |
| buildCodeCallerAndCallees() { |
| if (_builtCodeCalls) { |
| return; |
| } |
| _builtCodeCalls = true; |
| var tree = loadCodeTree(M.ProfileTreeDirection.inclusive); |
| tree._recordCallerAndCallees(); |
| } |
| |
| buildFunctionCallerAndCallees() { |
| if (_builtFunctionCalls) { |
| return; |
| } |
| _builtFunctionCalls = true; |
| var tree = loadFunctionTree(M.ProfileTreeDirection.inclusive); |
| tree._markFunctionCalls(); |
| } |
| |
| clear() { |
| pid = -1; |
| sampleCount = 0; |
| samplePeriod = 0; |
| sampleRate = 0.0; |
| maxStackDepth = 0; |
| timeSpan = 0.0; |
| codes.clear(); |
| functions.clear(); |
| _builtCodeCalls = false; |
| _builtFunctionCalls = false; |
| } |
| |
| Future load(ServiceObjectOwner owner, ServiceMap profile) async { |
| await loadProgress(owner, profile).drain(); |
| } |
| |
| static Future sleep([Duration duration = const Duration(microseconds: 0)]) => |
| Future.delayed(duration); |
| |
| Future _loadCommon(ServiceObjectOwner owner, ServiceMap profile, |
| [StreamController<double> progress]) async { |
| final watch = Stopwatch(); |
| watch.start(); |
| int count = 0; |
| var needToUpdate = () { |
| if (progress == null) { |
| return false; |
| } |
| count++; |
| if (((count % 256) == 0) && (watch.elapsedMilliseconds > 16)) { |
| watch.reset(); |
| return true; |
| } |
| return false; |
| }; |
| var signal = (double p) { |
| if (progress == null) { |
| return null; |
| } |
| progress.add(p); |
| return sleep(); |
| }; |
| try { |
| clear(); |
| progress?.add(0.0); |
| if (profile == null) { |
| return; |
| } |
| |
| if ((owner != null) && (owner is Isolate)) { |
| isolate = owner; |
| isolate.resetCachedProfileData(); |
| } |
| |
| pid = profile[_kPid]; |
| sampleCount = profile[_kSampleCount]; |
| samplePeriod = profile[_kSamplePeriod]; |
| sampleRate = (Duration.microsecondsPerSecond / samplePeriod); |
| maxStackDepth = profile[_kMaxStackDepth]; |
| timeSpan = profile[_kTimeSpan]; |
| |
| num length = 0; |
| |
| if (profile.containsKey(_kCodes)) { |
| length += profile[_kCodes].length; |
| } |
| length += profile[_kFunctions].length; |
| |
| // Process code table. |
| int tableIndex = 0; |
| if (profile.containsKey(_kCodes)) { |
| for (var codeRegion in profile[_kCodes]) { |
| if (needToUpdate()) { |
| await signal(count * 100.0 / length); |
| } |
| Code code = codeRegion[_kCode]; |
| assert(code != null); |
| codes.add(ProfileCode.fromMap(tableIndex, this, code, codeRegion)); |
| ++tableIndex; |
| } |
| } |
| // Process function table. |
| tableIndex = 0; |
| for (var profileFunction in profile[_kFunctions]) { |
| if (needToUpdate()) { |
| await signal(count * 100 / length); |
| } |
| ServiceFunction function = profileFunction[_kFunction]; |
| assert(function != null); |
| functions.add(ProfileFunction.fromMap( |
| tableIndex, this, function, profileFunction)); |
| ++tableIndex; |
| } |
| if (profile.containsKey(_kCodes)) { |
| _buildCodeTagMapping(); |
| } |
| |
| _buildFunctionTagMapping(); |
| |
| samples.addAll(profile[_kSamples]); |
| } finally { |
| progress?.close(); |
| } |
| } |
| |
| Stream<double> loadProgress(ServiceObjectOwner owner, ServiceMap profile) { |
| Logger.root.info('sampling counters ${profile['_counters']}'); |
| |
| var progress = StreamController<double>.broadcast(); |
| |
| _loadCommon(owner, profile, progress); |
| return progress.stream; |
| } |
| |
| // Helpers for reading optional flags from a sample. |
| static bool _isNativeEntryTag(Map sample) => |
| sample.containsKey('nativeEntryTag'); |
| static bool _isRuntimeEntryTag(Map sample) => |
| sample.containsKey('runtimeEntryTag'); |
| static bool _isTruncated(Map sample) => sample.containsKey('truncated'); |
| static bool _isNativeAllocationSample(Map sample) => |
| sample.containsKey('_nativeAllocationSizeBytes'); |
| |
| int _getProfileFunctionTagIndex(String tag) { |
| if (_functionTagMapping.containsKey(tag)) { |
| return _functionTagMapping[tag]; |
| } |
| throw ArgumentError('$tag is not a valid tag!'); |
| } |
| |
| int _getProfileCodeTagIndex(String tag) { |
| if (_codeTagMapping.containsKey(tag)) { |
| return _codeTagMapping[tag]; |
| } |
| throw ArgumentError('$tag is not a valid tag!'); |
| } |
| |
| void _buildFunctionTagMapping() { |
| for (int i = 0; i < functions.length; ++i) { |
| final function = functions[i].function; |
| if (function.kind == M.FunctionKind.tag) { |
| _functionTagMapping[function.name] = i; |
| } |
| } |
| } |
| |
| void _buildCodeTagMapping() { |
| for (int i = 0; i < codes.length; ++i) { |
| final code = codes[i].code; |
| if (code.kind == M.CodeKind.tag) { |
| _codeTagMapping[code.name] = i; |
| } |
| } |
| } |
| |
| void _clearProfileFunctionTagTicks() => |
| _functionTagMapping.forEach((String name, int tableIndex) { |
| // Truncated tag is ticked in the VM, so don't clear it. |
| if (name != kTruncatedTag) { |
| functions[tableIndex].clearTicks(); |
| } |
| }); |
| |
| void _clearProfileCodeTagTicks() => |
| _codeTagMapping.forEach((String name, int tableIndex) { |
| // Truncated tag is ticked in the VM, so don't clear it. |
| if (name != kTruncatedTag) { |
| codes[tableIndex].clearTicks(); |
| } |
| }); |
| |
| NodeT _appendUserTag<NodeT extends CallTreeNode>( |
| String userTag, NodeT current, Map sample) { |
| bool isCode = (current is CodeCallTreeNode); |
| try { |
| final tableIndex = isCode |
| ? _getProfileCodeTagIndex(userTag) |
| : _getProfileFunctionTagIndex(userTag); |
| current = current.getChild(tableIndex); |
| current.tick(sample); |
| } catch (_) {/* invalid tag */} finally { |
| return current; |
| } |
| } |
| |
| NodeT _appendTruncatedTag<NodeT extends CallTreeNode>( |
| NodeT current, Map sample) { |
| final isCode = (current is CodeCallTreeNode); |
| try { |
| final tableIndex = isCode |
| ? _getProfileCodeTagIndex(kTruncatedTag) |
| : _getProfileFunctionTagIndex(kTruncatedTag); |
| current = current.getChild(tableIndex); |
| current.tick(sample); |
| // We don't need to tick the tag itself since this is done in the VM for |
| // the truncated tag, unlike other VM and user tags. |
| } catch (_) {/* invalid tag */} finally { |
| return current; |
| } |
| } |
| |
| FunctionCallTreeNode _getFunctionTagAndTick( |
| String tag, FunctionCallTreeNode current, Map sample) { |
| try { |
| final tableIndex = _getProfileFunctionTagIndex(tag); |
| current = current.getChild(tableIndex); |
| current.tick(sample); |
| } catch (_) {/* invalid tag */} finally { |
| return current; |
| } |
| } |
| |
| CodeCallTreeNode _getCodeTagAndTick( |
| String tag, CodeCallTreeNode current, Map sample) { |
| try { |
| final tableIndex = _getProfileCodeTagIndex(tag); |
| current = current.getChild(tableIndex); |
| current.tick(sample); |
| } catch (_) {/* invalid tag */} finally { |
| return current; |
| } |
| } |
| |
| NodeT _getTagAndTick<NodeT extends CallTreeNode>( |
| String tag, NodeT current, Map sample) { |
| if (current is FunctionCallTreeNode) { |
| return _getFunctionTagAndTick(tag, current, sample) as NodeT; |
| } else if (current is CodeCallTreeNode) { |
| return _getCodeTagAndTick(tag, current, sample) as NodeT; |
| } |
| throw ArgumentError('Unexpected tree type: $NodeT'); |
| } |
| |
| NodeT _appendVMTag<NodeT extends CallTreeNode>( |
| String vmTag, NodeT current, Map sample) { |
| if (_isNativeEntryTag(sample)) { |
| return _getTagAndTick(kNativeTag, current, sample); |
| } else if (_isRuntimeEntryTag(sample)) { |
| return _getTagAndTick(kRuntimeTag, current, sample); |
| } else { |
| return _getTagAndTick(vmTag, current, sample); |
| } |
| } |
| |
| NodeT _appendSpecificNativeRuntimeEntryVMTag<NodeT extends CallTreeNode>( |
| NodeT current, Map sample) { |
| // Only Native and Runtime entries have a second VM tag. |
| if (!_isNativeEntryTag(sample) && !_isRuntimeEntryTag(sample)) { |
| return current; |
| } |
| final vmTag = sample[_kVmTag]; |
| return _getTagAndTick(vmTag, current, sample); |
| } |
| |
| NodeT _appendVMTags<NodeT extends CallTreeNode>( |
| String vmTag, NodeT current, Map sample) { |
| current = _appendVMTag(vmTag, current, sample); |
| current = _appendSpecificNativeRuntimeEntryVMTag(current, sample); |
| return current; |
| } |
| |
| void _tickTags(String vmTag, String userTag, bool tickCode) { |
| if (tickCode) { |
| final vmTagIndex = _getProfileCodeTagIndex(vmTag); |
| codes[vmTagIndex].tickTag(); |
| |
| final userTagIndex = _getProfileCodeTagIndex(userTag); |
| codes[userTagIndex].tickTag(); |
| } else { |
| final vmTagIndex = _getProfileFunctionTagIndex(vmTag); |
| functions[vmTagIndex].tickTag(); |
| |
| final userTagIndex = _getProfileFunctionTagIndex(userTag); |
| functions[userTagIndex].tickTag(); |
| } |
| } |
| |
| NodeT _appendTags<NodeT extends CallTreeNode>( |
| String vmTag, String userTag, NodeT current, Map sample) { |
| final tickCode = (current is M.CodeCallTreeNode); |
| _tickTags(vmTag, userTag, tickCode); |
| if (tagOrder == M.SampleProfileTag.none) { |
| return current; |
| } |
| // User first. |
| if (tagOrder == M.SampleProfileTag.userVM || |
| tagOrder == M.SampleProfileTag.userOnly) { |
| current = _appendUserTag(userTag, current, sample); |
| // Only user. |
| if (tagOrder == M.SampleProfileTag.userOnly) { |
| return current; |
| } |
| return _appendVMTags(vmTag, current, sample); |
| } |
| |
| // VM first. |
| current = _appendVMTags(vmTag, current, sample); |
| // Only VM. |
| if (tagOrder == M.SampleProfileTag.vmOnly) { |
| return current; |
| } |
| return _appendUserTag(userTag, current, sample); |
| } |
| |
| NodeT _processFrame<NodeT extends CallTreeNode>(NodeT parent, int sampleIndex, |
| Map sample, List<int> stack, int frameIndex, bool inclusive) { |
| final child = parent.getChild(stack[frameIndex]); |
| child.tick(sample, exclusive: (frameIndex == 0)); |
| return child; |
| } |
| |
| FunctionCallTreeNode buildFunctionTrie(bool inclusive) { |
| final root = FunctionCallTreeNode.fromIndex( |
| this, _getProfileFunctionTagIndex(kRootTag)); |
| |
| for (int sampleIndex = 0; sampleIndex < samples.length; ++sampleIndex) { |
| final sample = samples[sampleIndex]; |
| FunctionCallTreeNode current = root; |
| // Tick the root for each sample as we always visit the root node. |
| root.tick(sample); |
| |
| // VM + User tags. |
| final vmTag = sample[_kVmTag]; |
| final userTag = sample[_kUserTag]; |
| final stack = sample[_kStack].cast<int>(); |
| current = _appendTags(vmTag, userTag, current, sample); |
| |
| if (inclusive) { |
| if (_isTruncated(sample)) { |
| current = _appendTruncatedTag(current, sample); |
| } |
| for (int frameIndex = stack.length - 1; frameIndex >= 0; --frameIndex) { |
| current = _processFrame( |
| current, sampleIndex, sample, stack, frameIndex, true); |
| } |
| |
| // Used by the timeline to find the root of each sample. |
| sample[kTimelineFunctionTrie] = current; |
| } else { |
| for (int frameIndex = 0; frameIndex < stack.length; ++frameIndex) { |
| current = _processFrame( |
| current, sampleIndex, sample, stack, frameIndex, false); |
| } |
| |
| if (_isTruncated(sample)) { |
| current = _appendTruncatedTag(current, sample); |
| } |
| } |
| } |
| return root; |
| } |
| |
| CodeCallTreeNode buildCodeTrie(bool inclusive) { |
| final root = |
| CodeCallTreeNode.fromIndex(this, _getProfileCodeTagIndex(kRootTag)); |
| |
| for (int sampleIndex = 0; sampleIndex < samples.length; ++sampleIndex) { |
| final sample = samples[sampleIndex]; |
| |
| CodeCallTreeNode current = root; |
| // Tick the root for each sample as we always visit the root node. |
| root.tick(sample); |
| |
| // VM + User tags. |
| final vmTag = sample[_kVmTag]; |
| final userTag = sample[_kUserTag]; |
| final stack = sample[_kCodeStack].cast<int>(); |
| current = _appendTags(vmTag, userTag, current, sample); |
| |
| if (inclusive) { |
| if (_isTruncated(sample)) { |
| current = _appendTruncatedTag(current, sample); |
| } |
| for (int frameIndex = stack.length - 1; frameIndex >= 0; --frameIndex) { |
| current = _processFrame( |
| current, sampleIndex, sample, stack, frameIndex, true); |
| } |
| } else { |
| for (int frameIndex = 0; frameIndex < stack.length; ++frameIndex) { |
| current = _processFrame( |
| current, sampleIndex, sample, stack, frameIndex, false); |
| } |
| |
| if (_isTruncated(sample)) { |
| current = _appendTruncatedTag(current, sample); |
| } |
| } |
| } |
| return root; |
| } |
| |
| FunctionCallTree _loadFunctionTree(bool inclusive) { |
| // Since we're only ticking tag functions when building the trie, we need |
| // to clean up ticks from previous tree builds. |
| _clearProfileFunctionTagTicks(); |
| |
| // Read the tree, returns the root node. |
| final root = buildFunctionTrie(inclusive); |
| root.sortChildren(); |
| return FunctionCallTree(inclusive, root); |
| } |
| |
| CodeCallTree _loadCodeTree(bool inclusive) { |
| // Since we're only ticking tag code when building the trie, we need |
| // to clean up ticks from previous tree builds. |
| _clearProfileCodeTagTicks(); |
| |
| // Read the tree, returns the root node. |
| final root = buildCodeTrie(inclusive); |
| root.sortChildren(); |
| return CodeCallTree(inclusive, root); |
| } |
| |
| int approximateMillisecondsForCount(count) { |
| return (count * samplePeriod) ~/ Duration.microsecondsPerMillisecond; |
| } |
| |
| double approximateSecondsForCount(count) { |
| return (count * samplePeriod) / Duration.microsecondsPerSecond; |
| } |
| } |