diff --git a/runtime/tests/vm/dart/heap_snapshot_regress_63180_test.dart b/runtime/tests/vm/dart/heap_snapshot_regress_63180_test.dart new file mode 100644 index 00000000000..5c494b43140 --- /dev/null +++ b/runtime/tests/vm/dart/heap_snapshot_regress_63180_test.dart @@ -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); + }); +} diff --git a/runtime/tools/heapsnapshot/test/cli_test.dart b/runtime/tools/heapsnapshot/test/cli_test.dart index 7b1920ba918..91073182076 100644 --- a/runtime/tools/heapsnapshot/test/cli_test.dart +++ b/runtime/tools/heapsnapshot/test/cli_test.dart @@ -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'; } diff --git a/runtime/vm/object.h b/runtime/vm/object.h index 46dd66637c0..46de032f0cf 100644 --- a/runtime/vm/object.h +++ b/runtime/vm/object.h @@ -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(ptr_); } UntaggedLinkedHashBase* untag() const { diff --git a/runtime/vm/object_graph.cc b/runtime/vm/object_graph.cc index d6b8f546347..13f4e37524f 100644 --- a/runtime/vm/object_graph.cc +++ b/runtime/vm/object_graph.cc @@ -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. diff --git a/runtime/vm/raw_object_fields.cc b/runtime/vm/raw_object_fields.cc index 8ad2257de96..b00099eed89 100644 --- a/runtime/vm/raw_object_fields.cc +++ b/runtime/vm/raw_object_fields.cc @@ -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) diff --git a/runtime/vm/raw_object_fields.h b/runtime/vm/raw_object_fields.h index e28cd4a901e..dc66eeadf54 100644 --- a/runtime/vm/raw_object_fields.h +++ b/runtime/vm/raw_object_fields.h @@ -19,6 +19,8 @@ namespace dart { +class Zone; + #if defined(DART_PRECOMPILER) || defined(DART_ENABLE_HEAP_SNAPSHOT_WRITER) class OffsetsTable : public ZoneObject {