blob: 80379124749bab4d5944a05d423dd9f813581db9 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. 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:developer';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';
import '_service.dart';
final _log = Logger('_retaining_path.dart');
Future<RetainingPath> obtainRetainingPath(Type type, int code) async {
await _connect();
final fp = _ObjectFingerprint(type, code);
final theObject = await _objectInIsolate(fp);
if (theObject == null) {
throw Exception('Could not find object in heap');
}
_log.info('Requesting retaining path.');
final result = await _theService.getRetainingPath(
theObject.isolateId,
theObject.itemId,
100000,
);
_log.info('Recieved retaining path.');
return result;
}
final List<String> _isolateIds = [];
late VmService _theService;
bool _connected = false;
Future<void> _connect() async {
if (_connected) return;
final info = await Service.getInfo();
if (info.serverWebSocketUri == null) {
throw StateError(
'Leak troubleshooting is not available in release mode. Run your application or test with flag "--debug" '
'(Not supported for Flutter yet: https://github.com/flutter/flutter/issues/127331).',
);
}
_theService = await connectWithWebSocket(info.serverWebSocketUri!, (error) {
throw error ?? Exception('Error connecting to service protocol');
});
await _theService.getVersion();
await _getIdForTwoIsolates();
_connected = true;
}
/// Tries to wait for two isolates to be available.
///
/// Depending on environment (command line / IDE, Flutter / Dart), isolates may have different names,
/// and there can be one or two. Sometimes the second one appears with latency.
/// And sometimes there are two isolates with name 'main'.
Future<void> _getIdForTwoIsolates() async {
_log.info('Waiting for two isolates to be available.');
const isolatesToGet = 2;
const watingTime = Duration(seconds: 2);
final stopwatch = Stopwatch()..start();
while (_isolateIds.length < isolatesToGet && stopwatch.elapsed < watingTime) {
_isolateIds.clear();
await forEachIsolate(
_theService,
(IsolateRef r) async => _isolateIds.add(r.id!),
);
if (_isolateIds.length < isolatesToGet) {
await Future.delayed(const Duration(milliseconds: 100));
}
}
if (_isolateIds.isEmpty) {
throw StateError('Could not connect to isolates.');
}
_log.info('Number of isolates: ${_isolateIds.length}');
}
class _ObjectFingerprint {
_ObjectFingerprint(this.type, this.code);
final Type type;
final int code;
}
Future<_ItemInIsolate?> _objectInIsolate(_ObjectFingerprint object) async {
final classes = await _findClasses(object.type.toString());
for (final theClass in classes) {
const pathLengthLimit = 10000000;
final instances = (await _theService.getInstances(
theClass.isolateId,
theClass.itemId,
pathLengthLimit,
))
.instances ??
<ObjRef>[];
final result = instances.firstWhereOrNull(
(objRef) =>
objRef is InstanceRef && objRef.identityHashCode == object.code,
);
if (result != null) {
return _ItemInIsolate(isolateId: theClass.isolateId, itemId: result.id!);
}
}
return null;
}
/// Represents an item in an isolate.
///
/// It can be class or object.
class _ItemInIsolate {
_ItemInIsolate({required this.isolateId, required this.itemId});
/// Id of the isolate.
final String isolateId;
/// Id of the item in the isolate.
final String itemId;
}
Future<List<_ItemInIsolate>> _findClasses(String runtimeClassName) async {
final result = <_ItemInIsolate>[];
for (final isolateId in _isolateIds) {
var classes = await _theService.getClassList(isolateId);
const watingTime = Duration(seconds: 2);
final stopwatch = Stopwatch()..start();
// In the beginning list of classes may be empty.
while (classes.classes?.isEmpty ?? true && stopwatch.elapsed < watingTime) {
await Future.delayed(const Duration(milliseconds: 100));
classes = await _theService.getClassList(isolateId);
}
if (classes.classes?.isEmpty ?? true) {
throw StateError('Could not get list of classes.');
}
final filtered =
classes.classes?.where((ref) => runtimeClassName == ref.name) ?? [];
result.addAll(
filtered.map(
(classRef) =>
_ItemInIsolate(itemId: classRef.id!, isolateId: isolateId),
),
);
}
return result;
}