Skip to content

Commit ea55a77

Browse files
authored
Stop double counting self-referenced classes (flutter#4955)
1 parent 9ccf1cd commit ea55a77

File tree

6 files changed

+236
-25
lines changed

6 files changed

+236
-25
lines changed

packages/devtools_app/lib/src/screens/memory/panes/diff/controller/heap_diff.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,17 @@ class ObjectSetDiff {
166166
final object = before.objectsByCodes[code] ?? after.objectsByCodes[code]!;
167167

168168
if (inBefore) {
169-
deleted.countInstance(object);
170-
delta.uncountInstance(object);
169+
final excludeFromRetained =
170+
before.notCountedInRetained.contains(object.code);
171+
deleted.countInstance(object, excludeFromRetained: excludeFromRetained);
172+
delta.uncountInstance(object, excludeFromRetained: excludeFromRetained);
171173
continue;
172174
}
173175
if (inAfter) {
174-
created.countInstance(object);
175-
delta.countInstance(object);
176+
final excludeFromRetained =
177+
after.notCountedInRetained.contains(object.code);
178+
created.countInstance(object, excludeFromRetained: excludeFromRetained);
179+
delta.countInstance(object, excludeFromRetained: excludeFromRetained);
176180
continue;
177181
}
178182
assert(false);

packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,19 @@ class SingleClassStats extends ClassStats {
133133
assert(!isSealed);
134134
final object = data.objects[objectIndex];
135135
assert(object.heapClass.fullName == heapClass.fullName);
136-
objects.countInstance(object);
137136

138137
final path = data.retainingPath(objectIndex);
138+
objects.countInstance(
139+
object,
140+
excludeFromRetained: path?.isRetainedBySameClass ?? false,
141+
);
142+
139143
if (path == null) return;
140144
final objectsForPath = statsByPath.putIfAbsent(
141145
ClassOnlyHeapPath(path),
142146
() => ObjectSet(),
143147
);
144-
objectsForPath.countInstance(object);
148+
objectsForPath.countInstance(object, excludeFromRetained: false);
145149
}
146150

147151
bool get isZero => objects.isZero;
@@ -174,16 +178,22 @@ class ObjectSetStats with Sealable {
174178
bool get isZero =>
175179
shallowSize == 0 && retainedSize == 0 && instanceCount == 0;
176180

177-
void countInstance(AdaptedHeapObject object) {
181+
void countInstance(
182+
AdaptedHeapObject object, {
183+
required bool excludeFromRetained,
184+
}) {
178185
assert(!isSealed);
179-
retainedSize += object.retainedSize!;
186+
if (!excludeFromRetained) retainedSize += object.retainedSize!;
180187
shallowSize += object.shallowSize;
181188
instanceCount++;
182189
}
183190

184-
void uncountInstance(AdaptedHeapObject object) {
191+
void uncountInstance(
192+
AdaptedHeapObject object, {
193+
required bool excludeFromRetained,
194+
}) {
185195
assert(!isSealed);
186-
retainedSize -= object.retainedSize!;
196+
if (!excludeFromRetained) retainedSize -= object.retainedSize!;
187197
shallowSize -= object.shallowSize;
188198
instanceCount--;
189199
}
@@ -194,19 +204,27 @@ class ObjectSet extends ObjectSetStats {
194204
static ObjectSet empty = ObjectSet()..seal();
195205

196206
final objectsByCodes = <IdentityHashCode, AdaptedHeapObject>{};
207+
final notCountedInRetained = <IdentityHashCode>{};
197208

198209
@override
199210
bool get isZero => objectsByCodes.isEmpty;
200211

201212
@override
202-
void countInstance(AdaptedHeapObject object) {
213+
void countInstance(
214+
AdaptedHeapObject object, {
215+
required bool excludeFromRetained,
216+
}) {
203217
if (objectsByCodes.containsKey(object.code)) return;
204-
super.countInstance(object);
218+
super.countInstance(object, excludeFromRetained: excludeFromRetained);
205219
objectsByCodes[object.code] = object;
220+
if (excludeFromRetained) notCountedInRetained.add(object.code);
206221
}
207222

208223
@override
209-
void uncountInstance(AdaptedHeapObject object) {
224+
void uncountInstance(
225+
AdaptedHeapObject object, {
226+
required bool excludeFromRetained,
227+
}) {
210228
throw AssertionError('uncountInstance is not valid for $ObjectSet');
211229
}
212230
}

packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,26 @@ class AdaptedHeapData {
108108
typedef IdentityHashCode = int;
109109

110110
/// Sequence of ids of objects in the heap.
111-
///
112-
/// TODO(polina-c): maybe we do not need to store path by objects.
113-
/// It can be that only classes are interesting, and we can save some
114-
/// performance on this object. It will become clear when the leak tracking
115-
/// feature stabilizes.
116111
class HeapPath {
117112
HeapPath(this.objects);
118113

119114
final List<AdaptedHeapObject> objects;
120115

116+
late final bool isRetainedBySameClass = () {
117+
if (objects.length < 2) return false;
118+
119+
final theClass = objects.last.heapClass;
120+
121+
return objects
122+
.take(objects.length - 1)
123+
.any((object) => object.heapClass == theClass);
124+
}();
125+
121126
/// Retaining path for the object in string format.
122-
String? shortPath() => '/${objects.map((o) => o.shortName).join('/')}/';
127+
String shortPath() => '/${objects.map((o) => o.shortName).join('/')}/';
123128

124129
/// Retaining path for the object as an array of the retaining objects.
125-
List<String>? detailedPath() =>
130+
List<String> detailedPath() =>
126131
objects.map((o) => o.name).toList(growable: false);
127132
}
128133

packages/devtools_app/test/memory/shared/heap/heap_analyzer_test.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,9 @@ class _SizeTest {
230230
required this.unreachableSize,
231231
}) : assert(_assertHeapIndexIsCode(heap));
232232

233-
/// For convenience of testing each heap object has code equal to the
234-
/// index in array.
235233
final AdaptedHeapData heap;
236-
237234
final String name;
238-
239235
final int rootRetainedSize;
240-
241236
final int unreachableSize;
242237
}
243238

@@ -272,6 +267,8 @@ AdaptedHeapObject _createOneByteWeakObject(
272267
return result;
273268
}
274269

270+
/// For convenience of testing each heap object has code equal to the
271+
/// index in array.
275272
bool _assertHeapIndexIsCode(AdaptedHeapData heap) => heap.objects
276273
.asMap()
277274
.entries
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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:devtools_app/src/screens/memory/shared/heap/heap.dart';
6+
import 'package:devtools_app/src/screens/memory/shared/heap/model.dart';
7+
import 'package:devtools_app/src/screens/memory/shared/heap/spanning_tree.dart';
8+
import 'package:devtools_app/src/screens/memory/shared/primitives/class_name.dart';
9+
10+
import 'package:flutter_test/flutter_test.dart';
11+
12+
class _ClassSizeTest {
13+
_ClassSizeTest({
14+
required this.name,
15+
required this.heap,
16+
required this.expectedClassARetainedSize,
17+
}) : assert(_assertHeapIndexIsCode(heap)) {
18+
buildSpanningTree(heap);
19+
}
20+
21+
final AdaptedHeapData heap;
22+
final String name;
23+
final int expectedClassARetainedSize;
24+
}
25+
26+
final _root = HeapClassName(className: 'Root', library: 'l');
27+
final _classA = HeapClassName(className: 'A', library: 'l');
28+
final _classB = HeapClassName(className: 'B', library: 'l');
29+
30+
final _classSizeTests = <_ClassSizeTest>[
31+
_ClassSizeTest(
32+
name: 'separate',
33+
heap: AdaptedHeapData(
34+
[
35+
_createOneByteObject(0, [1, 2, 3], _root),
36+
_createOneByteObject(1, [], _classA),
37+
_createOneByteObject(2, [], _classA),
38+
_createOneByteObject(3, [], _classA),
39+
],
40+
rootIndex: 0,
41+
),
42+
expectedClassARetainedSize: 3,
43+
),
44+
_ClassSizeTest(
45+
name: 'linked',
46+
heap: AdaptedHeapData(
47+
[
48+
_createOneByteObject(0, [1], _root),
49+
_createOneByteObject(1, [2], _classA),
50+
_createOneByteObject(2, [3], _classA),
51+
_createOneByteObject(3, [], _classA),
52+
],
53+
rootIndex: 0,
54+
),
55+
expectedClassARetainedSize: 3,
56+
),
57+
_ClassSizeTest(
58+
name: 'full graph',
59+
heap: AdaptedHeapData(
60+
[
61+
_createOneByteObject(0, [1], _root),
62+
_createOneByteObject(1, [2, 3], _classA),
63+
_createOneByteObject(2, [3, 1], _classA),
64+
_createOneByteObject(3, [1, 2], _classA),
65+
],
66+
rootIndex: 0,
67+
),
68+
expectedClassARetainedSize: 3,
69+
),
70+
_ClassSizeTest(
71+
name: 'with global B',
72+
heap: AdaptedHeapData(
73+
[
74+
_createOneByteObject(0, [1], _root),
75+
_createOneByteObject(1, [2, 4], _classA),
76+
_createOneByteObject(2, [3, 4], _classA),
77+
_createOneByteObject(3, [4], _classA),
78+
_createOneByteObject(4, [], _classB),
79+
],
80+
rootIndex: 0,
81+
),
82+
expectedClassARetainedSize: 4,
83+
),
84+
];
85+
86+
void main() {
87+
test('$SingleClassStats does not double-count self-referenced classes.', () {
88+
for (final t in _classSizeTests) {
89+
final classes = SingleClassStats(_classA);
90+
for (final o in t.heap.objects) {
91+
if (o.heapClass == _classA) classes.countInstance(t.heap, o.code);
92+
}
93+
expect(
94+
classes.objects.retainedSize,
95+
t.expectedClassARetainedSize,
96+
reason: t.name,
97+
);
98+
}
99+
});
100+
}
101+
102+
AdaptedHeapObject _createOneByteObject(
103+
int codeAndIndex,
104+
List<int> references,
105+
HeapClassName theClass,
106+
) =>
107+
AdaptedHeapObject(
108+
code: codeAndIndex,
109+
references: references,
110+
heapClass: theClass,
111+
shallowSize: 1,
112+
);
113+
114+
/// For convenience of testing each heap object has code equal to the
115+
/// index in array.
116+
bool _assertHeapIndexIsCode(AdaptedHeapData heap) => heap.objects
117+
.asMap()
118+
.entries
119+
.every((entry) => entry.key == entry.value.code);

packages/devtools_app/test/memory/shared/heap/model_test.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,56 @@ import 'package:devtools_app/src/screens/memory/shared/heap/model.dart';
66
import 'package:devtools_app/src/screens/memory/shared/primitives/class_name.dart';
77
import 'package:flutter_test/flutter_test.dart';
88

9+
class _HeapPathTest {
10+
_HeapPathTest(
11+
this.name,
12+
this.heapPath, {
13+
required this.isRetainedBySameClass,
14+
});
15+
16+
final String name;
17+
final HeapPath heapPath;
18+
final bool isRetainedBySameClass;
19+
}
20+
21+
final _heapPathTests = <_HeapPathTest>[
22+
_HeapPathTest(
23+
'empty',
24+
HeapPath([]),
25+
isRetainedBySameClass: false,
26+
),
27+
_HeapPathTest(
28+
'one item',
29+
HeapPath([_objectForClass('myLib', 'myClass')]),
30+
isRetainedBySameClass: false,
31+
),
32+
_HeapPathTest(
33+
'two different',
34+
HeapPath([
35+
_objectForClass('myLib1', 'myClass'),
36+
_objectForClass('myLib2', 'myClass'),
37+
]),
38+
isRetainedBySameClass: false,
39+
),
40+
_HeapPathTest(
41+
'two identical',
42+
HeapPath([
43+
_objectForClass('myLib', 'myClass'),
44+
_objectForClass('myLib', 'myClass'),
45+
]),
46+
isRetainedBySameClass: true,
47+
),
48+
_HeapPathTest(
49+
'three identical',
50+
HeapPath([
51+
_objectForClass('myLib', 'myClass'),
52+
_objectForClass('myLib', 'myClass'),
53+
_objectForClass('myLib', 'myClass'),
54+
]),
55+
isRetainedBySameClass: true,
56+
),
57+
];
58+
959
void main() {
1060
test('$AdaptedHeapData serializes.', () {
1161
final json = AdaptedHeapData(
@@ -26,4 +76,22 @@ void main() {
2676

2777
expect(json, AdaptedHeapData.fromJson(json).toJson());
2878
});
79+
80+
test('$HeapPath.isRetainedBySameClass returns expected result for.', () {
81+
for (final t in _heapPathTests) {
82+
expect(
83+
t.heapPath.isRetainedBySameClass,
84+
t.isRetainedBySameClass,
85+
reason: t.name,
86+
);
87+
}
88+
});
2989
}
90+
91+
AdaptedHeapObject _objectForClass(String lib, String theClass) =>
92+
AdaptedHeapObject(
93+
code: 1,
94+
references: [],
95+
heapClass: HeapClassName(className: theClass, library: lib),
96+
shallowSize: 1,
97+
);

0 commit comments

Comments
 (0)