Skip to content

Commit f0491f7

Browse files
authored
Reorganize leak tracking code. (#4832)
1 parent dd3fade commit f0491f7

File tree

5 files changed

+273
-254
lines changed

5 files changed

+273
-254
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2022 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
8+
import 'package:flutter/material.dart';
9+
import 'package:vm_service/vm_service.dart';
10+
11+
import '../../../../config_specific/import_export/import_export.dart';
12+
import '../../../../config_specific/logger/logger.dart' as logger;
13+
import '../../../../primitives/utils.dart';
14+
import '../../../../service/service_extensions.dart';
15+
import '../../../../shared/globals.dart';
16+
import '../../primitives/memory_utils.dart';
17+
import 'diagnostics/formatter.dart';
18+
import 'diagnostics/leak_analyzer.dart';
19+
import 'diagnostics/model.dart';
20+
import 'instrumentation/model.dart';
21+
import 'primitives/analysis_status.dart';
22+
23+
// TODO(polina-c): reference these constants in dart SDK, when it gets submitted
24+
// there.
25+
// https://github.com/flutter/devtools/issues/3951
26+
const _extensionKindToReceiveLeaksSummary = 'memory_leaks_summary';
27+
const _extensionKindToReceiveLeaksDetails = 'memory_leaks_details';
28+
29+
const yamlFilePrefix = 'memory_leaks';
30+
31+
class LeaksPaneController {
32+
LeaksPaneController() {
33+
_subscribeForMemoryLeaksMessages();
34+
}
35+
36+
final status = AnalysisStatusController();
37+
38+
final leakSummaryHistory = ValueNotifier<String>('');
39+
final leakSummaryReceived = ValueNotifier<bool>(false);
40+
LeakSummary? _lastLeakSummary;
41+
42+
final _exportController = ExportController();
43+
44+
late StreamSubscription summarySubscription;
45+
late StreamSubscription detailsSubscription;
46+
47+
/// Subscribes for summary with history and for details without history.
48+
void _subscribeForMemoryLeaksMessages() {
49+
detailsSubscription =
50+
serviceManager.service!.onExtensionEvent.listen(_receivedLeaksDetails);
51+
52+
summarySubscription = serviceManager.service!.onExtensionEventWithHistory
53+
.listen(_receivedLeaksSummary);
54+
}
55+
56+
void dispose() {
57+
unawaited(summarySubscription.cancel());
58+
unawaited(detailsSubscription.cancel());
59+
status.dispose();
60+
}
61+
62+
void _receivedLeaksSummary(Event event) {
63+
if (event.extensionKind != _extensionKindToReceiveLeaksSummary) return;
64+
leakSummaryReceived.value = true;
65+
try {
66+
final newSummary = LeakSummary.fromJson(event.json!['extensionData']!);
67+
final time = event.timestamp != null
68+
? DateTime.fromMicrosecondsSinceEpoch(event.timestamp!)
69+
: DateTime.now();
70+
71+
if (newSummary.matches(_lastLeakSummary)) return;
72+
_lastLeakSummary = newSummary;
73+
leakSummaryHistory.value =
74+
'${formatDateTime(time)}: ${newSummary.toMessage()}\n'
75+
'${leakSummaryHistory.value}';
76+
} catch (error, trace) {
77+
leakSummaryHistory.value = 'error: $error\n${leakSummaryHistory.value}';
78+
logger.log(error);
79+
logger.log(trace);
80+
}
81+
}
82+
83+
Future<NotGCedAnalyzerTask> _createAnalysisTask(
84+
List<LeakReport> reports,
85+
) async {
86+
final graph = (await snapshotMemory())!;
87+
return NotGCedAnalyzerTask.fromSnapshot(graph, reports);
88+
}
89+
90+
Future<void> _receivedLeaksDetails(Event event) async {
91+
if (event.extensionKind != _extensionKindToReceiveLeaksDetails) return;
92+
if (status.status.value != AnalysisStatus.Ongoing) return;
93+
NotGCedAnalyzerTask? task;
94+
95+
try {
96+
await _setMessageWithDelay('Received details. Parsing...');
97+
final leakDetails = Leaks.fromJson(event.json!['extensionData']!);
98+
99+
final notGCed = leakDetails.byType[LeakType.notGCed] ?? [];
100+
101+
NotGCedAnalyzed? notGCedAnalyzed;
102+
if (notGCed.isNotEmpty) {
103+
await _setMessageWithDelay('Taking heap snapshot...');
104+
task = await _createAnalysisTask(notGCed);
105+
await _setMessageWithDelay('Detecting retaining paths...');
106+
notGCedAnalyzed = analyseNotGCed(task);
107+
}
108+
109+
await _setMessageWithDelay('Formatting...');
110+
111+
final yaml = analyzedLeaksToYaml(
112+
gcedLate: leakDetails.gcedLate,
113+
notDisposed: leakDetails.notDisposed,
114+
notGCed: notGCedAnalyzed,
115+
);
116+
117+
_saveResultAndSetStatus(yaml, task);
118+
} catch (error, trace) {
119+
var message = '${status.message.value}\nError: $error';
120+
if (task != null) {
121+
final fileName = _saveTask(task, DateTime.now());
122+
message += '\nDownloaded raw data to $fileName.';
123+
await _setMessageWithDelay(message);
124+
status.status.value = AnalysisStatus.ShowingError;
125+
}
126+
logger.log(error);
127+
logger.log(trace);
128+
}
129+
}
130+
131+
void _saveResultAndSetStatus(String yaml, NotGCedAnalyzerTask? task) async {
132+
final now = DateTime.now();
133+
final yamlFile = ExportController.generateFileName(
134+
time: now,
135+
prefix: yamlFilePrefix,
136+
type: ExportFileType.yaml,
137+
);
138+
_exportController.downloadFile(yaml, fileName: yamlFile);
139+
final String? taskFile = _saveTask(task, now);
140+
141+
final taskFileMessage = taskFile == null ? '' : ' and $taskFile';
142+
await _setMessageWithDelay(
143+
'Downloaded the leak analysis to $yamlFile$taskFileMessage.',
144+
);
145+
status.status.value = AnalysisStatus.ShowingResult;
146+
}
147+
148+
/// Saves raw analysis task for troubleshooting and deeper analysis.
149+
String? _saveTask(NotGCedAnalyzerTask? task, DateTime? now) {
150+
if (task == null) return null;
151+
152+
final json = jsonEncode(task.toJson());
153+
final jsonFile = ExportController.generateFileName(
154+
time: now,
155+
prefix: yamlFilePrefix,
156+
postfix: '.raw',
157+
type: ExportFileType.json,
158+
);
159+
return _exportController.downloadFile(json, fileName: jsonFile);
160+
}
161+
162+
Future<void> _setMessageWithDelay(String message) async {
163+
status.message.value = message;
164+
await delayForBatchProcessing(micros: 5000);
165+
}
166+
167+
Future<void> forceGC() async {
168+
status.status.value = AnalysisStatus.Ongoing;
169+
await _setMessageWithDelay('Forcing full garbage collection...');
170+
await _invokeMemoryLeakTrackingExtension(
171+
<String, dynamic>{
172+
// TODO(polina-c): reference the constant in Flutter
173+
// https://github.com/flutter/devtools/issues/3951
174+
'forceGC': 'true',
175+
},
176+
);
177+
status.status.value = AnalysisStatus.ShowingResult;
178+
await _setMessageWithDelay('Full garbage collection initiated.');
179+
}
180+
181+
Future<void> requestLeaks() async {
182+
status.status.value = AnalysisStatus.Ongoing;
183+
await _setMessageWithDelay('Requested details from the application.');
184+
185+
await _invokeMemoryLeakTrackingExtension(
186+
<String, dynamic>{
187+
// TODO(polina-c): reference the constant in Flutter
188+
// https://github.com/flutter/devtools/issues/3951
189+
'requestDetails': 'true',
190+
},
191+
);
192+
}
193+
194+
Future<void> _invokeMemoryLeakTrackingExtension(
195+
Map<String, dynamic> args,
196+
) async {
197+
await serviceManager.service!.callServiceExtension(
198+
memoryLeakTracking,
199+
isolateId: serviceManager.isolateManager.mainIsolate.value!.id!,
200+
args: args,
201+
);
202+
}
203+
}

packages/devtools_app/lib/src/screens/memory/panes/leaks/formatter.dart renamed to packages/devtools_app/lib/src/screens/memory/panes/leaks/diagnostics/formatter.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
import 'package:collection/collection.dart';
66

7-
import 'diagnostics/model.dart';
8-
import 'instrumentation/model.dart';
7+
import '../instrumentation/model.dart';
8+
import 'model.dart';
99

1010
const linkToGuidance =
1111
'https://github.com/flutter/devtools/blob/master/packages/devtools_app/lib/src/screens/memory/panes/leaks/LEAK_TRACKING.md';

0 commit comments

Comments
 (0)