Skip to content

Commit be51b72

Browse files
authored
Integrate with DevTools. (#20)
1 parent b07e9b3 commit be51b72

24 files changed

+673
-104
lines changed

lib/leak_analysis.dart renamed to lib/devtools_integration.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
/// The library should be used by DevTools to analyse the collected leaks.
66
///
77
/// Should not be used in the application itself.
8-
library leak_analysis;
8+
library devtools_integration;
99

10-
export 'src/leak_analysis_model.dart';
10+
export 'src/devtools_integration/delivery.dart';
11+
export 'src/devtools_integration/messages.dart';
12+
export 'src/devtools_integration/primitives.dart';
13+
export 'src/model.dart';

lib/leak_tracker.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44

55
export 'src/leak_tracker.dart';
66
export 'src/leak_tracker_model.dart';
7+
export 'src/model.dart';

lib/src/_leak_checker.dart

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6-
import 'dart:developer';
76

8-
import 'leak_analysis_model.dart';
7+
import 'devtools_integration/delivery.dart';
98
import 'leak_tracker_model.dart';
9+
import 'model.dart';
1010

1111
class LeakChecker {
1212
LeakChecker({
@@ -23,7 +23,7 @@ class LeakChecker {
2323

2424
late final Timer? _timer;
2525

26-
LeakSummary _previousResult = const LeakSummary({});
26+
LeakSummary _previousResult = LeakSummary({});
2727

2828
/// Period to check for leaks.
2929
///
@@ -32,11 +32,11 @@ class LeakChecker {
3232

3333
// If not null, then the leak summary will be printed here, when
3434
// leak totals change.
35-
final StdoutSink? stdoutSink;
35+
final StdoutSummarySink? stdoutSink;
3636

3737
// If not null, the leak summary will be sent here, when
3838
// leak totals change.
39-
final DevToolsSink? devToolsSink;
39+
final DevToolsSummarySink? devToolsSink;
4040

4141
/// Listener for leaks.
4242
///
@@ -50,8 +50,8 @@ class LeakChecker {
5050
if (summary.matches(_previousResult)) return;
5151

5252
leakListener?.call(summary);
53-
stdoutSink?.print(summary.toMessage());
54-
devToolsSink?.send(summary.toJson());
53+
stdoutSink?.send(summary);
54+
devToolsSink?.send(summary);
5555

5656
_previousResult = summary;
5757
}
@@ -61,11 +61,10 @@ class LeakChecker {
6161
}
6262
}
6363

64-
class StdoutSink {
65-
void print(String content) => print(content);
64+
class StdoutSummarySink {
65+
void send(LeakSummary summary) => print(summary.toMessage());
6666
}
6767

68-
class DevToolsSink {
69-
void send(Map<String, dynamic> content) =>
70-
postEvent(EventNames.memoryLeaksSummary, content);
68+
class DevToolsSummarySink {
69+
void send(LeakSummary summary) => EventFromApp(summary).post();
7170
}

lib/src/_object_record.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import '_gc_counter.dart';
66
import '_primitives.dart';
7-
import 'leak_analysis_model.dart';
7+
import 'model.dart';
88

99
/// Object collections to track leaks.
1010
///

lib/src/_object_tracker.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'package:clock/clock.dart';
77
import '_gc_counter.dart';
88
import '_object_record.dart';
99
import '_primitives.dart';
10-
import 'leak_analysis_model.dart';
10+
import 'model.dart';
1111

1212
class ObjectTracker implements LeakProvider {
1313
/// The optional parameters are injected for testing purposes.
@@ -167,6 +167,7 @@ class ObjectTracker implements LeakProvider {
167167
return result;
168168
}
169169

170+
@override
170171
Leaks collectLeaks() {
171172
throwIfDisposed();
172173
_checkForNewNotGCedLeaks();

lib/src/_primitives.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ String fullClassName({
2020
required String shortClassName,
2121
}) =>
2222
'$library/$shortClassName';
23+
24+
class ObjectRef<T> {
25+
ObjectRef(this.value);
26+
T value;
27+
}

lib/src/_util.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,11 @@ bool mapEquals<T, U>(Map<T, U>? a, Map<T, U>? b) {
2323
}
2424
return true;
2525
}
26+
27+
/// This function is better than `as`, because `as` does not provide callstack on failure.
28+
T cast<T>(value) {
29+
if (value is T) return value;
30+
throw ArgumentError(
31+
'$value is of type ${value.runtimeType} that is not subtype of $T',
32+
);
33+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 'package:meta/meta.dart';
6+
7+
import '../model.dart';
8+
import 'messages.dart';
9+
10+
/// Generic parameter is not used for encoder, because the message type cannot be detected in runtime.
11+
typedef AppMessageEncoder = Map<String, dynamic> Function(dynamic message);
12+
13+
typedef AppMessageDecoder<T> = T Function(Map<String, dynamic> message);
14+
15+
enum Channel {
16+
requestToApp,
17+
eventFromApp,
18+
responseFromApp,
19+
}
20+
21+
/// Codes to identify event types in interaction between appliocation and DevTools.
22+
///
23+
/// When application starts real tracking, it sends [started]. As soon as it
24+
/// catch new leaks, it sends [summary] information about collected leaks.
25+
///
26+
/// When user wants to get more about the collected leaks, they request details in
27+
/// DevTools, devtools sends [detailsRequest] to the app, and the app responds with
28+
/// [leakDetails].
29+
@visibleForTesting
30+
enum Codes {
31+
// Events from app.
32+
started,
33+
summary,
34+
35+
// Requests to app.
36+
detailsRequest,
37+
38+
// Successfull responses from app.
39+
leakDetails,
40+
41+
// Error responses from app.
42+
leakTrackingTurnedOffError,
43+
unexpectedError,
44+
unexpectedRequestTypeError,
45+
;
46+
47+
static Codes byName(String name) =>
48+
Codes.values.where((e) => e.name == name).single;
49+
}
50+
51+
class _JsonFields {
52+
static const envelopeCode = 'code';
53+
static const content = 'content';
54+
}
55+
56+
/// Serializes an object so that it's type can be reconstructed.
57+
Map<String, dynamic> sealEnvelope(Object message, Channel channel) {
58+
final theEnvelope = envelopeByType(message.runtimeType);
59+
assert(theEnvelope.channel == channel);
60+
return {
61+
_JsonFields.envelopeCode: theEnvelope.code.name,
62+
_JsonFields.content: theEnvelope.encode(message),
63+
};
64+
}
65+
66+
/// Deserialize [message] into an opbejct of a right type.
67+
Object openEnvelope(
68+
Map<String, dynamic> message,
69+
Channel channel,
70+
) {
71+
final envelope = envelopeByCode(message[_JsonFields.envelopeCode] as String);
72+
assert(envelope.channel == channel);
73+
return envelope.decode(message[_JsonFields.content]);
74+
}
75+
76+
/// Information necessary to serialize and deserialize an instance of type [T],
77+
/// so that the message type can be auto-detected.
78+
class _Envelope<T> {
79+
const _Envelope(this.code, this.channel, this.decode, this.encode);
80+
81+
/// Serialization code, that corresponts to [T].
82+
final Codes code;
83+
84+
/// Communication channel, that should be used for messages of type [T].
85+
final Channel channel;
86+
87+
/// Decoder for the message.
88+
final AppMessageDecoder<T> decode;
89+
90+
/// Encoder for the message.
91+
final AppMessageEncoder encode;
92+
93+
Type get type => T;
94+
}
95+
96+
/// Envelopes should be unique by message type.
97+
@visibleForTesting
98+
final envelopes = [
99+
// Events from app.
100+
101+
_Envelope<LeakTrackingStarted>(
102+
Codes.started,
103+
Channel.eventFromApp,
104+
(Map<String, dynamic> json) => LeakTrackingStarted.fromJson(json),
105+
(message) => (message as LeakTrackingStarted).toJson(),
106+
),
107+
_Envelope<LeakSummary>(
108+
Codes.summary,
109+
Channel.eventFromApp,
110+
(Map<String, dynamic> json) => LeakSummary.fromJson(json),
111+
(message) => (message as LeakSummary).toJson(),
112+
),
113+
114+
// Requests to app.
115+
116+
_Envelope<RequestForLeakDetails>(
117+
Codes.detailsRequest,
118+
Channel.requestToApp,
119+
(Map<String, dynamic> json) => RequestForLeakDetails(),
120+
(message) => {},
121+
),
122+
123+
// Responses from app.
124+
125+
_Envelope<Leaks>(
126+
Codes.leakDetails,
127+
Channel.responseFromApp,
128+
(Map<String, dynamic> json) => Leaks.fromJson(json),
129+
(message) => (message as Leaks).toJson(),
130+
),
131+
132+
_Envelope<LeakTrackingTurnedOffError>(
133+
Codes.leakTrackingTurnedOffError,
134+
Channel.responseFromApp,
135+
(Map<String, dynamic> json) => LeakTrackingTurnedOffError(),
136+
(message) => {},
137+
),
138+
139+
_Envelope<UnexpectedRequestTypeError>(
140+
Codes.unexpectedRequestTypeError,
141+
Channel.responseFromApp,
142+
(Map<String, dynamic> json) => UnexpectedRequestTypeError.fromJson(json),
143+
(message) => (message as UnexpectedRequestTypeError).toJson(),
144+
),
145+
146+
_Envelope<UnexpectedError>(
147+
Codes.unexpectedError,
148+
Channel.responseFromApp,
149+
(Map<String, dynamic> json) => UnexpectedError.fromJson(json),
150+
(message) => (message as UnexpectedError).toJson(),
151+
),
152+
];
153+
154+
_Envelope<T> envelopeByCode<T>(String codeString) {
155+
return _envelopesByCode[codeString]! as _Envelope<T>;
156+
}
157+
158+
_Envelope envelopeByType(Type type) => _envelopesByType[type]!;
159+
160+
late final _envelopesByCode = Map<String, _Envelope>.fromIterable(
161+
envelopes,
162+
key: (e) => (e as _Envelope).code.name,
163+
value: (e) => e,
164+
);
165+
166+
late final _envelopesByType = Map<Type, _Envelope>.fromIterable(
167+
envelopes,
168+
key: (e) => e.type,
169+
value: (e) => e,
170+
);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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:convert';
6+
import 'dart:developer';
7+
8+
import '../_primitives.dart';
9+
import '../model.dart';
10+
import 'delivery.dart';
11+
import 'messages.dart';
12+
import 'primitives.dart';
13+
14+
bool _extentsionRegistered = false;
15+
16+
/// Registers service extension to signal that leak tracking is
17+
/// already enabled and other leak tracking systems
18+
/// (for example, the one built into Flutter framework)
19+
/// should not be activated.
20+
///
21+
/// If the extension is already registered, returns false.
22+
bool registerLeakTrackingServiceExtension() => _registerServiceExtension(
23+
(p0, p1) async => ServiceExtensionResponse.result(jsonEncode({})),
24+
);
25+
26+
/// Registers service extension for DevTools integration.
27+
///
28+
/// If the extension is already registered, returns false.
29+
bool setupDevToolsIntegration(
30+
ObjectRef<LeakProvider?> leakProvider,
31+
) {
32+
final handler = (String method, Map<String, String> parameters) async {
33+
try {
34+
assert(method == memoryLeakTrackingExtensionName);
35+
36+
final theLeakProvider = leakProvider.value;
37+
38+
if (theLeakProvider == null) {
39+
return ResponseFromApp(LeakTrackingTurnedOffError())
40+
.toServiceResponse();
41+
}
42+
43+
final request = RequestToApp.fromRequestParameters(parameters).message;
44+
45+
if (request is RequestForLeakDetails) {
46+
return ResponseFromApp(theLeakProvider.collectLeaks())
47+
.toServiceResponse();
48+
}
49+
50+
return ResponseFromApp(
51+
UnexpectedRequestTypeError(request.runtimeType),
52+
).toServiceResponse();
53+
} catch (error, stack) {
54+
print(
55+
'Error handling leak tracking request from DevTools to application.',
56+
);
57+
print(error);
58+
print(stack);
59+
60+
return ResponseFromApp(UnexpectedError(error, stack)).toServiceResponse();
61+
}
62+
};
63+
64+
final result = _registerServiceExtension(handler);
65+
66+
EventFromApp(LeakTrackingStarted(appLeakTrackerProtocolVersion)).post();
67+
68+
return result;
69+
}
70+
71+
bool _registerServiceExtension(
72+
Future<ServiceExtensionResponse> Function(String, Map<String, String>)
73+
handler,
74+
) {
75+
if (_extentsionRegistered) return false;
76+
try {
77+
registerExtension(
78+
memoryLeakTrackingExtensionName,
79+
handler,
80+
);
81+
_extentsionRegistered = true;
82+
83+
return true;
84+
} on ArgumentError catch (ex) {
85+
// Return false if extension is already registered.
86+
final bool isAlreadyRegisteredError = ex.toString().contains('registered');
87+
if (isAlreadyRegisteredError) {
88+
return false;
89+
} else {
90+
rethrow;
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)