532c116cd2
This CL implements `NativeFinalizer` in the GC. `FinalizerEntry`s are extended to track `external_size` and in which `Heap::Space` the finalizable value is. On attaching a native finalizer, the external size is added to the relevant heap. When the finalizable value is promoted from new to old space, the external size is promoted as well. And when a native finalizer is run or is detached, the external size is removed from the relevant heap again. In contrast to Dart `Finalizer`s, `NativeFinalizer`s are run on isolate shutdown. When the `NativeFinalizer`s themselves are collected, the finalizers are not run. Users should stick the native finalizer in a global variable to ensure finalization. We will revisit this design when we add send and exit support, because there is a design space to explore what to do in that case. This current solution promises the least to users. In this implementation native finalizers have a Dart entry to clean up the entries from the `all_entries` field of the finalizer. We should consider using another data structure that avoids the need for this Dart entry. See the TODO left in the code. Bug: https://github.com/dart-lang/sdk/issues/47777 TEST=runtime/tests/vm/dart(_2)/isolates/fast_object_copy_test.dart TEST=runtime/vm/object_test.cc TEST=tests/ffi(_2)/vmspecific_native_finalizer_* Change-Id: I8f594c80c3c344ad83e1f2de10de028eb8456121 Cq-Include-Trybots: luci.dart.try:vm-kernel-reload-rollback-linux-debug-x64-try,vm-kernel-reload-linux-debug-x64-try,vm-ffi-android-debug-arm64c-try,dart-sdk-mac-arm64-try,vm-kernel-mac-release-arm64-try,pkg-mac-release-arm64-try,vm-kernel-precomp-nnbd-mac-release-arm64-try,vm-kernel-win-debug-x64c-try,vm-kernel-win-debug-x64-try,vm-kernel-precomp-win-debug-x64c-try,vm-kernel-nnbd-win-release-ia32-try,vm-ffi-android-debug-arm-try,vm-precomp-ffi-qemu-linux-release-arm-try,vm-kernel-mac-debug-x64-try,vm-kernel-nnbd-mac-debug-x64-try,vm-kernel-nnbd-linux-debug-ia32-try,benchmark-linux-try,flutter-frontend-try,pkg-linux-debug-try,vm-kernel-asan-linux-release-x64-try,vm-kernel-gcc-linux-try,vm-kernel-optcounter-threshold-linux-release-x64-try,vm-kernel-precomp-linux-debug-simarm_x64-try,vm-kernel-precomp-obfuscate-linux-release-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-debug-x64c-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/236320 Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Slava Egorov <vegorov@google.com> Commit-Queue: Daco Harkes <dacoharkes@google.com>
268 lines
10 KiB
C++
268 lines
10 KiB
C++
// Copyright (c) 2022, 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.
|
|
|
|
// Logic shared between the Scavenger and Marker.
|
|
|
|
#ifndef RUNTIME_VM_HEAP_GC_SHARED_H_
|
|
#define RUNTIME_VM_HEAP_GC_SHARED_H_
|
|
|
|
#include "vm/compiler/runtime_api.h"
|
|
#if defined(SHOULD_NOT_INCLUDE_RUNTIME)
|
|
#error "Should not include runtime"
|
|
#endif
|
|
|
|
#include "vm/dart_api_state.h"
|
|
#include "vm/heap/scavenger.h"
|
|
#include "vm/log.h"
|
|
#include "vm/message_handler.h"
|
|
#include "vm/object.h"
|
|
|
|
namespace dart {
|
|
|
|
// These object types have a linked list chaining all pending objects when
|
|
// processing these in the GC.
|
|
// The field should not be visited by pointer visitors.
|
|
// The field should only be set during a GC.
|
|
//
|
|
// Macro params:
|
|
// - type
|
|
// - variable name
|
|
#define GC_LINKED_LIST(V) \
|
|
V(WeakProperty, weak_properties) \
|
|
V(WeakReference, weak_references) \
|
|
V(FinalizerEntry, finalizer_entries)
|
|
|
|
template <typename Type, typename PtrType>
|
|
class GCLinkedList {
|
|
public:
|
|
void Enqueue(PtrType ptr) {
|
|
ASSERT(ptr->untag()->next_seen_by_gc().IsRawNull());
|
|
ptr->untag()->next_seen_by_gc_ = head_;
|
|
if (head_ == Type::null()) {
|
|
tail_ = ptr;
|
|
}
|
|
head_ = ptr;
|
|
}
|
|
|
|
void FlushInto(GCLinkedList<Type, PtrType>* to) {
|
|
if (to->head_ == Type::null()) {
|
|
ASSERT(to->tail_ == Type::null());
|
|
to->head_ = head_;
|
|
to->tail_ = tail_;
|
|
} else {
|
|
ASSERT(to->tail_ != Type::null());
|
|
ASSERT(to->tail_->untag()->next_seen_by_gc() == Type::null());
|
|
to->tail_->untag()->next_seen_by_gc_ = head_;
|
|
to->tail_ = tail_;
|
|
}
|
|
Release();
|
|
}
|
|
|
|
PtrType Release() {
|
|
PtrType return_value = head_;
|
|
head_ = Type::null();
|
|
tail_ = Type::null();
|
|
return return_value;
|
|
}
|
|
|
|
bool IsEmpty() { return head_ == Type::null() && tail_ == Type::null(); }
|
|
|
|
private:
|
|
PtrType head_ = Type::null();
|
|
PtrType tail_ = Type::null();
|
|
};
|
|
|
|
struct GCLinkedLists {
|
|
void Release();
|
|
bool IsEmpty();
|
|
void FlushInto(GCLinkedLists* to);
|
|
|
|
#define FOREACH(type, var) GCLinkedList<type, type##Ptr> var;
|
|
GC_LINKED_LIST(FOREACH)
|
|
#undef FOREACH
|
|
};
|
|
|
|
#ifdef DEBUG
|
|
#define TRACE_FINALIZER(format, ...) \
|
|
if (FLAG_trace_finalizers) { \
|
|
THR_Print("%s %p " format "\n", GCVisitorType::kName, visitor, \
|
|
__VA_ARGS__); \
|
|
}
|
|
#else
|
|
#define TRACE_FINALIZER(format, ...)
|
|
#endif
|
|
|
|
// The space in which `raw_entry`'s `value` is.
|
|
Heap::Space SpaceForExternal(FinalizerEntryPtr raw_entry);
|
|
|
|
// Runs the finalizer if not detached, detaches the value and set external size
|
|
// to 0.
|
|
// TODO(http://dartbug.com/47777): Can this be merged with
|
|
// NativeFinalizer::RunCallback?
|
|
template <typename GCVisitorType>
|
|
void RunNativeFinalizerCallback(NativeFinalizerPtr raw_finalizer,
|
|
FinalizerEntryPtr raw_entry,
|
|
Heap::Space before_gc_space,
|
|
GCVisitorType* visitor) {
|
|
PointerPtr callback_pointer = raw_finalizer->untag()->callback();
|
|
const auto callback = reinterpret_cast<NativeFinalizer::Callback>(
|
|
callback_pointer->untag()->data());
|
|
ObjectPtr token_object = raw_entry->untag()->token();
|
|
const bool is_detached = token_object == raw_entry;
|
|
const intptr_t external_size = raw_entry->untag()->external_size();
|
|
if (is_detached) {
|
|
// Detached from Dart code.
|
|
ASSERT(token_object == raw_entry);
|
|
ASSERT(external_size == 0);
|
|
if (FLAG_trace_finalizers) {
|
|
TRACE_FINALIZER("Not running native finalizer %p callback %p, detached",
|
|
raw_finalizer->untag(), callback);
|
|
}
|
|
} else {
|
|
// TODO(http://dartbug.com/48615): Unbox pointer address in entry.
|
|
ASSERT(token_object.IsPointer());
|
|
PointerPtr token = static_cast<PointerPtr>(token_object);
|
|
void* peer = reinterpret_cast<void*>(token->untag()->data());
|
|
if (FLAG_trace_finalizers) {
|
|
TRACE_FINALIZER("Running native finalizer %p callback %p with token %p",
|
|
raw_finalizer->untag(), callback, peer);
|
|
}
|
|
raw_entry.untag()->set_token(raw_entry);
|
|
callback(peer);
|
|
if (external_size > 0) {
|
|
if (FLAG_trace_finalizers) {
|
|
TRACE_FINALIZER("Clearing external size %" Pd " bytes in %s space",
|
|
external_size, before_gc_space == 0 ? "new" : "old");
|
|
}
|
|
visitor->isolate_group()->heap()->FreedExternal(external_size,
|
|
before_gc_space);
|
|
raw_entry->untag()->set_external_size(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function processes all finalizer entries discovered by a scavenger or
|
|
// marker. If an entry is referencing an object that is going to die, such entry
|
|
// is cleared and enqueued in the respective finalizer.
|
|
//
|
|
// Finalizer entries belonging to unreachable finalizer entries do not get
|
|
// processed, so the callback will not be called for these finalizers.
|
|
//
|
|
// For more documentation see runtime/docs/gc.md.
|
|
//
|
|
// |GCVisitorType| is a concrete type implementing either marker or scavenger.
|
|
// It is expected to provide |ForwardOrSetNullIfCollected| method for clearing
|
|
// fields referring to dead objects and |kName| field which contains visitor
|
|
// name for tracing output.
|
|
template <typename GCVisitorType>
|
|
void MournFinalized(GCVisitorType* visitor) {
|
|
FinalizerEntryPtr current_entry =
|
|
visitor->delayed_.finalizer_entries.Release();
|
|
while (current_entry != FinalizerEntry::null()) {
|
|
TRACE_FINALIZER("Processing Entry %p", current_entry->untag());
|
|
FinalizerEntryPtr next_entry =
|
|
current_entry->untag()->next_seen_by_gc_.Decompress(
|
|
current_entry->heap_base());
|
|
current_entry->untag()->next_seen_by_gc_ = FinalizerEntry::null();
|
|
|
|
uword heap_base = current_entry->heap_base();
|
|
const Heap::Space before_gc_space = SpaceForExternal(current_entry);
|
|
const bool value_collected_this_gc =
|
|
GCVisitorType::ForwardOrSetNullIfCollected(
|
|
heap_base, ¤t_entry->untag()->value_);
|
|
if (!value_collected_this_gc && before_gc_space == Heap::kNew) {
|
|
const Heap::Space after_gc_space = SpaceForExternal(current_entry);
|
|
if (after_gc_space == Heap::kOld) {
|
|
const intptr_t external_size = current_entry->untag()->external_size_;
|
|
TRACE_FINALIZER("Promoting external size %" Pd
|
|
" bytes from new to old space",
|
|
external_size);
|
|
visitor->isolate_group()->heap()->PromotedExternal(external_size);
|
|
}
|
|
}
|
|
GCVisitorType::ForwardOrSetNullIfCollected(
|
|
heap_base, ¤t_entry->untag()->detach_);
|
|
GCVisitorType::ForwardOrSetNullIfCollected(
|
|
heap_base, ¤t_entry->untag()->finalizer_);
|
|
|
|
ObjectPtr token_object = current_entry->untag()->token();
|
|
// See sdk/lib/_internal/vm/lib/internal_patch.dart FinalizerBase.detach.
|
|
const bool is_detached = token_object == current_entry;
|
|
|
|
if (value_collected_this_gc && !is_detached) {
|
|
FinalizerBasePtr finalizer = current_entry->untag()->finalizer();
|
|
|
|
if (finalizer.IsRawNull()) {
|
|
TRACE_FINALIZER("Value collected entry %p finalizer null",
|
|
current_entry->untag());
|
|
|
|
// Do nothing, the finalizer has been GCed.
|
|
} else {
|
|
TRACE_FINALIZER("Value collected entry %p finalizer %p",
|
|
current_entry->untag(), finalizer->untag());
|
|
|
|
FinalizerPtr finalizer_dart = static_cast<FinalizerPtr>(finalizer);
|
|
// Move entry to entries collected and current head of that list as
|
|
// the next element. Using a atomic exchange satisfies concurrency
|
|
// between the parallel GC tasks.
|
|
// We rely on the fact that the mutator thread is not running to avoid
|
|
// races between GC and mutator modifying Finalizer.entries_collected.
|
|
//
|
|
// We only run in serial marker or in the finalize step in the marker,
|
|
// both are in safepoint.
|
|
// The main scavenger worker is at safepoint, the other scavenger
|
|
// workers are are not, but they bypass safepoint because the main
|
|
// worker is at a safepoint already.
|
|
ASSERT(Thread::Current()->IsAtSafepoint() ||
|
|
Thread::Current()->BypassSafepoints());
|
|
|
|
if (finalizer.IsNativeFinalizer()) {
|
|
NativeFinalizerPtr native_finalizer =
|
|
static_cast<NativeFinalizerPtr>(finalizer);
|
|
|
|
// Immediately call native callback.
|
|
RunNativeFinalizerCallback(native_finalizer, current_entry,
|
|
before_gc_space, visitor);
|
|
|
|
// Fall-through sending a message to clear the entries and remove
|
|
// from detachments.
|
|
}
|
|
|
|
FinalizerEntryPtr previous_head =
|
|
finalizer_dart->untag()->exchange_entries_collected(current_entry);
|
|
current_entry->untag()->set_next(previous_head);
|
|
const bool first_entry = previous_head.IsRawNull();
|
|
// Schedule calling Dart finalizer.
|
|
if (first_entry) {
|
|
Isolate* isolate = finalizer->untag()->isolate_;
|
|
if (isolate == nullptr) {
|
|
TRACE_FINALIZER(
|
|
"Not scheduling finalizer %p callback on isolate null",
|
|
finalizer->untag());
|
|
} else {
|
|
TRACE_FINALIZER("Scheduling finalizer %p callback on isolate %p",
|
|
finalizer->untag(), isolate);
|
|
|
|
PersistentHandle* handle =
|
|
isolate->group()->api_state()->AllocatePersistentHandle();
|
|
handle->set_ptr(finalizer);
|
|
MessageHandler* message_handler = isolate->message_handler();
|
|
message_handler->PostMessage(
|
|
Message::New(handle, Message::kNormalPriority),
|
|
/*before_events*/ false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
current_entry = next_entry;
|
|
}
|
|
}
|
|
|
|
#undef TRACE_FINALIZER
|
|
|
|
} // namespace dart
|
|
|
|
#endif // RUNTIME_VM_HEAP_GC_SHARED_H_
|