diff --git a/kotlin-native/runtime/src/alloc/common/cpp/MainThreadFinalizerProcessor.hpp b/kotlin-native/runtime/src/alloc/common/cpp/MainThreadFinalizerProcessor.hpp new file mode 100644 index 00000000000..eb785034dd8 --- /dev/null +++ b/kotlin-native/runtime/src/alloc/common/cpp/MainThreadFinalizerProcessor.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#pragma once + +#include + +#include "RunLoopFinalizerProcessor.hpp" +#include "Utils.hpp" + +namespace kotlin::alloc { + +// Finalizer processor that runs on the main thread. +// +// Only enabled if `compiler::objcDisposeOnMain()` is true and if the main run loop is processed. +// +// It just wraps `RunLoopFinalizerProcessor`, see it for implementation details. +template +class MainThreadFinalizerProcessor : private Pinned { +public: + MainThreadFinalizerProcessor() noexcept { +#if KONAN_HAS_FOUNDATION_FRAMEWORK + if (compiler::objcDisposeOnMain()) { + CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{ + [[clang::no_destroy]] static auto subscription = processor_.attachToCurrentRunLoop(); + available_.store(true, std::memory_order_release); + }); + } +#endif + } + + bool available() const noexcept { +#if KONAN_HAS_FOUNDATION_FRAMEWORK + return available_.load(std::memory_order_acquire); +#else + return false; +#endif + } + + void schedule(FinalizerQueue tasks, uint64_t epoch) noexcept { + if (FinalizerQueueTraits::isEmpty(tasks)) return; +#if KONAN_HAS_FOUNDATION_FRAMEWORK + RuntimeAssert(available(), "MainThreadFinalizerProcessor is unavailable"); + processor_.schedule(std::move(tasks), epoch); +#else + RuntimeAssert(false, "MainThreadFinalizerProcessor is unavailable"); +#endif + } + + template + std::invoke_result_t withConfig(F&& f) noexcept { +#if KONAN_HAS_FOUNDATION_FRAMEWORK + return processor_.withConfig(std::forward(f)); +#else + RuntimeAssert(false, "MainThreadFinalizerProcessor is unavailable"); +#endif + } + +private: +#if KONAN_HAS_FOUNDATION_FRAMEWORK + std::atomic available_ = false; + RunLoopFinalizerProcessor processor_; +#endif +}; + +} // namespace kotlin::alloc \ No newline at end of file diff --git a/kotlin-native/runtime/src/alloc/common/cpp/RunLoopFinalizerProcessor.hpp b/kotlin-native/runtime/src/alloc/common/cpp/RunLoopFinalizerProcessor.hpp index 3ca8619e798..590d51f3efb 100644 --- a/kotlin-native/runtime/src/alloc/common/cpp/RunLoopFinalizerProcessor.hpp +++ b/kotlin-native/runtime/src/alloc/common/cpp/RunLoopFinalizerProcessor.hpp @@ -18,6 +18,8 @@ namespace kotlin::alloc { +// Configuration for `RunLoopFinalizerProcessor` +// When updating the default values, do not forget to update stdlib API docs. struct RunLoopFinalizerProcessorConfig { // How long can finalizers be processed in a single task. If some finalizer takes too long, the entire // batch of `batchSize` will overshoot this target. @@ -27,7 +29,7 @@ struct RunLoopFinalizerProcessorConfig { // This cannot be too small to allow the attached run loop process other tasks (not from this finalizer processor). std::chrono::nanoseconds minTimeBetweenTasks = std::chrono::milliseconds(1); // How many finalizers are processed in a single batch in a single autoreleasepool. - size_t batchSize = 100; + uint64_t batchSize = 100; }; #if KONAN_HAS_FOUNDATION_FRAMEWORK @@ -109,7 +111,7 @@ private: } } steady_clock::time_point deadline; - size_t batchCount; + uint64_t batchCount; { std::unique_lock guard(configMutex_); RuntimeLogDebug( @@ -118,14 +120,14 @@ private: deadline = startTime + config_.maxTimeInTask; batchCount = config_.batchSize; } - size_t processedCount = 0; + uint64_t processedCount = 0; while (true) { auto now = steady_clock::now(); if (now > deadline) { // Finalization is being run too long. Stop processing and reschedule until the next allowed time. std::unique_lock guard(configMutex_); RuntimeLogDebug( - {kTagGC}, "Processing %zu finalizers on a run loop has taken %" PRId64 " ms. Stopping for %" PRId64 "ms.", + {kTagGC}, "Processing %" PRIu64 " finalizers on a run loop has taken %" PRId64 " ms. Stopping for %" PRId64 "ms.", processedCount, std::chrono::duration_cast(now - startTime).count().value, std::chrono::duration_cast(config_.minTimeBetweenTasks).count()); timer_.setNextFiring(config_.minTimeBetweenTasks); @@ -134,7 +136,7 @@ private: } { objc_support::AutoreleasePool autoreleasePool; - for (size_t i = 0; i < batchCount; ++i) { + for (uint64_t i = 0; i < batchCount; ++i) { // There's no point checking `deadline` here since the majority of the time will probably // be spent in `AutoreleasePool` destructor. if (!FinalizerQueueTraits::processSingle(currentQueue_.queue)) { @@ -154,7 +156,7 @@ private: // would definitely have to wait long enough to see the updated lastProcessTimestamp_. lastProcessTimestamp_ = steady_clock::now(); RuntimeLogDebug( - {kTagGC}, "Processing %zu finalizers on a run loop has finished in %" PRId64 "ms.", processedCount, + {kTagGC}, "Processing %" PRIu64 " finalizers on a run loop has finished in %" PRId64 "ms.", processedCount, std::chrono::duration_cast(lastProcessTimestamp_ - startTime).count().value); return; } diff --git a/kotlin-native/runtime/src/alloc/common/cpp/SegregatedFinalizerQueue.hpp b/kotlin-native/runtime/src/alloc/common/cpp/SegregatedFinalizerQueue.hpp new file mode 100644 index 00000000000..2447b5c7c66 --- /dev/null +++ b/kotlin-native/runtime/src/alloc/common/cpp/SegregatedFinalizerQueue.hpp @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#pragma once + +#include +#include + +namespace kotlin::alloc { + +// Finalizer queue split between threads. +template +struct SegregatedFinalizerQueue { + FinalizerQueue regular; + FinalizerQueue mainThread; + + size_t size() noexcept { return regular.size() + mainThread.size(); } + + void mergeIntoRegular() noexcept { + regular.TransferAllFrom(std::move(mainThread)); + mainThread = FinalizerQueue{}; + } + + void mergeFrom(SegregatedFinalizerQueue rhs) noexcept { + regular.TransferAllFrom(std::move(rhs.regular)); + mainThread.TransferAllFrom(std::move(rhs.mainThread)); + } +}; + +} // namespace kotlin::alloc \ No newline at end of file diff --git a/kotlin-native/runtime/src/alloc/custom/cpp/CustomFinalizerProcessor.hpp b/kotlin-native/runtime/src/alloc/custom/cpp/CustomFinalizerProcessor.hpp index 78d17db33f0..ceb248412b7 100644 --- a/kotlin-native/runtime/src/alloc/custom/cpp/CustomFinalizerProcessor.hpp +++ b/kotlin-native/runtime/src/alloc/custom/cpp/CustomFinalizerProcessor.hpp @@ -6,22 +6,29 @@ #ifndef CUSTOM_ALLOC_CPP_CUSTOMFINALIZERPROCESSOR_HPP_ #define CUSTOM_ALLOC_CPP_CUSTOMFINALIZERPROCESSOR_HPP_ +#include "Allocator.hpp" #include "AtomicStack.hpp" -#include "ExtraObjectData.hpp" -#include "ExtraObjectPage.hpp" +#include "ExtraObjectCell.hpp" #include "FinalizerHooks.hpp" +#include "SegregatedFinalizerQueue.hpp" namespace kotlin::alloc { -using FinalizerQueue = kotlin::alloc::AtomicStack; +using FinalizerQueueSingle = AtomicStack; +using FinalizerQueue = SegregatedFinalizerQueue; struct FinalizerQueueTraits { - static bool isEmpty(const FinalizerQueue& queue) noexcept { return queue.isEmpty(); } + static bool isEmpty(const FinalizerQueueSingle& queue) noexcept { return queue.isEmpty(); } - static void add(FinalizerQueue& into, FinalizerQueue from) noexcept { into.TransferAllFrom(std::move(from)); } + static void add(FinalizerQueueSingle& into, FinalizerQueueSingle from) noexcept { into.TransferAllFrom(std::move(from)); } - static void process(FinalizerQueue queue) noexcept { - while (auto* cell = queue.Pop()) { + static void process(FinalizerQueueSingle queue) noexcept { + while (processSingle(queue)) { + } + } + + static bool processSingle(FinalizerQueueSingle& queue) noexcept { + if (auto* cell = queue.Pop()) { auto* extraObject = cell->Data(); if (auto* baseObject = extraObject->GetBaseObject()) { RunFinalizers(baseObject); @@ -30,7 +37,9 @@ struct FinalizerQueueTraits { // that the only finalization step is destroying it. destroyExtraObjectData(*extraObject); } + return true; } + return false; } }; diff --git a/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectCell.hpp b/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectCell.hpp new file mode 100644 index 00000000000..910e4d8bee1 --- /dev/null +++ b/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectCell.hpp @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#pragma once + +#include "ExtraObjectData.hpp" + +namespace kotlin::alloc { + +struct ExtraObjectCell { + mm::ExtraObjectData* Data() { return reinterpret_cast(data_); } + + // This is used to simultaneously build two lists: a free list and a finalizers queue. + // A cell cannot exist in both of them, but can be in neither when it's alive. + std::atomic next_; + struct alignas(mm::ExtraObjectData) { + uint8_t data_[sizeof(mm::ExtraObjectData)]; + }; + + static ExtraObjectCell* fromExtraObject(mm::ExtraObjectData* extraObjectData) { + return reinterpret_cast(reinterpret_cast(extraObjectData) - offsetof(ExtraObjectCell, data_)); + } +}; + +} // namespace kotlin::alloc \ No newline at end of file diff --git a/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPage.hpp b/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPage.hpp index 1a2cde11b6c..53dd502d0d7 100644 --- a/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPage.hpp +++ b/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPage.hpp @@ -11,28 +11,13 @@ #include #include "AtomicStack.hpp" +#include "CustomFinalizerProcessor.hpp" +#include "ExtraObjectCell.hpp" #include "ExtraObjectData.hpp" #include "GCStatistics.hpp" namespace kotlin::alloc { -struct ExtraObjectCell { - mm::ExtraObjectData* Data() { return reinterpret_cast(data_); } - - // This is used to simultaneously build two lists: a free list and a finalizers queue. - // A cell cannot exist in both of them, but can be in neither when it's alive. - std::atomic next_; - struct alignas(mm::ExtraObjectData) { - uint8_t data_[sizeof(mm::ExtraObjectData)]; - }; - - static ExtraObjectCell* fromExtraObject(mm::ExtraObjectData* extraObjectData) { - return reinterpret_cast(reinterpret_cast(extraObjectData) - offsetof(ExtraObjectCell, data_)); - } -}; - -using FinalizerQueue = AtomicStack; - class alignas(8) ExtraObjectPage { public: using GCSweepScope = gc::GCHandle::GCSweepExtraObjectsScope; diff --git a/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPageTest.cpp b/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPageTest.cpp index e795053c518..85cc288e7be 100644 --- a/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPageTest.cpp +++ b/kotlin-native/runtime/src/alloc/custom/cpp/ExtraObjectPageTest.cpp @@ -17,7 +17,6 @@ namespace { using Data = typename kotlin::mm::ExtraObjectData; using Cell = typename kotlin::alloc::ExtraObjectCell; using Page = typename kotlin::alloc::ExtraObjectPage; -using Queue = typename kotlin::alloc::AtomicStack; Data* alloc(Page* page) { Data* ptr = page->TryAllocate(); @@ -40,7 +39,7 @@ TEST(CustomAllocTest, ExtraObjectPageConsequtiveAlloc) { TEST(CustomAllocTest, ExtraObjectPageSweepEmptyPage) { Page* page = Page::Create(0); - Queue finalizerQueue; + kotlin::alloc::FinalizerQueue finalizerQueue; auto gcHandle = kotlin::gc::GCHandle::createFakeForTests(); auto gcScope = gcHandle.sweepExtraObjects(); EXPECT_FALSE(page->Sweep(gcScope, finalizerQueue)); @@ -57,7 +56,7 @@ TEST(CustomAllocTest, ExtraObjectPageSweepFullFinalizedPage) { ++count; } EXPECT_EQ(count, EXTRA_OBJECT_COUNT); - Queue finalizerQueue; + kotlin::alloc::FinalizerQueue finalizerQueue; auto gcHandle = kotlin::gc::GCHandle::createFakeForTests(); auto gcScope = gcHandle.sweepExtraObjects(); EXPECT_FALSE(page->Sweep(gcScope, finalizerQueue)); diff --git a/kotlin-native/runtime/src/alloc/custom/cpp/GCApi.cpp b/kotlin-native/runtime/src/alloc/custom/cpp/GCApi.cpp index 25c9ec5d6e2..047c7d652f4 100644 --- a/kotlin-native/runtime/src/alloc/custom/cpp/GCApi.cpp +++ b/kotlin-native/runtime/src/alloc/custom/cpp/GCApi.cpp @@ -49,7 +49,12 @@ bool SweepObject(uint8_t* object, FinalizerQueue& finalizerQueue, gc::GCHandle:: extraObject->setFlag(mm::ExtraObjectData::FLAGS_IN_FINALIZER_QUEUE); extraObject->ClearRegularWeakReferenceImpl(); CustomAllocDebug("SweepObject: fromExtraObject(%p) = %p", extraObject, ExtraObjectCell::fromExtraObject(extraObject)); - finalizerQueue.Push(ExtraObjectCell::fromExtraObject(extraObject)); + auto* cell = ExtraObjectCell::fromExtraObject(extraObject); + if (compiler::objcDisposeOnMain() && extraObject->getFlag(mm::ExtraObjectData::FLAGS_RELEASE_ON_MAIN_QUEUE)) { + finalizerQueue.mainThread.Push(cell); + } else { + finalizerQueue.regular.Push(cell); + } if (HasFinalizersDataInObject(heapObjHeader->object())) { // The object must survive until the finalizers for it are finished. gcHandle.addMarkedObject(); diff --git a/kotlin-native/runtime/src/alloc/custom/cpp/Heap.cpp b/kotlin-native/runtime/src/alloc/custom/cpp/Heap.cpp index 01548cc5d8f..783b396df7b 100644 --- a/kotlin-native/runtime/src/alloc/custom/cpp/Heap.cpp +++ b/kotlin-native/runtime/src/alloc/custom/cpp/Heap.cpp @@ -80,7 +80,7 @@ ExtraObjectPage* Heap::GetExtraObjectPage(FinalizerQueue& finalizerQueue) noexce void Heap::AddToFinalizerQueue(FinalizerQueue queue) noexcept { std::unique_lock guard(pendingFinalizerQueueMutex_); - pendingFinalizerQueue_.TransferAllFrom(std::move(queue)); + pendingFinalizerQueue_.mergeFrom(std::move(queue)); } FinalizerQueue Heap::ExtractFinalizerQueue() noexcept { diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp b/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp index cba96724817..b1e40bafd39 100644 --- a/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp +++ b/kotlin-native/runtime/src/alloc/legacy/cpp/AllocatorImpl.hpp @@ -9,11 +9,12 @@ #include "ExtraObjectDataFactory.hpp" #include "GC.hpp" +#include "GCScheduler.hpp" #include "GlobalData.hpp" -#include "Logging.hpp" #include "ObjectFactory.hpp" #include "ObjectFactoryAllocator.hpp" #include "ObjectFactorySweep.hpp" +#include "Logging.hpp" namespace kotlin::alloc { @@ -32,6 +33,10 @@ struct ObjectFactoryTraits { using ObjectFactoryImpl = ObjectFactory; +using FinalizerQueueSingle = ObjectFactoryImpl::FinalizerQueue; +using FinalizerQueue = SegregatedFinalizerQueue; +using FinalizerQueueTraits = ObjectFactoryImpl::FinalizerQueueTraits; + class Allocator::Impl : private Pinned { public: Impl() noexcept = default; @@ -59,7 +64,4 @@ private: ExtraObjectDataFactory::ThreadQueue extraObjectDataFactoryThreadQueue_; }; -using FinalizerQueue = ObjectFactoryImpl::FinalizerQueue; -using FinalizerQueueTraits = ObjectFactoryImpl::FinalizerQueueTraits; - } // namespace kotlin::alloc diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactory.hpp b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactory.hpp index 7ce81c94f46..7e0acd464a3 100644 --- a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactory.hpp +++ b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactory.hpp @@ -325,6 +325,20 @@ public: AssertCorrect(); } + unique_ptr Pop() noexcept { + AssertCorrect(); + auto result = std::move(root_); + if (result) { + --size_; + root_ = std::move(result->next_); + if (last_ == result.get()) { + last_ = nullptr; + } + } + AssertCorrect(); + return result; + } + private: friend class ObjectFactoryStorage; @@ -679,10 +693,15 @@ public: } } - void MergeWith(FinalizerQueue &&other) { - consumer_.MergeWith(std::move(other.consumer_)); + bool FinalizeSingle() noexcept { + if (auto node = consumer_.Pop()) { + RunFinalizers(NodeRef(*node).GetObjHeader()); + } + return false; } + void TransferAllFrom(FinalizerQueue&& other) { consumer_.MergeWith(std::move(other.consumer_)); } + Iterable IterForTests() noexcept { return Iterable(*this); } private: @@ -694,9 +713,11 @@ public: struct FinalizerQueueTraits { static bool isEmpty(const FinalizerQueue& queue) noexcept { return queue.size() == 0; } - static void add(FinalizerQueue& into, FinalizerQueue from) noexcept { into.MergeWith(std::move(from)); } + static void add(FinalizerQueue& into, FinalizerQueue from) noexcept { into.TransferAllFrom(std::move(from)); } static void process(FinalizerQueue queue) noexcept { queue.Finalize(); } + + static bool processSingle(FinalizerQueue& queue) noexcept { return queue.FinalizeSingle(); } }; class Iterable { diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweep.hpp b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweep.hpp index d8d26f4dc6e..8733e71eadb 100644 --- a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweep.hpp +++ b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweep.hpp @@ -5,10 +5,11 @@ #pragma once +#include "ExtraObjectDataFactory.hpp" #include "FinalizerHooks.hpp" #include "GC.hpp" #include "GCStatistics.hpp" -#include "ExtraObjectDataFactory.hpp" +#include "SegregatedFinalizerQueue.hpp" namespace kotlin::alloc { @@ -43,8 +44,9 @@ void SweepExtraObjects(gc::GCHandle handle, typename Traits::ExtraObjectsFactory } template -typename Traits::ObjectFactory::FinalizerQueue Sweep(gc::GCHandle handle, typename Traits::ObjectFactory::Iterable& objectFactoryIter) noexcept { - typename Traits::ObjectFactory::FinalizerQueue finalizerQueue; +SegregatedFinalizerQueue Sweep( + gc::GCHandle handle, typename Traits::ObjectFactory::Iterable& objectFactoryIter) noexcept { + SegregatedFinalizerQueue finalizerQueue; auto sweepHandle = handle.sweep(); for (auto it = objectFactoryIter.begin(); it != objectFactoryIter.end();) { @@ -56,7 +58,12 @@ typename Traits::ObjectFactory::FinalizerQueue Sweep(gc::GCHandle handle, typena } sweepHandle.addSweptObject(); if (HasFinalizers(objHeader)) { - objectFactoryIter.MoveAndAdvance(finalizerQueue, it); + auto* extraObject = mm::ExtraObjectData::Get(objHeader); + if (compiler::objcDisposeOnMain() && extraObject->getFlag(mm::ExtraObjectData::FLAGS_RELEASE_ON_MAIN_QUEUE)) { + objectFactoryIter.MoveAndAdvance(finalizerQueue.mainThread, it); + } else { + objectFactoryIter.MoveAndAdvance(finalizerQueue.regular, it); + } } else { objectFactoryIter.EraseAndAdvance(it); } @@ -66,7 +73,8 @@ typename Traits::ObjectFactory::FinalizerQueue Sweep(gc::GCHandle handle, typena } template -typename Traits::ObjectFactory::FinalizerQueue Sweep(gc::GCHandle handle, typename Traits::ObjectFactory& objectFactory) noexcept { +SegregatedFinalizerQueue Sweep( + gc::GCHandle handle, typename Traits::ObjectFactory& objectFactory) noexcept { auto iter = objectFactory.LockForIter(); return Sweep(handle, iter); } diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweepTest.cpp b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweepTest.cpp index 87b4c064049..78d25d0094f 100644 --- a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweepTest.cpp +++ b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactorySweepTest.cpp @@ -198,11 +198,12 @@ public: gc::processWeaks(gc::GCHandle::getByEpoch(0), specialRefRegistry_); alloc::SweepExtraObjects(gc::GCHandle::getByEpoch(0), extraObjectFactory_); auto finalizers = alloc::Sweep(gc::GCHandle::getByEpoch(0), objectFactory_); + finalizers.mergeIntoRegular(); std::vector objects; - for (auto node : finalizers.IterForTests()) { + for (auto node : finalizers.regular.IterForTests()) { objects.push_back(node.GetObjHeader()); } - finalizers_.push_back(std::move(finalizers)); + finalizers_.push_back(std::move(finalizers.regular)); return objects; } diff --git a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactoryTest.cpp b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactoryTest.cpp index 7127420ab87..dabecf26b58 100644 --- a/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactoryTest.cpp +++ b/kotlin-native/runtime/src/alloc/legacy/cpp/ObjectFactoryTest.cpp @@ -489,6 +489,40 @@ TEST(ObjectFactoryStorageTest, MoveAll) { EXPECT_THAT(consumer.size(), 3); } +TEST(ObjectFactoryStorageTest, Pop) { + ObjectFactoryStorageRegular storage; + Producer producer(storage, SimpleAllocator()); + Consumer consumer; + + producer.Insert(1); + producer.Insert(2); + producer.Insert(3); + + producer.Publish(); + + { + auto iter = storage.LockForIter(); + for (auto it = iter.begin(); it != iter.end();) { + iter.MoveAndAdvance(consumer, it, ObjectFactoryStorageRegular::Node::GetSizeForDataSize(sizeof(int))); + } + } + + std::vector popped; + while (auto element = consumer.Pop()) { + popped.push_back(element->Data()); + } + + auto actual = Collect(storage); + auto actualConsumer = Collect(consumer); + + EXPECT_THAT(actual, testing::IsEmpty()); + EXPECT_THAT(actualConsumer, testing::IsEmpty()); + EXPECT_THAT(popped, testing::ElementsAre(1, 2, 3)); + EXPECT_THAT(storage.GetSizeUnsafe(), 0); + EXPECT_THAT(producer.size(), 0); + EXPECT_THAT(consumer.size(), 0); +} + TEST(ObjectFactoryStorageTest, MergeWith) { ObjectFactoryStorageRegular storage; Producer producer(storage, SimpleAllocator()); diff --git a/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.cpp b/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.cpp index bf4dcc9b02f..1a973f71918 100644 --- a/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.cpp +++ b/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.cpp @@ -181,17 +181,21 @@ void gc::ConcurrentMarkAndSweep::PerformFullGC(int64_t epoch) noexcept { // also sweeps extraObjects auto finalizerQueue = allocator_.impl().heap().Sweep(gcHandle); for (auto& thread : kotlin::mm::ThreadRegistry::Instance().LockForIter()) { - finalizerQueue.TransferAllFrom(thread.allocator().impl().alloc().ExtractFinalizerQueue()); + finalizerQueue.mergeFrom(thread.allocator().impl().alloc().ExtractFinalizerQueue()); } - finalizerQueue.TransferAllFrom(allocator_.impl().heap().ExtractFinalizerQueue()); + finalizerQueue.mergeFrom(allocator_.impl().heap().ExtractFinalizerQueue()); #endif scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes() + threadCount * allocator_.estimateOverheadPerThread()); state_.finish(epoch); gcHandle.finalizersScheduled(finalizerQueue.size()); gcHandle.finished(); + if (!mainThreadFinalizerProcessor_.available()) { + finalizerQueue.mergeIntoRegular(); + } // This may start a new thread. On some pthreads implementations, this may block waiting for concurrent thread // destructors running. So, it must ensured that no locks are held by this point. // TODO: Consider having an always on sleeping finalizer thread. - finalizerProcessor_.ScheduleTasks(std::move(finalizerQueue), epoch); + finalizerProcessor_.ScheduleTasks(std::move(finalizerQueue.regular), epoch); + mainThreadFinalizerProcessor_.schedule(std::move(finalizerQueue.mainThread), epoch); } diff --git a/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.hpp b/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.hpp index 43422f405c8..cbf5e89709f 100644 --- a/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.hpp +++ b/kotlin-native/runtime/src/gc/cms/cpp/ConcurrentMarkAndSweep.hpp @@ -17,6 +17,7 @@ #include "GCState.hpp" #include "GCStatistics.hpp" #include "IntrusiveList.hpp" +#include "MainThreadFinalizerProcessor.hpp" #include "MarkAndSweepUtils.hpp" #include "ObjectData.hpp" #include "ConcurrentMark.hpp" @@ -72,6 +73,9 @@ public: bool FinalizersThreadIsRunning() noexcept; GCStateHolder& state() noexcept { return state_; } + alloc::MainThreadFinalizerProcessor& mainThreadFinalizerProcessor() noexcept { + return mainThreadFinalizerProcessor_; + } private: void mainGCThreadBody(); @@ -81,7 +85,8 @@ private: gcScheduler::GCScheduler& gcScheduler_; GCStateHolder state_; - FinalizerProcessor finalizerProcessor_; + FinalizerProcessor finalizerProcessor_; + alloc::MainThreadFinalizerProcessor mainThreadFinalizerProcessor_; mark::ConcurrentMark markDispatcher_; ScopedThread mainThread_; diff --git a/kotlin-native/runtime/src/gc/cms/cpp/GCImpl.cpp b/kotlin-native/runtime/src/gc/cms/cpp/GCImpl.cpp index 21991220dc9..c3af42b518b 100644 --- a/kotlin-native/runtime/src/gc/cms/cpp/GCImpl.cpp +++ b/kotlin-native/runtime/src/gc/cms/cpp/GCImpl.cpp @@ -79,6 +79,14 @@ void gc::GC::WaitFinalizers(int64_t epoch) noexcept { impl_->gc().state().waitEpochFinalized(epoch); } +void gc::GC::configureMainThreadFinalizerProcessor(std::function f) noexcept { + impl_->gc().mainThreadFinalizerProcessor().withConfig(std::move(f)); +} + +bool gc::GC::mainThreadFinalizerProcessorAvailable() noexcept { + return impl_->gc().mainThreadFinalizerProcessor().available(); +} + ALWAYS_INLINE void gc::beforeHeapRefUpdate(mm::DirectRefAccessor ref, ObjHeader* value) noexcept { barriers::beforeHeapRefUpdate(ref, value); } diff --git a/kotlin-native/runtime/src/gc/common/cpp/GC.hpp b/kotlin-native/runtime/src/gc/common/cpp/GC.hpp index cbedd069f74..28db8460de5 100644 --- a/kotlin-native/runtime/src/gc/common/cpp/GC.hpp +++ b/kotlin-native/runtime/src/gc/common/cpp/GC.hpp @@ -12,6 +12,7 @@ #include "ExtraObjectData.hpp" #include "GCScheduler.hpp" #include "ReferenceOps.hpp" +#include "RunLoopFinalizerProcessor.hpp" #include "Utils.hpp" namespace kotlin { @@ -78,6 +79,9 @@ public: void WaitFinished(int64_t epoch) noexcept; void WaitFinalizers(int64_t epoch) noexcept; + void configureMainThreadFinalizerProcessor(std::function f) noexcept; + bool mainThreadFinalizerProcessorAvailable() noexcept; + private: std::unique_ptr impl_; }; diff --git a/kotlin-native/runtime/src/gc/noop/cpp/GCImpl.cpp b/kotlin-native/runtime/src/gc/noop/cpp/GCImpl.cpp index 7e4b72cfbd0..929e34a2ce3 100644 --- a/kotlin-native/runtime/src/gc/noop/cpp/GCImpl.cpp +++ b/kotlin-native/runtime/src/gc/noop/cpp/GCImpl.cpp @@ -56,6 +56,12 @@ void gc::GC::WaitFinished(int64_t epoch) noexcept {} void gc::GC::WaitFinalizers(int64_t epoch) noexcept {} +void gc::GC::configureMainThreadFinalizerProcessor(std::function f) noexcept {} + +bool gc::GC::mainThreadFinalizerProcessorAvailable() noexcept { + return false; +} + ALWAYS_INLINE void gc::beforeHeapRefUpdate(mm::DirectRefAccessor ref, ObjHeader* value) noexcept {} ALWAYS_INLINE OBJ_GETTER(gc::weakRefReadBarrier, std::atomic& weakReferee) noexcept { diff --git a/kotlin-native/runtime/src/gc/pmcs/cpp/GCImpl.cpp b/kotlin-native/runtime/src/gc/pmcs/cpp/GCImpl.cpp index 5a5025fb391..2b83d0fdd61 100644 --- a/kotlin-native/runtime/src/gc/pmcs/cpp/GCImpl.cpp +++ b/kotlin-native/runtime/src/gc/pmcs/cpp/GCImpl.cpp @@ -79,6 +79,14 @@ void gc::GC::WaitFinalizers(int64_t epoch) noexcept { impl_->gc().state().waitEpochFinalized(epoch); } +void gc::GC::configureMainThreadFinalizerProcessor(std::function f) noexcept { + impl_->gc().mainThreadFinalizerProcessor().withConfig(std::move(f)); +} + +bool gc::GC::mainThreadFinalizerProcessorAvailable() noexcept { + return impl_->gc().mainThreadFinalizerProcessor().available(); +} + ALWAYS_INLINE void gc::beforeHeapRefUpdate(mm::DirectRefAccessor ref, ObjHeader* value) noexcept {} ALWAYS_INLINE OBJ_GETTER(gc::weakRefReadBarrier, std::atomic& weakReferee) noexcept { diff --git a/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp b/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp index f24c07aa9c0..34b02a43431 100644 --- a/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp +++ b/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.cpp @@ -204,19 +204,23 @@ void gc::ParallelMarkConcurrentSweep::PerformFullGC(int64_t epoch) noexcept { // also sweeps extraObjects auto finalizerQueue = allocator_.impl().heap().Sweep(gcHandle); for (auto& thread : kotlin::mm::ThreadRegistry::Instance().LockForIter()) { - finalizerQueue.TransferAllFrom(thread.allocator().impl().alloc().ExtractFinalizerQueue()); + finalizerQueue.mergeFrom(thread.allocator().impl().alloc().ExtractFinalizerQueue()); } - finalizerQueue.TransferAllFrom(allocator_.impl().heap().ExtractFinalizerQueue()); + finalizerQueue.mergeFrom(allocator_.impl().heap().ExtractFinalizerQueue()); #endif scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes() + threadCount * allocator_.estimateOverheadPerThread()); state_.finish(epoch); gcHandle.finalizersScheduled(finalizerQueue.size()); gcHandle.finished(); + if (!mainThreadFinalizerProcessor_.available()) { + finalizerQueue.mergeIntoRegular(); + } // This may start a new thread. On some pthreads implementations, this may block waiting for concurrent thread // destructors running. So, it must ensured that no locks are held by this point. // TODO: Consider having an always on sleeping finalizer thread. - finalizerProcessor_.ScheduleTasks(std::move(finalizerQueue), epoch); + finalizerProcessor_.ScheduleTasks(std::move(finalizerQueue.regular), epoch); + mainThreadFinalizerProcessor_.schedule(std::move(finalizerQueue.mainThread), epoch); } void gc::ParallelMarkConcurrentSweep::reconfigure(std::size_t maxParallelism, bool mutatorsCooperate, std::size_t auxGCThreads) noexcept { diff --git a/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.hpp b/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.hpp index 17ec84413bc..fa62c4877e5 100644 --- a/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.hpp +++ b/kotlin-native/runtime/src/gc/pmcs/cpp/ParallelMarkConcurrentSweep.hpp @@ -17,6 +17,7 @@ #include "GCState.hpp" #include "GCStatistics.hpp" #include "IntrusiveList.hpp" +#include "MainThreadFinalizerProcessor.hpp" #include "MarkAndSweepUtils.hpp" #include "ObjectData.hpp" #include "ParallelMark.hpp" @@ -72,6 +73,9 @@ public: void reconfigure(std::size_t maxParallelism, bool mutatorsCooperate, size_t auxGCThreads) noexcept; GCStateHolder& state() noexcept { return state_; } + alloc::MainThreadFinalizerProcessor& mainThreadFinalizerProcessor() noexcept { + return mainThreadFinalizerProcessor_; + } private: void mainGCThreadBody(); @@ -82,7 +86,8 @@ private: gcScheduler::GCScheduler& gcScheduler_; GCStateHolder state_; - FinalizerProcessor finalizerProcessor_; + FinalizerProcessor finalizerProcessor_; + alloc::MainThreadFinalizerProcessor mainThreadFinalizerProcessor_; mark::ParallelMark markDispatcher_; ScopedThread mainThread_; diff --git a/kotlin-native/runtime/src/gc/stms/cpp/GCImpl.cpp b/kotlin-native/runtime/src/gc/stms/cpp/GCImpl.cpp index e3e103ab449..11ca5853598 100644 --- a/kotlin-native/runtime/src/gc/stms/cpp/GCImpl.cpp +++ b/kotlin-native/runtime/src/gc/stms/cpp/GCImpl.cpp @@ -72,6 +72,14 @@ void gc::GC::WaitFinalizers(int64_t epoch) noexcept { impl_->gc().state().waitEpochFinalized(epoch); } +void gc::GC::configureMainThreadFinalizerProcessor(std::function f) noexcept { + impl_->gc().mainThreadFinalizerProcessor().withConfig(std::move(f)); +} + +bool gc::GC::mainThreadFinalizerProcessorAvailable() noexcept { + return impl_->gc().mainThreadFinalizerProcessor().available(); +} + ALWAYS_INLINE void gc::beforeHeapRefUpdate(mm::DirectRefAccessor ref, ObjHeader* value) noexcept {} ALWAYS_INLINE OBJ_GETTER(gc::weakRefReadBarrier, std::atomic& weakReferee) noexcept { diff --git a/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.cpp b/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.cpp index 91c2a6ed5a4..804e66e8cf2 100644 --- a/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.cpp +++ b/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.cpp @@ -93,9 +93,9 @@ void gc::SameThreadMarkAndSweep::PerformFullGC(int64_t epoch) noexcept { // also sweeps extraObjects auto finalizerQueue = allocator_.impl().heap().Sweep(gcHandle); for (auto& thread : kotlin::mm::ThreadRegistry::Instance().LockForIter()) { - finalizerQueue.TransferAllFrom(thread.allocator().impl().alloc().ExtractFinalizerQueue()); + finalizerQueue.mergeFrom(thread.allocator().impl().alloc().ExtractFinalizerQueue()); } - finalizerQueue.TransferAllFrom(allocator_.impl().heap().ExtractFinalizerQueue()); + finalizerQueue.mergeFrom(allocator_.impl().heap().ExtractFinalizerQueue()); #endif scheduler.onGCFinish(epoch, gcHandle.getKeptSizeBytes() + threadCount * allocator_.estimateOverheadPerThread()); @@ -105,5 +105,9 @@ void gc::SameThreadMarkAndSweep::PerformFullGC(int64_t epoch) noexcept { state_.finish(epoch); gcHandle.finalizersScheduled(finalizerQueue.size()); gcHandle.finished(); - finalizerProcessor_.ScheduleTasks(std::move(finalizerQueue), epoch); + if (!mainThreadFinalizerProcessor_.available()) { + finalizerQueue.mergeIntoRegular(); + } + finalizerProcessor_.ScheduleTasks(std::move(finalizerQueue.regular), epoch); + mainThreadFinalizerProcessor_.schedule(std::move(finalizerQueue.mainThread), epoch); } diff --git a/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.hpp b/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.hpp index 602f426a269..0eb189bcdfb 100644 --- a/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.hpp +++ b/kotlin-native/runtime/src/gc/stms/cpp/SameThreadMarkAndSweep.hpp @@ -15,6 +15,7 @@ #include "GCState.hpp" #include "GlobalData.hpp" #include "IntrusiveList.hpp" +#include "MainThreadFinalizerProcessor.hpp" #include "ObjectData.hpp" #include "Types.h" #include "Utils.hpp" @@ -49,6 +50,9 @@ public: bool FinalizersThreadIsRunning() noexcept; GCStateHolder& state() noexcept { return state_; } + alloc::MainThreadFinalizerProcessor& mainThreadFinalizerProcessor() noexcept { + return mainThreadFinalizerProcessor_; + } private: void PerformFullGC(int64_t epoch) noexcept; @@ -58,7 +62,8 @@ private: GCStateHolder state_; ScopedThread gcThread_; - FinalizerProcessor finalizerProcessor_; + FinalizerProcessor finalizerProcessor_; + alloc::MainThreadFinalizerProcessor mainThreadFinalizerProcessor_; MarkQueue markQueue_; }; diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/native/runtime/GC.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/native/runtime/GC.kt index 220a455fed7..3e43c77861e 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/native/runtime/GC.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/native/runtime/GC.kt @@ -321,6 +321,111 @@ public object GC { @Deprecated("No-op in modern GC implementation") public external fun findCycle(root: Any): Array? + /** + * This objects allows to customize the behavior of the finalizer processor that runs on the main thread. + * + * On Apple platforms Kotlin/Native releases ObjC/Swift objects that were passed to Kotlin when it processes the finalizers after GC. + * Kotlin/Native can also utilize main run loop to release objects that were passed to Kotlin on the main thread. This can be turned + * off by setting `objcDisposeOnMain` binary option to `false`. + * For more information, see [iOS integration](https://kotlinlang.org/docs/native-ios-integration.html#deinitializers). + * + * This finalizer processor works as follows: + * - Finalizers that must be run on the main thread get scheduled by the GC after the main GC phase is finished + * - A task will be posted on the main run loop in which the scheduled finalizers will start processing + * - Finalizers will be processed inside an `autoreleasepool` in batches of size [batchSize] + * - If at some point during task the processor detected that more than [maxTimeInTask] has passed, it will + * stop and post another task to the main thread to continue processing finalizers later. Note that if some + * finalizer takes a very long time, the task will still process the entire [batchSize] and may significantly overflow [maxTimeInTask] + * - It's guaranteed that the time interval between tasks is at least [minTimeBetweenTasks]. + * + * [maxTimeInTask] and [minTimeBetweenTasks] allow other tasks posted on the main thread (e.g. UI events) be processed without + * significant delays. + */ + public object MainThreadFinalizerProcessor { + /** + * `true` if Kotlin/Native will use [MainThreadFinalizerProcessor] to process finalizers. + * + * __Note__: at the very start of the program this will be `false`, but may turn `true` later, if Kotlin/Native determines that the + * main run loop is being processed. + */ + public val available: Boolean + get() = isAvailable() + + /** + * How much time can each task take. + * + * There is no guarantee that the task will be completed under this time, this is only a hint. + * + * Setting this value too high makes some other main thread tasks (e.g. UI events) be processed with high delays. + * Setting this value too low makes ObjC/Swift objects be released with high delays which contributes to memory usage. + * + * Default: 5ms + * + * @throws [IllegalArgumentException] when value is negative. + */ + public var maxTimeInTask: Duration + get() = getMaxTimeInTask().microseconds + set(value) { + require(!value.isNegative()) { "mainThreadMaxTimeInTask must not be negative: $value" } + setMaxTimeInTask(value.inWholeMicroseconds) + } + + /** + * The minimum interval between two tasks. + * + * Setting this value too high makes ObjC/Swift objects be released with high delays which contributes to memory usage. + * Setting this value too low makes some other main thread tasks (e.g. UI events) be processed with high delays. + * + * Default: 10ms + * + * @throws [IllegalArgumentException] when value is negative. + */ + public var minTimeBetweenTasks: Duration + get() = getMinTimeBetweenTasks().microseconds + set(value) { + require(!value.isNegative()) { "mainThreadMinTimeBetweenTasks must not be negative: $value" } + setMinTimeBetweenTasks(value.inWholeMicroseconds) + } + + /** + * How many finalizers will be processed inside a single `autoreleasepool`. + * + * Setting this value too high makes it more likely that [maxTimeInTask] will not be respected. + * Setting this value too low makes each single finalizer take more time and so fewer finalizers will be processed in one task. + * + * Default: 100 + * + * @throws [IllegalArgumentException] when value is 0. + */ + public var batchSize: ULong + get() = getBatchSize() + set(value) { + require(value > 0U) { "mainThreadBatchSize must not be 0" } + setBatchSize(value) + } + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_isAvailable") + private external fun isAvailable(): Boolean + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_getMaxTimeInTask") + private external fun getMaxTimeInTask(): Long + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_setMaxTimeInTask") + private external fun setMaxTimeInTask(value: Long) + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_getMinTimeBetweenTasks") + private external fun getMinTimeBetweenTasks(): Long + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_setMinTimeBetweenTasks") + private external fun setMinTimeBetweenTasks(value: Long) + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_getBatchSize") + private external fun getBatchSize(): ULong + + @GCUnsafeCall("Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_setBatchSize") + private external fun setBatchSize(value: ULong) + } + @GCUnsafeCall("Kotlin_native_internal_GC_getThreshold") private external fun getThreshold(): Int diff --git a/kotlin-native/runtime/src/mm/cpp/Memory.cpp b/kotlin-native/runtime/src/mm/cpp/Memory.cpp index fb2f3a07d61..c4828da733a 100644 --- a/kotlin-native/runtime/src/mm/cpp/Memory.cpp +++ b/kotlin-native/runtime/src/mm/cpp/Memory.cpp @@ -438,6 +438,47 @@ extern "C" void Kotlin_native_internal_GC_setCyclicCollector(ObjHeader* gc, bool // Nothing to do. } +extern "C" KBoolean Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_isAvailable(ObjHeader* gc) { + return mm::GlobalData::Instance().gc().mainThreadFinalizerProcessorAvailable(); +} + +extern "C" KLong Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_getMaxTimeInTask(ObjHeader* gc) { + KLong result; + mm::GlobalData::Instance().gc().configureMainThreadFinalizerProcessor([&](auto& config) noexcept -> void { + result = std::chrono::duration_cast(config.maxTimeInTask).count(); + }); + return result; +} + +extern "C" void Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_setMaxTimeInTask(ObjHeader* gc, KLong value) { + mm::GlobalData::Instance().gc().configureMainThreadFinalizerProcessor( + [=](auto& config) noexcept -> void { config.maxTimeInTask = std::chrono::microseconds(value); }); +} + +extern "C" KLong Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_getMinTimeBetweenTasks(ObjHeader* gc) { + KLong result; + mm::GlobalData::Instance().gc().configureMainThreadFinalizerProcessor([&](auto& config) noexcept -> void { + result = std::chrono::duration_cast(config.minTimeBetweenTasks).count(); + }); + return result; +} + +extern "C" void Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_setMinTimeBetweenTasks(ObjHeader* gc, KLong value) { + mm::GlobalData::Instance().gc().configureMainThreadFinalizerProcessor( + [=](auto& config) noexcept -> void { config.minTimeBetweenTasks = std::chrono::microseconds(value); }); +} + +extern "C" KULong Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_getBatchSize(ObjHeader* gc) { + KULong result; + mm::GlobalData::Instance().gc().configureMainThreadFinalizerProcessor( + [&](auto& config) noexcept -> void { result = config.batchSize; }); + return result; +} + +extern "C" void Kotlin_native_runtime_GC_MainThreadFinalizerProcessor_setBatchSize(ObjHeader* gc, KULong value) { + mm::GlobalData::Instance().gc().configureMainThreadFinalizerProcessor([=](auto& config) noexcept -> void { config.batchSize = value; }); +} + extern "C" bool Kotlin_Any_isShareable(ObjHeader* thiz) { // TODO: Remove when legacy MM is gone. return true;