[vm] Update ObjectSlots to handle _LinkedHashBase

Generalize code which computes slots map to handle class hierarchies
which include base classes with VM-described layout like
_LinkedHashBase.

Fixes https://github.com/dart-lang/sdk/issues/63180

TEST=vm/dart/heap_snapshot_regress_63180

Change-Id: I32ab0e258e735f745d45f25813177d0f6a6a6964
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/496861
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Slava Egorov <vegorov@google.com>
This commit is contained in:
Slava Egorov
2026-04-21 04:02:25 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 5ae14a808c
commit 488f97eb76
6 changed files with 86 additions and 76 deletions
@@ -0,0 +1,26 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
import 'dart:developer';
import 'package:expect/expect.dart';
import 'package:path/path.dart' as path;
import 'heap_snapshot_test.dart';
import 'use_flag_test_helper.dart';
main() async {
if (const bool.fromEnvironment('dart.vm.product')) return;
await withTempDir('heap_snapshot_test', (String dir) async {
final file = path.join(dir, 'state1.heapsnapshot');
final map = LinkedHashMap(
equals: (a, b) => a == b,
hashCode: (a) => a.hashCode,
);
map[1] = 2;
NativeRuntime.writeHeapSnapshotToFile(file);
});
}
@@ -212,7 +212,8 @@ size unique-size count class data
}
await run(
'stats foobar = (follow (follow global) ^:type_arguments ^Root ^Smi)');
'stats foobar = (follow (follow global) ^:type_arguments ^Root ^Smi)',
);
expectLogPattern('''
size count class
-------- -------- --------
@@ -225,13 +226,13 @@ size count class
await run('examine users foobar');
expectLogPattern(r'''
_List@\d+ .* {
type_arguments_
type_arguments
length_
\[0\] *Foo@\d+ .*/cli_test.dart
\[1\] *Foo@\d+ .*/cli_test.dart
}
_List@\d+ .* {
type_arguments_
type_arguments
length_
\[0\] *Bar@\d+ .*/cli_test.dart
\[1\] *Bar@\d+ .*/cli_test.dart
@@ -367,8 +368,9 @@ class Global {
// report their length (such as /dev/zero).
final bool supportsExternalTypedDataTest = File('/dev/zero').existsSync();
final Uint8List externalTypedData1234567 =
File('/dev/zero').openSync().readSync(1234567);
final Uint8List externalTypedData1234567 = File(
'/dev/zero',
).openSync().readSync(1234567);
final weakTest = WeakTest(Object());
@@ -378,8 +380,8 @@ class WeakTest {
final Finalizer finalizer;
WeakTest(this.object)
: weakList = List.filled(1, WeakReference(object)),
finalizer = Finalizer((_) {})..attach(object, Object(), detach: object);
: weakList = List.filled(1, WeakReference(object)),
finalizer = Finalizer((_) {})..attach(object, Object(), detach: object);
String get use => '$object|$weakList|$finalizer';
}
+2
View File
@@ -12417,6 +12417,8 @@ class LinkedHashBase : public Instance {
static constexpr intptr_t kInitialIndexSize = 1 << (kInitialIndexBits + 1);
static constexpr intptr_t kUninitializedIndexSize = 1;
static const ClassId kClassId = kLinkedHashBaseCid;
private:
LinkedHashBasePtr ptr() const { return static_cast<LinkedHashBasePtr>(ptr_); }
UntaggedLinkedHashBase* untag() const {
+41 -37
View File
@@ -67,48 +67,52 @@ class ObjectSlots {
if (!cls.is_finalized()) continue;
auto slots = cid2object_slots_[cid] = new ObjectSlotsType();
for (const auto& entry : OffsetsTable::offsets_table()) {
if (entry.class_id == cid) {
slots->Add(ObjectSlot(entry.offset, entry.is_compressed_pointer,
entry.field_name));
}
// If the class has native fields, the native fields array is the first
// field and therefore starts after the `kWordSize` tagging word.
if (cls.num_native_fields() > 0) {
slots->Add(ObjectSlot(kWordSize, true, "native_fields"));
}
// If the class or any super class is generic, it will have a type
// arguments vector.
const auto tav_offset = cls.host_type_arguments_field_offset();
if (tav_offset != Class::kNoTypeArguments) {
slots->Add(ObjectSlot(tav_offset, true, "type_arguments"));
}
// The VM doesn't define a layout for the object, so it's a regular Dart
// class.
if (slots->is_empty()) {
// If the class has native fields, the native fields array is the first
// field and therefore starts after the `kWordSize` tagging word.
if (cls.num_native_fields() > 0) {
slots->Add(ObjectSlot(kWordSize, true, "native_fields"));
}
// If the class or any super class is generic, it will have a type
// arguments vector.
const auto tav_offset = cls.host_type_arguments_field_offset();
if (tav_offset != Class::kNoTypeArguments) {
slots->Add(ObjectSlot(tav_offset, true, "type_arguments"));
}
// Add slots for all user-defined instance fields in the hierarchy.
while (!cls.IsNull()) {
fields = cls.fields();
if (!fields.IsNull()) {
for (intptr_t i = 0; i < fields.Length(); ++i) {
field ^= fields.At(i);
if (!field.is_instance()) continue;
name = field.name();
// If the field is unboxed, we don't know the size of it (may be
// multiple words) - but that doesn't matter because
// a) we will process instances using the slots we collect
// (instead of regular GC visitor);
// b) we will not write the value of the field and instead treat
// it like a dummy reference to 0 (like we do with Smis).
slots->Add(ObjectSlot(field.HostOffset(), !field.is_unboxed(),
name.ToCString()));
// Add slots for all user-defined instance fields in the hierarchy.
while (!cls.IsNull()) {
const intptr_t current_cid = cls.id();
if (current_cid < kNumPredefinedCids) {
bool slots_added = false;
for (const auto& entry : OffsetsTable::offsets_table()) {
if (entry.class_id == current_cid) {
slots->Add(ObjectSlot(entry.offset, entry.is_compressed_pointer,
entry.field_name));
slots_added = true;
}
}
cls = cls.SuperClass();
if (slots_added) {
break;
}
}
fields = cls.fields();
if (!fields.IsNull()) {
for (intptr_t i = 0; i < fields.Length(); ++i) {
field ^= fields.At(i);
if (!field.is_instance()) continue;
name = field.name();
// If the field is unboxed, we don't know the size of it (may be
// multiple words) - but that doesn't matter because
// a) we will process instances using the slots we collect
// (instead of regular GC visitor);
// b) we will not write the value of the field and instead treat
// it like a dummy reference to 0 (like we do with Smis).
slots->Add(ObjectSlot(field.HostOffset(), !field.is_unboxed(),
name.ToCString()));
}
}
cls = cls.SuperClass();
}
// We sort the slots, so we'll visit the slots in memory order.
+6 -32
View File
@@ -155,37 +155,15 @@ namespace dart {
F(Closure, length_and_flags_) \
F(Closure, hash_) \
F(String, length_) \
F(Array, type_arguments_) \
F(Array, length_) \
F(ImmutableArray, type_arguments_) \
F(ImmutableArray, length_) \
F(GrowableObjectArray, type_arguments_) \
F(GrowableObjectArray, length_) \
F(GrowableObjectArray, data_) \
F(Map, type_arguments_) \
F(Map, index_) \
F(Map, hash_mask_) \
F(Map, data_) \
F(Map, used_data_) \
F(Map, deleted_keys_) \
F(ConstMap, type_arguments_) \
F(ConstMap, index_) \
F(ConstMap, hash_mask_) \
F(ConstMap, data_) \
F(ConstMap, used_data_) \
F(ConstMap, deleted_keys_) \
F(Set, type_arguments_) \
F(Set, index_) \
F(Set, hash_mask_) \
F(Set, data_) \
F(Set, used_data_) \
F(Set, deleted_keys_) \
F(ConstSet, type_arguments_) \
F(ConstSet, index_) \
F(ConstSet, hash_mask_) \
F(ConstSet, data_) \
F(ConstSet, used_data_) \
F(ConstSet, deleted_keys_) \
F(LinkedHashBase, hash_mask_) \
F(LinkedHashBase, data_) \
F(LinkedHashBase, used_data_) \
F(LinkedHashBase, deleted_keys_) \
F(LinkedHashBase, index_) \
F(TypedData, length_) \
F(ExternalTypedData, length_) \
F(ReceivePort, send_port_) \
@@ -207,12 +185,10 @@ namespace dart {
F(WeakProperty, key_) \
F(WeakProperty, value_) \
F(WeakReference, target_) \
F(WeakReference, type_arguments_) \
F(Finalizer, detachments_) \
F(Finalizer, all_entries_) \
F(Finalizer, entries_collected_) \
F(Finalizer, callback_) \
F(Finalizer, type_arguments_) \
F(NativeFinalizer, detachments_) \
F(NativeFinalizer, all_entries_) \
F(NativeFinalizer, entries_collected_) \
@@ -225,7 +201,6 @@ namespace dart {
F(MirrorReference, referent_) \
F(UserTag, label_) \
F(Pointer, data_) \
F(Pointer, type_arguments_) \
F(DynamicLibrary, handle_) \
F(DynamicLibrary, isClosed_) \
F(DynamicLibrary, canBeClosed_) \
@@ -234,8 +209,7 @@ namespace dart {
F(FfiTrampolineData, callback_exceptional_return_) \
F(TypedDataView, length_) \
F(TypedDataView, typed_data_) \
F(TypedDataView, offset_in_bytes_) \
F(FutureOr, type_arguments_)
F(TypedDataView, offset_in_bytes_)
#define AOT_CLASSES_AND_FIELDS(F)
+2
View File
@@ -19,6 +19,8 @@
namespace dart {
class Zone;
#if defined(DART_PRECOMPILER) || defined(DART_ENABLE_HEAP_SNAPSHOT_WRITER)
class OffsetsTable : public ZoneObject {