vmservice redux (#5437)

diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index c94c817..0ad84a0 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -143,7 +143,7 @@
         return 1;
       }
     } else {
-      if (argResults['control-pipe']) {
+      if (argResults['control-pipe'] != null) {
         printError('--control-pipe requires --hot');
         return 1;
       }
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart
index 2632c7f..2fd1dc9 100644
--- a/packages/flutter_tools/lib/src/commands/trace.dart
+++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -95,40 +95,39 @@
 }
 
 class Tracing {
-  Tracing(this.observatory);
+  Tracing(this.vmService);
 
   static Future<Tracing> connect(int port) {
     return VMService.connect(port).then((VMService observatory) => new Tracing(observatory));
   }
 
-  final VMService observatory;
+  final VMService vmService;
 
   Future<Null> startTracing() async {
-    await observatory.setVMTimelineFlags(<String>['Compiler', 'Dart', 'Embedder', 'GC']);
-    await observatory.clearVMTimeline();
+    await vmService.vm.setVMTimelineFlags(<String>['Compiler', 'Dart', 'Embedder', 'GC']);
+    await vmService.vm.clearVMTimeline();
   }
 
   /// Stops tracing; optionally wait for first frame.
   Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({
     bool waitForFirstFrame: false
   }) async {
-    Response timeline;
+    Map<String, dynamic> timeline;
 
     if (!waitForFirstFrame) {
       // Stop tracing immediately and get the timeline
-      await observatory.setVMTimelineFlags(<String>[]);
-      timeline = await observatory.getVMTimeline();
+      await vmService.vm.setVMTimelineFlags(<String>[]);
+      timeline = await vmService.vm.getVMTimeline();
     } else {
       Completer<Null> whenFirstFrameRendered = new Completer<Null>();
 
-      observatory.onTimelineEvent.listen((Event timelineEvent) {
-        List<Map<String, dynamic>> events = timelineEvent['timelineEvents'];
+      vmService.onTimelineEvent.listen((ServiceEvent timelineEvent) {
+        List<Map<String, dynamic>> events = timelineEvent.timelineEvents;
         for (Map<String, dynamic> event in events) {
           if (event['name'] == kFirstUsefulFrameEventName)
             whenFirstFrameRendered.complete();
         }
       });
-      await observatory.streamListen('Timeline');
 
       await whenFirstFrameRendered.future.timeout(
         const Duration(seconds: 10),
@@ -142,12 +141,12 @@
         }
       );
 
-      timeline = await observatory.getVMTimeline();
+      timeline = await vmService.vm.getVMTimeline();
 
-      await observatory.setVMTimelineFlags(<String>[]);
+      await vmService.vm.setVMTimelineFlags(<String>[]);
     }
 
-    return timeline.response;
+    return timeline;
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index fdc5ec3..80a40f2 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -106,22 +106,22 @@
 }
 
 /// An implementation of [DevFSOperations] that speaks to the
-/// service protocol.
+/// vm service.
 class ServiceProtocolDevFSOperations implements DevFSOperations {
-  final VMService serviceProtocol;
+  final VMService vmService;
 
-  ServiceProtocolDevFSOperations(this.serviceProtocol);
+  ServiceProtocolDevFSOperations(this.vmService);
 
   @override
   Future<Uri> create(String fsName) async {
-    Response response = await serviceProtocol.createDevFS(fsName);
+    Map<String, dynamic> response = await vmService.vm.createDevFS(fsName);
     return Uri.parse(response['uri']);
   }
 
   @override
   Future<dynamic> destroy(String fsName) async {
-    await serviceProtocol.sendRequest('_deleteDevFS',
-                                      <String, dynamic> { 'fsName': fsName });
+    await vmService.vm.invokeRpcRaw('_deleteDevFS',
+                                    <String, dynamic> { 'fsName': fsName });
   }
 
   @override
@@ -134,12 +134,12 @@
     }
     String fileContents = BASE64.encode(bytes);
     try {
-      return await serviceProtocol.sendRequest('_writeDevFSFile',
-                                               <String, dynamic> {
-                                                  'fsName': fsName,
-                                                  'path': entry.devicePath,
-                                                  'fileContents': fileContents
-                                               });
+      return await vmService.vm.invokeRpcRaw('_writeDevFSFile',
+                                             <String, dynamic> {
+                                                'fsName': fsName,
+                                                'path': entry.devicePath,
+                                                'fileContents': fileContents
+                                             });
     } catch (e) {
       printTrace('DevFS: Failed to write ${entry.devicePath}: $e');
     }
@@ -155,12 +155,12 @@
                               String devicePath,
                               String contents) async {
     String fileContents = BASE64.encode(UTF8.encode(contents));
-    return await serviceProtocol.sendRequest('_writeDevFSFile',
-                                             <String, dynamic> {
-                                                'fsName': fsName,
-                                                'path': devicePath,
-                                                'fileContents': fileContents
-                                             });
+    return await vmService.vm.invokeRpcRaw('_writeDevFSFile',
+                                           <String, dynamic> {
+                                              'fsName': fsName,
+                                              'path': devicePath,
+                                              'fileContents': fileContents
+                                           });
   }
 }
 
@@ -251,7 +251,8 @@
   final Set<DevFSEntry> _deletedEntries = new Set<DevFSEntry>();
   final Set<DevFSEntry> dirtyAssetEntries = new Set<DevFSEntry>();
 
-  final List<Future<Response>> _pendingOperations = new List<Future<Response>>();
+  final List<Future<Map<String, dynamic>>> _pendingOperations =
+      new List<Future<Map<String, dynamic>>>();
 
   int _bytes = 0;
   int get bytes => _bytes;
@@ -358,7 +359,8 @@
     if (_deletedEntries.length > 0) {
       status = logger.startProgress('Removing deleted files...');
       for (DevFSEntry entry in _deletedEntries) {
-        Future<Response> operation = _operations.deleteFile(fsName, entry);
+        Future<Map<String, dynamic>> operation =
+            _operations.deleteFile(fsName, entry);
         if (operation != null)
           _pendingOperations.add(operation);
       }
@@ -382,7 +384,8 @@
       } else {
         // Make service protocol requests for each.
         for (DevFSEntry entry in _dirtyEntries) {
-          Future<Response> operation = _operations.writeFile(fsName, entry);
+          Future<Map<String, dynamic>> operation =
+              _operations.writeFile(fsName, entry);
           if (operation != null)
             _pendingOperations.add(operation);
         }
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart
index 2e1cad5..80d98a2 100644
--- a/packages/flutter_tools/lib/src/hot.dart
+++ b/packages/flutter_tools/lib/src/hot.dart
@@ -23,7 +23,6 @@
 import 'vmservice.dart';
 import 'resident_runner.dart';
 import 'toolchain.dart';
-import 'view.dart';
 
 String getDevFSLoaderScript() {
   return path.absolute(path.join(Cache.flutterRoot,
@@ -80,18 +79,18 @@
 
 
 class FirstFrameTimer {
-  FirstFrameTimer(this.serviceProtocol);
+  FirstFrameTimer(this.vmService);
 
   void start() {
     stopwatch.reset();
     stopwatch.start();
-    _subscription = serviceProtocol.onExtensionEvent.listen(_onExtensionEvent);
+    _subscription = vmService.onExtensionEvent.listen(_onExtensionEvent);
   }
 
   /// Returns a Future which completes after the first frame event is received.
   Future<Null> firstFrame() => _completer.future;
 
-  void _onExtensionEvent(Event event) {
+  void _onExtensionEvent(ServiceEvent event) {
     if (event.extensionKind == 'Flutter.FirstFrame')
       _stop();
   }
@@ -108,10 +107,10 @@
     return stopwatch.elapsed;
   }
 
-  final VMService serviceProtocol;
+  final VMService vmService;
   final Stopwatch stopwatch = new Stopwatch();
   final Completer<Null> _completer = new Completer<Null>();
-  StreamSubscription<Event> _subscription;
+  StreamSubscription<ServiceEvent> _subscription;
 }
 
 class HotRunner extends ResidentRunner {
@@ -294,8 +293,8 @@
       return 3;
     }
 
-    await viewManager.refresh();
-    printStatus('Connected to view \'${viewManager.mainView}\'.');
+    await vmService.vm.refreshViews();
+    printStatus('Connected to view \'${vmService.vm.mainView}\'.');
 
     printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
     _loaderShowMessage('Launching...');
@@ -332,13 +331,13 @@
   }
 
   void _loaderShowMessage(String message, { int progress, int max }) {
-    serviceProtocol.flutterLoaderShowMessage(serviceProtocol.firstIsolateId, message);
+    currentView.uiIsolate.flutterLoaderShowMessage(message);
     if (progress != null) {
-      serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, progress.toDouble());
-      serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, max?.toDouble() ?? 0.0);
+      currentView.uiIsolate.flutterLoaderSetProgress(progress.toDouble());
+      currentView.uiIsolate.flutterLoaderSetProgressMax(max?.toDouble() ?? 0.0);
     } else {
-      serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, 0.0);
-      serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, -1.0);
+      currentView.uiIsolate.flutterLoaderSetProgress(0.0);
+      currentView.uiIsolate.flutterLoaderSetProgressMax(-1.0);
     }
   }
 
@@ -346,7 +345,7 @@
 
   Future<Uri> _initDevFS() {
     String fsName = path.basename(_projectRootPath);
-    _devFS = new DevFS(serviceProtocol,
+    _devFS = new DevFS(vmService,
                        fsName,
                        new Directory(_projectRootPath));
     return _devFS.create();
@@ -377,11 +376,10 @@
   Future<Null> _evictDirtyAssets() async {
     if (_devFS.dirtyAssetEntries.length == 0)
       return;
-    if (serviceProtocol.firstIsolateId == null)
+    if (currentView.uiIsolate == null)
       throw 'Application isolate not found';
     for (DevFSEntry entry in _devFS.dirtyAssetEntries) {
-      await serviceProtocol.flutterEvictAsset(serviceProtocol.firstIsolateId,
-                                              entry.assetPath);
+      await currentView.uiIsolate.flutterEvictAsset(entry.assetPath);
     }
   }
 
@@ -400,7 +398,7 @@
   Future<Null> _launchInView(String entryPath,
                              String packagesPath,
                              String assetsDirectoryPath) async {
-    FlutterView view = viewManager.mainView;
+    FlutterView view = vmService.vm.mainView;
     return view.runFromSource(entryPath, packagesPath, assetsDirectoryPath);
   }
 
@@ -419,7 +417,7 @@
   }
 
   Future<Null> _restartFromSources() async {
-    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol);
+    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService);
     firstFrameTimer.start();
     await _updateDevFS();
     await _launchFromDevFS(_package, _mainPath);
@@ -459,16 +457,16 @@
   }
 
   Future<bool> _reloadSources() async {
-    if (serviceProtocol.firstIsolateId == null)
+    if (currentView.uiIsolate == null)
       throw 'Application isolate not found';
-    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol);
+    FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService);
     firstFrameTimer.start();
     if (_devFS != null)
       await _updateDevFS();
     Status reloadStatus = logger.startProgress('Performing hot reload...');
     try {
       Map<String, dynamic> reloadReport =
-          await serviceProtocol.reloadSources(serviceProtocol.firstIsolateId);
+          await currentView.uiIsolate.reloadSources();
       reloadStatus.stop(showElapsedTime: true);
       if (!_printReloadReport(reloadReport)) {
         // Reload failed.
@@ -477,16 +475,16 @@
       } else {
         flutterUsage.sendEvent('hot', 'reload');
       }
-    } catch (errorMessage) {
+    } catch (errorMessage, st) {
       reloadStatus.stop(showElapsedTime: true);
-      printError('Hot reload failed:\n$errorMessage');
+      printError('Hot reload failed:\n$errorMessage\n$st');
       return false;
     }
     await _evictDirtyAssets();
     Status reassembleStatus =
         logger.startProgress('Reassembling application...');
     try {
-      await serviceProtocol.flutterReassemble(serviceProtocol.firstIsolateId);
+      await currentView.uiIsolate.flutterReassemble();
     } catch (_) {
       reassembleStatus.stop(showElapsedTime: true);
       printError('Reassembling application failed.');
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 470d721..166f729 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -12,7 +12,6 @@
 import 'device.dart';
 import 'globals.dart';
 import 'vmservice.dart';
-import 'view.dart';
 
 // Shared code between different resident application runners.
 abstract class ResidentRunner {
@@ -28,8 +27,8 @@
   final bool usesTerminalUI;
   final Completer<int> _finished = new Completer<int>();
 
-  VMService serviceProtocol;
-  ViewManager viewManager;
+  VMService vmService;
+  FlutterView currentView;
   StreamSubscription<String> _loggingSubscription;
 
   /// Start the app and keep the process running during its lifetime.
@@ -48,11 +47,11 @@
   }
 
   Future<Null> _debugDumpApp() async {
-    await serviceProtocol.flutterDebugDumpApp(serviceProtocol.firstIsolateId);
+    await currentView.uiIsolate.flutterDebugDumpApp();
   }
 
   Future<Null> _debugDumpRenderTree() async {
-    await serviceProtocol.flutterDebugDumpRenderTree(serviceProtocol.firstIsolateId);
+    await currentView.uiIsolate.flutterDebugDumpRenderTree();
   }
 
   void registerSignalHandlers() {
@@ -90,22 +89,23 @@
     if (!debuggingOptions.debuggingEnabled) {
       return new Future<Null>.error('Error the service protocol is not enabled.');
     }
-    serviceProtocol = await VMService.connect(port);
+    vmService = await VMService.connect(port);
     printTrace('Connected to service protocol on port $port');
-    serviceProtocol.populateIsolateInfo();
-    serviceProtocol.onExtensionEvent.listen((Event event) {
+    await vmService.getVM();
+    vmService.onExtensionEvent.listen((ServiceEvent event) {
       printTrace(event.toString());
     });
-    serviceProtocol.onIsolateEvent.listen((Event event) {
+    vmService.onIsolateEvent.listen((ServiceEvent event) {
       printTrace(event.toString());
     });
 
-    // Setup view manager and refresh the view list.
-    viewManager = new ViewManager(serviceProtocol);
-    await viewManager.refresh();
+    // Refresh the view list.
+    await vmService.vm.refreshViews();
+    currentView = vmService.vm.mainView;
+    assert(currentView != null);
 
     // Listen for service protocol connection to close.
-    serviceProtocol.done.whenComplete(() {
+    vmService.done.whenComplete(() {
       appFinished();
     });
   }
@@ -175,9 +175,10 @@
   Future<Null> preStop() async { }
 
   Future<Null> stopApp() async {
-    if (serviceProtocol != null && !serviceProtocol.isClosed) {
-      if (serviceProtocol.isolates.isNotEmpty) {
-        serviceProtocol.flutterExit(serviceProtocol.firstIsolateId);
+    if (vmService != null && !vmService.isClosed) {
+      if ((currentView != null) && (currentView.uiIsolate != null)) {
+        // TODO(johnmccutchan): Wait for the exit command to complete.
+        currentView.uiIsolate.flutterExit();
         await new Future<Null>.delayed(new Duration(milliseconds: 100));
       }
     }
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index a3a5ca9..9b0a35e 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -59,17 +59,17 @@
 
   @override
   Future<bool> restart({ bool fullRestart: false }) async {
-    if (serviceProtocol == null) {
+    if (vmService == null) {
       printError('Debugging is not enabled.');
       return false;
     } else {
       Status status = logger.startProgress('Re-starting application...');
 
-      Future<Event> extensionAddedEvent;
+      Future<ServiceEvent> extensionAddedEvent;
 
       if (device.restartSendsFrameworkInitEvent) {
-        extensionAddedEvent = serviceProtocol.onExtensionEvent
-          .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization')
+        extensionAddedEvent = vmService.onExtensionEvent
+          .where((ServiceEvent event) => event.extensionKind == 'Flutter.FrameworkInitialization')
           .first;
       }
 
@@ -77,7 +77,7 @@
         _package,
         _result,
         mainPath: _mainPath,
-        observatory: serviceProtocol
+        observatory: vmService
       );
 
       status.stop(showElapsedTime: true);
@@ -178,16 +178,20 @@
     if (debuggingOptions.debuggingEnabled) {
       await connectToServiceProtocol(_result.observatoryPort);
 
-      if (benchmark)
-        await serviceProtocol.waitFirstIsolate;
+      if (benchmark) {
+        await vmService.getVM();
+      }
     }
 
     printStatus('Application running.');
 
-    if (serviceProtocol != null && traceStartup) {
+    await vmService.vm.refreshViews();
+    printStatus('Connected to view \'${vmService.vm.mainView}\'.');
+
+    if (vmService != null && traceStartup) {
       printStatus('Downloading startup trace info...');
       try {
-        await downloadStartupTrace(serviceProtocol);
+        await downloadStartupTrace(vmService);
       } catch(error) {
         printError(error);
         return 2;
diff --git a/packages/flutter_tools/lib/src/view.dart b/packages/flutter_tools/lib/src/view.dart
deleted file mode 100644
index f425fb8..0000000
--- a/packages/flutter_tools/lib/src/view.dart
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-
-import 'globals.dart';
-import 'vmservice.dart';
-
-/// Peered to a Android/iOS FlutterView widget on a device.
-class FlutterView {
-  FlutterView(this.viewId, this.viewManager);
-
-  final String viewId;
-  final ViewManager viewManager;
-  String _uiIsolateId;
-  String get uiIsolateId => _uiIsolateId;
-
-  Future<Null> runFromSource(String entryPath,
-                             String packagesPath,
-                             String assetsDirectoryPath) async {
-    return viewManager._runFromSource(this,
-                                      entryPath,
-                                      packagesPath,
-                                      assetsDirectoryPath);
-  }
-
-  @override
-  String toString() => viewId;
-
-  @override
-  bool operator ==(FlutterView other) {
-    return other.viewId == viewId;
-  }
-
-  @override
-  int get hashCode => viewId.hashCode;
-}
-
-/// Manager of FlutterViews.
-class ViewManager {
-  ViewManager(this.serviceProtocol);
-
-  final VMService serviceProtocol;
-
-  Future<Null> refresh() async {
-    List<Map<String, String>> viewList = await serviceProtocol.getViewList();
-    for (Map<String, String> viewDescription in viewList) {
-      FlutterView view = new FlutterView(viewDescription['id'], this);
-      if (!views.contains(view)) {
-        // Canonicalize views against the view set.
-        views.add(view);
-      }
-    }
-  }
-
-  // TODO(johnmccutchan): Report errors when running failed.
-  Future<Null> _runFromSource(FlutterView view,
-                              String entryPath,
-                              String packagesPath,
-                              String assetsDirectoryPath) async {
-    final String viewId = await serviceProtocol.getFirstViewId();
-    // When this completer completes the isolate is running.
-    final Completer<Null> completer = new Completer<Null>();
-    final StreamSubscription<Event> subscription =
-      serviceProtocol.onIsolateEvent.listen((Event event) {
-      // TODO(johnmccutchan): Listen to the debug stream and catch initial
-      // launch errors.
-      if (event.kind == 'IsolateRunnable') {
-        printTrace('Isolate is runnable.');
-        completer.complete(null);
-      }
-    });
-    await serviceProtocol.runInView(viewId,
-                                   entryPath,
-                                   packagesPath,
-                                   assetsDirectoryPath);
-    await completer.future;
-    await subscription.cancel();
-  }
-
-  // TODO(johnmccutchan): Remove this accessor and make the runner multi-view
-  // aware.
-  FlutterView get mainView {
-    return views.first;
-  }
-
-  final Set<FlutterView> views = new Set<FlutterView>();
-}
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 569a35e..de019fc 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -11,23 +11,17 @@
 
 import 'globals.dart';
 
-// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService.
+/// A connection to the Dart VM Service.
 class VMService {
   VMService._(this.peer, this.port, this.httpAddress) {
+    _vm = new VM._empty(this);
+
     peer.registerMethod('streamNotify', (rpc.Parameters event) {
       _handleStreamNotify(event.asMap);
     });
-
-    onIsolateEvent.listen((Event event) {
-      if (event.kind == 'IsolateStart') {
-        _addIsolate(event.isolate);
-      } else if (event.kind == 'IsolateExit') {
-        String removedId = event.isolate.id;
-        isolates.removeWhere((IsolateRef ref) => ref.id == removedId);
-      }
-    });
   }
 
+  /// Connect to '127.0.0.1' at [port].
   static Future<VMService> connect(int port) async {
     Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: port, path: 'ws');
     WebSocket ws = await WebSocket.connect(uri.toString());
@@ -37,158 +31,572 @@
     return new VMService._(peer, port, httpAddress);
   }
   final Uri httpAddress;
-  final rpc.Peer peer;
   final int port;
+  final rpc.Peer peer;
 
-  List<IsolateRef> isolates = <IsolateRef>[];
-  Completer<IsolateRef> _waitFirstIsolateCompleter;
+  VM _vm;
+  /// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects.
+  VM get vm => _vm;
 
-  Map<String, StreamController<Event>> _eventControllers = <String, StreamController<Event>>{};
+  final Map<String, StreamController<ServiceEvent>> _eventControllers =
+      <String, StreamController<ServiceEvent>>{};
 
   Set<String> _listeningFor = new Set<String>();
 
   bool get isClosed => peer.isClosed;
   Future<Null> get done => peer.done;
 
-  String get firstIsolateId => isolates.isEmpty ? null : isolates.first.id;
-
   // Events
-
-  Stream<Event> get onExtensionEvent => onEvent('Extension');
+  Stream<ServiceEvent> get onDebugEvent => onEvent('Debug');
+  Stream<ServiceEvent> get onExtensionEvent => onEvent('Extension');
   // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded
-  Stream<Event> get onIsolateEvent => onEvent('Isolate');
-  Stream<Event> get onTimelineEvent => onEvent('Timeline');
+  Stream<ServiceEvent> get onIsolateEvent => onEvent('Isolate');
+  Stream<ServiceEvent> get onTimelineEvent => onEvent('Timeline');
+  // TODO(johnmccutchan): Add FlutterView events.
 
   // Listen for a specific event name.
-  Stream<Event> onEvent(String streamId) {
-    streamListen(streamId);
+  Stream<ServiceEvent> onEvent(String streamId) {
+    _streamListen(streamId);
     return _getEventController(streamId).stream;
   }
 
-  StreamController<Event> _getEventController(String eventName) {
-    StreamController<Event> controller = _eventControllers[eventName];
+  StreamController<ServiceEvent> _getEventController(String eventName) {
+    StreamController<ServiceEvent> controller = _eventControllers[eventName];
     if (controller == null) {
-      controller = new StreamController<Event>.broadcast();
+      controller = new StreamController<ServiceEvent>.broadcast();
       _eventControllers[eventName] = controller;
     }
     return controller;
   }
 
   void _handleStreamNotify(Map<String, dynamic> data) {
-    Event event = new Event(data['event']);
-    _getEventController(data['streamId']).add(event);
+    final String streamId = data['streamId'];
+    final Map<String, dynamic> eventData = data['event'];
+    final Map<String, dynamic> eventIsolate = eventData['isolate'];
+    ServiceEvent event;
+    if (eventIsolate != null) {
+      // getFromMap creates the Isolate if necessary.
+      Isolate isolate = vm.getFromMap(eventIsolate);
+      event = new ServiceObject._fromMap(isolate, eventData);
+      if (event.kind == ServiceEvent.kIsolateExit) {
+        vm._isolateCache.remove(isolate.id);
+        vm._buildIsolateList();
+      } else if (event.kind == ServiceEvent.kIsolateRunnable) {
+        // Force reload once the isolate becomes runnable so that we
+        // update the root library.
+        isolate.reload();
+      }
+    } else {
+      // The event doesn't have an isolate, so it is owned by the VM.
+      event = new ServiceObject._fromMap(vm, eventData);
+    }
+    _getEventController(streamId).add(event);
   }
 
-  Future<Null> populateIsolateInfo() async {
-    // Calling this has the side effect of populating the isolate information.
-    await waitFirstIsolate;
-  }
-
-  Future<IsolateRef> get waitFirstIsolate async {
-    if (isolates.isNotEmpty)
-      return isolates.first;
-
-    _waitFirstIsolateCompleter ??= new Completer<IsolateRef>();
-
-    getVM().then((VM vm) {
-      for (IsolateRef isolate in vm.isolates)
-        _addIsolate(isolate);
-    });
-
-    return _waitFirstIsolateCompleter.future;
-  }
-
-  // Requests
-
-  Future<Response> sendRequest(String method, [Map<String, dynamic> args]) {
-    return peer.sendRequest(method, args).then((dynamic result) => new Response(result));
-  }
-
-  Future<Null> streamListen(String streamId) async {
+  Future<Null> _streamListen(String streamId) async {
     if (!_listeningFor.contains(streamId)) {
       _listeningFor.add(streamId);
-      sendRequest('streamListen', <String, dynamic>{ 'streamId': streamId });
+      await peer.sendRequest('streamListen',
+                             <String, dynamic>{ 'streamId': streamId });
     }
   }
 
+  /// Reloads the VM.
   Future<VM> getVM() {
-    return peer.sendRequest('getVM').then((dynamic result) {
-      return new VM(result);
-    });
+    return _vm.reload();
+  }
+}
+
+/// An error that is thrown when constructing/updating a service object.
+class VMServiceObjectLoadError {
+  VMServiceObjectLoadError(this.message, this.map);
+  final String message;
+  final Map<String, dynamic> map;
+}
+
+bool _isServiceMap(Map<String, dynamic> m) {
+  return (m != null) && (m['type'] != null);
+}
+bool _hasRef(String type) => (type != null) && type.startsWith('@');
+String _stripRef(String type) => (_hasRef(type) ? type.substring(1) : type);
+
+/// Given a raw response from the service protocol and a [ServiceObjectOwner],
+/// recursively walk the response and replace values that are service maps with
+/// actual [ServiceObject]s. During the upgrade the owner is given a chance
+/// to return a cached / canonicalized object.
+void _upgradeCollection(dynamic collection,
+                        ServiceObjectOwner owner) {
+  if (collection is ServiceMap) {
+    return;
+  }
+  if (collection is Map) {
+    _upgradeMap(collection, owner);
+  } else if (collection is List) {
+    _upgradeList(collection, owner);
+  }
+}
+
+void _upgradeMap(Map<String, dynamic> map, ServiceObjectOwner owner) {
+  map.forEach((String k, dynamic v) {
+    if ((v is Map) && _isServiceMap(v)) {
+      map[k] = owner.getFromMap(v);
+    } else if (v is List) {
+      _upgradeList(v, owner);
+    } else if (v is Map) {
+      _upgradeMap(v, owner);
+    }
+  });
+}
+
+void _upgradeList(List<dynamic> list, ServiceObjectOwner owner) {
+  for (int i = 0; i < list.length; i++) {
+    dynamic v = list[i];
+    if ((v is Map) && _isServiceMap(v)) {
+      list[i] = owner.getFromMap(v);
+    } else if (v is List) {
+      _upgradeList(v, owner);
+    } else if (v is Map) {
+      _upgradeMap(v, owner);
+    }
+  }
+}
+
+/// Base class of all objects received over the service protocol.
+abstract class ServiceObject {
+  ServiceObject._empty(this._owner);
+
+  /// Factory constructor given a [ServiceObjectOwner] and a service map,
+  /// upgrade the map into a proper [ServiceObject]. This function always
+  /// returns a new instance and does not interact with caches.
+  factory ServiceObject._fromMap(ServiceObjectOwner owner,
+                                 Map<String, dynamic> map) {
+    if (map == null)
+      return null;
+
+    if (!_isServiceMap(map))
+      throw new VMServiceObjectLoadError("Expected a service map", map);
+
+    String type = _stripRef(map['type']);
+
+    ServiceObject serviceObject;
+    switch (type) {
+      case 'Event':
+        serviceObject = new ServiceEvent._empty(owner);
+      break;
+      case 'FlutterView':
+        serviceObject = new FlutterView._empty(owner.vm);
+      break;
+      case 'Isolate':
+        serviceObject = new Isolate._empty(owner.vm);
+      break;
+      default:
+        printTrace("Unsupported service object type: $type");
+    }
+    if (serviceObject == null) {
+      // If we don't have a model object for this service object type, as a
+      // fallback return a ServiceMap object.
+      serviceObject = new ServiceMap._empty(owner);
+    }
+    // We have now constructed an emtpy service object, call update to
+    // populate it.
+    serviceObject.update(map);
+    return serviceObject;
   }
 
-  Future<Map<String, dynamic>> reloadSources(String isolateId) async {
-    try {
-      Response response =
-          await sendRequest('_reloadSources',
-                            <String, dynamic>{ 'isolateId': isolateId });
-      return response.response;
-    } catch (e) {
-      return new Future<Map<String, dynamic>>.error(e.data['details']);
+  final ServiceObjectOwner _owner;
+  ServiceObjectOwner get owner => _owner;
+
+  /// The id of this object.
+  String get id => _id;
+  String _id;
+
+  /// The user-level type of this object.
+  String get type => _type;
+  String _type;
+
+  /// The vm-level type of this object. Usually the same as [type].
+  String get vmType => _vmType;
+  String _vmType;
+
+  /// Is it safe to cache this object?
+  bool _canCache = false;
+  bool get canCache => _canCache;
+
+  /// Has this object been fully loaded?
+  bool get loaded => _loaded;
+  bool _loaded = false;
+
+  /// Is this object immutable after it is [loaded]?
+  bool get immutable => false;
+
+  String get name => _name;
+  String _name;
+
+  String get vmName => _vmName;
+  String _vmName;
+
+  /// If this is not already loaded, load it. Otherwise reload.
+  Future<ServiceObject> load() async {
+    if (loaded) {
+      return this;
+    }
+    return reload();
+  }
+
+  /// Fetch this object from vmService and return the response directly.
+  Future<Map<String, dynamic>> _fetchDirect() {
+    Map<String, dynamic> params = <String, dynamic>{
+      'objectId': id,
+    };
+    return _owner.isolate.invokeRpcRaw('getObject', params);
+  }
+
+  Future<ServiceObject> _inProgressReload;
+  /// Reload the service object (if possible).
+  Future<ServiceObject> reload() async {
+    bool hasId = (id != null) && (id != '');
+    bool isVM = this is VM;
+    // We should always reload the VM.
+    // We can't reload objects without an id.
+    // We shouldn't reload an immutable and already loaded object.
+    bool skipLoad = !isVM && (!hasId || (immutable && loaded));
+    if (skipLoad) {
+      return this;
+    }
+
+    if (_inProgressReload == null) {
+      Completer<ServiceObject> completer = new Completer<ServiceObject>();
+      _inProgressReload = completer.future;
+
+      try {
+        Map<String, dynamic> response = await _fetchDirect();
+        if (_stripRef(response['type']) == 'Sentinel') {
+          // An object may have been collected.
+          completer.complete(new ServiceObject._fromMap(owner, response));
+        } else {
+          update(response);
+          completer.complete(this);
+        }
+      } catch (e, st) {
+        completer.completeError(e, st);
+      }
+      _inProgressReload = null;
+    }
+
+    return _inProgressReload;
+  }
+
+  /// Update [this] using [map] as a source. [map] can be a service reference.
+  void update(Map<String, dynamic> map) {
+    // Don't allow the type to change on an object update.
+    final bool mapIsRef = _hasRef(map['type']);
+    final String mapType = _stripRef(map['type']);
+
+    if ((_type != null) && (_type != mapType)) {
+      throw new VMServiceObjectLoadError("ServiceObject types must not change",
+                                         map);
+    }
+    _type = mapType;
+    _vmType = map.containsKey('_vmType') ? _stripRef(map['_vmType']) : _type;
+
+    _canCache = map['fixedId'] == true;
+    if ((_id != null) && (_id != map['id']) && _canCache) {
+      throw new VMServiceObjectLoadError("ServiceObject id changed", map);
+    }
+    _id = map['id'];
+
+    // Copy name properties.
+    _name = map['name'];
+    _vmName = map.containsKey('_vmName') ? map['_vmName'] : _name;
+
+    // We have now updated all common properties, let the subclasses update
+    // their specific properties.
+    _update(map, mapIsRef);
+  }
+
+  /// Implemented by subclasses to populate their model.
+  void _update(Map<String, dynamic> map, bool mapIsRef);
+}
+
+class ServiceEvent extends ServiceObject {
+  /// The possible 'kind' values.
+  static const String kVMUpdate               = 'VMUpdate';
+  static const String kIsolateStart           = 'IsolateStart';
+  static const String kIsolateRunnable        = 'IsolateRunnable';
+  static const String kIsolateExit            = 'IsolateExit';
+  static const String kIsolateUpdate          = 'IsolateUpdate';
+  static const String kIsolateReload          = 'IsolateReload';
+  static const String kIsolateSpawn           = 'IsolateSpawn';
+  static const String kServiceExtensionAdded  = 'ServiceExtensionAdded';
+  static const String kPauseStart             = 'PauseStart';
+  static const String kPauseExit              = 'PauseExit';
+  static const String kPauseBreakpoint        = 'PauseBreakpoint';
+  static const String kPauseInterrupted       = 'PauseInterrupted';
+  static const String kPauseException         = 'PauseException';
+  static const String kNone                   = 'None';
+  static const String kResume                 = 'Resume';
+  static const String kBreakpointAdded        = 'BreakpointAdded';
+  static const String kBreakpointResolved     = 'BreakpointResolved';
+  static const String kBreakpointRemoved      = 'BreakpointRemoved';
+  static const String kGraph                  = '_Graph';
+  static const String kGC                     = 'GC';
+  static const String kInspect                = 'Inspect';
+  static const String kDebuggerSettingsUpdate = '_DebuggerSettingsUpdate';
+  static const String kConnectionClosed       = 'ConnectionClosed';
+  static const String kLogging                = '_Logging';
+  static const String kExtension              = 'Extension';
+
+  ServiceEvent._empty(ServiceObjectOwner owner) : super._empty(owner);
+
+  String _kind;
+  String get kind => _kind;
+  DateTime _timestamp;
+  DateTime get timestmap => _timestamp;
+  String _extensionKind;
+  String get extensionKind => _extensionKind;
+  Map<String, dynamic> _extensionData;
+  Map<String, dynamic> get extensionData => _extensionData;
+  List<Map<String, dynamic>> _timelineEvents;
+  List<Map<String, dynamic>> get timelineEvents => _timelineEvents;
+
+  @override
+  void _update(Map<String, dynamic> map, bool mapIsRef) {
+    _loaded = true;
+    _upgradeCollection(map, owner);
+    _kind = map['kind'];
+    assert(map['isolate'] == null || owner == map['isolate']);
+    _timestamp =
+        new DateTime.fromMillisecondsSinceEpoch(map['timestamp']);
+    if (map['extensionKind'] != null) {
+      _extensionKind = map['extensionKind'];
+      _extensionData = map['extensionData'];
+    }
+    _timelineEvents = map['timelineEvents'];
+  }
+
+  bool get isPauseEvent {
+    return (kind == kPauseStart ||
+            kind == kPauseExit ||
+            kind == kPauseBreakpoint ||
+            kind == kPauseInterrupted ||
+            kind == kPauseException ||
+            kind == kNone);
+  }
+}
+
+/// A ServiceObjectOwner is either a [VM] or an [Isolate]. Owners can cache
+/// and/or canonicalize service objets received over the wire.
+abstract class ServiceObjectOwner extends ServiceObject {
+  ServiceObjectOwner._empty(ServiceObjectOwner owner) : super._empty(owner);
+
+  /// Returns the owning VM.
+  VM get vm => null;
+
+  /// Returns the owning isolate (if any).
+  Isolate get isolate => null;
+
+  /// Returns the vmService connection.
+  VMService get vmService => null;
+
+  /// Builds a [ServiceObject] corresponding to the [id] from [map].
+  /// The result may come from the cache.  The result will not necessarily
+  /// be [loaded].
+  ServiceObject getFromMap(Map<String, dynamic> map);
+}
+
+/// There is only one instance of the VM class. The VM class owns [Isolate]
+/// and [FlutterView] objects.
+class VM extends ServiceObjectOwner {
+  VM._empty(this._vmService) : super._empty(null);
+
+  /// Connection to the VMService.
+  final VMService _vmService;
+  @override
+  VMService get vmService => _vmService;
+
+  @override
+  VM get vm => this;
+
+  @override
+  Future<Map<String, dynamic>> _fetchDirect() async {
+    return invokeRpcRaw('getVM', <String, dynamic> {});
+  }
+
+  @override
+  void _update(Map<String, dynamic> map, bool mapIsRef) {
+    if (mapIsRef)
+      return;
+
+    // Upgrade the collection. A side effect of this call is that any new
+    // isolates in the map are created and added to the isolate cache.
+    _upgradeCollection(map, this);
+    _loaded = true;
+
+    // TODO(johnmccutchan): Extract any properties we care about here.
+
+    // Remove any isolates which are now dead from the isolate cache.
+    _removeDeadIsolates(map['isolates']);
+  }
+
+  final Map<String, ServiceObject> _cache = new Map<String,ServiceObject>();
+  final Map<String,Isolate> _isolateCache = new Map<String,Isolate>();
+
+  /// The list of live isolates, ordered by isolate start time.
+  final List<Isolate> isolates = new List<Isolate>();
+
+  /// The set of live views.
+  final Map<String, FlutterView> _viewCache = new Map<String, FlutterView>();
+
+  int _compareIsolates(Isolate a, Isolate b) {
+    DateTime aStart = a.startTime;
+    DateTime bStart = b.startTime;
+    if (aStart == null) {
+      if (bStart == null) {
+        return 0;
+      } else {
+        return 1;
+      }
+    }
+    if (bStart == null) {
+      return -1;
+    }
+    return aStart.compareTo(bStart);
+  }
+
+  void _buildIsolateList() {
+    List<Isolate> isolateList = _isolateCache.values.toList();
+    isolateList.sort(_compareIsolates);
+    isolates.clear();
+    isolates.addAll(isolateList);
+  }
+
+  void _removeDeadIsolates(List<Isolate> newIsolates) {
+    // Build a set of new isolates.
+    Set<String> newIsolateSet = new Set<String>();
+    newIsolates.forEach((Isolate iso) => newIsolateSet.add(iso.id));
+
+    // Remove any old isolates which no longer exist.
+    List<String> toRemove = <String>[];
+    _isolateCache.forEach((String id, _) {
+      if (!newIsolateSet.contains(id)) {
+        toRemove.add(id);
+      }
+    });
+    toRemove.forEach((String id) => _isolateCache.remove(id));
+    _buildIsolateList();
+  }
+
+  @override
+  ServiceObject getFromMap(Map<String, dynamic> map) {
+    if (map == null) {
+      return null;
+    }
+    String type = _stripRef(map['type']);
+    if (type == 'VM') {
+      // Update this VM object.
+      update(map);
+      return this;
+    }
+
+    String mapId = map['id'];
+
+    switch (type) {
+      case 'Isolate': {
+        // Check cache.
+        Isolate isolate = _isolateCache[mapId];
+        if (isolate == null) {
+          // Add new isolate to the cache.
+          isolate = new ServiceObject._fromMap(this, map);
+          _isolateCache[mapId] = isolate;
+          _buildIsolateList();
+
+          // Eagerly load the isolate.
+          isolate.load().catchError((dynamic e, StackTrace stack) {
+            printTrace('Eagerly loading an isolate failed: $e\n$stack');
+          });
+        } else {
+          // Existing isolate, update data.
+          isolate.update(map);
+        }
+        return isolate;
+      }
+      break;
+      case 'FlutterView': {
+        FlutterView view = _viewCache[mapId];
+        if (view == null) {
+          // Add new view to the cache.
+          view = new ServiceObject._fromMap(this, map);
+          _viewCache[mapId] = view;
+        } else {
+          view.update(map);
+        }
+        return view;
+      }
+      break;
+      default:
+        throw new VMServiceObjectLoadError(
+            'VM.getFromMap called for something other than an isolate', map);
     }
   }
 
-  Future<List<Map<String, String>>> getViewList() async {
-    Map<String, dynamic> response = await peer.sendRequest('_flutter.listViews');
-    List<Map<String, String>> views = response['views'];
-    return views;
+  // Note that this function does not reload the isolate if it found
+  // in the cache.
+  Future<Isolate> getIsolate(String isolateId) {
+    if (!loaded) {
+      // Trigger a VM load, then get the isolate. Ignore any errors.
+      return load().then((_) => getIsolate(isolateId)).catchError((_) => null);
+    }
+    return new Future<Isolate>.value(_isolateCache[isolateId]);
   }
 
-  Future<String> getFirstViewId() async {
-    List<Map<String, String>> views = await getViewList();
-    return views[0]['id'];
+  /// Invoke the RPC and return the raw response.
+  Future<Map<String, dynamic>> invokeRpcRaw(
+      String method, [Map<String, dynamic> params]) async {
+    if (params == null) {
+      params = <String, dynamic>{};
+    }
+    Map<String, dynamic> result =
+        await _vmService.peer.sendRequest(method, params);
+    return result;
   }
 
-  Future<Null> runInView(String viewId,
-                         String main,
-                         String packages,
-                         String assetsDirectory) async {
-    await peer.sendRequest('_flutter.runInView',
-                           <String, dynamic> {
-                             'viewId': viewId,
-                             'mainScript': main,
-                             'packagesFile': packages,
-                             'assetDirectory': assetsDirectory
-                           });
-    return null;
+  /// Invoke the RPC and return a ServiceObject response.
+  Future<ServiceObject> invokeRpc(
+      String method, [Map<String, dynamic> params]) async {
+    Map<String, dynamic> response = await invokeRpcRaw(method, params);
+    ServiceObject serviceObject = new ServiceObject._fromMap(this, response);
+    if ((serviceObject != null) && (serviceObject._canCache)) {
+      String serviceObjectId = serviceObject.id;
+      _cache.putIfAbsent(serviceObjectId, () => serviceObject);
+    }
+    return serviceObject;
   }
 
-  Future<Response> clearVMTimeline() => sendRequest('_clearVMTimeline');
-
-  Future<Response> setVMTimelineFlags(List<String> recordedStreams) {
-    assert(recordedStreams != null);
-
-    return sendRequest('_setVMTimelineFlags', <String, dynamic> {
-      'recordedStreams': recordedStreams
-    });
+  /// Create a new development file system on the device.
+  Future<Map<String, dynamic>> createDevFS(String fsName) async {
+    Map<String, dynamic> response =
+        await invokeRpcRaw('_createDevFS', <String, dynamic> {
+                           'fsName': fsName
+                         });
+    return response;
   }
 
-  Future<Response> getVMTimeline() => sendRequest('_getVMTimeline');
-
-  // DevFS / VM virtual file system methods
-
-  /// Create a new file system.
-  Future<CreateDevFSResponse> createDevFS(String fsName) async {
-    Response response = await sendRequest('_createDevFS', <String, dynamic> { 'fsName': fsName });
-    return new CreateDevFSResponse(response.response);
-  }
-
-  /// List the available file systems.
-  Future<List<String>> listDevFS() {
-    return sendRequest('_listDevFS').then((Response response) {
-      return response.response['fsNames'];
-    });
+  /// List the development file system son the device.
+  Future<List<String>> listDevFS() async {
+    Map<String, dynamic> response =
+        await invokeRpcRaw('_listDevFS', <String, dynamic>{});
+    return response['fsNames'];
   }
 
   // Write one file into a file system.
-  Future<Response> writeDevFSFile(String fsName, {
+  Future<Map<String, dynamic>> writeDevFSFile(String fsName, {
     String path,
     List<int> fileContents
   }) {
     assert(path != null);
     assert(fileContents != null);
 
-    return sendRequest('_writeDevFSFile', <String, dynamic> {
+    return invokeRpcRaw('_writeDevFSFile', <String, dynamic> {
       'fsName': fsName,
       'path': path,
       'fileContents': BASE64.encode(fileContents)
@@ -197,159 +605,292 @@
 
   // Read one file from a file system.
   Future<List<int>> readDevFSFile(String fsName, String path) {
-    return sendRequest('_readDevFSFile', <String, dynamic> {
+    return invokeRpcRaw('_readDevFSFile', <String, dynamic> {
       'fsName': fsName,
       'path': path
-    }).then((Response response) {
-      return BASE64.decode(response.response['fileContents']);
+    }).then((Map<String, dynamic> response) {
+      return BASE64.decode(response['fileContents']);
     });
   }
 
   /// The complete list of a file system.
   Future<List<String>> listDevFSFiles(String fsName) {
-    return sendRequest('_listDevFSFiles', <String, dynamic> {
+    return invokeRpcRaw('_listDevFSFiles', <String, dynamic> {
       'fsName': fsName
-    }).then((Response response) {
-      return response.response['files'];
+    }).then((Map<String, dynamic> response) {
+      return response['files'];
     });
   }
 
   /// Delete an existing file system.
-  Future<Response> deleteDevFS(String fsName) {
-    return sendRequest('_deleteDevFS', <String, dynamic> { 'fsName': fsName });
+  Future<Map<String, dynamic>> deleteDevFS(String fsName) {
+    return invokeRpcRaw('_deleteDevFS',  <String, dynamic> { 'fsName': fsName });
+  }
+
+  Future<ServiceMap> runInView(String viewId,
+                               String main,
+                               String packages,
+                               String assetsDirectory) {
+    return invokeRpc('_flutter.runInView',
+                    <String, dynamic> {
+                      'viewId': viewId,
+                      'mainScript': main,
+                      'packagesFile': packages,
+                      'assetDirectory': assetsDirectory
+                    });
+  }
+
+  Future<Map<String, dynamic>> clearVMTimeline() {
+    return invokeRpcRaw('_clearVMTimeline', <String, dynamic>{});
+  }
+
+  Future<Map<String, dynamic>> setVMTimelineFlags(
+      List<String> recordedStreams) {
+    assert(recordedStreams != null);
+
+    return invokeRpcRaw('_setVMTimelineFlags', <String, dynamic> {
+      'recordedStreams': recordedStreams
+    });
+  }
+
+  Future<Map<String, dynamic>> getVMTimeline() {
+    return invokeRpcRaw('_getVMTimeline', <String, dynamic> {});
+  }
+
+  Future<Null> refreshViews() async {
+    await vmService.vm.invokeRpc('_flutter.listViews');
+  }
+
+  FlutterView get mainView {
+    return _viewCache.values.first;
+  }
+}
+
+/// An isolate running inside the VM. Instances of the Isolate class are always
+/// canonicalized.
+class Isolate extends ServiceObjectOwner {
+  Isolate._empty(ServiceObjectOwner owner) : super._empty(owner);
+
+  @override
+  VM get vm => owner;
+
+  @override
+  VMService get vmService => vm.vmService;
+
+  @override
+  Isolate get isolate => this;
+
+  DateTime startTime;
+
+  final Map<String, ServiceObject> _cache = new Map<String, ServiceObject>();
+
+  @override
+  ServiceObject getFromMap(Map<String, dynamic> map) {
+    if (map == null) {
+      return null;
+    }
+    String mapType = _stripRef(map['type']);
+    if (mapType == 'Isolate') {
+      // There are sometimes isolate refs in ServiceEvents.
+      return vm.getFromMap(map);
+    }
+
+    String mapId = map['id'];
+    ServiceObject serviceObject = (mapId != null) ? _cache[mapId] : null;
+    if (serviceObject != null) {
+      serviceObject.update(map);
+      return serviceObject;
+    }
+    // Build the object from the map directly.
+    serviceObject = new ServiceObject._fromMap(this, map);
+    if ((serviceObject != null) && serviceObject.canCache) {
+      _cache[mapId] = serviceObject;
+    }
+    return serviceObject;
+  }
+
+  @override
+  Future<Map<String, dynamic>> _fetchDirect() {
+    return invokeRpcRaw('getIsolate', <String, dynamic>{});
+  }
+
+  /// Invoke the RPC and return the raw response.
+  Future<Map<String, dynamic>> invokeRpcRaw(
+      String method, [Map<String, dynamic> params]) {
+    // Inject the 'isolateId' parameter.
+    if (params == null) {
+      params = <String, dynamic>{
+        'isolateId': id
+      };
+    } else {
+      params['isolateId'] = id;
+    }
+    return vm.invokeRpcRaw(method, params);
+  }
+
+  /// Invoke the RPC and return a ServiceObject response.
+  Future<ServiceObject> invokeRpc(
+      String method, Map<String, dynamic> params) async {
+    Map<String, dynamic> response = await invokeRpcRaw(method, params);
+    return getFromMap(response);
+  }
+
+  @override
+  void _update(Map<String, dynamic> map, bool mapIsRef) {
+    if (mapIsRef) {
+      return;
+    }
+    _loaded = true;
+
+    int startTimeMillis = map['startTime'];
+    startTime = new DateTime.fromMillisecondsSinceEpoch(startTimeMillis);
+
+    // TODO(johnmccutchan): Extract any properties we care about here.
+    _upgradeCollection(map, this);
+  }
+
+  Future<Map<String, dynamic>> reloadSources() async {
+    try {
+      Map<String, dynamic> response = await invokeRpcRaw('_reloadSources');
+      return response;
+    } catch (e) {
+      return new Future<Map<String, dynamic>>.error(e.data['details']);
+    }
   }
 
   // Flutter extension methods.
 
-  Future<Response> flutterDebugDumpApp(String isolateId) {
-    return peer.sendRequest('ext.flutter.debugDumpApp', <String, dynamic>{
-      'isolateId': isolateId
-    }).then((dynamic result) => new Response(result));
+  Future<Map<String, dynamic>> flutterDebugDumpApp() {
+    return invokeRpcRaw('ext.flutter.debugDumpApp');
   }
 
-  Future<Response> flutterDebugDumpRenderTree(String isolateId) {
-    return peer.sendRequest('ext.flutter.debugDumpRenderTree', <String, dynamic>{
-      'isolateId': isolateId
-    }).then((dynamic result) => new Response(result));
+  Future<Map<String, dynamic>> flutterDebugDumpRenderTree() {
+    return invokeRpcRaw('ext.flutter.debugDumpRenderTree');
   }
 
   // Loader page extension methods.
 
-  Future<Response> flutterLoaderShowMessage(String isolateId, String message) {
-    return peer.sendRequest('ext.flutter.loaderShowMessage', <String, dynamic>{
-      'isolateId': isolateId,
+  Future<Map<String, dynamic>> flutterLoaderShowMessage(String message) {
+    return invokeRpcRaw('ext.flutter.loaderShowMessage', <String, dynamic> {
       'value': message
-    }).then(
-      (dynamic result) => new Response(result),
-      onError: (dynamic exception) { printTrace('ext.flutter.loaderShowMessage: $exception'); }
-    );
+    });
   }
 
-  Future<Response> flutterLoaderSetProgress(String isolateId, double progress) {
-    return peer.sendRequest('ext.flutter.loaderSetProgress', <String, dynamic>{
-      'isolateId': isolateId,
+  Future<Map<String, dynamic>> flutterLoaderSetProgress(double progress) {
+    return invokeRpcRaw('ext.flutter.loaderSetProgress', <String, dynamic>{
       'loaderSetProgress': progress
-    }).then(
-      (dynamic result) => new Response(result),
-      onError: (dynamic exception) { printTrace('ext.flutter.loaderSetProgress: $exception'); }
-    );
+    });
   }
 
-  Future<Response> flutterLoaderSetProgressMax(String isolateId, double max) {
-    return peer.sendRequest('ext.flutter.loaderSetProgressMax', <String, dynamic>{
-      'isolateId': isolateId,
+  Future<Map<String, dynamic>> flutterLoaderSetProgressMax(double max) {
+    return invokeRpcRaw('ext.flutter.loaderSetProgressMax', <String, dynamic>{
       'loaderSetProgressMax': max
-    }).then(
-      (dynamic result) => new Response(result),
-      onError: (dynamic exception) { printTrace('ext.flutter.loaderSetProgressMax: $exception'); }
-    );
+    });
   }
 
   /// Causes the application to pick up any changed code.
-  Future<Response> flutterReassemble(String isolateId) {
-    return peer.sendRequest('ext.flutter.reassemble', <String, dynamic>{
-      'isolateId': isolateId
-    }).then((dynamic result) => new Response(result));
+  Future<Map<String, dynamic>> flutterReassemble() {
+    return invokeRpcRaw('ext.flutter.reassemble');
   }
 
-  Future<Response> flutterEvictAsset(String isolateId, String assetPath) {
-    return peer.sendRequest('ext.flutter.evict', <String, dynamic>{
-      'isolateId': isolateId,
+  Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) {
+    return invokeRpcRaw('ext.flutter.evict', <String, dynamic>{
       'value': assetPath
-    }).then((dynamic result) => new Response(result));
+    });
   }
 
-  Future<Response> flutterExit(String isolateId) {
-    return peer
-      .sendRequest('ext.flutter.exit', <String, dynamic>{ 'isolateId': isolateId })
-      .then((dynamic result) => new Response(result))
-      .timeout(new Duration(seconds: 2), onTimeout: () => null);
+  Future<Map<String, dynamic>> flutterExit() {
+    return invokeRpcRaw('ext.flutter.exit').timeout(
+        const Duration(seconds: 2), onTimeout: () => null);
+  }
+}
+
+class ServiceMap extends ServiceObject implements Map<String, dynamic> {
+  ServiceMap._empty(ServiceObjectOwner owner) : super._empty(owner);
+
+  final Map<String, dynamic> _map = new Map<String, dynamic>();
+
+  @override
+  void _update(Map<String, dynamic> map, bool mapIsRef) {
+    _loaded = !mapIsRef;
+    _upgradeCollection(map, owner);
+    _map.clear();
+    _map.addAll(map);
   }
 
-  void _addIsolate(IsolateRef isolate) {
-    if (!isolates.contains(isolate)) {
-      isolates.add(isolate);
+  // Forward Map interface calls.
+  @override
+  void addAll(Map<String, dynamic> other) => _map.addAll(other);
+  @override
+  void clear() => _map.clear();
+  @override
+  bool containsValue(dynamic v) => _map.containsValue(v);
+  @override
+  bool containsKey(String k) => _map.containsKey(k);
+  @override
+  void forEach(Function f) => _map.forEach(f);
+  @override
+  dynamic putIfAbsent(String key, Function ifAbsent) => _map.putIfAbsent(key, ifAbsent);
+  @override
+  void remove(String key) => _map.remove(key);
+  @override
+  dynamic operator [](String k) => _map[k];
+  @override
+  void operator []=(String k, dynamic v) => _map[k] = v;
+  @override
+  bool get isEmpty => _map.isEmpty;
+  @override
+  bool get isNotEmpty => _map.isNotEmpty;
+  @override
+  Iterable<String> get keys => _map.keys;
+  @override
+  Iterable<dynamic> get values => _map.values;
+  @override
+  int get length => _map.length;
+  @override
+  String toString() => _map.toString();
+}
 
-      if (_waitFirstIsolateCompleter != null) {
-        _waitFirstIsolateCompleter.complete(isolate);
-        _waitFirstIsolateCompleter = null;
+/// Peered to a Android/iOS FlutterView widget on a device.
+class FlutterView extends ServiceObject {
+  FlutterView._empty(ServiceObjectOwner owner) : super._empty(owner);
+
+  Isolate _uiIsolate;
+  Isolate get uiIsolate => _uiIsolate;
+
+  @override
+  void _update(Map<String, dynamic> map, bool mapIsRef) {
+    _loaded = !mapIsRef;
+    _upgradeCollection(map, owner);
+    _uiIsolate = map['isolate'];
+  }
+
+  // TODO(johnmccutchan): Report errors when running failed.
+  Future<Null> runFromSource(String entryPath,
+                             String packagesPath,
+                             String assetsDirectoryPath) async {
+    final String viewId = id;
+    // When this completer completes the isolate is running.
+    final Completer<Null> completer = new Completer<Null>();
+    final StreamSubscription<ServiceEvent> subscription =
+      owner.vm.vmService.onIsolateEvent.listen((ServiceEvent event) {
+      // TODO(johnmccutchan): Listen to the debug stream and catch initial
+      // launch errors.
+      if (event.kind == ServiceEvent.kIsolateRunnable) {
+        printTrace('Isolate is runnable.');
+        completer.complete(null);
       }
-    }
-  }
-}
-
-class Response {
-  Response(this.response);
-
-  final Map<String, dynamic> response;
-
-  String get type => response['type'];
-
-  dynamic operator[](String key) => response[key];
-
-  @override
-  String toString() => response.toString();
-}
-
-class CreateDevFSResponse extends Response {
-  CreateDevFSResponse(Map<String, dynamic> response) : super(response);
-
-  String get name => response['name'];
-  String get uri => response['uri'];
-}
-
-class VM extends Response {
-  VM(Map<String, dynamic> response) : super(response);
-
-  List<IsolateRef> get isolates => response['isolates'].map((dynamic ref) => new IsolateRef(ref)).toList();
-}
-
-class Event extends Response {
-  Event(Map<String, dynamic> response) : super(response);
-
-  String get kind => response['kind'];
-  IsolateRef get isolate => new IsolateRef.from(response['isolate']);
-
-  /// Only valid for [kind] == `Extension`.
-  String get extensionKind => response['extensionKind'];
-}
-
-class IsolateRef extends Response {
-  IsolateRef(Map<String, dynamic> response) : super(response);
-  factory IsolateRef.from(dynamic ref) => ref == null ? null : new IsolateRef(ref);
-
-  String get id => response['id'];
-
-  @override
-  bool operator ==(dynamic other) {
-    if (identical(this, other))
-      return true;
-    if (other is! IsolateRef)
-      return false;
-    final IsolateRef typedOther = other;
-    return id == typedOther.id;
+    });
+    await owner.vm.runInView(viewId,
+                             entryPath,
+                             packagesPath,
+                             assetsDirectoryPath);
+    await completer.future;
+    await subscription.cancel();
   }
 
+  bool get hasIsolate => _uiIsolate != null;
+
   @override
-  int get hashCode => id.hashCode;
+  String toString() => id;
 }