Skip to content

Commit 1504dc6

Browse files
mkustermannCommit Queue
authored andcommitted
[vm] Extend analyze_snapshot tool to emit more detailed information
In release builds most `Field` objects are tree shaken, but if an instance field is e.g. a `late final` field then the `Field` object is retained in the snapshot for the `LateInitializationError` slow path. Though `Field` will not appear in `Class.fields()`, it will only be referenced in the `ObjectPool`. => Instead of dumping `Class.fields()` we collect all `Field`s with the same owner and dump those (a superset of `Class.fields()`) We dump more information for a `Field`: Whether it's a reference field or unboxed field (in which case we dump what unboxed type it is). We also dump field flags (e.g. `late`, `final`, `static`). We also dump now a `instance_slots` property on the class which describes every slot in the instance. If a slot in an instance * has a corresponding `Field` we emit `instance_field` type and the id of the `Field` object. * has no corresponding `Field` we emit a `unknown_slot` type * we emit whether the slot is a reference field This brings the information the `analyze_snapshot` tool dumps closer to what Blutter uses, but some differences remain, e.g.: * Blutter seems to scan constant instances of classes (if there's any) and tries to determine based on those constants what unboxed slots could mean. It may look at 8 bytes and "guess" whether it's more likely to be an integer or a double. (Side note: It doesn't consider `Float32x4`/`Float64x2`) => The guess can be incorrect, so we do not do that. * Blutter seems to scan constant instances of classes (if there's any) and tries to guess the type of a reference field based on what that constant's field points to. => The guess can be incorrect, so we do not do that. go/dart-ama TEST=vm/dart/analyze_snapshot_binary_test Change-Id: I116560ba5e5f89d4349f2227362b8494e3af7a1c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/425261 Reviewed-by: Slava Egorov <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
1 parent a077989 commit 1504dc6

File tree

3 files changed

+511
-11
lines changed

3 files changed

+511
-11
lines changed

runtime/tests/vm/dart/analyze_snapshot_binary_test.dart

Lines changed: 289 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:convert';
66
import 'dart:io';
7+
import 'dart:ffi';
78
import 'dart:async';
89

910
import 'package:expect/expect.dart';
@@ -12,6 +13,10 @@ import 'package:path/path.dart' as path;
1213

1314
import 'use_flag_test_helper.dart';
1415

16+
const int headerSize = 8;
17+
final int compressedWordSize =
18+
sizeOf<Pointer>() == 8 && !Platform.executable.contains('64C') ? 8 : 4;
19+
1520
// Used to ensure we don't have multiple equivalent calls to test.
1621
final _seenDescriptions = <String>{};
1722

@@ -23,7 +28,8 @@ Future<void> testAOT(
2328
bool stripFlag = false,
2429
bool disassemble = false,
2530
}) async {
26-
if (const bool.fromEnvironment('dart.vm.product') && disassemble) {
31+
const isProduct = const bool.fromEnvironment('dart.vm.product');
32+
if (isProduct && disassemble) {
2733
Expect.isFalse(disassemble, 'no use of disassembler in PRODUCT mode');
2834
}
2935

@@ -196,6 +202,156 @@ Future<void> testAOT(
196202
implementedInterfaces[mySubClassId]!.single,
197203
);
198204

205+
// Ensure instance fields of classes are reported.
206+
final baseClass =
207+
objects[classnames.entries
208+
.singleWhere((e) => e.value == 'FieldTestBase')
209+
.key];
210+
final subClass =
211+
objects[classnames.entries
212+
.singleWhere((e) => e.value == 'FieldTestSub')
213+
.key];
214+
final baseFieldIds = baseClass['fields'];
215+
final baseFields = [for (final int id in baseFieldIds) objects[id]];
216+
final baseSlots = baseClass['instance_slots'];
217+
final subFieldIds = subClass['fields'];
218+
final subFields = [for (final int id in subFieldIds) objects[id]];
219+
final subSlots = subClass['instance_slots'];
220+
221+
// We have:
222+
// class Base {
223+
// static int baseS = int.parse('1');
224+
// int base0;
225+
// double base1;
226+
// Object? base2;
227+
// Float32x4 base3;
228+
// Float64x2 base4;
229+
// }
230+
//
231+
// This static field is never tree shaken.
232+
expectField(baseFields[0], name: 'baseS', flags: ['static']);
233+
if (isProduct) {
234+
// Most [Field] objests are tree shaken.
235+
Expect.equals(1, baseFields.length);
236+
237+
int slotOffset = 0;
238+
slotOffset += expectUnknown8Bytes(
239+
baseSlots.skip(slotOffset),
240+
offsetReferences: 0,
241+
offsetBytes: 0,
242+
);
243+
slotOffset += expectUnknown8Bytes(
244+
baseSlots.skip(slotOffset),
245+
offsetReferences: 0,
246+
offsetBytes: 8,
247+
);
248+
slotOffset += expectUnknownReference(
249+
baseSlots.skip(slotOffset),
250+
offsetReferences: 0,
251+
offsetBytes: 16,
252+
);
253+
slotOffset += expectUnknown16Bytes(
254+
baseSlots.skip(slotOffset),
255+
offsetReferences: 1,
256+
offsetBytes: 16,
257+
);
258+
slotOffset += expectUnknown16Bytes(
259+
baseSlots.skip(slotOffset),
260+
offsetReferences: 1,
261+
offsetBytes: 32,
262+
);
263+
} else {
264+
// We don't tree shake [Field] objects in non-product builds.
265+
Expect.equals(6, baseFields.length);
266+
expectField(
267+
baseFields[1],
268+
name: 'base0',
269+
isReference: false,
270+
unboxedType: 'int',
271+
);
272+
expectField(
273+
baseFields[2],
274+
name: 'base1',
275+
isReference: false,
276+
unboxedType: 'double',
277+
);
278+
expectField(baseFields[3], name: 'base2');
279+
expectField(
280+
baseFields[4],
281+
name: 'base3',
282+
isReference: false,
283+
unboxedType: 'Float32x4',
284+
);
285+
expectField(
286+
baseFields[5],
287+
name: 'base4',
288+
isReference: false,
289+
unboxedType: 'Float64x2',
290+
);
291+
int slotOffset = 0;
292+
slotOffset += expectInstanceSlot(
293+
baseSlots[slotOffset],
294+
offsetReferences: 0,
295+
offsetBytes: 0,
296+
isReference: false,
297+
fieldId: baseFieldIds[1],
298+
);
299+
slotOffset += expectInstanceSlot(
300+
baseSlots[slotOffset],
301+
offsetReferences: 0,
302+
offsetBytes: 8,
303+
isReference: false,
304+
fieldId: baseFieldIds[2],
305+
);
306+
slotOffset += expectInstanceSlot(
307+
baseSlots[slotOffset],
308+
offsetReferences: 0,
309+
offsetBytes: 16,
310+
fieldId: baseFieldIds[3],
311+
);
312+
slotOffset += expectInstanceSlot(
313+
baseSlots[slotOffset],
314+
offsetReferences: 1,
315+
offsetBytes: 16,
316+
isReference: false,
317+
fieldId: baseFieldIds[4],
318+
);
319+
slotOffset += expectInstanceSlot(
320+
baseSlots[slotOffset],
321+
offsetReferences: 1,
322+
offsetBytes: 32,
323+
isReference: false,
324+
fieldId: baseFieldIds[5],
325+
);
326+
}
327+
// We have:
328+
// class Sub<T> extends Base{
329+
// late int subL1 = int.parse('1');
330+
// late final double subL2 = double.parse('1.2');
331+
// }
332+
// These late instance fields are never tree shaken.
333+
expectField(subFields[0], name: 'subL1', flags: ['late']);
334+
expectField(subFields[1], name: 'subL2', flags: ['final', 'late']);
335+
expectTypeArgumentsSlot(
336+
subSlots[0],
337+
offsetReferences: 1,
338+
offsetBytes: 8 + 8 + 16 + 16,
339+
);
340+
expectSlot(
341+
subSlots[1],
342+
offsetReferences: 2,
343+
offsetBytes: 8 + 8 + 16 + 16,
344+
type: 'instance_field',
345+
fieldId: subFieldIds[0],
346+
);
347+
expectSlot(
348+
subSlots[2],
349+
offsetReferences: 3,
350+
offsetBytes: 8 + 8 + 16 + 16,
351+
type: 'instance_field',
352+
fieldId: subFieldIds[1],
353+
);
354+
199355
Expect.isTrue(
200356
analyzerJson['metadata'].containsKey('analyzer_version'),
201357
'snapshot analyzer version must be reported',
@@ -207,6 +363,136 @@ Future<void> testAOT(
207363
});
208364
}
209365

366+
void expectField(
367+
Map fieldJson, {
368+
required String name,
369+
List<String> flags = const [],
370+
bool isReference = true,
371+
String? unboxedType,
372+
}) {
373+
Expect.equals(name, fieldJson['name']);
374+
Expect.equals(isReference, fieldJson['is_reference']);
375+
Expect.listEquals(flags, fieldJson['flags']);
376+
if (unboxedType != null) {
377+
Expect.equals(unboxedType, fieldJson['unboxed_type']);
378+
} else {
379+
Expect.isFalse(fieldJson.containsKey('unboxed_type'));
380+
}
381+
}
382+
383+
void expectSlot(
384+
Map slotJson, {
385+
required int offsetReferences,
386+
required int offsetBytes,
387+
required String type,
388+
bool isReference = true,
389+
int? fieldId,
390+
}) {
391+
Expect.equals(type, slotJson['slot_type']);
392+
if (fieldId != null) {
393+
Expect.equals(fieldId, slotJson['field']);
394+
} else {
395+
Expect.isFalse(slotJson.containsKey('field'));
396+
}
397+
final int offset =
398+
headerSize + offsetReferences * compressedWordSize + offsetBytes;
399+
Expect.equals(offset, slotJson['offset']);
400+
Expect.equals(isReference, slotJson['is_reference']);
401+
}
402+
403+
int expectInstanceSlot(
404+
Map slotJson, {
405+
required int offsetReferences,
406+
required int offsetBytes,
407+
bool isReference = true,
408+
int? fieldId,
409+
}) {
410+
expectSlot(
411+
slotJson,
412+
isReference: isReference,
413+
fieldId: fieldId,
414+
offsetReferences: offsetReferences,
415+
offsetBytes: offsetBytes,
416+
type: 'instance_field',
417+
);
418+
return 1;
419+
}
420+
421+
int expectUnknownReference(
422+
Map slotJson, {
423+
required int offsetReferences,
424+
required int offsetBytes,
425+
}) {
426+
expectSlot(
427+
slotJson,
428+
offsetReferences: offsetReferences,
429+
offsetBytes: offsetBytes,
430+
type: 'unknown_slot',
431+
);
432+
return 1;
433+
}
434+
435+
int expectTypeArgumentsSlot(
436+
Map slotJson, {
437+
required int offsetReferences,
438+
required int offsetBytes,
439+
}) {
440+
expectSlot(
441+
slotJson,
442+
offsetReferences: offsetReferences,
443+
offsetBytes: offsetBytes,
444+
type: 'type_arguments_field',
445+
);
446+
return 1;
447+
}
448+
449+
int expectUnknown8Bytes(
450+
Iterable<Map> slotJson, {
451+
required int offsetReferences,
452+
required int offsetBytes,
453+
}) {
454+
final it = slotJson.iterator;
455+
Expect.isTrue(it.moveNext());
456+
expectSlot(
457+
it.current,
458+
isReference: false,
459+
offsetReferences: offsetReferences,
460+
offsetBytes: offsetBytes,
461+
type: 'unknown_slot',
462+
);
463+
if (compressedWordSize == 8) {
464+
return 1;
465+
}
466+
Expect.isTrue(it.moveNext());
467+
expectSlot(
468+
it.current,
469+
isReference: false,
470+
offsetReferences: offsetReferences,
471+
offsetBytes: offsetBytes + compressedWordSize,
472+
type: 'unknown_slot',
473+
);
474+
return 2;
475+
}
476+
477+
int expectUnknown16Bytes(
478+
Iterable<Map> slotJson, {
479+
required int offsetReferences,
480+
required int offsetBytes,
481+
}) {
482+
int slots = 0;
483+
slots += expectUnknown8Bytes(
484+
slotJson,
485+
offsetReferences: offsetReferences,
486+
offsetBytes: offsetBytes,
487+
);
488+
slots += expectUnknown8Bytes(
489+
slotJson.skip(slots),
490+
offsetReferences: offsetReferences,
491+
offsetBytes: offsetBytes + 8,
492+
);
493+
return slots;
494+
}
495+
210496
main() async {
211497
void printSkip(String description) => print(
212498
'Skipping $description for ${path.basename(buildDir)} '
@@ -222,7 +508,7 @@ main() async {
222508

223509
await withTempDir('analyze_snapshot_binary', (String tempDir) async {
224510
// We only need to generate the dill file once for all JIT tests.
225-
final _thisTestPath = path.join(
511+
final thisTestPath = path.join(
226512
sdkDir,
227513
'runtime',
228514
'tests',
@@ -245,7 +531,7 @@ main() async {
245531
),
246532
'-o',
247533
aotDillPath,
248-
_thisTestPath,
534+
thisTestPath,
249535
]);
250536

251537
// Just as a reminder for AOT tests:

runtime/tests/vm/dart/analyze_snapshot_program.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:typed_data';
6+
57
class MethodChannel {
68
final String name;
79
const MethodChannel(this.name);
@@ -28,4 +30,42 @@ main() {
2830
mcs[i].dump();
2931
}
3032
print('Class: ${MySub()}');
33+
34+
final l = [
35+
FieldTestBase(2, 2.2, null, Float32x4.zero(), Float64x2.zero()),
36+
FieldTestSub<int>(),
37+
FieldTestSub<double>(),
38+
];
39+
for (final x in l) print(x.foo());
40+
}
41+
42+
class FieldTestBase {
43+
static int baseS = int.parse('1');
44+
int base0;
45+
double base1;
46+
Object? base2;
47+
Float32x4 base3;
48+
Float64x2 base4;
49+
50+
FieldTestBase(this.base0, this.base1, this.base2, this.base3, this.base4) {
51+
baseS++;
52+
}
53+
54+
String foo() => 'Base.foo: [$baseS, $base0, $base1, $base2, $base3, $base4]';
55+
}
56+
57+
class FieldTestSub<T> extends FieldTestBase {
58+
late int subL1 = int.parse('1');
59+
late final double subL2 = double.parse('1.2');
60+
61+
FieldTestSub()
62+
: super(
63+
1,
64+
1.2,
65+
Object(),
66+
Float32x4(1.1, 1.2, 1.3, 1.4),
67+
Float64x2(2.1, 2.2),
68+
);
69+
70+
String foo() => '${super.foo()} Sub<$T>.foo: [$subL1, $subL2]';
3171
}

0 commit comments

Comments
 (0)