| // Copyright (c) 2012, 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. |
| |
| #ifndef RUNTIME_VM_CLASS_TABLE_H_ |
| #define RUNTIME_VM_CLASS_TABLE_H_ |
| |
| #include <memory> |
| #include <tuple> |
| #include <utility> |
| |
| #include "platform/allocation.h" |
| #include "platform/assert.h" |
| #include "platform/atomic.h" |
| #include "platform/utils.h" |
| |
| #include "vm/bitfield.h" |
| #include "vm/class_id.h" |
| #include "vm/flags.h" |
| #include "vm/globals.h" |
| #include "vm/tagged_pointer.h" |
| |
| namespace dart { |
| |
| class Class; |
| class ClassTable; |
| class Isolate; |
| class IsolateGroup; |
| class JSONArray; |
| class JSONObject; |
| class JSONStream; |
| template <typename T> |
| class MallocGrowableArray; |
| class ObjectPointerVisitor; |
| class PersistentHandle; |
| |
| // A 64-bit bitmap describing unboxed fields in a class. |
| // |
| // There is a bit for each word in an instance of the class. |
| // |
| // Words corresponding to set bits must be ignored by the GC because they |
| // don't contain pointers. All words beyond the first 64 words of an object |
| // are expected to contain pointers. |
| class UnboxedFieldBitmap { |
| public: |
| UnboxedFieldBitmap() : bitmap_(0) {} |
| explicit UnboxedFieldBitmap(uint64_t bitmap) : bitmap_(bitmap) {} |
| UnboxedFieldBitmap(const UnboxedFieldBitmap&) = default; |
| UnboxedFieldBitmap& operator=(const UnboxedFieldBitmap&) = default; |
| |
| DART_FORCE_INLINE bool Get(intptr_t position) const { |
| if (position >= Length()) return false; |
| return Utils::TestBit(bitmap_, position); |
| } |
| DART_FORCE_INLINE void Set(intptr_t position) { |
| ASSERT(position < Length()); |
| bitmap_ |= Utils::Bit<decltype(bitmap_)>(position); |
| } |
| DART_FORCE_INLINE void Clear(intptr_t position) { |
| ASSERT(position < Length()); |
| bitmap_ &= ~Utils::Bit<decltype(bitmap_)>(position); |
| } |
| DART_FORCE_INLINE uint64_t Value() const { return bitmap_; } |
| DART_FORCE_INLINE bool IsEmpty() const { return bitmap_ == 0; } |
| DART_FORCE_INLINE void Reset() { bitmap_ = 0; } |
| |
| DART_FORCE_INLINE static constexpr intptr_t Length() { |
| return sizeof(decltype(bitmap_)) * kBitsPerByte; |
| } |
| |
| private: |
| uint64_t bitmap_; |
| }; |
| |
| // Allocator used to manage memory for ClassTable arrays and ClassTable |
| // objects themselves. |
| // |
| // This allocator provides delayed free functionality: normally class tables |
| // can't be freed unless all mutator and helper threads are stopped because |
| // some of these threads might be holding a pointer to a table which we |
| // want to free. Instead of stopping the world whenever we need to free |
| // a table (e.g. freeing old table after growing) we delay freeing until an |
| // occasional GC which will need to stop the world anyway. |
| class ClassTableAllocator : public ValueObject { |
| public: |
| ClassTableAllocator(); |
| ~ClassTableAllocator(); |
| |
| // Allocate an array of T with |len| elements. |
| // |
| // Does *not* initialize the memory. |
| template <class T> |
| inline T* Alloc(intptr_t len) { |
| return reinterpret_cast<T*>(dart::malloc(len * sizeof(T))); |
| } |
| |
| // Allocate a zero initialized array of T with |len| elements. |
| template <class T> |
| inline T* AllocZeroInitialized(intptr_t len) { |
| return reinterpret_cast<T*>(dart::calloc(len, sizeof(T))); |
| } |
| |
| // Clone the given |array| with |size| elements. |
| template <class T> |
| inline T* Clone(T* array, intptr_t size) { |
| if (array == nullptr) { |
| ASSERT(size == 0); |
| return nullptr; |
| } |
| auto result = Alloc<T>(size); |
| memmove(result, array, size * sizeof(T)); |
| return result; |
| } |
| |
| // Copy |size| elements from the given |array| into a new |
| // array with space for |new_size| elements. Then |Free| |
| // the original |array|. |
| // |
| // |new_size| is expected to be larger than |size|. |
| template <class T> |
| inline T* Realloc(T* array, intptr_t size, intptr_t new_size) { |
| ASSERT(size < new_size); |
| auto result = AllocZeroInitialized<T>(new_size); |
| if (size != 0) { |
| ASSERT(result != nullptr); |
| memmove(result, array, size * sizeof(T)); |
| } |
| Free(array); |
| return result; |
| } |
| |
| // Schedule deletion of the given ClassTable. |
| void Free(ClassTable* table); |
| |
| // Schedule freeing of the given pointer. |
| void Free(void* ptr); |
| |
| // Free all objects which were scheduled by |Free|. Expected to only be |
| // called on |IsolateGroup| shutdown or when the world is stopped and no |
| // thread can be using a stale class table pointer. |
| void FreePending(); |
| |
| private: |
| typedef void (*Deleter)(void*); |
| MallocGrowableArray<std::pair<void*, Deleter>>* pending_freed_; |
| }; |
| |
| // A table with the given |Columns| indexed by class id. |
| // |
| // Each column is a continuous array of a the given type. All columns have |
| // the same number of used elements (|num_cids()|) and the same capacity. |
| template <typename CidType, typename... Columns> |
| class CidIndexedTable { |
| public: |
| explicit CidIndexedTable(ClassTableAllocator* allocator) |
| : allocator_(allocator) {} |
| |
| ~CidIndexedTable() { |
| std::apply([&](auto&... column) { (allocator_->Free(column.load()), ...); }, |
| columns_); |
| } |
| |
| CidIndexedTable(const CidIndexedTable& other) = delete; |
| |
| void SetNumCidsAndCapacity(intptr_t new_num_cids, intptr_t new_capacity) { |
| columns_ = std::apply( |
| [&](auto&... column) { |
| return std::make_tuple( |
| allocator_->Realloc(column.load(), num_cids_, new_capacity)...); |
| }, |
| columns_); |
| capacity_ = new_capacity; |
| SetNumCids(new_num_cids); |
| } |
| |
| void AllocateIndex(intptr_t index, bool* did_grow) { |
| *did_grow = EnsureCapacity(index); |
| SetNumCids(Utils::Maximum(num_cids_, index + 1)); |
| } |
| |
| intptr_t AddRow(bool* did_grow) { |
| *did_grow = EnsureCapacity(num_cids_); |
| intptr_t id = num_cids_; |
| SetNumCids(num_cids_ + 1); |
| return id; |
| } |
| |
| void ShrinkTo(intptr_t new_num_cids) { |
| ASSERT(new_num_cids <= num_cids_); |
| num_cids_ = new_num_cids; |
| } |
| |
| bool IsValidIndex(intptr_t index) const { |
| return 0 <= index && index < num_cids_; |
| } |
| |
| void CopyFrom(const CidIndexedTable& other) { |
| ASSERT(allocator_ == other.allocator_); |
| |
| std::apply([&](auto&... column) { (allocator_->Free(column.load()), ...); }, |
| columns_); |
| |
| columns_ = std::apply( |
| [&](auto&... column) { |
| return std::make_tuple( |
| allocator_->Clone(column.load(), other.num_cids_)...); |
| }, |
| other.columns_); |
| capacity_ = num_cids_ = other.num_cids_; |
| } |
| |
| void Remap(intptr_t* old_to_new_cid) { |
| CidIndexedTable clone(allocator_); |
| clone.CopyFrom(*this); |
| RemapAllColumns(clone, old_to_new_cid, |
| std::index_sequence_for<Columns...>{}); |
| } |
| |
| template < |
| intptr_t kColumnIndex, |
| typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>> |
| T* GetColumn() { |
| return std::get<kColumnIndex>(columns_).load(); |
| } |
| |
| template < |
| intptr_t kColumnIndex, |
| typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>> |
| const T* GetColumn() const { |
| return std::get<kColumnIndex>(columns_).load(); |
| } |
| |
| template < |
| intptr_t kColumnIndex, |
| typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>> |
| T& At(intptr_t index) { |
| ASSERT(IsValidIndex(index)); |
| return GetColumn<kColumnIndex>()[index]; |
| } |
| |
| template < |
| intptr_t kColumnIndex, |
| typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>> |
| const T& At(intptr_t index) const { |
| ASSERT(IsValidIndex(index)); |
| return GetColumn<kColumnIndex>()[index]; |
| } |
| |
| intptr_t num_cids() const { return num_cids_; } |
| intptr_t capacity() const { return capacity_; } |
| |
| private: |
| friend class ClassTable; |
| |
| // Wrapper around AcqRelAtomic<T*> which makes it assignable and copyable |
| // so that we could put it inside an std::tuple. |
| template <typename T> |
| struct Ptr { |
| Ptr() : ptr(nullptr) {} |
| Ptr(T* ptr) : ptr(ptr) {} // NOLINT |
| |
| Ptr(const Ptr& other) { ptr.store(other.ptr.load()); } |
| |
| Ptr& operator=(const Ptr& other) { |
| ptr.store(other.load()); |
| return *this; |
| } |
| |
| T* load() const { return ptr.load(); } |
| |
| AcqRelAtomic<T*> ptr = {nullptr}; |
| }; |
| |
| void SetNumCids(intptr_t new_num_cids) { |
| if (new_num_cids > kClassIdTagMax) { |
| FATAL("Too many classes"); |
| } |
| num_cids_ = new_num_cids; |
| } |
| |
| bool EnsureCapacity(intptr_t index) { |
| if (index >= capacity_) { |
| SetNumCidsAndCapacity(num_cids_, index + kCapacityIncrement); |
| return true; |
| } |
| return false; |
| } |
| |
| template <intptr_t kColumnIndex> |
| void RemapColumn(const CidIndexedTable& old, intptr_t* old_to_new_cid) { |
| auto new_column = GetColumn<kColumnIndex>(); |
| auto old_column = old.GetColumn<kColumnIndex>(); |
| for (intptr_t i = 0; i < num_cids_; i++) { |
| new_column[old_to_new_cid[i]] = old_column[i]; |
| } |
| } |
| |
| template <std::size_t... Is> |
| void RemapAllColumns(const CidIndexedTable& old, |
| intptr_t* old_to_new_cid, |
| std::index_sequence<Is...>) { |
| (RemapColumn<Is>(old, old_to_new_cid), ...); |
| } |
| |
| static constexpr intptr_t kCapacityIncrement = 256; |
| |
| ClassTableAllocator* allocator_; |
| intptr_t num_cids_ = 0; |
| intptr_t capacity_ = 0; |
| std::tuple<Ptr<Columns>...> columns_; |
| }; |
| |
| // Registry of all known classes. |
| // |
| // The GC will only use information about instance size and unboxed field maps |
| // to scan instances and will not access class objects themselves. This |
| // information is stored in separate columns of the |classes_| table. |
| // |
| // # Concurrency & atomicity |
| // |
| // This table is read concurrently without locking (e.g. by GC threads) so |
| // there are some invariants that need to be observed when working with it. |
| // |
| // * When table is updated (e.g. when the table is grown or a new class is |
| // registered in a table) there must be a release barrier after the update. |
| // Such barrier will ensure that stores which populate the table are not |
| // reordered past the store which exposes the new grown table or exposes |
| // a new class id; |
| // * Old versions of the table can only be freed when the world is stopped: |
| // no mutator and no helper threads are running. To avoid freeing a table |
| // which some other thread is reading from. |
| // |
| // Note that torn reads are not a concern (e.g. it is fine to use |
| // memmove to copy class table contents) as long as an appropriate |
| // barrier is issued before the copy of the table can be observed. |
| // |
| // # Hot reload |
| // |
| // Each IsolateGroup contains two ClassTable fields: |class_table| and |
| // |heap_walk_class_table|. GC visitors use the second field to get ClassTable |
| // instance which they will use for visiting pointers inside instances in |
| // the heap. Usually these two fields will be pointing to the same table, |
| // except when IsolateGroup is in the middle of reload. |
| // |
| // When reloading |class_table| will be pointing to a copy of the original |
| // table. Kernel loading will be modifying this table, while GC |
| // workers can continue using original table still available through |
| // |heap_walk_class_table|. If hot reload succeeds, |heap_walk_class_table| |
| // will be dropped and |class_table| will become the source of truth. Otherwise, |
| // original table will be restored from |heap_walk_class_table|. |
| // |
| // See IsolateGroup methods CloneClassTableForReload, RestoreOriginalClassTable, |
| // DropOriginalClassTable. |
| class ClassTable : public MallocAllocated { |
| public: |
| explicit ClassTable(ClassTableAllocator* allocator); |
| |
| ~ClassTable(); |
| |
| ClassTable* Clone() const { return new ClassTable(*this); } |
| |
| ClassPtr At(intptr_t cid) const { |
| if (IsTopLevelCid(cid)) { |
| return top_level_classes_.At<kClassIndex>(IndexFromTopLevelCid(cid)); |
| } |
| return classes_.At<kClassIndex>(cid); |
| } |
| |
| int32_t SizeAt(intptr_t index) const { |
| if (IsTopLevelCid(index)) { |
| return 0; |
| } |
| return classes_.At<kSizeIndex>(index); |
| } |
| |
| void SetAt(intptr_t index, ClassPtr raw_cls); |
| void UpdateClassSize(intptr_t cid, ClassPtr raw_cls); |
| |
| bool IsValidIndex(intptr_t cid) const { |
| if (IsTopLevelCid(cid)) { |
| return top_level_classes_.IsValidIndex(IndexFromTopLevelCid(cid)); |
| } |
| return classes_.IsValidIndex(cid); |
| } |
| |
| bool HasValidClassAt(intptr_t cid) const { return At(cid) != nullptr; } |
| |
| UnboxedFieldBitmap GetUnboxedFieldsMapAt(intptr_t cid) const { |
| ASSERT(IsValidIndex(cid)); |
| return classes_.At<kUnboxedFieldBitmapIndex>(cid); |
| } |
| |
| void SetUnboxedFieldsMapAt(intptr_t cid, UnboxedFieldBitmap map) { |
| ASSERT(IsValidIndex(cid)); |
| classes_.At<kUnboxedFieldBitmapIndex>(cid) = map; |
| } |
| |
| #if !defined(PRODUCT) |
| bool ShouldTraceAllocationFor(intptr_t cid) { |
| return !IsTopLevelCid(cid) && |
| (classes_.At<kAllocationTracingStateIndex>(cid) != kTracingDisabled); |
| } |
| |
| void SetTraceAllocationFor(intptr_t cid, bool trace) { |
| classes_.At<kAllocationTracingStateIndex>(cid) = |
| trace ? kTraceAllocationBit : kTracingDisabled; |
| } |
| |
| void SetCollectInstancesFor(intptr_t cid, bool trace) { |
| auto& slot = classes_.At<kAllocationTracingStateIndex>(cid); |
| if (trace) { |
| slot |= kCollectInstancesBit; |
| } else { |
| slot &= ~kCollectInstancesBit; |
| } |
| } |
| |
| bool CollectInstancesFor(intptr_t cid) { |
| auto& slot = classes_.At<kAllocationTracingStateIndex>(cid); |
| return (slot & kCollectInstancesBit) != 0; |
| } |
| |
| void UpdateCachedAllocationTracingStateTablePointer() { |
| cached_allocation_tracing_state_table_.store( |
| classes_.GetColumn<kAllocationTracingStateIndex>()); |
| } |
| #else |
| void UpdateCachedAllocationTracingStateTablePointer() {} |
| #endif // !defined(PRODUCT) |
| |
| #if !defined(PRODUCT) || defined(FORCE_INCLUDE_SAMPLING_HEAP_PROFILER) |
| void PopulateUserVisibleNames(); |
| |
| const char* UserVisibleNameFor(intptr_t cid) { |
| if (!classes_.IsValidIndex(cid)) { |
| return nullptr; |
| } |
| return classes_.At<kClassNameIndex>(cid); |
| } |
| |
| void SetUserVisibleNameFor(intptr_t cid, const char* name) { |
| ASSERT(classes_.At<kClassNameIndex>(cid) == nullptr); |
| classes_.At<kClassNameIndex>(cid) = name; |
| } |
| #endif // !defined(PRODUCT) || defined(FORCE_INCLUDE_SAMPLING_HEAP_PROFILER) |
| |
| intptr_t NumCids() const { return classes_.num_cids(); } |
| intptr_t Capacity() const { return classes_.capacity(); } |
| |
| intptr_t NumTopLevelCids() const { return top_level_classes_.num_cids(); } |
| |
| void Register(const Class& cls); |
| void AllocateIndex(intptr_t index); |
| |
| void RegisterTopLevel(const Class& cls); |
| void UnregisterTopLevel(intptr_t index); |
| |
| void Remap(intptr_t* old_to_new_cids); |
| |
| void VisitObjectPointers(ObjectPointerVisitor* visitor); |
| |
| // If a snapshot reader has populated the class table then the |
| // sizes in the class table are not correct. Iterates through the |
| // table, updating the sizes. |
| void CopySizesFromClassObjects(); |
| |
| void Validate(); |
| |
| void Print(); |
| |
| #if defined(DART_PRECOMPILER) |
| void PrintObjectLayout(const char* filename); |
| #endif |
| |
| #ifndef PRODUCT |
| // Describes layout of heap stats for code generation. See offset_extractor.cc |
| struct ArrayTraits { |
| static intptr_t elements_start_offset() { return 0; } |
| |
| static constexpr intptr_t kElementSize = sizeof(uint8_t); |
| }; |
| |
| static intptr_t allocation_tracing_state_table_offset() { |
| static_assert(sizeof(cached_allocation_tracing_state_table_) == kWordSize); |
| return OFFSET_OF(ClassTable, cached_allocation_tracing_state_table_); |
| } |
| |
| void AllocationProfilePrintJSON(JSONStream* stream, bool internal); |
| |
| void PrintToJSONObject(JSONObject* object); |
| #endif // !PRODUCT |
| |
| // Deallocates table copies. Do not call during concurrent access to table. |
| void FreeOldTables(); |
| |
| static bool IsTopLevelCid(intptr_t cid) { return cid >= kTopLevelCidOffset; } |
| |
| static intptr_t IndexFromTopLevelCid(intptr_t cid) { |
| ASSERT(IsTopLevelCid(cid)); |
| return cid - kTopLevelCidOffset; |
| } |
| |
| static intptr_t CidFromTopLevelIndex(intptr_t index) { |
| return kTopLevelCidOffset + index; |
| } |
| |
| private: |
| friend class ClassTableAllocator; |
| friend class Dart; |
| friend Isolate* CreateWithinExistingIsolateGroup(IsolateGroup* group, |
| const char* name, |
| char** error); |
| friend class IsolateGroup; // for table() |
| static constexpr int kInitialCapacity = 512; |
| |
| static constexpr intptr_t kTopLevelCidOffset = kClassIdTagMax + 1; |
| |
| ClassTable(const ClassTable& original) |
| : allocator_(original.allocator_), |
| classes_(original.allocator_), |
| top_level_classes_(original.allocator_) { |
| classes_.CopyFrom(original.classes_); |
| |
| #if !defined(PRODUCT) || defined(FORCE_INCLUDE_SAMPLING_HEAP_PROFILER) |
| // Copying classes_ doesn't perform a deep copy. Ensure we duplicate |
| // the class names to avoid double free crashes at shutdown. |
| for (intptr_t cid = 1; cid < classes_.num_cids(); ++cid) { |
| if (classes_.IsValidIndex(cid)) { |
| const char* cls_name = classes_.At<kClassNameIndex>(cid); |
| if (cls_name != nullptr) { |
| classes_.At<kClassNameIndex>(cid) = Utils::StrDup(cls_name); |
| } |
| } |
| } |
| #endif // !defined(PRODUCT) || defined(FORCE_INCLUDE_SAMPLING_HEAP_PROFILER) |
| |
| top_level_classes_.CopyFrom(original.top_level_classes_); |
| UpdateCachedAllocationTracingStateTablePointer(); |
| } |
| |
| void AllocateTopLevelIndex(intptr_t index); |
| |
| ClassPtr* table() { return classes_.GetColumn<kClassIndex>(); } |
| |
| // Used to drop recently added classes. |
| void SetNumCids(intptr_t num_cids, intptr_t num_tlc_cids) { |
| classes_.ShrinkTo(num_cids); |
| top_level_classes_.ShrinkTo(num_tlc_cids); |
| } |
| |
| ClassTableAllocator* allocator_; |
| |
| // Unfortunately std::tuple used by CidIndexedTable does not have a stable |
| // layout so we can't refer to its elements from generated code. |
| NOT_IN_PRODUCT(AcqRelAtomic<uint8_t*> cached_allocation_tracing_state_table_ = |
| {nullptr}); |
| |
| enum { |
| kClassIndex = 0, |
| kSizeIndex, |
| kUnboxedFieldBitmapIndex, |
| #if !defined(PRODUCT) |
| kAllocationTracingStateIndex, |
| #endif |
| #if !defined(PRODUCT) || defined(FORCE_INCLUDE_SAMPLING_HEAP_PROFILER) |
| kClassNameIndex, |
| #endif |
| }; |
| |
| #if !defined(PRODUCT) |
| CidIndexedTable<ClassIdTagType, |
| ClassPtr, |
| uint32_t, |
| UnboxedFieldBitmap, |
| uint8_t, |
| const char*> |
| classes_; |
| #elif defined(FORCE_INCLUDE_SAMPLING_HEAP_PROFILER) |
| CidIndexedTable<ClassIdTagType, |
| ClassPtr, |
| uint32_t, |
| UnboxedFieldBitmap, |
| const char*> |
| classes_; |
| #else |
| CidIndexedTable<ClassIdTagType, ClassPtr, uint32_t, UnboxedFieldBitmap> |
| classes_; |
| #endif |
| |
| #ifndef PRODUCT |
| enum { |
| kTracingDisabled = 0, |
| kTraceAllocationBit = (1 << 0), |
| kCollectInstancesBit = (1 << 1), |
| }; |
| #endif // !PRODUCT |
| |
| CidIndexedTable<classid_t, ClassPtr> top_level_classes_; |
| }; |
| |
| } // namespace dart |
| |
| #endif // RUNTIME_VM_CLASS_TABLE_H_ |