blob: df02db560e4bdf59122783d046f4f707444813fb [file] [log] [blame]
// Copyright (c) 2022, 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.
// TODO(https://github.com/dart-lang/sdk/issues/48161) investigate whether we
// can delete this file or not.
import 'dart:async';
import 'dart:io' as io;
import 'package:collection/collection.dart';
import 'package:devtools_shared/devtools_shared.dart';
import 'package:vm_service/vm_service.dart';
class MemoryProfile {
MemoryProfile(this.service, String profileFilename, this._verboseMode) {
onConnectionClosed.listen(_handleConnectionStop);
service!.onEvent('Service').listen(handleServiceEvent);
_jsonFile = MemoryJsonFile.create(profileFilename);
_hookUpEvents();
}
late MemoryJsonFile _jsonFile;
final bool _verboseMode;
void _hookUpEvents() async {
final streamIds = [
EventStreams.kExtension,
EventStreams.kGC,
EventStreams.kIsolate,
EventStreams.kLogging,
EventStreams.kStderr,
EventStreams.kStdout,
// TODO(Kenzi): Collect timeline data too.
// EventStreams.kTimeline,
EventStreams.kVM,
EventStreams.kService,
];
await Future.wait(streamIds.map((String id) async {
try {
await service!.streamListen(id);
} catch (e) {
if (id.endsWith('Logging')) {
// Don't complain about '_Logging' or 'Logging' events (new VMs don't
// have the private names, and older ones don't have the public ones).
} else {
print("Service client stream not supported: '$id'\n $e");
}
}
}));
}
bool get hasConnection => service != null;
void handleServiceEvent(Event e) {
if (e.kind == EventKind.kServiceRegistered) {
final serviceName = e.service!;
_registeredMethodsForService
.putIfAbsent(serviceName, () => [])
.add(e.method!);
}
if (e.kind == EventKind.kServiceUnregistered) {
final serviceName = e.service!;
_registeredMethodsForService.remove(serviceName);
}
}
late IsolateRef _selectedIsolate;
Future<Response?> getAdbMemoryInfo() async {
return await callService(
flutterMemory.service,
isolateId: _selectedIsolate.id,
);
}
/// Call a service that is registered by exactly one client.
Future<Response?> callService(
String name, {
String? isolateId,
Map<String, dynamic>? args,
}) async {
final registered = _registeredMethodsForService[name] ?? const [];
if (registered.isEmpty) {
throw Exception('There are no registered methods for service "$name"');
}
return service!.callMethod(
registered.first,
isolateId: isolateId,
args: args,
);
}
Map<String, List<String>> get registeredMethodsForService =>
_registeredMethodsForService;
final _registeredMethodsForService = <String, List<String>>{};
static const Duration updateDelay = Duration(milliseconds: 500);
VmService? service;
late Timer _pollingTimer;
/// Polled VM current RSS.
int? processRss;
final Map<String, List<HeapSpace>> isolateHeaps = <String, List<HeapSpace>>{};
final List<HeapSample> samples = <HeapSample>[];
AdbMemoryInfo? adbMemoryInfo;
EventSample eventSample = EventSample.empty();
RasterCache? rasterCache;
late int heapMax;
Stream<void> get onConnectionClosed => _connectionClosedController.stream;
final _connectionClosedController = StreamController<void>.broadcast();
void _handleConnectionStop(dynamic event) {
// TODO(terry): Gracefully handle connection loss.
}
// TODO(terry): Investigate moving code from this point through end of class to devtools_shared.
void startPolling() {
_pollingTimer = Timer(updateDelay, _pollMemory);
service!.onGCEvent.listen(_handleGCEvent);
}
void _handleGCEvent(Event event) {
//final bool ignore = event.json['reason'] == 'compact';
final json = event.json!;
final List<HeapSpace> heaps = <HeapSpace>[
HeapSpace.parse(json['new'])!,
HeapSpace.parse(json['old'])!
];
_updateGCEvent(event.isolate!.id!, heaps);
// TODO(terry): expose when GC occurred as markers in memory timeline.
}
void stopPolling() {
_pollingTimer.cancel();
service = null;
}
Future<void> _pollMemory() async {
final service = this.service!;
final VM vm = await service.getVM();
// TODO(terry): Need to handle a possible Sentinel being returned.
final List<Isolate?> isolates =
await Future.wait(vm.isolates!.map((IsolateRef ref) async {
try {
return await service.getIsolate(ref.id!);
} catch (e) {
// TODO(terry): Seem to sometimes get a sentinel not sure how? VM issue?
// Unhandled Exception: type 'Sentinel' is not a subtype of type 'FutureOr<Isolate>'
print('Error [MEMORY_PROTOCOL]: $e');
return Future<Isolate?>.value();
}
}));
// Polls for current Android meminfo using:
// > adb shell dumpsys meminfo -d <package_name>
final isolate = isolates[0]!;
_selectedIsolate = IsolateRef(
id: isolate.id,
name: isolate.name,
number: isolate.number,
isSystemIsolate: isolate.isSystemIsolate,
);
if (hasConnection && vm.operatingSystem == 'android') {
// Poll ADB meminfo
adbMemoryInfo = await _fetchAdbInfo();
} else {
// TODO(terry): TBD alternative for iOS memory info - all values zero.
adbMemoryInfo = AdbMemoryInfo.empty();
}
// Query the engine's rasterCache estimate.
rasterCache = await _fetchRasterCacheInfo(_selectedIsolate);
// TODO(terry): There are no user interactions. However, might be nice to
// record VM GC's on the timeline.
eventSample = EventSample.empty();
// Polls for current RSS size.
_update(vm, isolates);
_pollingTimer = Timer(updateDelay, _pollMemory);
}
/// Poll ADB meminfo
Future<AdbMemoryInfo?> _fetchAdbInfo() async {
final adbMemInfo = await getAdbMemoryInfo();
if (adbMemInfo?.json != null) {
return AdbMemoryInfo.fromJsonInKB(adbMemInfo!.json!);
}
return null;
}
/// Poll Flutter engine's Raster Cache metrics.
/// @returns engine's rasterCache estimates or null.
Future<RasterCache?> _fetchRasterCacheInfo(IsolateRef selectedIsolate) async {
final response = await getRasterCacheMetrics(selectedIsolate);
return RasterCache.parse(response?.json);
}
/// @returns view id of selected isolate's 'FlutterView'.
/// @throws Exception if no 'FlutterView'.
Future<String?> getFlutterViewId(IsolateRef selectedIsolate) async {
final flutterViewListResponse = await service!.callServiceExtension(
flutterListViews,
isolateId: selectedIsolate.id,
);
final List<dynamic> views =
flutterViewListResponse.json!['views'].cast<Map<String, dynamic>>();
// Each isolate should only have one FlutterView.
final flutterView = views.firstWhereOrNull(
(view) => view['type'] == 'FlutterView',
);
if (flutterView == null) {
final message =
'No Flutter Views to query: ${flutterViewListResponse.json}';
print('ERROR: $message');
throw Exception(message);
}
final String flutterViewId = flutterView['id']!;
return flutterViewId;
}
/// Flutter engine returns estimate how much memory is used by layer/picture raster
/// cache entries in bytes.
///
/// Call to returns JSON payload 'EstimateRasterCacheMemory' with two entries:
/// layerBytes - layer raster cache entries in bytes
/// pictureBytes - picture raster cache entries in bytes
Future<Response?> getRasterCacheMetrics(IsolateRef selectedIsolate) async {
final viewId = await getFlutterViewId(selectedIsolate);
return await service!.callServiceExtension(
flutterEngineRasterCache,
args: {'viewId': viewId},
isolateId: selectedIsolate.id,
);
}
void _update(VM vm, List<Isolate?> isolates) {
processRss = vm.json!['_currentRSS'];
isolateHeaps.clear();
for (Isolate? isolate in isolates) {
if (isolate != null) {
isolateHeaps[isolate.id!] = getHeaps(isolate);
}
}
_recalculate();
}
void _updateGCEvent(String id, List<HeapSpace> heaps) {
isolateHeaps[id] = heaps;
_recalculate(true);
}
void _recalculate([bool fromGC = false]) {
int total = 0;
int used = 0;
int capacity = 0;
int external = 0;
for (List<HeapSpace> heaps in isolateHeaps.values) {
used += heaps.fold<int>(0, (i, heap) => i + heap.used!);
capacity += heaps.fold<int>(0, (i, heap) => i + heap.capacity!);
external += heaps.fold<int>(0, (i, heap) => i + heap.external!);
capacity += external;
total +=
heaps.fold<int>(0, (i, heap) => i + heap.capacity! + heap.external!);
}
heapMax = total;
final time = DateTime.now().millisecondsSinceEpoch;
final sample = HeapSample(
time,
processRss ?? -1,
capacity,
used,
external,
fromGC,
adbMemoryInfo,
eventSample,
rasterCache,
);
if (_verboseMode) {
final timeCollected = _formatTime(
DateTime.fromMillisecondsSinceEpoch(time),
);
print(' Collected Sample: [$timeCollected] capacity=$capacity, '
'ADB MemoryInfo total=${adbMemoryInfo!.total}${fromGC ? ' [GC]' : ''}');
}
_jsonFile.writeSample(sample);
}
static List<HeapSpace> getHeaps(Isolate isolate) {
final Map<String, dynamic> heaps = isolate.json!['_heaps'];
final heapList = <HeapSpace>[];
for (final heapJson in heaps.values) {
final heap = HeapSpace.parse(heapJson);
if (heap != null) {
heapList.add(heap);
}
}
return heapList;
}
static String _formatTime(DateTime value) {
String toStringLength(int value, int length) {
final result = '$value';
assert(length >= result.length);
return '0' * (length - result.length) + result;
}
return toStringLength(value.hour, 2) +
':' +
toStringLength(value.minute, 2) +
':' +
toStringLength(value.second, 2) +
'.' +
toStringLength(value.millisecond, 3);
}
}
class MemoryJsonFile {
MemoryJsonFile.create(this._absoluteFileName) {
_open();
}
final String _absoluteFileName;
late io.File _fs;
late io.RandomAccessFile _raFile;
bool _multipleSamples = false;
void _open() {
_fs = io.File(_absoluteFileName);
_raFile = _fs.openSync(mode: io.FileMode.writeOnly);
_populateJsonHeader();
}
void _populateJsonHeader() {
final payload = '${SamplesMemoryJson.header}${MemoryJson.trailer}';
_raFile.writeStringSync(payload);
_raFile.flushSync();
}
void _setPositionToWriteSample() {
// Set the file position to the data array field contents - inside of [].
final filePosition = _raFile.positionSync();
_raFile.setPositionSync(filePosition - MemoryJson.trailer.length);
}
void writeSample(HeapSample sample) {
_setPositionToWriteSample();
String encodedSample;
if (_multipleSamples) {
encodedSample = SamplesMemoryJson().encodeAnother(sample);
} else {
encodedSample = SamplesMemoryJson().encode(sample);
}
_raFile.writeStringSync('$encodedSample${MemoryJson.trailer}');
_raFile.flushSync();
_multipleSamples = true;
}
}