Skip to content

Commit 0960f24

Browse files
Merge branch 'master' into dcm-2
2 parents f29602d + c5a863a commit 0960f24

6 files changed

Lines changed: 170 additions & 32 deletions

File tree

analysis_options.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ dart_code_metrics:
166166
- integration_test/**
167167
# Investigate internal usages of inspector_controller before removing.
168168
- lib/src/screens/inspector/**_controller.dart
169-
- lib/src/extensions/**
170-
- lib/src/framework/**
171169
- lib/src/service/**
172170
- lib/src/shared/**
173171
- lib/src/standalone_ui/**

packages/devtools_app/lib/src/framework/home_screen.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class _ConnectInputState extends State<ConnectInput> with BlockingActionMixin {
171171
// developers who tend to repeatedly restart DevTools to debug the same
172172
// test application.
173173
final uri = await storage.getValue(_debugVmServiceUriKey);
174-
if (uri != null) {
174+
if (uri != null && mounted) {
175175
setState(() {
176176
connectDialogController.text = uri;
177177
});
@@ -253,10 +253,9 @@ class _ConnectInputState extends State<ConnectInput> with BlockingActionMixin {
253253
return;
254254
}
255255

256-
assert(() {
256+
if (kDebugMode) {
257257
safeUnawaited(storage.setValue(_debugVmServiceUriKey, uri));
258-
return true;
259-
}());
258+
}
260259

261260
// Cache the routerDelegate and notifications providers before the async
262261
// gap as the landing screen may not be displayed by the time the async gap

packages/devtools_app/lib/src/screens/logging/logging_controller.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ class LoggingController extends DevToolsScreenController
233233

234234
late final LogDetailsController logDetailsController;
235235

236+
/// Tracks the previous cumulative GC times (in seconds) for each active isolate.
237+
///
238+
/// This is used to compute the actual duration of the current GC event (by taking
239+
/// the delta between consecutive cumulative times), rather than displaying the
240+
/// total cumulative GC time.
241+
final _previousGcTimesByIsolate = <String, double>{};
242+
236243
List<LogData> data = <LogData>[];
237244

238245
final selectedLog = ValueNotifier<LogData?>(null);
@@ -281,6 +288,7 @@ class LoggingController extends DevToolsScreenController
281288

282289
void clear() {
283290
_updateData([]);
291+
_previousGcTimesByIsolate.clear();
284292
serviceConnection.errorBadgeManager.clearErrorCount(LoggingScreen.id);
285293
}
286294

@@ -448,11 +456,24 @@ class LoggingController extends DevToolsScreenController
448456
final usedBytes = newSpace.used! + oldSpace.used!;
449457
final capacityBytes = newSpace.capacity! + oldSpace.capacity!;
450458

451-
final time = ((newSpace.time! + oldSpace.time!) * 1000).round();
459+
final isolateId = e.isolate?.id;
460+
// Cumulative time, in seconds.
461+
final newCumulativeTime = newSpace.time! + oldSpace.time!;
462+
463+
String durationText = '';
464+
if (isolateId != null) {
465+
final previousGcTime = _previousGcTimesByIsolate[isolateId];
466+
_previousGcTimesByIsolate[isolateId] = newCumulativeTime;
467+
if (previousGcTime != null && newCumulativeTime >= previousGcTime) {
468+
// Multiply by 1000 to display in milliseconds.
469+
final durationMs = (newCumulativeTime - previousGcTime) * 1000;
470+
durationText = ' in ${durationMs.toStringAsFixed(1)} ms';
471+
}
472+
}
452473

453474
final summary =
454475
'${isolateRef['name']} • '
455-
'${e.json!['reason']} collection in $time ms • '
476+
'${e.json!['reason']} collection$durationText • '
456477
'${printBytes(usedBytes, unit: ByteUnit.mb, includeUnit: true)} used of '
457478
'${printBytes(capacityBytes, unit: ByteUnit.mb, includeUnit: true)}';
458479

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ TODO: Remove this section if there are not any updates.
4545

4646
## Logging updates
4747

48-
TODO: Remove this section if there are not any updates.
48+
* Correct time units and cumulative nature of GC events.
49+
[#9890](https://github.com/flutter/devtools/pull/9890)
4950

5051
## App size tool updates
5152

packages/devtools_app/test/screens/logging/logging_controller_test.dart

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,42 @@ import 'package:vm_service/vm_service.dart';
1919
void main() {
2020
var timestampCounter = 0;
2121

22+
Event gcEvent({
23+
required double newTime,
24+
required double oldTime,
25+
required String isolateId,
26+
}) => Event.parse({
27+
'kind': EventKind.kGC,
28+
'timestamp': ++timestampCounter,
29+
'isolate': {
30+
'type': '@Isolate',
31+
'id': isolateId,
32+
'name': 'main',
33+
'number': '1',
34+
},
35+
'reason': 'idle',
36+
'new': {
37+
'type': 'HeapSpace',
38+
'name': 'new',
39+
'collections': 1,
40+
'avgCollectionPeriodMillis': 100.0,
41+
'used': 1024,
42+
'capacity': 2048,
43+
'external': 0,
44+
'time': newTime,
45+
},
46+
'old': {
47+
'type': 'HeapSpace',
48+
'name': 'old',
49+
'collections': 1,
50+
'avgCollectionPeriodMillis': 100.0,
51+
'used': 4096,
52+
'capacity': 8192,
53+
'external': 0,
54+
'time': oldTime,
55+
},
56+
})!;
57+
2258
group('LoggingController', () {
2359
late LoggingController controller;
2460

@@ -194,6 +230,94 @@ void main() {
194230
expect(controller.filteredData.value, isEmpty);
195231
});
196232

233+
test('GC events track duration delta', () async {
234+
final fakeService =
235+
serviceConnection.serviceManager.service as FakeVmServiceWrapper;
236+
237+
expect(controller.data, isEmpty);
238+
239+
// First GC event: baseline established, no duration printed.
240+
fakeService.emitGCEvent(
241+
event: gcEvent(newTime: 0.1, oldTime: 0.2, isolateId: 'isolates/123'),
242+
);
243+
await Future<void>.delayed(const Duration(milliseconds: 10));
244+
245+
expect(controller.data, hasLength(1));
246+
expect(
247+
controller.data.first.summary,
248+
'main • idle collection • 0.0 MB used of 0.0 MB',
249+
);
250+
251+
// Second GC event: delta is
252+
// (0.15 + 0.25) - (0.1 + 0.2) = 0.4 - 0.3 = 0.1s = 100ms.
253+
fakeService.emitGCEvent(
254+
event: gcEvent(newTime: 0.15, oldTime: 0.25, isolateId: 'isolates/123'),
255+
);
256+
await Future<void>.delayed(const Duration(milliseconds: 10));
257+
258+
expect(controller.data, hasLength(2));
259+
expect(
260+
controller.data[1].summary,
261+
'main • idle collection in 100.0 ms • 0.0 MB used of 0.0 MB',
262+
);
263+
264+
// Third GC event on a different isolate: no baseline yet, so no duration
265+
// printed.
266+
fakeService.emitGCEvent(
267+
event: gcEvent(newTime: 0.05, oldTime: 0.05, isolateId: 'isolates/456'),
268+
);
269+
await Future<void>.delayed(const Duration(milliseconds: 10));
270+
271+
expect(controller.data, hasLength(3));
272+
expect(
273+
controller.data[2].summary,
274+
'main • idle collection • 0.0 MB used of 0.0 MB',
275+
);
276+
277+
// Fourth GC event: clock reset on isolates/123 (cumulative goes from 0.4s to 0.1s).
278+
// Since newCumulativeTime (0.1s) < previousGcTime (0.4s), duration delta is skipped,
279+
// and baseline is updated to 0.1s.
280+
fakeService.emitGCEvent(
281+
event: gcEvent(newTime: 0.05, oldTime: 0.05, isolateId: 'isolates/123'),
282+
);
283+
await Future<void>.delayed(const Duration(milliseconds: 10));
284+
285+
expect(controller.data, hasLength(4));
286+
expect(
287+
controller.data[3].summary,
288+
'main • idle collection • 0.0 MB used of 0.0 MB',
289+
);
290+
291+
// Fifth GC event: delta since reset baseline (0.1s) is (0.08 + 0.12) - 0.1 = 0.2 - 0.1 = 0.1s = 100ms.
292+
fakeService.emitGCEvent(
293+
event: gcEvent(newTime: 0.08, oldTime: 0.12, isolateId: 'isolates/123'),
294+
);
295+
await Future<void>.delayed(const Duration(milliseconds: 10));
296+
297+
expect(controller.data, hasLength(5));
298+
expect(
299+
controller.data[4].summary,
300+
'main • idle collection in 100.0 ms • 0.0 MB used of 0.0 MB',
301+
);
302+
303+
// Clear logs: resets baselines.
304+
controller.clear();
305+
expect(controller.data, isEmpty);
306+
307+
// Sixth GC event (same isolate as first): baseline was cleared, so no
308+
// duration printed.
309+
fakeService.emitGCEvent(
310+
event: gcEvent(newTime: 0.2, oldTime: 0.3, isolateId: 'isolates/123'),
311+
);
312+
await Future<void>.delayed(const Duration(milliseconds: 10));
313+
314+
expect(controller.data, hasLength(1));
315+
expect(
316+
controller.data.first.summary,
317+
'main • idle collection • 0.0 MB used of 0.0 MB',
318+
);
319+
});
320+
197321
test('matchesForSearch - default filters', () {
198322
prepareTestLogs();
199323

packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,30 +128,25 @@ class FakeVmServiceWrapper extends Fake implements VmServiceWrapper {
128128
}
129129

130130
@override
131-
Future<UriList> lookupPackageUris(String isolateId, List<String> uris) {
132-
return Future.value(
133-
UriList(
134-
uris: _resolvedUriMap != null
135-
? (uris.map((e) => _resolvedUriMap[e]).toList())
136-
: null,
137-
),
138-
);
139-
}
131+
Future<UriList> lookupPackageUris(
132+
String isolateId,
133+
List<String> uris,
134+
) => Future.syncValue(UriList(
135+
uris: _resolvedUriMap != null
136+
? (uris.map((e) => _resolvedUriMap[e]).toList())
137+
: null
138+
));
140139

141140
@override
142141
Future<UriList> lookupResolvedPackageUris(
143142
String isolateId,
144143
List<String> uris, {
145144
bool? local,
146-
}) {
147-
return Future.value(
148-
UriList(
149-
uris: _reverseResolvedUriMap != null
150-
? (uris.map((e) => _reverseResolvedUriMap[e]).toList())
151-
: null,
152-
),
153-
);
154-
}
145+
}) => Future.syncValue(UriList(
146+
uris: _reverseResolvedUriMap != null
147+
? (uris.map((e) => _reverseResolvedUriMap[e]).toList())
148+
: null,
149+
));
155150

156151
@override
157152
String get wsUri => 'ws://127.0.0.1:56137/ISsyt6ki0no=/ws';
@@ -190,14 +185,14 @@ class FakeVmServiceWrapper extends Fake implements VmServiceWrapper {
190185
allocationProfile.json = allocationProfile.toJson();
191186
// Fake GC statistics
192187
allocationProfile.json![AllocationProfilePrivateViewExtension.heapsKey] =
193-
<String, dynamic>{
194-
AllocationProfilePrivateViewExtension.newSpaceKey: <String, dynamic>{
188+
<String, Object?>{
189+
AllocationProfilePrivateViewExtension.newSpaceKey: <String, Object?>{
195190
GCStats.usedKey: 1234,
196191
GCStats.capacityKey: 12345,
197192
GCStats.collectionsKey: 42,
198193
GCStats.timeKey: 69,
199194
},
200-
AllocationProfilePrivateViewExtension.oldSpaceKey: <String, dynamic>{
195+
AllocationProfilePrivateViewExtension.oldSpaceKey: <String, Object?>{
201196
GCStats.usedKey: 4321,
202197
GCStats.capacityKey: 54321,
203198
GCStats.collectionsKey: 24,
@@ -522,8 +517,8 @@ class FakeVmServiceWrapper extends Fake implements VmServiceWrapper {
522517
@override
523518
Stream<Event> get onGCEvent => _gcEventStream.stream;
524519

525-
void emitGCEvent() {
526-
_gcEventStream.sink.add(Event(kind: EventKind.kGC));
520+
void emitGCEvent({Event? event}) {
521+
_gcEventStream.sink.add(event ?? Event(kind: EventKind.kGC));
527522
}
528523

529524
@override

0 commit comments

Comments
 (0)