| Index: runtime/observatory/lib/src/elements/cpu_profile.dart
 | 
| diff --git a/runtime/observatory/lib/src/elements/cpu_profile.dart b/runtime/observatory/lib/src/elements/cpu_profile.dart
 | 
| index d82b1f784849d9558bd629823a32c47e552efb99..892c1ce044f0c3280c0f8749def688c8b2af2223 100644
 | 
| --- a/runtime/observatory/lib/src/elements/cpu_profile.dart
 | 
| +++ b/runtime/observatory/lib/src/elements/cpu_profile.dart
 | 
| @@ -14,68 +14,36 @@ import 'package:observatory/cpu_profile.dart';
 | 
|  import 'package:observatory/elements.dart';
 | 
|  import 'package:polymer/polymer.dart';
 | 
|  
 | 
| -class ProfileCodeTrieNodeTreeRow extends TableTreeRow {
 | 
| -  final CpuProfile profile;
 | 
| -  @reflectable final CodeTrieNode root;
 | 
| -  @reflectable final CodeTrieNode node;
 | 
| -  @reflectable Code get code => node.profileCode.code;
 | 
| -
 | 
| -  @reflectable String tipKind = '';
 | 
| -  @reflectable String tipParent = '';
 | 
| -  @reflectable String tipExclusive = '';
 | 
| -  @reflectable String tipTicks = '';
 | 
| -  @reflectable String tipTime = '';
 | 
| -
 | 
| -  ProfileCodeTrieNodeTreeRow(this.profile, this.root, this.node,
 | 
| -                             TableTree tree,
 | 
| -                             ProfileCodeTrieNodeTreeRow parent)
 | 
| -  : super(tree, parent) {
 | 
| -    assert(root != null);
 | 
| -    assert(node != null);
 | 
| -    tipTicks = '${node.count}';
 | 
| -    var seconds = profile.approximateSecondsForCount(node.count);
 | 
| -    tipTime = Utils.formatTimePrecise(seconds);
 | 
| -    if (code.kind == CodeKind.Tag) {
 | 
| -      tipKind = 'Tag (category)';
 | 
| -      if (parent == null) {
 | 
| -        tipParent = Utils.formatPercent(node.count, root.count);
 | 
| -      } else {
 | 
| -        tipParent = Utils.formatPercent(node.count, parent.node.count);
 | 
| -      }
 | 
| -      tipExclusive = Utils.formatPercent(node.count, root.count);
 | 
| -    } else {
 | 
| -      if ((code.kind == CodeKind.Collected) ||
 | 
| -      (code.kind == CodeKind.Reused)) {
 | 
| -        tipKind = 'Garbage Collected Code';
 | 
| -      } else {
 | 
| -        tipKind = '${code.kind} (Function)';
 | 
| -      }
 | 
| -      if (parent == null) {
 | 
| -        tipParent = Utils.formatPercent(node.count, root.count);
 | 
| -      } else {
 | 
| -        tipParent = Utils.formatPercent(node.count, parent.node.count);
 | 
| -      }
 | 
| -      tipExclusive =
 | 
| -      Utils.formatPercent(node.profileCode.exclusiveTicks, root.count);
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  bool shouldDisplayChild(CodeTrieNode childNode, double threshold) {
 | 
| -    return ((childNode.count / node.count) > threshold) ||
 | 
| -    ((childNode.profileCode.exclusiveTicks / root.count) > threshold);
 | 
| -  }
 | 
| +List<String> sorted(Set<String> attributes) {
 | 
| +  var list = attributes.toList();
 | 
| +  list.sort();
 | 
| +  return list;
 | 
| +}
 | 
|  
 | 
| -  void _buildTooltip(DivElement memberList, Map<String, String> items) {
 | 
| +abstract class ProfileTreeRow<T> extends TableTreeRow {
 | 
| +  final CpuProfile profile;
 | 
| +  final T node;
 | 
| +  final String selfPercent;
 | 
| +  final String percent;
 | 
| +  bool _infoBoxShown = false;
 | 
| +  HtmlElement infoBox;
 | 
| +  HtmlElement infoButton;
 | 
| +
 | 
| +  ProfileTreeRow(TableTree tree, TableTreeRow parent,
 | 
| +                 this.profile, this.node, double selfPercent, double percent)
 | 
| +      : super(tree, parent),
 | 
| +        selfPercent = Utils.formatPercentNormalized(selfPercent),
 | 
| +        percent = Utils.formatPercentNormalized(percent);
 | 
| +
 | 
| +  static _addToMemberList(DivElement memberList, Map<String, String> items) {
 | 
|      items.forEach((k, v) {
 | 
|        var item = new DivElement();
 | 
|        item.classes.add('memberItem');
 | 
|        var name = new DivElement();
 | 
|        name.classes.add('memberName');
 | 
| -      name.classes.add('white');
 | 
|        name.text = k;
 | 
|        var value = new DivElement();
 | 
|        value.classes.add('memberValue');
 | 
| -      value.classes.add('white');
 | 
|        value.text = v;
 | 
|        item.children.add(name);
 | 
|        item.children.add(value);
 | 
| @@ -83,209 +51,385 @@ class ProfileCodeTrieNodeTreeRow extends TableTreeRow {
 | 
|      });
 | 
|    }
 | 
|  
 | 
| +  makeInfoBox() {
 | 
| +    if (infoBox != null) {
 | 
| +      return;
 | 
| +    }
 | 
| +    infoBox = new DivElement();
 | 
| +    infoBox.classes.add('infoBox');
 | 
| +    infoBox.classes.add('shadow');
 | 
| +    infoBox.style.display = 'none';
 | 
| +    infoBox.onClick.listen((e) => e.stopPropagation());
 | 
| +  }
 | 
| +
 | 
| +  makeInfoButton() {
 | 
| +    infoButton = new SpanElement();
 | 
| +    infoButton.style.marginLeft = 'auto';
 | 
| +    infoButton.style.marginRight = '1em';
 | 
| +    infoButton.children.add(new Element.tag('icon-info-outline'));
 | 
| +    infoButton.onClick.listen((event) {
 | 
| +      event.stopPropagation();
 | 
| +      toggleInfoBox();
 | 
| +    });
 | 
| +  }
 | 
| +
 | 
| +  static const attributes = const {
 | 
| +    'optimized' : const ['O', null, 'Optimized'],
 | 
| +    'unoptimized' : const ['U', null, 'Unoptimized'],
 | 
| +    'inlined' : const ['I', null, 'Inlined'],
 | 
| +    'dart' : const ['D', null, 'Dart'],
 | 
| +    'tag' : const ['T', null, 'Tag'],
 | 
| +    'native' : const ['N', null, 'Native'],
 | 
| +    'stub': const ['S', null, 'Stub'],
 | 
| +    'synthetic' : const ['?', null, 'Synthetic'],
 | 
| +  };
 | 
| +
 | 
| +  HtmlElement newAttributeBox(String attribute) {
 | 
| +    List attributeDetails = attributes[attribute];
 | 
| +    if (attributeDetails == null) {
 | 
| +      print('could not find attribute $attribute');
 | 
| +      return null;
 | 
| +    }
 | 
| +    var element = new SpanElement();
 | 
| +    element.style.border = 'solid 2px #ECECEC';
 | 
| +    element.style.height = '100%';
 | 
| +    element.style.display = 'inline-block';
 | 
| +    element.style.textAlign = 'center';
 | 
| +    element.style.minWidth = '1.5em';
 | 
| +    element.style.fontWeight = 'bold';
 | 
| +    if (attributeDetails[1] != null) {
 | 
| +      element.style.backgroundColor = attributeDetails[1];
 | 
| +    }
 | 
| +    element.text = attributeDetails[0];
 | 
| +    element.title = attributeDetails[2];
 | 
| +    return element;
 | 
| +  }
 | 
| +
 | 
| +  onHide() {
 | 
| +    super.onHide();
 | 
| +    infoBox = null;
 | 
| +    infoButton = null;
 | 
| +  }
 | 
| +
 | 
| +  showInfoBox() {
 | 
| +    if ((infoButton == null) || (infoBox == null)) {
 | 
| +      return;
 | 
| +    }
 | 
| +    _infoBoxShown = true;
 | 
| +    infoBox.style.display = 'block';
 | 
| +    infoButton.children.clear();
 | 
| +    infoButton.children.add(new Element.tag('icon-info'));
 | 
| +  }
 | 
| +
 | 
| +  hideInfoBox() {
 | 
| +    _infoBoxShown = false;
 | 
| +    if ((infoButton == null) || (infoBox == null)) {
 | 
| +      return;
 | 
| +    }
 | 
| +    infoBox.style.display = 'none';
 | 
| +    infoButton.children.clear();
 | 
| +    infoButton.children.add(new Element.tag('icon-info-outline'));
 | 
| +  }
 | 
| +
 | 
| +  toggleInfoBox() {
 | 
| +   if (_infoBoxShown) {
 | 
| +     hideInfoBox();
 | 
| +   } else {
 | 
| +     showInfoBox();
 | 
| +   }
 | 
| +  }
 | 
| +
 | 
| +  hideAllInfoBoxes() {
 | 
| +    final List<ProfileTreeRow> rows = tree.rows;
 | 
| +    for (var row in rows) {
 | 
| +      row.hideInfoBox();
 | 
| +    }
 | 
| +  }
 | 
| +
 | 
| +  onClick(MouseEvent e) {
 | 
| +    e.stopPropagation();
 | 
| +    if (e.altKey) {
 | 
| +      bool show = !_infoBoxShown;
 | 
| +      hideAllInfoBoxes();
 | 
| +      if (show) {
 | 
| +        showInfoBox();
 | 
| +      }
 | 
| +      return;
 | 
| +    }
 | 
| +    super.onClick(e);
 | 
| +  }
 | 
| +
 | 
| +  HtmlElement newCodeRef(ProfileCode code) {
 | 
| +    var codeRef = new Element.tag('code-ref');
 | 
| +    codeRef.ref = code.code;
 | 
| +    return codeRef;
 | 
| +  }
 | 
| +
 | 
| +  HtmlElement newFunctionRef(ProfileFunction function) {
 | 
| +    var ref = new Element.tag('function-ref');
 | 
| +    ref.ref = function.function;
 | 
| +    return ref;
 | 
| +  }
 | 
| +
 | 
| +  HtmlElement hr() {
 | 
| +    var element = new HRElement();
 | 
| +    return element;
 | 
| +  }
 | 
| +
 | 
| +  HtmlElement div(String text) {
 | 
| +    var element = new DivElement();
 | 
| +    element.text = text;
 | 
| +    return element;
 | 
| +  }
 | 
| +
 | 
| +  HtmlElement br() {
 | 
| +    return new BRElement();
 | 
| +  }
 | 
| +
 | 
| +  HtmlElement span(String text) {
 | 
| +    var element = new SpanElement();
 | 
| +    element.style.minWidth = '1em';
 | 
| +    element.text = text;
 | 
| +    return element;
 | 
| +  }
 | 
| +}
 | 
| +
 | 
| +class CodeProfileTreeRow extends ProfileTreeRow<CodeCallTreeNode> {
 | 
| +  CodeProfileTreeRow(TableTree tree, CodeProfileTreeRow parent,
 | 
| +                     CpuProfile profile, CodeCallTreeNode node)
 | 
| +      : super(tree, parent, profile, node,
 | 
| +              node.profileCode.normalizedExclusiveTicks,
 | 
| +              node.percentage) {
 | 
| +    // fill out attributes.
 | 
| +  }
 | 
| +
 | 
| +  bool hasChildren() => node.children.length > 0;
 | 
| +
 | 
|    void onShow() {
 | 
|      super.onShow();
 | 
| +
 | 
|      if (children.length == 0) {
 | 
| -      var threshold = profile.displayThreshold;
 | 
|        for (var childNode in node.children) {
 | 
| -        if (!shouldDisplayChild(childNode, threshold)) {
 | 
| -          continue;
 | 
| -        }
 | 
| -        var row =
 | 
| -        new ProfileCodeTrieNodeTreeRow(profile, root, childNode, tree, this);
 | 
| +        var row = new CodeProfileTreeRow(tree, this, profile, childNode);
 | 
|          children.add(row);
 | 
|        }
 | 
|      }
 | 
|  
 | 
| -    var methodCell = tableColumns[0];
 | 
| -    // Enable expansion by clicking anywhere on the method column.
 | 
| -    methodCell.onClick.listen(onClick);
 | 
| +    // Fill in method column.
 | 
| +    var methodColumn = flexColumns[0];
 | 
| +    methodColumn.style.justifyContent = 'flex-start';
 | 
| +    methodColumn.style.position = 'relative';
 | 
|  
 | 
| -    // Grab the flex-row Div inside the methodCell.
 | 
| -    methodCell = methodCell.children[0];
 | 
| -
 | 
| -    // Insert the parent percentage
 | 
| -    var parentPercent = new DivElement();
 | 
| -    parentPercent.text = tipParent;
 | 
| -    methodCell.children.add(parentPercent);
 | 
| +    // Percent.
 | 
| +    var percentNode = new DivElement();
 | 
| +    percentNode.text = percent;
 | 
| +    percentNode.style.minWidth = '5em';
 | 
| +    percentNode.style.textAlign = 'right';
 | 
| +    percentNode.title = 'Self: $selfPercent';
 | 
| +    methodColumn.children.add(percentNode);
 | 
|  
 | 
| +    // Gap.
 | 
|      var gap = new SpanElement();
 | 
|      gap.style.minWidth = '1em';
 | 
| -    methodCell.children.add(gap);
 | 
| +    methodColumn.children.add(gap);
 | 
|  
 | 
| -    var codeRef = new Element.tag('code-ref');
 | 
| -    codeRef.ref = code;
 | 
| -    methodCell.children.add(codeRef);
 | 
| -
 | 
| -    var selfCell = tableColumns[1];
 | 
| -    selfCell.style.position = 'relative';
 | 
| -    selfCell.text = tipExclusive;
 | 
| -
 | 
| -    var tooltipDiv = new DivElement();
 | 
| -    tooltipDiv.classes.add('tooltip');
 | 
| -
 | 
| -    var memberListDiv = new DivElement();
 | 
| -    memberListDiv.classes.add('memberList');
 | 
| -    tooltipDiv.children.add(memberListDiv);
 | 
| -    _buildTooltip(memberListDiv, {
 | 
| -        'Kind' : tipKind,
 | 
| -        'Percent of Parent' : tipParent,
 | 
| -        'Sample Count' : tipTicks,
 | 
| -        'Approximate Execution Time': tipTime,
 | 
| -    });
 | 
| -    selfCell.children.add(tooltipDiv);
 | 
| -  }
 | 
| +    // Code link.
 | 
| +    var codeRef = newCodeRef(node.profileCode);
 | 
| +    codeRef.style.alignSelf = 'center';
 | 
| +    methodColumn.children.add(codeRef);
 | 
|  
 | 
| -  bool hasChildren() {
 | 
| -    return node.children.length > 0;
 | 
| -  }
 | 
| -}
 | 
| +    gap = new SpanElement();
 | 
| +    gap.style.minWidth = '1em';
 | 
| +    methodColumn.children.add(gap);
 | 
|  
 | 
| -class ProfileFunctionTrieNodeTreeRow extends TableTreeRow {
 | 
| -  final CpuProfile profile;
 | 
| -  @reflectable final FunctionTrieNode root;
 | 
| -  @reflectable final FunctionTrieNode node;
 | 
| -  ProfileFunction get profileFunction => node.profileFunction;
 | 
| -  @reflectable ServiceFunction get function => node.profileFunction.function;
 | 
| -  @reflectable String tipKind = '';
 | 
| -  @reflectable String tipParent = '';
 | 
| -  @reflectable String tipExclusive = '';
 | 
| -  @reflectable String tipTime = '';
 | 
| -  @reflectable String tipTicks = '';
 | 
| -
 | 
| -  String tipOptimized = '';
 | 
| -
 | 
| -  ProfileFunctionTrieNodeTreeRow(this.profile, this.root, this.node,
 | 
| -                                 TableTree tree,
 | 
| -                                 ProfileFunctionTrieNodeTreeRow parent)
 | 
| -      : super(tree, parent) {
 | 
| -    assert(root != null);
 | 
| -    assert(node != null);
 | 
| -    tipTicks = '${node.count}';
 | 
| -    var seconds = profile.approximateSecondsForCount(node.count);
 | 
| -    tipTime = Utils.formatTimePrecise(seconds);
 | 
| -    if (parent == null) {
 | 
| -      tipParent = Utils.formatPercent(node.count, root.count);
 | 
| -    } else {
 | 
| -      tipParent = Utils.formatPercent(node.count, parent.node.count);
 | 
| -    }
 | 
| -    if (function.kind == FunctionKind.kTag) {
 | 
| -      tipExclusive = Utils.formatPercent(node.count, root.count);
 | 
| -    } else {
 | 
| -      tipExclusive =
 | 
| -          Utils.formatPercent(node.profileFunction.exclusiveTicks, root.count);
 | 
| +    for (var attribute in sorted(node.attributes)) {
 | 
| +      methodColumn.children.add(newAttributeBox(attribute));
 | 
|      }
 | 
|  
 | 
| -    if (function.kind == FunctionKind.kTag) {
 | 
| -      tipKind = 'Tag (category)';
 | 
| -    } else if (function.kind == FunctionKind.kCollected) {
 | 
| -      tipKind = 'Garbage Collected Code';
 | 
| -    } else {
 | 
| -      tipKind = '${function.kind} (Function)';
 | 
| +    makeInfoBox();
 | 
| +    methodColumn.children.add(infoBox);
 | 
| +
 | 
| +    infoBox.children.add(span('Code '));
 | 
| +    infoBox.children.add(newCodeRef(node.profileCode));
 | 
| +    infoBox.children.add(span(' '));
 | 
| +    for (var attribute in sorted(node.profileCode.attributes)) {
 | 
| +      infoBox.children.add(newAttributeBox(attribute));
 | 
|      }
 | 
| -  }
 | 
| +    infoBox.children.add(br());
 | 
| +    infoBox.children.add(br());
 | 
| +    var memberList = new DivElement();
 | 
| +    memberList.classes.add('memberList');
 | 
| +    infoBox.children.add(br());
 | 
| +    infoBox.children.add(memberList);
 | 
| +    ProfileTreeRow._addToMemberList(memberList, {
 | 
| +        'Exclusive ticks' : node.profileCode.formattedExclusiveTicks,
 | 
| +        'Cpu time' : node.profileCode.formattedCpuTime,
 | 
| +        'Inclusive ticks' : node.profileCode.formattedInclusiveTicks,
 | 
| +        'Call stack time' : node.profileCode.formattedOnStackTime,
 | 
| +    });
 | 
|  
 | 
| -  bool hasChildren() {
 | 
| -    return node.children.length > 0;
 | 
| +    makeInfoButton();
 | 
| +    methodColumn.children.add(infoButton);
 | 
| +
 | 
| +    // Fill in self column.
 | 
| +    var selfColumn = flexColumns[1];
 | 
| +    selfColumn.style.position = 'relative';
 | 
| +    selfColumn.style.alignItems = 'center';
 | 
| +    selfColumn.text = selfPercent;
 | 
|    }
 | 
| +}
 | 
|  
 | 
| -  void _buildTooltip(DivElement memberList, Map<String, String> items) {
 | 
| -    items.forEach((k, v) {
 | 
| -      var item = new DivElement();
 | 
| -      item.classes.add('memberItem');
 | 
| -      var name = new DivElement();
 | 
| -      name.classes.add('memberName');
 | 
| -      name.classes.add('white');
 | 
| -      name.text = k;
 | 
| -      var value = new DivElement();
 | 
| -      value.classes.add('memberValue');
 | 
| -      value.classes.add('white');
 | 
| -      value.text = v;
 | 
| -      item.children.add(name);
 | 
| -      item.children.add(value);
 | 
| -      memberList.children.add(item);
 | 
| -    });
 | 
| +class FunctionProfileTreeRow extends ProfileTreeRow<FunctionCallTreeNode> {
 | 
| +  FunctionProfileTreeRow(TableTree tree, FunctionProfileTreeRow parent,
 | 
| +                         CpuProfile profile, FunctionCallTreeNode node)
 | 
| +      : super(tree, parent, profile, node,
 | 
| +              node.profileFunction.normalizedExclusiveTicks,
 | 
| +              node.percentage) {
 | 
| +    // fill out attributes.
 | 
|    }
 | 
|  
 | 
| -  void onShow() {
 | 
| +  bool hasChildren() => node.children.length > 0;
 | 
| +
 | 
| +  onShow() {
 | 
|      super.onShow();
 | 
|      if (children.length == 0) {
 | 
|        for (var childNode in node.children) {
 | 
| -        var row = new ProfileFunctionTrieNodeTreeRow(profile,
 | 
| -                                                     root,
 | 
| -                                                     childNode, tree, this);
 | 
| +        var row = new FunctionProfileTreeRow(tree, this, profile, childNode);
 | 
|          children.add(row);
 | 
|        }
 | 
|      }
 | 
|  
 | 
| -    var selfCell = tableColumns[1];
 | 
| -    selfCell.style.position = 'relative';
 | 
| -    selfCell.text = tipExclusive;
 | 
| +    var methodColumn = flexColumns[0];
 | 
| +    methodColumn.style.justifyContent = 'flex-start';
 | 
|  
 | 
| -    var methodCell = tableColumns[0];
 | 
| -    // Enable expansion by clicking anywhere on the method column.
 | 
| -    methodCell.onClick.listen(onClick);
 | 
| +    var codeAndFunctionColumn = new DivElement();
 | 
| +    codeAndFunctionColumn.classes.add('flex-column');
 | 
| +    codeAndFunctionColumn.style.justifyContent = 'center';
 | 
| +    codeAndFunctionColumn.style.width = '100%';
 | 
| +    methodColumn.children.add(codeAndFunctionColumn);
 | 
|  
 | 
| -    // Grab the flex-row Div inside the methodCell.
 | 
| -    methodCell = methodCell.children[0];
 | 
| +    var functionRow = new DivElement();
 | 
| +    functionRow.classes.add('flex-row');
 | 
| +    functionRow.style.position = 'relative';
 | 
| +    functionRow.style.justifyContent = 'flex-start';
 | 
| +    codeAndFunctionColumn.children.add(functionRow);
 | 
|  
 | 
|      // Insert the parent percentage
 | 
| -    var parentPercent = new DivElement();
 | 
| -    parentPercent.text = tipParent;
 | 
| -    methodCell.children.add(parentPercent);
 | 
| -
 | 
| +    var parentPercent = new SpanElement();
 | 
| +    parentPercent.text = percent;
 | 
| +    parentPercent.style.minWidth = '4em';
 | 
| +    parentPercent.style.alignSelf = 'center';
 | 
| +    parentPercent.style.textAlign = 'right';
 | 
| +    parentPercent.title = 'Self: $selfPercent';
 | 
| +    functionRow.children.add(parentPercent);
 | 
| +
 | 
| +    // Gap.
 | 
|      var gap = new SpanElement();
 | 
|      gap.style.minWidth = '1em';
 | 
| -    methodCell.children.add(gap);
 | 
| -
 | 
| -    var functionAndCodeContainer = new DivElement();
 | 
| -    methodCell.children.add(functionAndCodeContainer);
 | 
| +    gap.text = ' ';
 | 
| +    functionRow.children.add(gap);
 | 
|  
 | 
|      var functionRef = new Element.tag('function-ref');
 | 
| -    functionRef.ref = function;
 | 
| -    functionAndCodeContainer.children.add(functionRef);
 | 
| +    functionRef.ref = node.profileFunction.function;
 | 
| +    functionRef.style.alignSelf = 'center';
 | 
| +    functionRow.children.add(functionRef);
 | 
|  
 | 
| -    var codeRow = new DivElement();
 | 
| -    codeRow.style.paddingTop = '1em';
 | 
| -    functionAndCodeContainer.children.add(codeRow);
 | 
| -    if (!function.kind.isSynthetic()) {
 | 
| +    gap = new SpanElement();
 | 
| +    gap.style.minWidth = '1em';
 | 
| +    gap.text = ' ';
 | 
| +    functionRow.children.add(gap);
 | 
|  
 | 
| +    for (var attribute in sorted(node.attributes)) {
 | 
| +      functionRow.children.add(newAttributeBox(attribute));
 | 
| +    }
 | 
| +
 | 
| +    makeInfoBox();
 | 
| +    functionRow.children.add(infoBox);
 | 
| +
 | 
| +    if (node.profileFunction.function.kind.hasDartCode()) {
 | 
| +      infoBox.children.add(div('Hot code for current node'));
 | 
| +      infoBox.children.add(br());
 | 
|        var totalTicks = node.totalCodesTicks;
 | 
|        var numCodes = node.codes.length;
 | 
| -      var label = new SpanElement();
 | 
| -      label.text = 'Compiled into:\n';
 | 
| -      codeRow.children.add(label);
 | 
| -      var curlyBlock = new Element.tag('curly-block');
 | 
| -      codeRow.children.add(curlyBlock);
 | 
|        for (var i = 0; i < numCodes; i++) {
 | 
|          var codeRowSpan = new DivElement();
 | 
|          codeRowSpan.style.paddingLeft = '1em';
 | 
| -        curlyBlock.children.add(codeRowSpan);
 | 
| +        infoBox.children.add(codeRowSpan);
 | 
|          var nodeCode = node.codes[i];
 | 
|          var ticks = nodeCode.ticks;
 | 
|          var percentage = Utils.formatPercent(ticks, totalTicks);
 | 
|          var percentageSpan = new SpanElement();
 | 
| -        percentageSpan.text = '($percentage) ';
 | 
| +        percentageSpan.style.display = 'inline-block';
 | 
| +        percentageSpan.text = '$percentage';
 | 
| +        percentageSpan.style.minWidth = '5em';
 | 
| +        percentageSpan.style.textAlign = 'right';
 | 
|          codeRowSpan.children.add(percentageSpan);
 | 
|          var codeRef = new Element.tag('code-ref');
 | 
|          codeRef.ref = nodeCode.code.code;
 | 
| +        codeRef.style.marginLeft = '1em';
 | 
| +        codeRef.style.marginRight = 'auto';
 | 
| +        codeRef.style.width = '100%';
 | 
|          codeRowSpan.children.add(codeRef);
 | 
|        }
 | 
| +      infoBox.children.add(hr());
 | 
|      }
 | 
| -
 | 
| -    var tooltipDiv = new DivElement();
 | 
| -    tooltipDiv.classes.add('tooltip');
 | 
| -
 | 
| -    var memberListDiv = new DivElement();
 | 
| -    memberListDiv.classes.add('memberList');
 | 
| -    tooltipDiv.children.add(memberListDiv);
 | 
| -    _buildTooltip(memberListDiv, {
 | 
| -        'Kind' : tipKind,
 | 
| -        'Percent of Parent' : tipParent,
 | 
| -        'Sample Count' : tipTicks,
 | 
| -        'Approximate Execution Time': tipTime,
 | 
| +    infoBox.children.add(span('Function '));
 | 
| +    infoBox.children.add(newFunctionRef(node.profileFunction));
 | 
| +    infoBox.children.add(span(' '));
 | 
| +    for (var attribute in sorted(node.profileFunction.attributes)) {
 | 
| +      infoBox.children.add(newAttributeBox(attribute));
 | 
| +    }
 | 
| +    var memberList = new DivElement();
 | 
| +    memberList.classes.add('memberList');
 | 
| +    infoBox.children.add(br());
 | 
| +    infoBox.children.add(br());
 | 
| +    infoBox.children.add(memberList);
 | 
| +    infoBox.children.add(br());
 | 
| +    ProfileTreeRow._addToMemberList(memberList, {
 | 
| +        'Exclusive ticks' : node.profileFunction.formattedExclusiveTicks,
 | 
| +        'Cpu time' : node.profileFunction.formattedCpuTime,
 | 
| +        'Inclusive ticks' : node.profileFunction.formattedInclusiveTicks,
 | 
| +        'Call stack time' : node.profileFunction.formattedOnStackTime,
 | 
|      });
 | 
| -    selfCell.children.add(tooltipDiv);
 | 
| +
 | 
| +    if (node.profileFunction.function.kind.hasDartCode()) {
 | 
| +      infoBox.children.add(div('Hot code containing function'));
 | 
| +      infoBox.children.add(br());
 | 
| +      var totalTicks = profile.sampleCount;
 | 
| +      var codes = node.profileFunction.profileCodes;
 | 
| +      var numCodes = codes.length;
 | 
| +      for (var i = 0; i < numCodes; i++) {
 | 
| +        var codeRowSpan = new DivElement();
 | 
| +        codeRowSpan.style.paddingLeft = '1em';
 | 
| +        infoBox.children.add(codeRowSpan);
 | 
| +        var profileCode = codes[i];
 | 
| +        var code = profileCode.code;
 | 
| +        var ticks = profileCode.inclusiveTicks;
 | 
| +        var percentage = Utils.formatPercent(ticks, totalTicks);
 | 
| +        var percentageSpan = new SpanElement();
 | 
| +        percentageSpan.style.display = 'inline-block';
 | 
| +        percentageSpan.text = '$percentage';
 | 
| +        percentageSpan.style.minWidth = '5em';
 | 
| +        percentageSpan.style.textAlign = 'right';
 | 
| +        percentageSpan.title = 'Inclusive ticks';
 | 
| +        codeRowSpan.children.add(percentageSpan);
 | 
| +        var codeRef = new Element.tag('code-ref');
 | 
| +        codeRef.ref = code;
 | 
| +        codeRef.style.marginLeft = '1em';
 | 
| +        codeRef.style.marginRight = 'auto';
 | 
| +        codeRef.style.width = '100%';
 | 
| +        codeRowSpan.children.add(codeRef);
 | 
| +      }
 | 
| +    }
 | 
| +
 | 
| +    makeInfoButton();
 | 
| +    methodColumn.children.add(infoButton);
 | 
| +
 | 
| +    // Fill in self column.
 | 
| +    var selfColumn = flexColumns[1];
 | 
| +    selfColumn.style.position = 'relative';
 | 
| +    selfColumn.style.alignItems = 'center';
 | 
| +    selfColumn.text = selfPercent;
 | 
|    }
 | 
|  }
 | 
|  
 | 
| @@ -299,11 +443,18 @@ class CpuProfileElement extends ObservatoryElement {
 | 
|    @observable String refreshTime = '';
 | 
|    @observable String sampleRate = '';
 | 
|    @observable String stackDepth = '';
 | 
| -  @observable String displayCutoff = '';
 | 
|    @observable String timeSpan = '';
 | 
| -
 | 
| +  @observable String fetchTime = '';
 | 
| +  @observable String loadTime = '';
 | 
|    @observable String tagSelector = 'UserVM';
 | 
|    @observable String modeSelector = 'Function';
 | 
| +  @observable String directionSelector = 'Up';
 | 
| +
 | 
| +  @observable String state = 'Requested';
 | 
| +  @observable var exception;
 | 
| +  @observable var stackTrace;
 | 
| +
 | 
| +  final Stopwatch _sw = new Stopwatch();
 | 
|  
 | 
|    final CpuProfile profile = new CpuProfile();
 | 
|  
 | 
| @@ -326,6 +477,10 @@ class CpuProfileElement extends ObservatoryElement {
 | 
|      _updateView();
 | 
|    }
 | 
|  
 | 
| +  void directionSelectorChanged(oldValue) {
 | 
| +    _updateView();
 | 
| +  }
 | 
| +
 | 
|    void clear(var done) {
 | 
|      _clearCpuProfile().whenComplete(done);
 | 
|    }
 | 
| @@ -345,16 +500,55 @@ class CpuProfileElement extends ObservatoryElement {
 | 
|      _getCpuProfile().whenComplete(done);
 | 
|    }
 | 
|  
 | 
| -  Future _getCpuProfile() {
 | 
| +  _onFetchStarted() {
 | 
| +    _sw.reset();
 | 
| +    _sw.start();
 | 
| +    state = 'Requested';
 | 
| +  }
 | 
| +
 | 
| +  _onFetchFinished() {
 | 
| +    _sw.stop();
 | 
| +    fetchTime = formatTimeMilliseconds(_sw.elapsedMilliseconds);
 | 
| +  }
 | 
| +
 | 
| +  _onLoadStarted() {
 | 
| +    _sw.reset();
 | 
| +    _sw.start();
 | 
| +    state = 'Loading';
 | 
| +  }
 | 
| +
 | 
| +  _onLoadFinished() {
 | 
| +    _sw.stop();
 | 
| +    loadTime = formatTimeMilliseconds(_sw.elapsedMilliseconds);
 | 
| +    state = 'Loaded';
 | 
| +  }
 | 
| +
 | 
| +  Future _getCpuProfile() async {
 | 
|      profile.clear();
 | 
| +    if (functionTree != null) {
 | 
| +      functionTree.clear();
 | 
| +    }
 | 
| +    if (codeTree != null) {
 | 
| +      codeTree.clear();
 | 
| +    }
 | 
|      if (isolate == null) {
 | 
|        return new Future.value(null);
 | 
|      }
 | 
| -    return isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector })
 | 
| -        .then((ServiceMap response) {
 | 
| -          profile.load(isolate, response);
 | 
| -          _updateView();
 | 
| -        });
 | 
| +    _onFetchStarted();
 | 
| +    var response =
 | 
| +        await isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector });
 | 
| +    _onFetchFinished();
 | 
| +    _onLoadStarted();
 | 
| +    await window.animationFrame;
 | 
| +    try {
 | 
| +      profile.load(isolate, response);
 | 
| +      _onLoadFinished();
 | 
| +      _updateView();
 | 
| +    } catch (e, st) {
 | 
| +      state = 'Exception';
 | 
| +      exception = e;
 | 
| +      stackTrace = st;
 | 
| +    }
 | 
|    }
 | 
|  
 | 
|    void _updateView() {
 | 
| @@ -363,69 +557,49 @@ class CpuProfileElement extends ObservatoryElement {
 | 
|      stackDepth = profile.stackDepth.toString();
 | 
|      sampleRate = profile.sampleRate.toStringAsFixed(0);
 | 
|      timeSpan = formatTime(profile.timeSpan);
 | 
| -    displayCutoff = '${(profile.displayThreshold * 100.0).toString()}%';
 | 
|      if (functionTree != null) {
 | 
|        functionTree.clear();
 | 
|      }
 | 
|      if (codeTree != null) {
 | 
|        codeTree.clear();
 | 
|      }
 | 
| +    bool exclusive = directionSelector == 'Up';
 | 
|      if (modeSelector == 'Code') {
 | 
| -      _buildCodeTree();
 | 
| +      _buildCodeTree(exclusive);
 | 
|      } else {
 | 
| -      _buildFunctionTree();
 | 
| +      _buildFunctionTree(exclusive);
 | 
|      }
 | 
|    }
 | 
|  
 | 
|    TableTree codeTree;
 | 
|    TableTree functionTree;
 | 
|  
 | 
| -  void _buildFunctionTree() {
 | 
| +  void _buildFunctionTree(bool exclusive) {
 | 
|      if (functionTree == null) {
 | 
|        var tableBody = shadowRoot.querySelector('#treeBody');
 | 
|        assert(tableBody != null);
 | 
|        functionTree = new TableTree(tableBody, 2);
 | 
|      }
 | 
| -    var root = profile.functionTrieRoot;
 | 
| -    if (root == null) {
 | 
| +    var tree = profile.functionTrees[exclusive ? 'exclusive' : 'inclusive'];
 | 
| +    if (tree == null) {
 | 
|        return;
 | 
|      }
 | 
| -    try {
 | 
| -      functionTree.initialize(
 | 
| -          new ProfileFunctionTrieNodeTreeRow(profile,
 | 
| -                                             root, root, functionTree, null));
 | 
| -    } catch (e, stackTrace) {
 | 
| -      print(e);
 | 
| -      print(stackTrace);
 | 
| -      Logger.root.warning('_buildFunctionTree', e, stackTrace);
 | 
| -    }
 | 
| -    // Check if we only have one node at the root and expand it.
 | 
| -    if (functionTree.rows.length == 1) {
 | 
| -      functionTree.toggle(functionTree.rows[0]);
 | 
| -    }
 | 
| +    var rootRow =
 | 
| +        new FunctionProfileTreeRow(functionTree, null, profile, tree.root);
 | 
| +    functionTree.initialize(rootRow);
 | 
|    }
 | 
|  
 | 
| -  void _buildCodeTree() {
 | 
| +  void _buildCodeTree(bool exclusive) {
 | 
|      if (codeTree == null) {
 | 
|        var tableBody = shadowRoot.querySelector('#treeBody');
 | 
|        assert(tableBody != null);
 | 
|        codeTree = new TableTree(tableBody, 2);
 | 
|      }
 | 
| -    var root = profile.codeTrieRoot;
 | 
| -    if (root == null) {
 | 
| +    var tree = profile.codeTrees[exclusive ? 'exclusive' : 'inclusive'];
 | 
| +    if (tree == null) {
 | 
|        return;
 | 
|      }
 | 
| -    try {
 | 
| -      codeTree.initialize(
 | 
| -          new ProfileCodeTrieNodeTreeRow(profile, root, root, codeTree, null));
 | 
| -    } catch (e, stackTrace) {
 | 
| -      print(e);
 | 
| -      print(stackTrace);
 | 
| -      Logger.root.warning('_buildCodeTree', e, stackTrace);
 | 
| -    }
 | 
| -    // Check if we only have one node at the root and expand it.
 | 
| -    if (codeTree.rows.length == 1) {
 | 
| -      codeTree.toggle(codeTree.rows[0]);
 | 
| -    }
 | 
| +    var rootRow = new CodeProfileTreeRow(codeTree, null, profile, tree.root);
 | 
| +    codeTree.initialize(rootRow);
 | 
|    }
 | 
|  }
 | 
| 
 |