Skip to content

Commit e79b571

Browse files
authored
Correct units and cumulative bug in GC times (#9890)
Fixes #7080 In the logging page, GC events were displayed with summaries like idle collection in 290 ms. The value 290 was calculated directly from the cumulative times (`newSpace.time` and `oldSpace.time`), meaning it represented the total cumulative GC time since isolate startup rather than the duration of this specific GC. In reality, the individual collection took 290 microseconds (which is 0.29 milliseconds), but since it was displaying the cumulative time of 290 ms, it was off by a factor of 1000x. In this fix, we update `_handleGCEvent` to track and subtract the previous cumulative GC time from the new cumulative GC time to compute the actual duration delta for this specific GC. Also format the duration as a millisecond double with 1 decimal place (consistent with how frame times are displayed), and appended it to the log summary (e.g. "idle collection in 100.0 ms").
1 parent 1c7e423 commit e79b571

4 files changed

Lines changed: 167 additions & 26 deletions

File tree

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

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

241241
late final LogDetailsController logDetailsController;
242242

243+
/// Tracks the previous cumulative GC times (in seconds) for each active isolate.
244+
///
245+
/// This is used to compute the actual duration of the current GC event (by taking
246+
/// the delta between consecutive cumulative times), rather than displaying the
247+
/// total cumulative GC time.
248+
final _previousGcTimesByIsolate = <String, double>{};
249+
243250
List<LogData> data = <LogData>[];
244251

245252
final selectedLog = ValueNotifier<LogData?>(null);
@@ -288,6 +295,7 @@ class LoggingController extends DevToolsScreenController
288295

289296
void clear() {
290297
_updateData([]);
298+
_previousGcTimesByIsolate.clear();
291299
serviceConnection.errorBadgeManager.clearErrorCount(LoggingScreen.id);
292300
}
293301

@@ -455,11 +463,24 @@ class LoggingController extends DevToolsScreenController
455463
final usedBytes = newSpace.used! + oldSpace.used!;
456464
final capacityBytes = newSpace.capacity! + oldSpace.capacity!;
457465

458-
final time = ((newSpace.time! + oldSpace.time!) * 1000).round();
466+
final isolateId = e.isolate?.id;
467+
// Cumulative time, in seconds.
468+
final newCumulativeTime = newSpace.time! + oldSpace.time!;
469+
470+
String durationText = '';
471+
if (isolateId != null) {
472+
final previousGcTime = _previousGcTimesByIsolate[isolateId];
473+
_previousGcTimesByIsolate[isolateId] = newCumulativeTime;
474+
if (previousGcTime != null && newCumulativeTime >= previousGcTime) {
475+
// Multiply by 1000 to display in milliseconds.
476+
final durationMs = (newCumulativeTime - previousGcTime) * 1000;
477+
durationText = ' in ${durationMs.toStringAsFixed(1)} ms';
478+
}
479+
}
459480

460481
final summary =
461482
'${isolateRef['name']} • '
462-
'${e.json!['reason']} collection in $time ms • '
483+
'${e.json!['reason']} collection$durationText • '
463484
'${printBytes(usedBytes, unit: ByteUnit.mb, includeUnit: true)} used of '
464485
'${printBytes(capacityBytes, unit: ByteUnit.mb, includeUnit: true)}';
465486

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)