| Index: runtime/observatory/lib/src/elements/timeline_page.dart
 | 
| diff --git a/runtime/observatory/lib/src/elements/timeline_page.dart b/runtime/observatory/lib/src/elements/timeline_page.dart
 | 
| index 28f3046e7cc77f0dd24ac1bec0fbaab52d533bbf..8e7b3ef144da8c56af6b08bbfc40d16af3c92000 100644
 | 
| --- a/runtime/observatory/lib/src/elements/timeline_page.dart
 | 
| +++ b/runtime/observatory/lib/src/elements/timeline_page.dart
 | 
| @@ -5,100 +5,231 @@
 | 
|  library timeline_page_element;
 | 
|  
 | 
|  import 'dart:async';
 | 
| -import 'dart:convert';
 | 
|  import 'dart:html';
 | 
| -import 'observatory_element.dart';
 | 
| -import 'package:observatory/elements.dart';
 | 
| -import 'package:observatory/service_html.dart';
 | 
| -import 'package:polymer/polymer.dart';
 | 
| +import 'dart:convert';
 | 
| +import 'package:observatory/service.dart' as S;
 | 
| +import 'package:observatory/service_html.dart' as SH;
 | 
| +import 'package:observatory/models.dart' as M;
 | 
| +import 'package:observatory/src/elements/helpers/rendering_scheduler.dart';
 | 
| +import 'package:observatory/src/elements/helpers/tag.dart';
 | 
| +import 'package:observatory/src/elements/nav/bar.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';
 | 
| +
 | 
| +enum _Profile {
 | 
| +  none,
 | 
| +  dart,
 | 
| +  vm,
 | 
| +  all,
 | 
| +  custom
 | 
| +}
 | 
|  
 | 
| +class TimelinePageElement extends HtmlElement implements Renderable {
 | 
| +  static const tag = const Tag<TimelinePageElement>('timeline-page',
 | 
| +                                                    dependencies: const [
 | 
| +                                                      NavBarElement.tag,
 | 
| +                                                      NavTopMenuElement.tag,
 | 
| +                                                      NavVMMenuElement.tag,
 | 
| +                                                      NavRefreshElement.tag,
 | 
| +                                                      NavNotifyElement.tag
 | 
| +                                                    ]);
 | 
|  
 | 
| -@CustomTag('timeline-page')
 | 
| -class TimelinePageElement extends ObservatoryElement {
 | 
| -  TimelinePageElement.created() : super.created() {
 | 
| +  RenderingScheduler<TimelinePageElement> _r;
 | 
| +
 | 
| +  Stream<RenderedEvent<TimelinePageElement>> get onRendered => _r.onRendered;
 | 
| +
 | 
| +  M.VM _vm;
 | 
| +  M.EventRepository _events;
 | 
| +  M.NotificationRepository _notifications;
 | 
| +  String _recorderName = '';
 | 
| +  _Profile _profile = _Profile.none;
 | 
| +  final Set<String> _availableStreams = new Set<String>();
 | 
| +  final Set<String> _recordedStreams = new Set<String>();
 | 
| +
 | 
| +  M.VMRef get vm => _vm;
 | 
| +  M.NotificationRepository get notifications => _notifications;
 | 
| +
 | 
| +  factory TimelinePageElement(M.VM vm, M.EventRepository events,
 | 
| +                              M.NotificationRepository notifications,
 | 
| +                              {RenderingQueue queue}) {
 | 
| +    assert(vm != null);
 | 
| +    assert(events != null);
 | 
| +    assert(notifications != null);
 | 
| +    TimelinePageElement e = document.createElement(tag.name);
 | 
| +    e._r = new RenderingScheduler(e, queue: queue);
 | 
| +    e._vm = vm;
 | 
| +    e._events = events;
 | 
| +    e._notifications = notifications;
 | 
| +    return e;
 | 
|    }
 | 
|  
 | 
| +   TimelinePageElement.created() : super.created();
 | 
| +
 | 
| +  @override
 | 
|    attached() {
 | 
|      super.attached();
 | 
| -    _resizeSubscription = window.onResize.listen((_) => _updateSize());
 | 
| -    _updateSize();
 | 
| +    _r.enable();
 | 
|      _setupInitialState();
 | 
|    }
 | 
|  
 | 
| +  @override
 | 
|    detached() {
 | 
|      super.detached();
 | 
| -    if (_resizeSubscription != null) {
 | 
| -      _resizeSubscription.cancel();
 | 
| +    _r.disable(notify: true);
 | 
| +    children = [];
 | 
| +  }
 | 
| +
 | 
| +  IFrameElement _frame;
 | 
| +  DivElement _content;
 | 
| +
 | 
| +  void render() {
 | 
| +    if (_frame == null) {
 | 
| +      _frame = new IFrameElement()..src = 'timeline.html';
 | 
| +    }
 | 
| +    if (_content == null) {
 | 
| +      _content = new DivElement()..classes = ['content-centered-big'];
 | 
| +    }
 | 
| +    _content.children = [
 | 
| +      new HeadingElement.h1()..text = 'Timeline settings',
 | 
| +      new DivElement()..classes = ['memberList']
 | 
| +        ..children = [
 | 
| +          new DivElement()..classes = ['memberItem']
 | 
| +            ..children = [
 | 
| +              new DivElement()..classes = ['memberName']
 | 
| +                ..text = 'Recorder:',
 | 
| +              new DivElement()..classes = ['memberValue']
 | 
| +                ..text = _recorderName
 | 
| +            ],
 | 
| +          new DivElement()..classes = ['memberItem']
 | 
| +            ..children = [
 | 
| +              new DivElement()..classes = ['memberName']
 | 
| +                ..text = 'Recorded Streams Profile:',
 | 
| +              new DivElement()..classes = ['memberValue']
 | 
| +                ..children = _createProfileSelect()
 | 
| +            ],
 | 
| +          new DivElement()..classes = ['memberItem']
 | 
| +            ..children = [
 | 
| +              new DivElement()..classes = ['memberName']
 | 
| +                ..text = 'Recorded Streams:',
 | 
| +              new DivElement()..classes = ['memberValue']
 | 
| +                ..children =
 | 
| +                  _availableStreams.map(_makeStreamToggle).toList()
 | 
| +            ]
 | 
| +        ]
 | 
| +    ];
 | 
| +    if (children.isEmpty) {
 | 
| +      children = [
 | 
| +        new NavBarElement(queue: _r.queue)
 | 
| +          ..children = [
 | 
| +            new NavTopMenuElement(queue: _r.queue),
 | 
| +            new NavVMMenuElement(_vm, _events, last: true, queue: _r.queue),
 | 
| +            new NavRefreshElement(queue: _r.queue)
 | 
| +                ..onRefresh.listen((e) async {
 | 
| +                  e.element.disabled = true;
 | 
| +                  await _refresh();
 | 
| +                  e.element.disabled = false;
 | 
| +                }),
 | 
| +            new NavRefreshElement(label: 'clear', queue: _r.queue)
 | 
| +                ..onRefresh.listen((e) async {
 | 
| +                  e.element.disabled = true;
 | 
| +                  await _clear();
 | 
| +                  e.element.disabled = false;
 | 
| +                }),
 | 
| +            new NavRefreshElement(label: 'save', queue: _r.queue)
 | 
| +                ..onRefresh.listen((e) async {
 | 
| +                  e.element.disabled = true;
 | 
| +                  await _save();
 | 
| +                  e.element.disabled = false;
 | 
| +                }),
 | 
| +            new NavRefreshElement(label: 'load', queue: _r.queue)
 | 
| +                ..onRefresh.listen((e) async {
 | 
| +                  e.element.disabled = true;
 | 
| +                  await _load();
 | 
| +                  e.element.disabled = false;
 | 
| +                }),
 | 
| +            new NavNotifyElement(_notifications, queue: _r.queue)
 | 
| +          ],
 | 
| +        _content,
 | 
| +        new DivElement()..classes = ['iframe']
 | 
| +          ..children = [
 | 
| +            _frame
 | 
| +          ]
 | 
| +      ];
 | 
|      }
 | 
|    }
 | 
|  
 | 
| -  Future postMessage(String method) {
 | 
| -    IFrameElement e = $['root'];
 | 
| -    var isolateIds = new List();
 | 
| -    for (var isolate in app.vm.isolates) {
 | 
| -      isolateIds.add(isolate.id);
 | 
| +  List<Element> _createProfileSelect() {
 | 
| +    var s;
 | 
| +    return [
 | 
| +      s = new SelectElement()..classes = ['direction-select']
 | 
| +        ..value = _profileToString(_profile)
 | 
| +        ..children = _Profile.values.map((direction) {
 | 
| +            return new OptionElement(value: _profileToString(direction),
 | 
| +                selected: _profile == direction)
 | 
| +              ..text = _profileToString(direction);
 | 
| +          }).toList(growable: false)
 | 
| +        ..onChange.listen((_) {
 | 
| +            _profile = _Profile.values[s.selectedIndex];
 | 
| +            _applyPreset();
 | 
| +            _r.dirty();
 | 
| +          })
 | 
| +    ];
 | 
| +  }
 | 
| +
 | 
| +  String _profileToString(_Profile profile) {
 | 
| +    switch (profile) {
 | 
| +     case _Profile.none: return 'none';
 | 
| +     case _Profile.dart: return 'Dart Developer';
 | 
| +     case _Profile.vm: return 'VM Developer';
 | 
| +     case _Profile.all: return 'All';
 | 
| +     case _Profile.custom: return 'Custom';
 | 
|      }
 | 
| -    var message = {
 | 
| -      'method': method,
 | 
| -      'params': {
 | 
| -        'vmAddress': (app.vm as WebSocketVM).target.networkAddress,
 | 
| -        'isolateIds': isolateIds
 | 
| -      }
 | 
| -    };
 | 
| -    e.contentWindow.postMessage(JSON.encode(message), window.location.href);
 | 
| -    return null;
 | 
| +    throw new Exception('Unkown Profile ${profile}');
 | 
|    }
 | 
|  
 | 
| -  void _processFlags(ServiceMap response) {
 | 
| -    // Grab the recorder name.
 | 
| -    recorderName = response['recorderName'];
 | 
| -    // Update the set of available streams.
 | 
| -    _availableStreams.clear();
 | 
| -    response['availableStreams'].forEach(
 | 
| -        (String streamName) => _availableStreams.add(streamName));
 | 
| -    // Update the set of recorded streams.
 | 
| -    _recordedStreams.clear();
 | 
| -    response['recordedStreams'].forEach(
 | 
| -        (String streamName) => _recordedStreams.add(streamName));
 | 
| +  Future _refresh() async {
 | 
| +    S.VM vm = _vm as S.VM;
 | 
| +    await vm.reload();
 | 
| +    await vm.reloadIsolates();
 | 
| +    return _postMessage('refresh');
 | 
|    }
 | 
|  
 | 
| -  Future _applyStreamChanges() {
 | 
| -    return app.vm.invokeRpc('_setVMTimelineFlags', {
 | 
| -      'recordedStreams': '[${_recordedStreams.join(', ')}]',
 | 
| -    });
 | 
| +  Future _clear() async {
 | 
| +    S.VM vm = _vm as S.VM;
 | 
| +    await vm.invokeRpc('_clearVMTimeline', {});
 | 
| +    return _postMessage('clear');
 | 
|    }
 | 
|  
 | 
| -  HtmlElement _makeStreamToggle(String streamName) {
 | 
| -    LabelElement label = new LabelElement();
 | 
| -    label.style.paddingLeft = '8px';
 | 
| -    SpanElement span = new SpanElement();
 | 
| -    span.text = streamName;
 | 
| -    InputElement checkbox = new InputElement();
 | 
| -    checkbox.onChange.listen((_) {
 | 
| -      if (checkbox.checked) {
 | 
| -        _recordedStreams.add(streamName);
 | 
| -      } else {
 | 
| -        _recordedStreams.remove(streamName);
 | 
| -      }
 | 
| -      _applyStreamChanges();
 | 
| -      _updateRecorderUI();
 | 
| -    });
 | 
| -    checkbox.type = 'checkbox';
 | 
| -    checkbox.checked = _recordedStreams.contains(streamName);
 | 
| -    label.children.add(checkbox);
 | 
| -    label.children.add(span);
 | 
| -    return label;
 | 
| +  Future _save() async {
 | 
| +    return _postMessage('save');
 | 
|    }
 | 
|  
 | 
| -  void _refreshRecorderUI() {
 | 
| -    DivElement e = $['streamList'];
 | 
| -    e.children.clear();
 | 
| +  Future _load() async {
 | 
| +    return _postMessage('load');
 | 
| +  }
 | 
|  
 | 
| -    for (String streamName in _availableStreams) {
 | 
| -      e.children.add(_makeStreamToggle(streamName));
 | 
| +  Future _postMessage(String method) {
 | 
| +    S.VM vm = _vm as S.VM;
 | 
| +    var isolateIds = new List();
 | 
| +    for (var isolate in vm.isolates) {
 | 
| +      isolateIds.add(isolate.id);
 | 
|      }
 | 
| +    var message = {
 | 
| +      'method': method,
 | 
| +      'params': {
 | 
| +        'vmAddress': (vm as SH.WebSocketVM).target.networkAddress,
 | 
| +        'isolateIds': isolateIds
 | 
| +      }
 | 
| +    };
 | 
| +    _frame.contentWindow.postMessage(JSON.encode(message), window.location.href);
 | 
| +    return null;
 | 
| +  }
 | 
|  
 | 
| -    streamPresetSelector = streamPresetFromRecordedStreams();
 | 
| +  Future _setupInitialState() async {
 | 
| +    await _updateRecorderUI();
 | 
| +    await _refresh();
 | 
|    }
 | 
|  
 | 
|    // Dart developers care about the following streams:
 | 
| @@ -109,45 +240,24 @@ class TimelinePageElement extends ObservatoryElement {
 | 
|    List<String> _vmPreset =
 | 
|        ['GC', 'Compiler', 'Dart', 'Debugger', 'Embedder', 'Isolate', 'VM'];
 | 
|  
 | 
| -  String streamPresetFromRecordedStreams() {
 | 
| -    if (_availableStreams.length == 0) {
 | 
| -      return 'None';
 | 
| -    }
 | 
| -    if (_recordedStreams.length == 0) {
 | 
| -      return 'None';
 | 
| -    }
 | 
| -    if (_recordedStreams.length == _availableStreams.length) {
 | 
| -      return 'All';
 | 
| -    }
 | 
| -    if ((_vmPreset.length == _recordedStreams.length) &&
 | 
| -        _recordedStreams.containsAll(_vmPreset)) {
 | 
| -      return 'VM';
 | 
| -    }
 | 
| -    if ((_dartPreset.length == _recordedStreams.length) &&
 | 
| -        _recordedStreams.containsAll(_dartPreset)) {
 | 
| -      return 'Dart';
 | 
| -    }
 | 
| -    return 'Custom';
 | 
| -  }
 | 
| -
 | 
|    void _applyPreset() {
 | 
| -    switch (streamPresetSelector) {
 | 
| -      case 'None':
 | 
| +    switch (_profile) {
 | 
| +      case _Profile.none:
 | 
|          _recordedStreams.clear();
 | 
|          break;
 | 
| -      case 'All':
 | 
| +      case _Profile.all:
 | 
|          _recordedStreams.clear();
 | 
|          _recordedStreams.addAll(_availableStreams);
 | 
|          break;
 | 
| -      case 'VM':
 | 
| +      case _Profile.vm:
 | 
|          _recordedStreams.clear();
 | 
|          _recordedStreams.addAll(_vmPreset);
 | 
|          break;
 | 
| -      case 'Dart':
 | 
| +      case _Profile.dart:
 | 
|          _recordedStreams.clear();
 | 
|          _recordedStreams.addAll(_dartPreset);
 | 
|          break;
 | 
| -      case 'Custom':
 | 
| +      case _Profile.custom:
 | 
|          return;
 | 
|      }
 | 
|      _applyStreamChanges();
 | 
| @@ -155,58 +265,56 @@ class TimelinePageElement extends ObservatoryElement {
 | 
|    }
 | 
|  
 | 
|    Future _updateRecorderUI() async {
 | 
| +    S.VM vm = _vm as S.VM;
 | 
|      // Grab the current timeline flags.
 | 
| -    ServiceMap response = await app.vm.invokeRpc('_getVMTimelineFlags', {});
 | 
| +    S.ServiceMap response = await vm.invokeRpc('_getVMTimelineFlags', {});
 | 
|      assert(response['type'] == 'TimelineFlags');
 | 
|      // Process them so we know available streams.
 | 
|      _processFlags(response);
 | 
|      // Refresh the UI.
 | 
| -    _refreshRecorderUI();
 | 
| +    _r.dirty();
 | 
|    }
 | 
|  
 | 
| -  Future _setupInitialState() async {
 | 
| -    await _updateRecorderUI();
 | 
| -    SelectElement e = $['selectPreset'];
 | 
| -    e.onChange.listen((_) {
 | 
| -      _applyPreset();
 | 
| +  Element _makeStreamToggle(String streamName) {
 | 
| +    LabelElement label = new LabelElement();
 | 
| +    label.style.paddingLeft = '8px';
 | 
| +    SpanElement span = new SpanElement();
 | 
| +    span.text = streamName;
 | 
| +    InputElement checkbox = new InputElement();
 | 
| +    checkbox.onChange.listen((_) {
 | 
| +      if (checkbox.checked) {
 | 
| +        _recordedStreams.add(streamName);
 | 
| +      } else {
 | 
| +        _recordedStreams.remove(streamName);
 | 
| +      }
 | 
| +      _applyStreamChanges();
 | 
| +      _updateRecorderUI();
 | 
|      });
 | 
| -    // Finally, trigger a reload so we start with the latest timeline.
 | 
| -    await refresh();
 | 
| -  }
 | 
| -
 | 
| -  Future refresh() async {
 | 
| -    await app.vm.reload();
 | 
| -    await app.vm.reloadIsolates();
 | 
| -    return postMessage('refresh');
 | 
| -  }
 | 
| -
 | 
| -  Future clear() async {
 | 
| -    await app.vm.invokeRpc('_clearVMTimeline', {});
 | 
| -    return postMessage('clear');
 | 
| -  }
 | 
| -
 | 
| -  Future saveTimeline() async {
 | 
| -    return postMessage('save');
 | 
| +    checkbox.type = 'checkbox';
 | 
| +    checkbox.checked = _recordedStreams.contains(streamName);
 | 
| +    label.children.add(checkbox);
 | 
| +    label.children.add(span);
 | 
| +    return label;
 | 
|    }
 | 
|  
 | 
| -  Future loadTimeline() async {
 | 
| -    return postMessage('load');
 | 
| +  Future _applyStreamChanges() {
 | 
| +    S.VM vm = _vm as S.VM;
 | 
| +    return vm.invokeRpc('_setVMTimelineFlags', {
 | 
| +      'recordedStreams': '[${_recordedStreams.join(', ')}]',
 | 
| +    });
 | 
|    }
 | 
|  
 | 
| -  _updateSize() {
 | 
| -    IFrameElement e = $['root'];
 | 
| -    final totalHeight = window.innerHeight;
 | 
| -    final top = e.offset.top;
 | 
| -    final bottomMargin = 32;
 | 
| -    final mainHeight = totalHeight - top - bottomMargin;
 | 
| -    e.style.setProperty('height', '${mainHeight}px');
 | 
| -    e.style.setProperty('width', '100%');
 | 
| +  void _processFlags(S.ServiceMap response) {
 | 
| +    // Grab the recorder name.
 | 
| +    _recorderName = response['recorderName'];
 | 
| +    // Update the set of available streams.
 | 
| +    _availableStreams.clear();
 | 
| +    response['availableStreams'].forEach(
 | 
| +        (String streamName) => _availableStreams.add(streamName));
 | 
| +    // Update the set of recorded streams.
 | 
| +    _recordedStreams.clear();
 | 
| +    response['recordedStreams'].forEach(
 | 
| +        (String streamName) => _recordedStreams.add(streamName));
 | 
| +    _r.dirty();
 | 
|    }
 | 
| -
 | 
| -
 | 
| -  StreamSubscription _resizeSubscription;
 | 
| -  @observable String recorderName;
 | 
| -  @observable String streamPresetSelector = 'None';
 | 
| -  final Set<String> _availableStreams = new Set<String>();
 | 
| -  final Set<String> _recordedStreams = new Set<String>();
 | 
|  }
 | 
| 
 |