vmservice.dart 41 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:convert' show BASE64;
7
import 'dart:math' as math;
8

9
import 'package:file/file.dart';
10
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
11
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
12
import 'package:meta/meta.dart' show required;
13
import 'package:stream_channel/stream_channel.dart';
14
import 'package:web_socket_channel/io.dart';
15
import 'package:web_socket_channel/web_socket_channel.dart';
16

17
import 'base/common.dart';
18
import 'base/file_system.dart';
19
import 'globals.dart';
20
import 'vmservice_record_replay.dart';
21

22
/// A function that opens a two-way communication channel to the specified [uri].
23
typedef StreamChannel<String> _OpenChannel(Uri uri);
24

25
_OpenChannel _openChannel = _defaultOpenChannel;
26

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
/// A function that reacts to the invocation of the 'reloadSources' service.
///
/// The VM Service Protocol allows clients to register custom services that
/// can be invoked by other clients through the service protocol itself.
///
/// Clients like Observatory use external 'reloadSources' services,
/// when available, instead of the VM internal one. This allows these clients to
/// invoke Flutter HotReload when connected to a Flutter Application started in
/// hot mode.
///
/// See: https://github.com/dart-lang/sdk/issues/30023
typedef Future<Null> ReloadSources(
  String isolateId, {
  bool force,
  bool pause,
});

44 45 46 47
const String _kRecordingType = 'vmservice';

StreamChannel<String> _defaultOpenChannel(Uri uri) =>
    new IOWebSocketChannel.connect(uri.toString()).cast();
48

49
/// The default VM service request timeout.
50
const Duration kDefaultRequestTimeout = const Duration(seconds: 30);
51 52 53 54

/// Used for RPC requests that may take a long time.
const Duration kLongRequestTimeout = const Duration(minutes: 1);

55 56 57
/// Used for RPC requests that should never take a long time.
const Duration kShortRequestTimeout = const Duration(seconds: 5);

58
/// A connection to the Dart VM Service.
59
class VMService {
60 61 62 63 64 65 66
  VMService._(
    this._peer,
    this.httpAddress,
    this.wsAddress,
    this._requestTimeout,
    ReloadSources reloadSources,
  ) {
67
    _vm = new VM._empty(this);
68
    _peer.listen().catchError(_connectionError.completeError);
69

70
    _peer.registerMethod('streamNotify', (rpc.Parameters event) {
71 72
      _handleStreamNotify(event.asMap);
    });
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

    if (reloadSources != null) {
      _peer.registerMethod('reloadSources', (rpc.Parameters params) async {
        final String isolateId = params['isolateId'].value;
        final bool force = params.asMap['force'] ?? false;
        final bool pause = params.asMap['pause'] ?? false;

        if (isolateId is! String || isolateId.isEmpty)
          throw new rpc.RpcException.invalidParams('Invalid \'isolateId\': $isolateId');
        if (force is! bool)
          throw new rpc.RpcException.invalidParams('Invalid \'force\': $force');
        if (pause is! bool)
          throw new rpc.RpcException.invalidParams('Invalid \'pause\': $pause');

        try {
          await reloadSources(isolateId, force: force, pause: pause);
          return <String, String>{'type': 'Success'};
        } on rpc.RpcException {
          rethrow;
        } catch (e, st) {
          throw new rpc.RpcException(rpc_error_code.SERVER_ERROR,
              'Error during Sources Reload: $e\n$st');
        }
      });

      // If the Flutter Engine doesn't support service registration this will
      // have no effect
      _peer.sendNotification('_registerService', <String, String>{
        'service': 'reloadSources',
        'alias': 'Flutter Tools'
      });
    }
105 106
  }

107 108 109 110 111 112 113 114
  /// Enables recording of VMService JSON-rpc activity to the specified base
  /// recording [location].
  ///
  /// Activity will be recorded in a subdirectory of [location] named
  /// `"vmservice"`. It is permissible for [location] to represent an existing
  /// non-empty directory as long as there is no collision with the
  /// `"vmservice"` subdirectory.
  static void enableRecordingConnection(String location) {
115
    final Directory dir = getRecordingSink(location, _kRecordingType);
116
    _openChannel = (Uri uri) {
117
      final StreamChannel<String> delegate = _defaultOpenChannel(uri);
118 119 120 121 122 123 124 125 126 127
      return new RecordingVMServiceChannel(delegate, dir);
    };
  }

  /// Enables VMService JSON-rpc replay mode.
  ///
  /// [location] must represent a directory to which VMService JSON-rpc
  /// activity has been recorded (i.e. the result of having been previously
  /// passed to [enableRecordingConnection]), or a [ToolExit] will be thrown.
  static void enableReplayConnection(String location) {
128
    final Directory dir = getReplaySource(location, _kRecordingType);
129
    _openChannel = (Uri uri) => new ReplayVMServiceChannel(dir);
130 131
  }

132 133
  /// Connect to a Dart VM Service at [httpUri].
  ///
134
  /// Requests made via the returned [VMService] time out after [requestTimeout]
135
  /// amount of time, which is [kDefaultRequestTimeout] by default.
136 137 138 139 140 141 142
  ///
  /// If the [reloadSources] parameter is not null, the 'reloadSources' service
  /// will be registered. The VM Service Protocol allows clients to register
  /// custom services that can be invoked by other clients through the service
  /// protocol itself.
  ///
  /// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217
143 144 145
  static VMService connect(
    Uri httpUri, {
    Duration requestTimeout: kDefaultRequestTimeout,
146
    ReloadSources reloadSources,
147
  }) {
148 149 150
    final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
    final StreamChannel<String> channel = _openChannel(wsUri);
    final rpc.Peer peer = new rpc.Peer.withoutJson(jsonDocument.bind(channel));
151
    return new VMService._(peer, httpUri, wsUri, requestTimeout, reloadSources);
152
  }
153

154
  final Uri httpAddress;
155
  final Uri wsAddress;
156
  final rpc.Peer _peer;
157
  final Duration _requestTimeout;
158
  final Completer<Map<String, dynamic>> _connectionError = new Completer<Map<String, dynamic>>();
159

160 161 162
  VM _vm;
  /// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects.
  VM get vm => _vm;
163

164 165
  final Map<String, StreamController<ServiceEvent>> _eventControllers =
      <String, StreamController<ServiceEvent>>{};
166

167
  final Set<String> _listeningFor = new Set<String>();
168

169 170 171
  /// Whether our connection to the VM service has been closed;
  bool get isClosed => _peer.isClosed;
  Future<Null> get done => _peer.done;
172 173

  // Events
174 175
  Stream<ServiceEvent> get onDebugEvent => onEvent('Debug');
  Stream<ServiceEvent> get onExtensionEvent => onEvent('Extension');
176
  // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded
177 178 179
  Stream<ServiceEvent> get onIsolateEvent => onEvent('Isolate');
  Stream<ServiceEvent> get onTimelineEvent => onEvent('Timeline');
  // TODO(johnmccutchan): Add FlutterView events.
180 181

  // Listen for a specific event name.
182 183
  Stream<ServiceEvent> onEvent(String streamId) {
    _streamListen(streamId);
184 185
    return _getEventController(streamId).stream;
  }
186

187 188 189 190 191 192 193 194 195 196
  Future<Map<String, dynamic>> _sendRequest(
    String method,
    Map<String, dynamic> params,
  ) {
    return Future.any(<Future<Map<String, dynamic>>>[
      _peer.sendRequest(method, params),
      _connectionError.future,
    ]);
  }

197 198
  StreamController<ServiceEvent> _getEventController(String eventName) {
    StreamController<ServiceEvent> controller = _eventControllers[eventName];
199
    if (controller == null) {
200
      controller = new StreamController<ServiceEvent>.broadcast();
201 202 203 204 205 206
      _eventControllers[eventName] = controller;
    }
    return controller;
  }

  void _handleStreamNotify(Map<String, dynamic> data) {
207 208 209
    final String streamId = data['streamId'];
    final Map<String, dynamic> eventData = data['event'];
    final Map<String, dynamic> eventIsolate = eventData['isolate'];
210 211 212 213

    // Log event information.
    printTrace(data.toString());

214 215 216
    ServiceEvent event;
    if (eventIsolate != null) {
      // getFromMap creates the Isolate if necessary.
217
      final Isolate isolate = vm.getFromMap(eventIsolate);
218 219 220 221 222 223 224 225 226 227 228 229 230 231
      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);
232 233
  }

234 235 236
  Future<Null> _streamListen(String streamId) async {
    if (!_listeningFor.contains(streamId)) {
      _listeningFor.add(streamId);
237
      await _sendRequest('streamListen', <String, dynamic>{ 'streamId': streamId });
238
    }
239 240
  }

241 242 243 244
  /// Reloads the VM.
  Future<VM> getVM() {
    return _vm.reload();
  }
245 246 247 248 249 250 251 252 253 254

  Future<Null> waitForViews({int attempts = 5, int attemptSeconds = 1}) async {
    await vm.refreshViews();
    for (int i = 0; (vm.firstView == null) && (i < attempts); i++) {
      // If the VM doesn't yet have a view, wait for one to show up.
      printTrace('Waiting for Flutter view');
      await new Future<Null>.delayed(new Duration(seconds: attemptSeconds));
      await vm.refreshViews();
    }
  }
255
}
256

257 258 259 260 261 262
/// 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;
}
263

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
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;
  }
279
  if (collection is Map<String, dynamic>) {
280 281 282 283 284 285 286 287
    _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) {
288
    if ((v is Map<String, dynamic>) && _isServiceMap(v)) {
289 290 291
      map[k] = owner.getFromMap(v);
    } else if (v is List) {
      _upgradeList(v, owner);
292
    } else if (v is Map<String, dynamic>) {
293 294 295 296
      _upgradeMap(v, owner);
    }
  });
}
297

298 299
void _upgradeList(List<dynamic> list, ServiceObjectOwner owner) {
  for (int i = 0; i < list.length; i++) {
300
    final dynamic v = list[i];
301
    if ((v is Map<String, dynamic>) && _isServiceMap(v)) {
302 303 304
      list[i] = owner.getFromMap(v);
    } else if (v is List) {
      _upgradeList(v, owner);
305
    } else if (v is Map<String, dynamic>) {
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
      _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);

326
    final String type = _stripRef(map['type']);
327 328 329 330 331 332 333 334 335 336 337 338 339

    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;
    }
340 341 342
    // 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);
343
    // We have now constructed an empty service object, call update to populate it.
344 345
    serviceObject.update(map);
    return serviceObject;
346 347
  }

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
  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;
373

374 375 376 377 378 379 380 381 382 383 384 385
  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();
386 387
  }

388 389
  /// Fetch this object from vmService and return the response directly.
  Future<Map<String, dynamic>> _fetchDirect() {
390
    final Map<String, dynamic> params = <String, dynamic>{
391 392
      'objectId': id,
    };
393
    return _owner.isolate.invokeRpcRaw('getObject', params: params);
394 395 396 397 398
  }

  Future<ServiceObject> _inProgressReload;
  /// Reload the service object (if possible).
  Future<ServiceObject> reload() async {
399 400
    final bool hasId = (id != null) && (id != '');
    final bool isVM = this is VM;
401 402 403
    // We should always reload the VM.
    // We can't reload objects without an id.
    // We shouldn't reload an immutable and already loaded object.
404
    final bool skipLoad = !isVM && (!hasId || (immutable && loaded));
405 406 407 408 409
    if (skipLoad) {
      return this;
    }

    if (_inProgressReload == null) {
410
      final Completer<ServiceObject> completer = new Completer<ServiceObject>();
411 412 413
      _inProgressReload = completer.future;

      try {
414
        final Map<String, dynamic> response = await _fetchDirect();
415 416 417 418 419 420 421 422 423 424 425
        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;
426
    }
427

428
    return await _inProgressReload;
429 430
  }

431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
  /// 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);
457 458
  }

459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
  /// 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';
478
  static const String kPausePostRequest       = 'PausePostRequest';
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
  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'];
516
    }
517
    _timelineEvents = map['timelineEvents'];
518 519
  }

520 521 522 523 524 525
  bool get isPauseEvent {
    return (kind == kPauseStart ||
            kind == kPauseExit ||
            kind == kPauseBreakpoint ||
            kind == kPauseInterrupted ||
            kind == kPauseException ||
526
            kind == kPausePostRequest ||
527
            kind == kNone);
528
  }
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
}

/// 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;
544

545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
  /// 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 {
566
    return invokeRpcRaw('getVM');
567 568
  }

569 570 571 572 573 574 575 576 577 578 579
  @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.
580
    _pid = map['pid'];
581 582 583 584
    if (map['_heapAllocatedMemoryUsage'] != null) {
      _heapAllocatedMemoryUsage = map['_heapAllocatedMemoryUsage'];
    }
    _maxRSS = map['_maxRSS'];
585 586 587

    // Remove any isolates which are now dead from the isolate cache.
    _removeDeadIsolates(map['isolates']);
588 589
  }

590 591
  final Map<String, ServiceObject> _cache = <String,ServiceObject>{};
  final Map<String,Isolate> _isolateCache = <String,Isolate>{};
592

593
  /// The list of live isolates, ordered by isolate start time.
594
  final List<Isolate> isolates = <Isolate>[];
595

596
  /// The set of live views.
597
  final Map<String, FlutterView> _viewCache = <String, FlutterView>{};
598

599 600 601 602
  /// The pid of the VM's process.
  int _pid;
  int get pid => _pid;

603 604 605 606 607 608 609 610 611 612
  /// The number of bytes allocated (e.g. by malloc) in the native heap.
  int _heapAllocatedMemoryUsage;
  int get heapAllocatedMemoryUsage {
    return _heapAllocatedMemoryUsage == null ? 0 : _heapAllocatedMemoryUsage;
  }

  /// The peak resident set size for the process.
  int _maxRSS;
  int get maxRSS => _maxRSS == null ? 0 : _maxRSS;

613
  int _compareIsolates(Isolate a, Isolate b) {
614 615
    final DateTime aStart = a.startTime;
    final DateTime bStart = b.startTime;
616 617 618 619 620 621 622 623 624 625 626 627 628 629
    if (aStart == null) {
      if (bStart == null) {
        return 0;
      } else {
        return 1;
      }
    }
    if (bStart == null) {
      return -1;
    }
    return aStart.compareTo(bStart);
  }

  void _buildIsolateList() {
630
    final List<Isolate> isolateList = _isolateCache.values.toList();
631 632 633 634 635 636 637
    isolateList.sort(_compareIsolates);
    isolates.clear();
    isolates.addAll(isolateList);
  }

  void _removeDeadIsolates(List<Isolate> newIsolates) {
    // Build a set of new isolates.
638
    final Set<String> newIsolateSet = new Set<String>();
639 640 641
    newIsolates.forEach((Isolate iso) => newIsolateSet.add(iso.id));

    // Remove any old isolates which no longer exist.
642
    final List<String> toRemove = <String>[];
643 644 645 646
    _isolateCache.forEach((String id, _) {
      if (!newIsolateSet.contains(id)) {
        toRemove.add(id);
      }
647
    });
648
    toRemove.forEach(_isolateCache.remove);
649 650 651 652 653 654 655 656
    _buildIsolateList();
  }

  @override
  ServiceObject getFromMap(Map<String, dynamic> map) {
    if (map == null) {
      return null;
    }
657
    final String type = _stripRef(map['type']);
658 659 660 661 662 663
    if (type == 'VM') {
      // Update this VM object.
      update(map);
      return this;
    }

664
    final String mapId = map['id'];
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702

    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);
    }
703 704
  }

705
  // This function does not reload the isolate if it's found in the cache.
706 707 708
  Future<Isolate> getIsolate(String isolateId) {
    if (!loaded) {
      // Trigger a VM load, then get the isolate. Ignore any errors.
709
      return load().then<Isolate>((ServiceObject serviceObject) => getIsolate(isolateId)).catchError((dynamic error) => null);
710 711 712
    }
    return new Future<Isolate>.value(_isolateCache[isolateId]);
  }
713

714
  /// Invoke the RPC and return the raw response.
715 716 717 718 719 720 721 722
  ///
  /// If `timeoutFatal` is false, then a timeout will result in a null return
  /// value. Otherwise, it results in an exception.
  Future<Map<String, dynamic>> invokeRpcRaw(String method, {
    Map<String, dynamic> params: const <String, dynamic>{},
    Duration timeout,
    bool timeoutFatal: true,
  }) async {
723 724
    printTrace('$method: $params');

725 726
    assert(params != null);
    timeout ??= _vmService._requestTimeout;
727
    try {
728
      final Map<String, dynamic> result = await _vmService
729
          ._sendRequest(method, params)
730
          .timeout(timeout);
731
      return result;
732 733 734 735 736
    } on TimeoutException {
      printTrace('Request to Dart VM Service timed out: $method($params)');
      if (timeoutFatal)
        throw new TimeoutException('Request to Dart VM Service timed out: $method($params)');
      return null;
737 738 739
    } on WebSocketChannelException catch (error) {
      throwToolExit('Error connecting to observatory: $error');
      return null;
740
    }
741
  }
742

743 744 745 746 747
  /// Invoke the RPC and return a [ServiceObject] response.
  Future<ServiceObject> invokeRpc(String method, {
    Map<String, dynamic> params: const <String, dynamic>{},
    Duration timeout,
  }) async {
748
    final Map<String, dynamic> response = await invokeRpcRaw(
749 750 751 752
      method,
      params: params,
      timeout: timeout,
    );
753
    final ServiceObject serviceObject = new ServiceObject._fromMap(this, response);
754
    if ((serviceObject != null) && (serviceObject._canCache)) {
755
      final String serviceObjectId = serviceObject.id;
756 757 758
      _cache.putIfAbsent(serviceObjectId, () => serviceObject);
    }
    return serviceObject;
759 760
  }

761
  /// Create a new development file system on the device.
762 763
  Future<Map<String, dynamic>> createDevFS(String fsName) {
    return invokeRpcRaw('_createDevFS', params: <String, dynamic> { 'fsName': fsName });
764 765 766 767
  }

  /// List the development file system son the device.
  Future<List<String>> listDevFS() async {
768
    return (await invokeRpcRaw('_listDevFS'))['fsNames'];
769 770 771
  }

  // Write one file into a file system.
772
  Future<Map<String, dynamic>> writeDevFSFile(String fsName, {
773 774
    @required String path,
    @required List<int> fileContents
775 776 777
  }) {
    assert(path != null);
    assert(fileContents != null);
778 779 780 781 782 783 784 785
    return invokeRpcRaw(
      '_writeDevFSFile',
      params: <String, dynamic>{
        'fsName': fsName,
        'path': path,
        'fileContents': BASE64.encode(fileContents),
      },
    );
786 787
  }

788
  // Read one file from a file system.
789
  Future<List<int>> readDevFSFile(String fsName, String path) async {
790
    final Map<String, dynamic> response = await invokeRpcRaw(
791 792 793 794 795 796 797
      '_readDevFSFile',
      params: <String, dynamic>{
        'fsName': fsName,
        'path': path,
      },
    );
    return BASE64.decode(response['fileContents']);
798 799 800
  }

  /// The complete list of a file system.
801 802
  Future<List<String>> listDevFSFiles(String fsName) async {
    return (await invokeRpcRaw('_listDevFSFiles', params: <String, dynamic>{ 'fsName': fsName }))['files'];
803 804 805
  }

  /// Delete an existing file system.
806
  Future<Map<String, dynamic>> deleteDevFS(String fsName) {
807
    return invokeRpcRaw('_deleteDevFS', params: <String, dynamic>{ 'fsName': fsName });
808 809
  }

810
  Future<ServiceMap> runInView(String viewId,
811 812 813 814
                               Uri main,
                               Uri packages,
                               Uri assetsDirectory) {
    // TODO(goderbauer): Transfer Uri (instead of file path) when remote end supports it.
815
    return invokeRpc('_flutter.runInView',
816
                    params: <String, dynamic> {
817
                      'viewId': viewId,
818 819 820
                      'mainScript': main.toFilePath(windows: false),
                      'packagesFile': packages.toFilePath(windows: false),
                      'assetDirectory': assetsDirectory.toFilePath(windows: false)
821 822
                    });
  }
823

824
  Future<Map<String, dynamic>> clearVMTimeline() {
825
    return invokeRpcRaw('_clearVMTimeline');
826 827
  }

828
  Future<Map<String, dynamic>> setVMTimelineFlags(List<String> recordedStreams) {
829
    assert(recordedStreams != null);
830 831 832 833 834 835
    return invokeRpcRaw(
      '_setVMTimelineFlags',
      params: <String, dynamic>{
        'recordedStreams': recordedStreams,
      },
    );
836 837
  }

838
  Future<Map<String, dynamic>> getVMTimeline() {
839
    return invokeRpcRaw('_getVMTimeline', timeout: kLongRequestTimeout);
840
  }
841

842
  Future<Null> refreshViews() async {
843
    _viewCache.clear();
844
    await vmService.vm.invokeRpc('_flutter.listViews', timeout: kLongRequestTimeout);
845 846
  }

847 848 849
  Iterable<FlutterView> get views => _viewCache.values;

  FlutterView get firstView {
850
    return _viewCache.values.isEmpty ? null : _viewCache.values.first;
851
  }
852

853 854
  List<FlutterView> allViewsWithName(String isolateFilter) {
    if (_viewCache.values.isEmpty)
855
      return null;
856 857 858
    return _viewCache.values.where(
      (FlutterView v) => v.uiIsolate.name.contains(isolateFilter)
    ).toList();
859
  }
860
}
861

862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899
class HeapSpace extends ServiceObject {
  HeapSpace._empty(ServiceObjectOwner owner) : super._empty(owner);

  int _used = 0;
  int _capacity = 0;
  int _external = 0;
  int _collections = 0;
  double _totalCollectionTimeInSeconds = 0.0;
  double _averageCollectionPeriodInMillis = 0.0;

  int get used => _used;
  int get capacity => _capacity;
  int get external => _external;

  Duration get avgCollectionTime {
    final double mcs = _totalCollectionTimeInSeconds *
      Duration.MICROSECONDS_PER_SECOND /
      math.max(_collections, 1);
    return new Duration(microseconds: mcs.ceil());
  }

  Duration get avgCollectionPeriod {
    final double mcs = _averageCollectionPeriodInMillis *
                       Duration.MICROSECONDS_PER_MILLISECOND;
    return new Duration(microseconds: mcs.ceil());
  }

  @override
  void _update(Map<String, dynamic> map, bool mapIsRef) {
    _used = map['used'];
    _capacity = map['capacity'];
    _external = map['external'];
    _collections = map['collections'];
    _totalCollectionTimeInSeconds = map['time'];
    _averageCollectionPeriodInMillis = map['avgCollectionPeriodMillis'];
  }
}

900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
/// 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;
915 916 917

  /// The last pause event delivered to the isolate. If the isolate is running,
  /// this will be a resume event.
918
  ServiceEvent pauseEvent;
919

920
  final Map<String, ServiceObject> _cache = <String, ServiceObject>{};
921

922 923 924 925 926 927
  HeapSpace _newSpace;
  HeapSpace _oldSpace;

  HeapSpace get newSpace => _newSpace;
  HeapSpace get oldSpace => _oldSpace;

928 929
  @override
  ServiceObject getFromMap(Map<String, dynamic> map) {
930
    if (map == null)
931
      return null;
932
    final String mapType = _stripRef(map['type']);
933 934 935 936 937
    if (mapType == 'Isolate') {
      // There are sometimes isolate refs in ServiceEvents.
      return vm.getFromMap(map);
    }

938
    final String mapId = map['id'];
939 940 941 942 943 944 945
    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);
946
    if ((serviceObject != null) && serviceObject.canCache)
947 948
      _cache[mapId] = serviceObject;
    return serviceObject;
949 950
  }

951 952
  @override
  Future<Map<String, dynamic>> _fetchDirect() {
953
    return invokeRpcRaw('getIsolate');
954 955
  }

956
  /// Invoke the RPC and return the raw response.
957 958 959 960 961
  Future<Map<String, dynamic>> invokeRpcRaw(String method, {
    Map<String, dynamic> params,
    Duration timeout,
    bool timeoutFatal: true,
  }) {
962 963 964 965 966 967 968 969
    // Inject the 'isolateId' parameter.
    if (params == null) {
      params = <String, dynamic>{
        'isolateId': id
      };
    } else {
      params['isolateId'] = id;
    }
970
    return vm.invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
971 972
  }

973
  /// Invoke the RPC and return a ServiceObject response.
974 975
  Future<ServiceObject> invokeRpc(String method, Map<String, dynamic> params) async {
    return getFromMap(await invokeRpcRaw(method, params: params));
976
  }
977

978 979 980 981 982 983 984 985 986
  void _updateHeaps(Map<String, dynamic> map, bool mapIsRef) {
    if (_newSpace == null)
      _newSpace = new HeapSpace._empty(this);
    _newSpace._update(map['new'], mapIsRef);
    if (_oldSpace == null)
      _oldSpace = new HeapSpace._empty(this);
    _oldSpace._update(map['old'], mapIsRef);
  }

987 988
  @override
  void _update(Map<String, dynamic> map, bool mapIsRef) {
989
    if (mapIsRef)
990 991 992
      return;
    _loaded = true;

993
    final int startTimeMillis = map['startTime'];
994
    startTime = new DateTime.fromMillisecondsSinceEpoch(startTimeMillis);
995

996
    _upgradeCollection(map, this);
997 998

    pauseEvent = map['pauseEvent'];
999 1000

    _updateHeaps(map['_heaps'], mapIsRef);
1001 1002
  }

1003 1004
  static final int kIsolateReloadBarred = 1005;

1005 1006
  Future<Map<String, dynamic>> reloadSources(
      { bool pause: false,
1007 1008
        Uri rootLibUri,
        Uri packagesUri}) async {
1009
    try {
1010
      final Map<String, dynamic> arguments = <String, dynamic>{
1011 1012
        'pause': pause
      };
1013 1014 1015 1016
      // TODO(goderbauer): Transfer Uri (instead of file path) when remote end supports it.
      //     Note: Despite the name, `rootLibUri` and `packagesUri` expect file paths.
      if (rootLibUri != null) {
        arguments['rootLibUri'] = rootLibUri.toFilePath(windows: false);
1017
      }
1018 1019
      if (packagesUri != null) {
        arguments['packagesUri'] = packagesUri.toFilePath(windows: false);
1020
      }
1021
      final Map<String, dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments);
1022
      return response;
1023 1024 1025 1026 1027 1028
    } on rpc.RpcException catch(e) {
      return new Future<Map<String, dynamic>>.error(<String, dynamic>{
        'code': e.code,
        'message': e.message,
        'data': e.data,
      });
1029 1030
    }
  }
1031

1032 1033 1034 1035 1036
  /// Resumes the isolate.
  Future<Map<String, dynamic>> resume() {
    return invokeRpcRaw('resume');
  }

1037
  // Flutter extension methods.
1038

1039 1040 1041
  // Invoke a flutter extension method, if the flutter extension is not
  // available, returns null.
  Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
1042 1043 1044 1045 1046 1047
    String method, {
      Map<String, dynamic> params,
      Duration timeout,
      bool timeoutFatal: true,
    }
  ) async {
1048
    try {
1049
      return await invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
1050
    } on rpc.RpcException catch (e) {
1051
      // If an application is not using the framework
1052
      if (e.code == rpc_error_code.METHOD_NOT_FOUND)
1053 1054 1055 1056 1057 1058 1059
        return null;
      rethrow;
    }
  }

  // Debug dump extension methods.

1060
  Future<Map<String, dynamic>> flutterDebugDumpApp() {
1061
    return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp', timeout: kLongRequestTimeout);
1062
  }
1063

1064
  Future<Map<String, dynamic>> flutterDebugDumpRenderTree() {
1065
    return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree', timeout: kLongRequestTimeout);
1066
  }
1067

1068 1069 1070 1071
  Future<Map<String, dynamic>> flutterDebugDumpLayerTree() {
    return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout);
  }

1072 1073 1074 1075 1076 1077
  Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() {
    return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder', timeout: kLongRequestTimeout);
  }

  Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() {
    return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', timeout: kLongRequestTimeout);
1078 1079
  }

1080 1081
  Future<Map<String, dynamic>> _flutterToggle(String name) async {
    Map<String, dynamic> state = await invokeFlutterExtensionRpcRaw('ext.flutter.$name');
1082
    if (state != null && state.containsKey('enabled') && state['enabled'] is String) {
1083
      state = await invokeFlutterExtensionRpcRaw(
1084
        'ext.flutter.$name',
1085
        params: <String, dynamic>{ 'enabled': state['enabled'] == 'true' ? 'false' : 'true' },
1086 1087 1088 1089
        timeout: const Duration(milliseconds: 150),
        timeoutFatal: false,
      );
    }
1090 1091 1092
    return state;
  }

1093 1094 1095 1096
  Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled() => _flutterToggle('debugPaint');

  Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay');

1097 1098
  Future<Map<String, dynamic>> flutterToggleWidgetInspector()  => _flutterToggle('debugWidgetInspector');

1099
  Future<Null> flutterDebugAllowBanner(bool show) async {
1100 1101
    await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugAllowBanner',
1102
      params: <String, dynamic>{ 'enabled': show ? 'true' : 'false' },
1103 1104 1105
      timeout: const Duration(milliseconds: 150),
      timeoutFatal: false,
    );
1106 1107
  }

1108 1109
  // Reload related extension methods.
  Future<Map<String, dynamic>> flutterReassemble() async {
1110 1111
    return await invokeFlutterExtensionRpcRaw(
      'ext.flutter.reassemble',
1112 1113
      timeout: kShortRequestTimeout,
      timeoutFatal: true,
1114
    );
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126
  }

  Future<bool> flutterFrameworkPresent() async {
    return (await invokeFlutterExtensionRpcRaw('ext.flutter.frameworkPresent') != null);
  }

  Future<Map<String, dynamic>> uiWindowScheduleFrame() async {
    return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame');
  }

  Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) async {
    return await invokeFlutterExtensionRpcRaw('ext.flutter.evict',
1127 1128 1129
      params: <String, dynamic>{
        'value': assetPath,
      }
1130
    );
1131
  }
1132

1133 1134
  // Application control extension methods.
  Future<Map<String, dynamic>> flutterExit() async {
1135 1136 1137 1138 1139
    return await invokeFlutterExtensionRpcRaw(
      'ext.flutter.exit',
      timeout: const Duration(seconds: 2),
      timeoutFatal: false,
    );
1140
  }
1141

1142
  Future<String> flutterPlatformOverride([String platform]) async {
1143
    final Map<String, String> result = await invokeFlutterExtensionRpcRaw(
1144 1145 1146 1147 1148
      'ext.flutter.platformOverride',
      params: platform != null ? <String, dynamic>{ 'value': platform } : <String, String>{},
      timeout: const Duration(seconds: 5),
      timeoutFatal: false,
    );
1149
    if (result != null && result['value'] is String)
1150 1151 1152
      return result['value'];
    return 'unknown';
  }
1153 1154 1155

  @override
  String toString() => 'Isolate $id';
1156 1157
}

1158 1159 1160
class ServiceMap extends ServiceObject implements Map<String, dynamic> {
  ServiceMap._empty(ServiceObjectOwner owner) : super._empty(owner);

1161
  final Map<String, dynamic> _map = <String, dynamic>{};
1162

1163 1164 1165 1166 1167 1168 1169
  @override
  void _update(Map<String, dynamic> map, bool mapIsRef) {
    _loaded = !mapIsRef;
    _upgradeCollection(map, owner);
    _map.clear();
    _map.addAll(map);
  }
1170

1171 1172 1173 1174 1175 1176 1177 1178
  // 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
Adam Barth's avatar
Adam Barth committed
1179
  bool containsKey(Object k) => _map.containsKey(k);
1180
  @override
1181
  void forEach(void f(String key, dynamic value)) => _map.forEach(f);
1182
  @override
1183
  dynamic putIfAbsent(String key, dynamic ifAbsent()) => _map.putIfAbsent(key, ifAbsent);
1184
  @override
Adam Barth's avatar
Adam Barth committed
1185
  void remove(Object key) => _map.remove(key);
1186
  @override
Adam Barth's avatar
Adam Barth committed
1187
  dynamic operator [](Object k) => _map[k];
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201
  @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();
1202
}
1203

1204 1205 1206
/// Peered to a Android/iOS FlutterView widget on a device.
class FlutterView extends ServiceObject {
  FlutterView._empty(ServiceObjectOwner owner) : super._empty(owner);
1207

1208 1209
  Isolate _uiIsolate;
  Isolate get uiIsolate => _uiIsolate;
1210

1211
  @override
1212 1213 1214 1215 1216 1217 1218
  void _update(Map<String, dynamic> map, bool mapIsRef) {
    _loaded = !mapIsRef;
    _upgradeCollection(map, owner);
    _uiIsolate = map['isolate'];
  }

  // TODO(johnmccutchan): Report errors when running failed.
1219 1220 1221
  Future<Null> runFromSource(Uri entryUri,
                             Uri packagesUri,
                             Uri assetsDirectoryUri) async {
1222 1223 1224 1225 1226 1227 1228 1229 1230
    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.');
1231 1232
        if (!completer.isCompleted)
          completer.complete(null);
1233 1234 1235
      }
    });
    await owner.vm.runInView(viewId,
1236 1237 1238
                             entryUri,
                             packagesUri,
                             assetsDirectoryUri);
1239
    await completer.future;
1240
    await owner.vm.refreshViews();
1241
    await subscription.cancel();
1242
  }
1243

1244 1245
  bool get hasIsolate => _uiIsolate != null;

1246 1247 1248 1249
  Future<Null> flushUIThreadTasks() async {
    await owner.vm.invokeRpcRaw('_flutter.flushUIThreadTasks');
  }

1250
  @override
1251
  String toString() => id;
1252
}