| Index: runtime/observatory/lib/src/elements/cpu_profile_table.dart
 | 
| diff --git a/runtime/observatory/lib/src/elements/cpu_profile_table.dart b/runtime/observatory/lib/src/elements/cpu_profile_table.dart
 | 
| index 2a76e17bc9275de1f03424cbbb7fbba50c566724..88edbc6b2cb20a2994662bbc656a456d301ccd0a 100644
 | 
| --- a/runtime/observatory/lib/src/elements/cpu_profile_table.dart
 | 
| +++ b/runtime/observatory/lib/src/elements/cpu_profile_table.dart
 | 
| @@ -6,924 +6,446 @@ library cpu_profile_table_element;
 | 
|  
 | 
|  import 'dart:async';
 | 
|  import 'dart:html';
 | 
| -import 'observatory_element.dart';
 | 
| -import 'sample_buffer_control.dart';
 | 
| -import 'stack_trace_tree_config.dart';
 | 
| -import 'cpu_profile/virtual_tree.dart';
 | 
| -import 'package:observatory/service.dart';
 | 
| -import 'package:observatory/app.dart';
 | 
| -import 'package:observatory/cpu_profile.dart';
 | 
| -import 'package:observatory/elements.dart';
 | 
|  import 'package:observatory/models.dart' as M;
 | 
| -import 'package:observatory/repositories.dart';
 | 
| -import 'package:polymer/polymer.dart';
 | 
| -
 | 
| -List<String> sorted(Set<String> attributes) {
 | 
| -  var list = attributes.toList();
 | 
| -  list.sort();
 | 
| -  return list;
 | 
| +import 'package:observatory/src/elements/containers/virtual_collection.dart';
 | 
| +import 'package:observatory/src/elements/cpu_profile/virtual_tree.dart';
 | 
| +import 'package:observatory/src/elements/function_ref.dart';
 | 
| +import 'package:observatory/src/elements/helpers/rendering_scheduler.dart';
 | 
| +import 'package:observatory/src/elements/helpers/tag.dart';
 | 
| +import 'package:observatory/src/elements/helpers/uris.dart';
 | 
| +import 'package:observatory/src/elements/nav/bar.dart';
 | 
| +import 'package:observatory/src/elements/nav/isolate_menu.dart';
 | 
| +import 'package:observatory/src/elements/nav/menu.dart';
 | 
| +import 'package:observatory/src/elements/nav/notify.dart';
 | 
| +import 'package:observatory/src/elements/nav/refresh.dart';
 | 
| +import 'package:observatory/src/elements/nav/top_menu.dart';
 | 
| +import 'package:observatory/src/elements/nav/vm_menu.dart';
 | 
| +import 'package:observatory/src/elements/sample_buffer_control.dart';
 | 
| +import 'package:observatory/src/elements/stack_trace_tree_config.dart';
 | 
| +import 'package:observatory/utils.dart';
 | 
| +
 | 
| +enum _Table {
 | 
| +  functions,
 | 
| +  caller,
 | 
| +  callee
 | 
|  }
 | 
|  
 | 
| -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.text = k;
 | 
| -      var value = new DivElement();
 | 
| -      value.classes.add('memberValue');
 | 
| -      value.text = v;
 | 
| -      item.children.add(name);
 | 
| -      item.children.add(value);
 | 
| -      memberList.children.add(item);
 | 
| -    });
 | 
| -  }
 | 
| -
 | 
| -  makeInfoBox() {
 | 
| -    if (infoBox != null) {
 | 
| -      return;
 | 
| -    }
 | 
| -    infoBox = new DivElement();
 | 
| -    infoBox.classes.add('infoBox');
 | 
| -    infoBox.classes.add('shadow');
 | 
| -    infoBox.style.display = 'none';
 | 
| -    listeners.add(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'));
 | 
| -    listeners.add(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'],
 | 
| -    'intrinsic' : const ['It', null, 'Intrinsic'],
 | 
| -    'ffi' : const ['F', null, 'FFI'],
 | 
| -    '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;
 | 
| -  }
 | 
| +enum _SortingField {
 | 
| +  exclusive,
 | 
| +  inclusive,
 | 
| +  caller,
 | 
| +  callee,
 | 
| +  method
 | 
|  }
 | 
|  
 | 
| -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) {
 | 
| -      for (var childNode in node.children) {
 | 
| -        var row = new CodeProfileTreeRow(tree, this, profile, childNode);
 | 
| -        children.add(row);
 | 
| -      }
 | 
| -    }
 | 
| -
 | 
| -    // Fill in method column.
 | 
| -    var methodColumn = flexColumns[0];
 | 
| -    methodColumn.style.justifyContent = 'flex-start';
 | 
| -    methodColumn.style.position = 'relative';
 | 
| -
 | 
| -    // Percent.
 | 
| -    var percentNode = new DivElement();
 | 
| -    percentNode.text = percent;
 | 
| -    percentNode.style.minWidth = '5em';
 | 
| -    percentNode.style.textAlign = 'right';
 | 
| -    percentNode.title = 'Executing: $selfPercent';
 | 
| -    methodColumn.children.add(percentNode);
 | 
| -
 | 
| -    // Gap.
 | 
| -    var gap = new SpanElement();
 | 
| -    gap.style.minWidth = '1em';
 | 
| -    methodColumn.children.add(gap);
 | 
| -
 | 
| -    // Code link.
 | 
| -    var codeRef = newCodeRef(node.profileCode);
 | 
| -    codeRef.style.alignSelf = 'center';
 | 
| -    methodColumn.children.add(codeRef);
 | 
| -
 | 
| -    gap = new SpanElement();
 | 
| -    gap.style.minWidth = '1em';
 | 
| -    methodColumn.children.add(gap);
 | 
| -
 | 
| -    for (var attribute in sorted(node.attributes)) {
 | 
| -      methodColumn.children.add(newAttributeBox(attribute));
 | 
| -    }
 | 
| -
 | 
| -    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,
 | 
| -    });
 | 
| -
 | 
| -    makeInfoButton();
 | 
| -    methodColumn.children.add(infoButton);
 | 
| -
 | 
| -    // Fill in self column.
 | 
| -    var selfColumn = flexColumns[1];
 | 
| -    selfColumn.style.position = 'relative';
 | 
| -    selfColumn.style.alignItems = 'center';
 | 
| -    selfColumn.text = selfPercent;
 | 
| -  }
 | 
| +enum _SortingDirection {
 | 
| +  ascending,
 | 
| +  descending
 | 
|  }
 | 
|  
 | 
| -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.
 | 
| -  }
 | 
| -
 | 
| -  bool hasChildren() => node.children.length > 0;
 | 
| -
 | 
| -  onShow() {
 | 
| -    super.onShow();
 | 
| -    if (children.length == 0) {
 | 
| -      for (var childNode in node.children) {
 | 
| -        var row = new FunctionProfileTreeRow(tree, this, profile, childNode);
 | 
| -        children.add(row);
 | 
| -      }
 | 
| -    }
 | 
| -
 | 
| -    var methodColumn = flexColumns[0];
 | 
| -    methodColumn.style.justifyContent = 'flex-start';
 | 
| -
 | 
| -    var codeAndFunctionColumn = new DivElement();
 | 
| -    codeAndFunctionColumn.classes.add('flex-column');
 | 
| -    codeAndFunctionColumn.style.justifyContent = 'center';
 | 
| -    codeAndFunctionColumn.style.width = '100%';
 | 
| -    methodColumn.children.add(codeAndFunctionColumn);
 | 
| -
 | 
| -    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 SpanElement();
 | 
| -    parentPercent.text = percent;
 | 
| -    parentPercent.style.minWidth = '4em';
 | 
| -    parentPercent.style.alignSelf = 'center';
 | 
| -    parentPercent.style.textAlign = 'right';
 | 
| -    parentPercent.title = 'Executing: $selfPercent';
 | 
| -    functionRow.children.add(parentPercent);
 | 
| -
 | 
| -    // Gap.
 | 
| -    var gap = new SpanElement();
 | 
| -    gap.style.minWidth = '1em';
 | 
| -    gap.text = ' ';
 | 
| -    functionRow.children.add(gap);
 | 
| -
 | 
| -    var functionRef = new Element.tag('function-ref');
 | 
| -    functionRef.ref = node.profileFunction.function;
 | 
| -    functionRef.style.alignSelf = 'center';
 | 
| -    functionRow.children.add(functionRef);
 | 
| -
 | 
| -    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 (M.hasDartCode(node.profileFunction.function.kind)) {
 | 
| -      infoBox.children.add(div('Code for current node'));
 | 
| -      infoBox.children.add(br());
 | 
| -      var totalTicks = node.totalCodesTicks;
 | 
| -      var numCodes = node.codes.length;
 | 
| -      for (var i = 0; i < numCodes; i++) {
 | 
| -        var codeRowSpan = new DivElement();
 | 
| -        codeRowSpan.style.paddingLeft = '1em';
 | 
| -        infoBox.children.add(codeRowSpan);
 | 
| -        var nodeCode = node.codes[i];
 | 
| -        var ticks = nodeCode.ticks;
 | 
| -        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';
 | 
| -        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());
 | 
| -    }
 | 
| -    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,
 | 
| -    });
 | 
| -
 | 
| -    if (M.hasDartCode(node.profileFunction.function.kind)) {
 | 
| -      infoBox.children.add(div('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;
 | 
| -  }
 | 
| -}
 | 
| +class CpuProfileTableElement  extends HtmlElement implements Renderable {
 | 
| +  static const tag = const Tag<CpuProfileTableElement>('cpu-profile-table',
 | 
| +                                            dependencies: const [
 | 
| +                                              FunctionRefElement.tag,
 | 
| +                                              NavBarElement.tag,
 | 
| +                                              NavTopMenuElement.tag,
 | 
| +                                              NavVMMenuElement.tag,
 | 
| +                                              NavIsolateMenuElement.tag,
 | 
| +                                              NavMenuElement.tag,
 | 
| +                                              NavRefreshElement.tag,
 | 
| +                                              NavNotifyElement.tag,
 | 
| +                                              SampleBufferControlElement.tag,
 | 
| +                                              StackTraceTreeConfigElement.tag,
 | 
| +                                              CpuProfileVirtualTreeElement.tag,
 | 
| +                                              VirtualCollectionElement.tag
 | 
| +                                            ]);
 | 
| +
 | 
| +  RenderingScheduler<CpuProfileTableElement> _r;
 | 
| +
 | 
| +  Stream<RenderedEvent<CpuProfileTableElement>> get onRendered => _r.onRendered;
 | 
| +
 | 
| +  M.VM _vm;
 | 
| +  M.IsolateRef _isolate;
 | 
| +  M.EventRepository _events;
 | 
| +  M.NotificationRepository _notifications;
 | 
| +  M.IsolateSampleProfileRepository _profiles;
 | 
| +  Stream<M.SampleProfileLoadingProgressEvent> _progressStream;
 | 
| +  M.SampleProfileLoadingProgress _progress;
 | 
| +  final _sortingField = <_Table, _SortingField>{
 | 
| +    _Table.functions :  _SortingField.exclusive,
 | 
| +    _Table.caller :  _SortingField.caller,
 | 
| +    _Table.callee :  _SortingField.callee,
 | 
| +  };
 | 
| +  final _sortingDirection = <_Table, _SortingDirection>{
 | 
| +    _Table.functions :  _SortingDirection.descending,
 | 
| +    _Table.caller :  _SortingDirection.descending,
 | 
| +    _Table.callee :  _SortingDirection.descending,
 | 
| +  };
 | 
| +  String _filter = '';
 | 
| +
 | 
| +
 | 
| +  M.IsolateRef get isolate => _isolate;
 | 
| +  M.NotificationRepository get notifications => _notifications;
 | 
| +  M.IsolateSampleProfileRepository get profiles => _profiles;
 | 
| +  M.VMRef get vm => _vm;
 | 
| +
 | 
| +  factory CpuProfileTableElement(M.VM vm, M.IsolateRef isolate,
 | 
| +                            M.EventRepository events,
 | 
| +                            M.NotificationRepository notifications,
 | 
| +                            M.IsolateSampleProfileRepository profiles,
 | 
| +                            {RenderingQueue queue}) {
 | 
| +    assert(vm != null);
 | 
| +    assert(isolate != null);
 | 
| +    assert(events != null);
 | 
| +    assert(notifications != null);
 | 
| +    assert(profiles != null);
 | 
| +    CpuProfileTableElement e = document.createElement(tag.name);
 | 
| +    e._r = new RenderingScheduler(e, queue: queue);
 | 
| +    e._vm = vm;
 | 
| +    e._isolate = isolate;
 | 
| +    e._events = events;
 | 
| +    e._notifications = notifications;
 | 
| +    e._profiles = profiles;
 | 
| +    return e;
 | 
| +  }
 | 
| +
 | 
| +  CpuProfileTableElement.created() : super.created();
 | 
|  
 | 
| -class NameSortedTable extends SortedTable {
 | 
| -  NameSortedTable(columns) : super(columns);
 | 
|    @override
 | 
| -  dynamic getSortKeyFor(int row, int col) {
 | 
| -    if (col == FUNCTION_COLUMN) {
 | 
| -      // Use name as sort key.
 | 
| -      return rows[row].values[col].name;
 | 
| -    }
 | 
| -    return super.getSortKeyFor(row, col);
 | 
| -  }
 | 
| -
 | 
| -  SortedTableRow rowFromIndex(int tableIndex) {
 | 
| -    final modelIndex = sortedRows[tableIndex];
 | 
| -    return rows[modelIndex];
 | 
| -  }
 | 
| -
 | 
| -  static const FUNCTION_SPACER_COLUMNS = const [];
 | 
| -  static const FUNCTION_COLUMN = 2;
 | 
| -  TableRowElement _makeFunctionRow() {
 | 
| -    var tr = new TableRowElement();
 | 
| -    var cell;
 | 
| -
 | 
| -    // Add percentage.
 | 
| -    cell = tr.insertCell(-1);
 | 
| -    cell = tr.insertCell(-1);
 | 
| -
 | 
| -    // Add function ref.
 | 
| -    cell = tr.insertCell(-1);
 | 
| -    var functionRef = new Element.tag('function-ref');
 | 
| -    cell.children.add(functionRef);
 | 
| -
 | 
| -    return tr;
 | 
| -  }
 | 
| -
 | 
| -  static const CALL_SPACER_COLUMNS = const [];
 | 
| -  static const CALL_FUNCTION_COLUMN = 1;
 | 
| -  TableRowElement _makeCallRow() {
 | 
| -    var tr = new TableRowElement();
 | 
| -    var cell;
 | 
| -
 | 
| -    // Add percentage.
 | 
| -    cell = tr.insertCell(-1);
 | 
| -    // Add function ref.
 | 
| -    cell = tr.insertCell(-1);
 | 
| -    var functionRef = new Element.tag('function-ref');
 | 
| -    cell.children.add(functionRef);
 | 
| -    return tr;
 | 
| -  }
 | 
| -
 | 
| -  _updateRow(TableRowElement tr,
 | 
| -             int rowIndex,
 | 
| -             List spacerColumns,
 | 
| -             int refColumn) {
 | 
| -    var row = rows[rowIndex];
 | 
| -    // Set reference
 | 
| -    var ref = tr.children[refColumn].children[0];
 | 
| -    ref.ref = row.values[refColumn];
 | 
| -
 | 
| -    for (var i = 0; i < row.values.length; i++) {
 | 
| -      if (spacerColumns.contains(i) || (i == refColumn)) {
 | 
| -        // Skip spacer columns.
 | 
| -        continue;
 | 
| -      }
 | 
| -      var cell = tr.children[i];
 | 
| -      cell.title = row.values[i].toString();
 | 
| -      cell.text = getFormattedValue(rowIndex, i);
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  _updateTableView(HtmlElement table,
 | 
| -                   HtmlElement makeEmptyRow(),
 | 
| -                   void onRowClick(TableRowElement tr),
 | 
| -                   List spacerColumns,
 | 
| -                   int refColumn) {
 | 
| -    assert(table != null);
 | 
| -
 | 
| -    // Resize DOM table.
 | 
| -    if (table.children.length > sortedRows.length) {
 | 
| -      // Shrink the table.
 | 
| -      var deadRows = table.children.length - sortedRows.length;
 | 
| -      for (var i = 0; i < deadRows; i++) {
 | 
| -        table.children.removeLast();
 | 
| -      }
 | 
| -    } else if (table.children.length < sortedRows.length) {
 | 
| -      // Grow table.
 | 
| -      var newRows = sortedRows.length - table.children.length;
 | 
| -      for (var i = 0; i < newRows; i++) {
 | 
| -        var row = makeEmptyRow();
 | 
| -        row.onClick.listen((e) {
 | 
| -          e.stopPropagation();
 | 
| -          e.preventDefault();
 | 
| -          onRowClick(row);
 | 
| -        });
 | 
| -        table.children.add(row);
 | 
| -      }
 | 
| -    }
 | 
| -
 | 
| -    assert(table.children.length == sortedRows.length);
 | 
| -
 | 
| -    // Fill table.
 | 
| -    for (var i = 0; i < sortedRows.length; i++) {
 | 
| -      var rowIndex = sortedRows[i];
 | 
| -      var tr = table.children[i];
 | 
| -      _updateRow(tr, rowIndex, spacerColumns, refColumn);
 | 
| -    }
 | 
| -  }
 | 
| -}
 | 
| -
 | 
| -@CustomTag('cpu-profile-table')
 | 
| -class CpuProfileTableElement extends ObservatoryElement {
 | 
| -
 | 
| -
 | 
| -  CpuProfileTableElement.created() : super.created() {
 | 
| -    _updateTask = new Task(update);
 | 
| -    _renderTask = new Task(render);
 | 
| -    var columns = [
 | 
| -        new SortedTableColumn.withFormatter('Executing (%)',
 | 
| -                                            Utils.formatPercentNormalized),
 | 
| -        new SortedTableColumn.withFormatter('In stack (%)',
 | 
| -                                            Utils.formatPercentNormalized),
 | 
| -        new SortedTableColumn('Method'),
 | 
| -    ];
 | 
| -    profileTable = new NameSortedTable(columns);
 | 
| -    profileTable.sortColumnIndex = 0;
 | 
| -
 | 
| -    columns = [
 | 
| -        new SortedTableColumn.withFormatter('Callees (%)',
 | 
| -                                            Utils.formatPercentNormalized),
 | 
| -        new SortedTableColumn('Method')
 | 
| -    ];
 | 
| -    profileCalleesTable = new NameSortedTable(columns);
 | 
| -    profileCalleesTable.sortColumnIndex = 0;
 | 
| -
 | 
| -    columns = [
 | 
| -        new SortedTableColumn.withFormatter('Callers (%)',
 | 
| -                                            Utils.formatPercentNormalized),
 | 
| -        new SortedTableColumn('Method')
 | 
| -    ];
 | 
| -    profileCallersTable = new NameSortedTable(columns);
 | 
| -    profileCallersTable.sortColumnIndex = 0;
 | 
| -  }
 | 
| -
 | 
|    attached() {
 | 
|      super.attached();
 | 
| -    _updateTask.queue();
 | 
| -    _resizeSubscription = window.onResize.listen((_) => _updateSize());
 | 
| -    _updateSize();
 | 
| +    _r.enable();
 | 
| +    _request();
 | 
|    }
 | 
|  
 | 
| +  @override
 | 
|    detached() {
 | 
|      super.detached();
 | 
| -    if (_resizeSubscription != null) {
 | 
| -      _resizeSubscription.cancel();
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  _updateSize() {
 | 
| -    HtmlElement e = $['main'];
 | 
| -    final totalHeight = window.innerHeight;
 | 
| -    final top = e.offset.top;
 | 
| -    final bottomMargin = 32;
 | 
| -    final mainHeight = totalHeight - top - bottomMargin;
 | 
| -    e.style.setProperty('height', '${mainHeight}px');
 | 
| -  }
 | 
| -
 | 
| -  isolateChanged(oldValue) {
 | 
| -    _updateTask.queue();
 | 
| -  }
 | 
| -
 | 
| -  update() async {
 | 
| -    _clearView();
 | 
| -    if (isolate == null) {
 | 
| -      return;
 | 
| -    }
 | 
| -    final stream = _repository.get(isolate, M.SampleProfileTag.none);
 | 
| -    var progress = (await stream.first).progress;
 | 
| -    shadowRoot.querySelector('#sampleBufferControl').children = [
 | 
| -      sampleBufferControlElement =
 | 
| -        new SampleBufferControlElement(progress, stream, showTag: false,
 | 
| -          selectedTag: M.SampleProfileTag.none, queue: app.queue)
 | 
| +    _r.disable(notify: true);
 | 
| +    children = [];
 | 
| +  }
 | 
| +
 | 
| +  void render() {
 | 
| +    var content = [
 | 
| +      new NavBarElement(queue: _r.queue)
 | 
| +        ..children = [
 | 
| +          new NavTopMenuElement(queue: _r.queue),
 | 
| +          new NavVMMenuElement(_vm, _events, queue: _r.queue),
 | 
| +          new NavIsolateMenuElement(_isolate, _events, queue: _r.queue),
 | 
| +          new NavMenuElement('cpu profile (table)',
 | 
| +              link: Uris.profiler(_isolate), last: true, queue: _r.queue),
 | 
| +          new NavRefreshElement(queue: _r.queue)
 | 
| +              ..onRefresh.listen(_refresh),
 | 
| +          new NavRefreshElement(label: 'Clear', queue: _r.queue)
 | 
| +              ..onRefresh.listen(_clearCpuProfile),
 | 
| +          new NavNotifyElement(_notifications, queue: _r.queue)
 | 
| +        ],
 | 
|      ];
 | 
| -    if (M.isSampleProcessRunning(progress.status)) {
 | 
| -      progress = (await stream.last).progress;
 | 
| -    }
 | 
| -    if (progress.status == M.SampleProfileLoadingStatus.loaded) {
 | 
| -      _profile = progress.profile;
 | 
| -      shadowRoot.querySelector('#stackTraceTreeConfig').children = [
 | 
| -        stackTraceTreeConfigElement =
 | 
| -            new StackTraceTreeConfigElement(showMode: false,
 | 
| -                showDirection: false, mode: ProfileTreeMode.function,
 | 
| -                direction: M.ProfileTreeDirection.exclusive, queue: app.queue)
 | 
| -              ..onModeChange.listen((e) {
 | 
| -                cpuProfileTreeElement.mode = e.element.mode;
 | 
| -                _renderTask.queue();
 | 
| -              })
 | 
| -              ..onDirectionChange.listen((e) {
 | 
| -                cpuProfileTreeElement.direction = e.element.direction;
 | 
| -                _renderTask.queue();
 | 
| -              })
 | 
| -              ..onFilterChange.listen((e) =>_renderTask.queue())
 | 
| -      ];
 | 
| -      shadowRoot.querySelector('#cpuProfileTree').children = [
 | 
| -        cpuProfileTreeElement =
 | 
| -            new CpuProfileVirtualTreeElement(isolate, _profile,
 | 
| -                queue: app.queue)
 | 
| -      ];
 | 
| -    }
 | 
| -    _renderTask.queue();
 | 
| -  }
 | 
| -
 | 
| -  Future clearCpuProfile() async {
 | 
| -    await isolate.invokeRpc('_clearCpuProfile', { });
 | 
| -    _updateTask.queue();
 | 
| -    return new Future.value(null);
 | 
| -  }
 | 
| -
 | 
| -  Future refresh() {
 | 
| -    _updateTask.queue();
 | 
| -    return new Future.value(null);
 | 
| -  }
 | 
| -
 | 
| -  render() {
 | 
| -    _updateView();
 | 
| -  }
 | 
| -
 | 
| -  checkParameters() {
 | 
| -    if (isolate == null) {
 | 
| -      return;
 | 
| -    }
 | 
| -    var functionId = app.locationManager.uri.queryParameters['functionId'];
 | 
| -    var functionName =
 | 
| -        app.locationManager.uri.queryParameters['functionName'];
 | 
| -    if (functionId == '') {
 | 
| -      // Fallback to searching by name.
 | 
| -      _focusOnFunction(_findFunction(functionName));
 | 
| -    } else {
 | 
| -      if (functionId == null) {
 | 
| -        _focusOnFunction(null);
 | 
| -        return;
 | 
| -      }
 | 
| -      isolate.getObject(functionId).then((func) => _focusOnFunction(func));
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  _clearView() {
 | 
| -    profileTable.clearRows();
 | 
| -    _renderTable();
 | 
| -  }
 | 
| -
 | 
| -  _updateView() {
 | 
| -    _buildFunctionTable();
 | 
| -    _renderTable();
 | 
| -    _updateFunctionTreeView();
 | 
| -  }
 | 
| -
 | 
| -  int _findFunctionRow(ServiceFunction function) {
 | 
| -    for (var i = 0; i < profileTable.sortedRows.length; i++) {
 | 
| -      var rowIndex = profileTable.sortedRows[i];
 | 
| -      var row = profileTable.rows[rowIndex];
 | 
| -      if (row.values[NameSortedTable.FUNCTION_COLUMN] == function) {
 | 
| -        return i;
 | 
| -      }
 | 
| -    }
 | 
| -    return -1;
 | 
| -  }
 | 
| -
 | 
| -  _scrollToFunction(ServiceFunction function) {
 | 
| -    TableSectionElement tableBody = $['profile-table'];
 | 
| -    var row = _findFunctionRow(function);
 | 
| -    if (row == -1) {
 | 
| +    if (_progress == null) {
 | 
| +      children = content;
 | 
|        return;
 | 
|      }
 | 
| -    tableBody.children[row].classes.remove('shake');
 | 
| -    // trigger reflow.
 | 
| -    tableBody.children[row].offsetHeight;
 | 
| -    tableBody.children[row].scrollIntoView(ScrollAlignment.CENTER);
 | 
| -    tableBody.children[row].classes.add('shake');
 | 
| -    // Focus on clicked function.
 | 
| -    _focusOnFunction(function);
 | 
| -  }
 | 
| -
 | 
| -  _clearFocusedFunction() {
 | 
| -    TableSectionElement tableBody = $['profile-table'];
 | 
| -    // Clear current focus.
 | 
| -    if (focusedRow != null) {
 | 
| -      tableBody.children[focusedRow].classes.remove('focused');
 | 
| -    }
 | 
| -    focusedRow = null;
 | 
| -    focusedFunction = null;
 | 
| -  }
 | 
| -
 | 
| -  ServiceFunction _findFunction(String functionName) {
 | 
| -    for (var func in _profile.functions) {
 | 
| -      if (func.function.name == functionName) {
 | 
| -        return func.function;
 | 
| -      }
 | 
| +    content.add(new SampleBufferControlElement(_progress, _progressStream,
 | 
| +      showTag: false, queue: _r.queue));
 | 
| +    if (_progress.status == M.SampleProfileLoadingStatus.loaded) {
 | 
| +      content.add(new BRElement());
 | 
| +      content.addAll(_createTables());
 | 
| +      content.add(new BRElement());
 | 
| +      content.addAll(_createTree());
 | 
|      }
 | 
| -    return null;
 | 
| +    children = content;
 | 
|    }
 | 
|  
 | 
| -  _focusOnFunction(ServiceFunction function) {
 | 
| -    if (focusedFunction == function) {
 | 
| -      // Do nothing.
 | 
| -      return;
 | 
| -    }
 | 
| -
 | 
| -    _clearFocusedFunction();
 | 
| +  M.ProfileFunction _selected;
 | 
| +  VirtualCollectionElement _functions;
 | 
| +  VirtualCollectionElement _callers;
 | 
| +  VirtualCollectionElement _callees;
 | 
|  
 | 
| -    if (function == null) {
 | 
| -      _updateFunctionTreeView();
 | 
| -      _clearCallTables();
 | 
| -      return;
 | 
| -    }
 | 
| -
 | 
| -    var row = _findFunctionRow(function);
 | 
| -    if (row == -1) {
 | 
| -      _updateFunctionTreeView();
 | 
| -      _clearCallTables();
 | 
| -      return;
 | 
| -    }
 | 
| -
 | 
| -    focusedRow = row;
 | 
| -    focusedFunction = function;
 | 
| -
 | 
| -    TableSectionElement tableBody = $['profile-table'];
 | 
| -    tableBody.children[focusedRow].classes.add('focused');
 | 
| -    _updateFunctionTreeView();
 | 
| -    _buildCallersTable(focusedFunction);
 | 
| -    _buildCalleesTable(focusedFunction);
 | 
| -  }
 | 
| -
 | 
| -  _onRowClick(TableRowElement tr) {
 | 
| -    var tableBody = $['profile-table'];
 | 
| -    var row = profileTable.rowFromIndex(tableBody.children.indexOf(tr));
 | 
| -    var function = row.values[NameSortedTable.FUNCTION_COLUMN];
 | 
| -    app.locationManager.goReplacingParameters(
 | 
| -        {
 | 
| -          'functionId': function.id,
 | 
| -          'functionName': function.vmName
 | 
| -        }
 | 
| +  List<Element> _createTables() {
 | 
| +    _functions = _functions ?? new VirtualCollectionElement(
 | 
| +        _createFunction,
 | 
| +        _updateFunction,
 | 
| +        createHeader: _createFunctionHeader,
 | 
| +        queue: _r.queue
 | 
|      );
 | 
| +    _functions.items = _progress.profile.functions.toList()
 | 
| +        ..sort(_createSorter(_Table.functions));
 | 
| +    _functions.takeIntoView(_selected);
 | 
| +    _callers = _callers ?? new VirtualCollectionElement(
 | 
| +        _createCaller,
 | 
| +        _updateCaller,
 | 
| +        createHeader: _createCallerHeader,
 | 
| +        queue: _r.queue
 | 
| +    );
 | 
| +    _callees = _callees ?? new VirtualCollectionElement(
 | 
| +        _createCallee,
 | 
| +        _updateCallee,
 | 
| +        createHeader: _createCalleeHeader,
 | 
| +        queue: _r.queue
 | 
| +    );
 | 
| +    if (_selected != null) {
 | 
| +      _callers.items = _selected.callers.keys.toList()
 | 
| +          ..sort(_createSorter(_Table.caller));
 | 
| +      _callees.items = _selected.callees.keys.toList()
 | 
| +          ..sort(_createSorter(_Table.callee));
 | 
| +    } else {
 | 
| +      _callers.items = const [];
 | 
| +      _callees.items = const [];
 | 
| +    }
 | 
| +    return [
 | 
| +      new DivElement()..classes = ['profile-trees']
 | 
| +        ..children = [
 | 
| +          new DivElement()..classes = ['profile-trees-all']
 | 
| +            ..children = [_functions],
 | 
| +          new DivElement()..classes = ['profile-trees-current']
 | 
| +            ..children = [
 | 
| +              new DivElement()..classes = ['profile-trees-caller']
 | 
| +                ..children = [_callers],
 | 
| +              new DivElement()..classes = ['profile-trees-selected']
 | 
| +                ..children = _selected == null
 | 
| +                    ? [new SpanElement()..text = 'No element selected']
 | 
| +                    : [new FunctionRefElement(_isolate, _selected.function,
 | 
| +                                             queue : _r.queue)],
 | 
| +              new DivElement()..classes = ['profile-trees-callee']
 | 
| +                ..children = [_callees]
 | 
| +            ]
 | 
| +        ]
 | 
| +    ];
 | 
|    }
 | 
|  
 | 
| -  _renderTable() {
 | 
| -    profileTable._updateTableView($['profile-table'],
 | 
| -                                  profileTable._makeFunctionRow,
 | 
| -                                  _onRowClick,
 | 
| -                                  NameSortedTable.FUNCTION_SPACER_COLUMNS,
 | 
| -                                  NameSortedTable.FUNCTION_COLUMN);
 | 
| -  }
 | 
| -
 | 
| -  _buildFunctionTable() {
 | 
| -    for (var func in _profile.functions) {
 | 
| -      if ((func.exclusiveTicks == 0) && (func.inclusiveTicks == 0)) {
 | 
| -        // Skip.
 | 
| -        continue;
 | 
| -      }
 | 
| -      var row = [
 | 
| -        func.normalizedExclusiveTicks,
 | 
| -        func.normalizedInclusiveTicks,
 | 
| -        func.function,
 | 
| -      ];
 | 
| -      profileTable.addRow(new SortedTableRow(row));
 | 
| -    }
 | 
| -    profileTable.sort();
 | 
| -  }
 | 
| -
 | 
| -  _renderCallTable(TableSectionElement view,
 | 
| -                   NameSortedTable model,
 | 
| -                   void onRowClick(TableRowElement tr)) {
 | 
| -    model._updateTableView(view,
 | 
| -                           model._makeCallRow,
 | 
| -                           onRowClick,
 | 
| -                           NameSortedTable.CALL_SPACER_COLUMNS,
 | 
| -                           NameSortedTable.CALL_FUNCTION_COLUMN);
 | 
| -  }
 | 
| -
 | 
| -  _buildCallTable(Map<ProfileFunction, int> calls,
 | 
| -                  NameSortedTable model) {
 | 
| -    model.clearRows();
 | 
| -    if (calls == null) {
 | 
| -      return;
 | 
| -    }
 | 
| -    var sum = 0;
 | 
| -    calls.values.forEach((i) => sum += i);
 | 
| -    calls.forEach((func, count) {
 | 
| -      var row = [
 | 
| -          count / sum,
 | 
| -          func.function,
 | 
| +  Element _createFunction() {
 | 
| +    final element = new DivElement()
 | 
| +      ..classes = const ['function-item']
 | 
| +      ..children = [
 | 
| +        new SpanElement()..classes = const ['exclusive']
 | 
| +          ..text = '0%',
 | 
| +        new SpanElement()..classes = const ['inclusive']
 | 
| +          ..text = '0%',
 | 
| +        new SpanElement()..classes = const ['name']
 | 
|        ];
 | 
| -      model.addRow(new SortedTableRow(row));
 | 
| +    element.onClick.listen((_) {
 | 
| +      _selected = _functions.getItemFromElement(element);
 | 
| +      _r.dirty();
 | 
|      });
 | 
| -    model.sort();
 | 
| -  }
 | 
| -
 | 
| -  _clearCallTables() {
 | 
| -    _buildCallersTable(null);
 | 
| -    _buildCalleesTable(null);
 | 
| -  }
 | 
| -
 | 
| -  _onCallersClick(TableRowElement tr) {
 | 
| -    var table = $['callers-table'];
 | 
| -    final row = profileCallersTable.rowFromIndex(table.children.indexOf(tr));
 | 
| -    var function = row.values[NameSortedTable.CALL_FUNCTION_COLUMN];
 | 
| -    _scrollToFunction(function);
 | 
| -  }
 | 
| -
 | 
| -  _buildCallersTable(ServiceFunction function) {
 | 
| -    var calls = (function != null) ? function.profile.callers : null;
 | 
| -    var table = $['callers-table'];
 | 
| -    _buildCallTable(calls, profileCallersTable);
 | 
| -    _renderCallTable(table, profileCallersTable, _onCallersClick);
 | 
| -  }
 | 
| -
 | 
| -  _onCalleesClick(TableRowElement tr) {
 | 
| -    var table = $['callees-table'];
 | 
| -    final row = profileCalleesTable.rowFromIndex(table.children.indexOf(tr));
 | 
| -    var function = row.values[NameSortedTable.CALL_FUNCTION_COLUMN];
 | 
| -    _scrollToFunction(function);
 | 
| +    return element;
 | 
|    }
 | 
|  
 | 
| -  _buildCalleesTable(ServiceFunction function) {
 | 
| -    var calls = (function != null) ? function.profile.callees : null;
 | 
| -    var table = $['callees-table'];
 | 
| -    _buildCallTable(calls, profileCalleesTable);
 | 
| -    _renderCallTable(table, profileCalleesTable, _onCalleesClick);
 | 
| -  }
 | 
| +  void _updateFunction(Element e, M.ProfileFunction item, int index) {
 | 
| +    if (item == _selected) {
 | 
| +      e.classes = const ['function-item', 'selected'];
 | 
| +    } else {
 | 
| +      e.classes = const ['function-item'];
 | 
| +    }
 | 
| +    e.children[0].text = Utils.formatPercentNormalized(_getExclusiveT(item));
 | 
| +    e.children[1].text = Utils.formatPercentNormalized(_getInclusiveT(item));
 | 
| +    e.children[2] = new FunctionRefElement(_isolate, item.function,
 | 
| +        queue: _r.queue)..classes = const ['name'];
 | 
| +  }
 | 
| +
 | 
| +  Element _createFunctionHeader() =>
 | 
| +    new DivElement()
 | 
| +      ..classes = const ['function-item']
 | 
| +        ..children = [
 | 
| +          _createHeaderButton(const ['exclusive'], 'Execution(%)',
 | 
| +                              _Table.functions,
 | 
| +                              _SortingField.exclusive,
 | 
| +                              _SortingDirection.descending),
 | 
| +          _createHeaderButton(const ['inclusive'], 'Stack(%)',
 | 
| +                              _Table.functions,
 | 
| +                              _SortingField.inclusive,
 | 
| +                              _SortingDirection.descending),
 | 
| +          _createHeaderButton(const ['name'], 'Method',
 | 
| +                              _Table.functions,
 | 
| +                              _SortingField.method,
 | 
| +                              _SortingDirection.descending),
 | 
| +      ];
 | 
|  
 | 
| -  _changeSort(Element target, NameSortedTable table) {
 | 
| -    if (target is TableCellElement) {
 | 
| -      if (table.sortColumnIndex != target.cellIndex) {
 | 
| -        table.sortColumnIndex = target.cellIndex;
 | 
| -        table.sortDescending = true;
 | 
| +    void _setSorting(_Table table,
 | 
| +                     _SortingField field,
 | 
| +                     _SortingDirection defaultDirection) {
 | 
| +      if (_sortingField[table] == field) {
 | 
| +        switch (_sortingDirection[table]) {
 | 
| +          case _SortingDirection.descending:
 | 
| +            _sortingDirection[table] = _SortingDirection.ascending;
 | 
| +            break;
 | 
| +          case _SortingDirection.ascending:
 | 
| +            _sortingDirection[table] = _SortingDirection.descending;
 | 
| +            break;
 | 
| +        }
 | 
|        } else {
 | 
| -        table.sortDescending = !profileTable.sortDescending;
 | 
| +        _sortingDirection[table] = defaultDirection;
 | 
| +        _sortingField[table] = field;
 | 
|        }
 | 
| -      table.sort();
 | 
| +      _r.dirty();
 | 
|      }
 | 
| -  }
 | 
|  
 | 
| -  changeSortProfile(Event e, var detail, Element target) {
 | 
| -    _changeSort(target, profileTable);
 | 
| -    _renderTable();
 | 
| -  }
 | 
| -
 | 
| -  changeSortCallers(Event e, var detail, Element target) {
 | 
| -    _changeSort(target, profileCallersTable);
 | 
| -    _renderCallTable($['callers-table'], profileCallersTable, _onCallersClick);
 | 
| +  Element _createCallee() {
 | 
| +    final element = new DivElement()
 | 
| +      ..classes = const ['function-item']
 | 
| +      ..children = [
 | 
| +        new SpanElement()..classes = const ['inclusive']
 | 
| +          ..text = '0%',
 | 
| +        new SpanElement()..classes = const ['name']
 | 
| +      ];
 | 
| +    element.onClick.listen((_) {
 | 
| +      _selected = _callees.getItemFromElement(element);
 | 
| +      _r.dirty();
 | 
| +    });
 | 
| +    return element;
 | 
|    }
 | 
|  
 | 
| -  changeSortCallees(Event e, var detail, Element target) {
 | 
| -    _changeSort(target, profileCalleesTable);
 | 
| -    _renderCallTable($['callees-table'], profileCalleesTable, _onCalleesClick);
 | 
| -  }
 | 
| +  void _updateCallee(Element e, item, int index) {
 | 
| +    e.children[0].text = Utils.formatPercentNormalized(_getCalleeT(item));
 | 
| +    e.children[1] = new FunctionRefElement(_isolate, item.function,
 | 
| +        queue: _r.queue)..classes = const ['name'];
 | 
| +  }
 | 
| +
 | 
| +  Element _createCalleeHeader() =>
 | 
| +    new DivElement()
 | 
| +      ..classes = const ['function-item']
 | 
| +        ..children = [
 | 
| +          _createHeaderButton(const ['inclusive'], 'Callees(%)',
 | 
| +                              _Table.callee,
 | 
| +                              _SortingField.callee,
 | 
| +                              _SortingDirection.descending),
 | 
| +          _createHeaderButton(const ['name'], 'Method',
 | 
| +                              _Table.callee,
 | 
| +                              _SortingField.method,
 | 
| +                              _SortingDirection.descending),
 | 
| +      ];
 | 
|  
 | 
| -  //////
 | 
| -  ///
 | 
| -  /// Function tree.
 | 
| -  ///
 | 
| -  TableTree functionTree;
 | 
| -  _updateFunctionTreeView() {
 | 
| -    if (cpuProfileTreeElement == null) {
 | 
| -      return;
 | 
| -    }
 | 
| -    cpuProfileTreeElement.filter = (FunctionCallTreeNode node) {
 | 
| -      return node.profileFunction.function == focusedFunction;
 | 
| -    };
 | 
| +  Element _createCaller() {
 | 
| +    final element = new DivElement()
 | 
| +      ..classes = const ['function-item']
 | 
| +      ..children = [
 | 
| +        new SpanElement()..classes = const ['inclusive']
 | 
| +          ..text = '0%',
 | 
| +        new SpanElement()..classes = const ['name']
 | 
| +      ];
 | 
| +    element.onClick.listen((_) {
 | 
| +      _selected = _callers.getItemFromElement(element);
 | 
| +      _r.dirty();
 | 
| +    });
 | 
| +    return element;
 | 
|    }
 | 
|  
 | 
| -  @published Isolate isolate;
 | 
| -  @observable NameSortedTable profileTable;
 | 
| -  @observable NameSortedTable profileCallersTable;
 | 
| -  @observable NameSortedTable profileCalleesTable;
 | 
| -  @observable ServiceFunction focusedFunction;
 | 
| -  @observable int focusedRow;
 | 
| -
 | 
| +  void _updateCaller(Element e, item, int index) {
 | 
| +    e.children[0].text = Utils.formatPercentNormalized(_getCallerT(item));
 | 
| +    e.children[1] = new FunctionRefElement(_isolate, item.function,
 | 
| +        queue: _r.queue)..classes = const ['name'];
 | 
| +  }
 | 
| +
 | 
| +  Element _createCallerHeader() =>
 | 
| +    new DivElement()
 | 
| +      ..classes = const ['function-item']
 | 
| +        ..children = [
 | 
| +          _createHeaderButton(const ['inclusive'], 'Callers(%)',
 | 
| +                              _Table.caller,
 | 
| +                              _SortingField.caller,
 | 
| +                              _SortingDirection.descending),
 | 
| +          _createHeaderButton(const ['name'], 'Method',
 | 
| +                              _Table.caller,
 | 
| +                              _SortingField.method,
 | 
| +                              _SortingDirection.descending),
 | 
| +      ];
 | 
|  
 | 
| -  StreamSubscription _resizeSubscription;
 | 
| -  Task _updateTask;
 | 
| -  Task _renderTask;
 | 
| +  ButtonElement _createHeaderButton(List<String> classes,
 | 
| +                                    String text,
 | 
| +                                    _Table table,
 | 
| +                                    _SortingField field,
 | 
| +                                    _SortingDirection direction) =>
 | 
| +      new ButtonElement()..classes = classes
 | 
| +          ..text = _sortingField[table] != field ? text :
 | 
| +                     _sortingDirection[table] == _SortingDirection.ascending
 | 
| +                     ? '$textâ–¼' : '$textâ–²'
 | 
| +          ..onClick.listen((_) => _setSorting(table, field, direction));
 | 
| +
 | 
| +  List<Element> _createTree() {
 | 
| +    CpuProfileVirtualTreeElement tree;
 | 
| +    return [
 | 
| +      new StackTraceTreeConfigElement(showMode: false,
 | 
| +        showDirection: false, mode: ProfileTreeMode.function,
 | 
| +        direction: M.ProfileTreeDirection.exclusive, filter: _filter,
 | 
| +        queue: _r.queue)
 | 
| +        ..onFilterChange.listen((e) {
 | 
| +          _filter = e.element.filter.trim();
 | 
| +          tree.filters = _filter.isNotEmpty
 | 
| +            ? [_filterTree, (node) { return node.name.contains(_filter); }]
 | 
| +            : [_filterTree];
 | 
| +        }),
 | 
| +      new BRElement(),
 | 
| +      tree = new CpuProfileVirtualTreeElement(_isolate, _progress.profile,
 | 
| +        mode: ProfileTreeMode.function,
 | 
| +        direction: M.ProfileTreeDirection.exclusive,
 | 
| +        queue: _r.queue)
 | 
| +          ..filters = _filter.isNotEmpty
 | 
| +            ? [_filterTree, (node) { return node.name.contains(_filter); }]
 | 
| +            : [_filterTree]
 | 
| +    ];
 | 
| +  }
 | 
|  
 | 
| -  IsolateSampleProfileRepository _repository =
 | 
| -      new IsolateSampleProfileRepository();
 | 
| -  CpuProfile _profile;
 | 
| -  SampleBufferControlElement sampleBufferControlElement;
 | 
| -  StackTraceTreeConfigElement stackTraceTreeConfigElement;
 | 
| -  CpuProfileVirtualTreeElement cpuProfileTreeElement;
 | 
| +  bool _filterTree(M.FunctionCallTreeNode node) =>
 | 
| +      node.profileFunction == _selected;
 | 
| +
 | 
| +  Future _request({bool clear: false, bool forceFetch: false}) async {
 | 
| +    _progress = null;
 | 
| +    _progressStream = _profiles.get(isolate, M.SampleProfileTag.none,
 | 
| +        clear: clear, forceFetch: forceFetch);
 | 
| +    _r.dirty();
 | 
| +    _progress = (await _progressStream.first).progress;
 | 
| +    _r.dirty();
 | 
| +    if (M.isSampleProcessRunning(_progress.status)) {
 | 
| +      _progress = (await _progressStream.last).progress;
 | 
| +      _r.dirty();
 | 
| +    }
 | 
| +  }
 | 
| +
 | 
| +  Future _clearCpuProfile(RefreshEvent e) async {
 | 
| +    e.element.disabled = true;
 | 
| +    await _request(clear: true);
 | 
| +    e.element.disabled = false;
 | 
| +  }
 | 
| +
 | 
| +  Future _refresh(e) async {
 | 
| +    e.element.disabled = true;
 | 
| +    await _request(forceFetch: true);
 | 
| +    e.element.disabled = false;
 | 
| +  }
 | 
| +
 | 
| +  _createSorter(_Table table) {
 | 
| +    var getter;
 | 
| +    switch (_sortingField[table]) {
 | 
| +      case _SortingField.exclusive:
 | 
| +        getter = _getExclusiveT;
 | 
| +        break;
 | 
| +      case _SortingField.inclusive:
 | 
| +        getter = _getInclusiveT;
 | 
| +        break;
 | 
| +      case _SortingField.callee:
 | 
| +        getter = _getCalleeT;
 | 
| +        break;
 | 
| +      case _SortingField.caller:
 | 
| +        getter = _getCallerT;
 | 
| +        break;
 | 
| +      case _SortingField.method:
 | 
| +        getter = (M.ProfileFunction s) =>
 | 
| +          M.getFunctionFullName(s.function);
 | 
| +        break;
 | 
| +    }
 | 
| +    switch (_sortingDirection[table]) {
 | 
| +      case _SortingDirection.ascending:
 | 
| +        return (a, b) => getter(a).compareTo(getter(b));
 | 
| +      case _SortingDirection.descending:
 | 
| +        return (a, b) => getter(b).compareTo(getter(a));
 | 
| +    }
 | 
| +  }
 | 
| +
 | 
| +  static double _getExclusiveT(M.ProfileFunction f) =>
 | 
| +      f.normalizedExclusiveTicks;
 | 
| +  static double _getInclusiveT(M.ProfileFunction f) =>
 | 
| +      f.normalizedExclusiveTicks;
 | 
| +  double _getCalleeT(M.ProfileFunction f) =>
 | 
| +      _selected.callees[f] / _selected.callees.values.reduce((a, b) => a + b);
 | 
| +  double _getCallerT(M.ProfileFunction f) =>
 | 
| +      _selected.callers[f] / _selected.callers.values.reduce((a, b) => a + b);
 | 
|  }
 | 
| 
 |