[K/N] Add hard memory boundary ^KT-54727
This commit is contained in:
committed by
Space Team
parent
220ecc4788
commit
576f6642f9
@@ -3150,8 +3150,7 @@ standaloneTest("stress_gc_allocations") {
|
||||
(project.testTarget != "watchos_simulator_arm64") &&
|
||||
!isNoopGC &&
|
||||
!isAggressiveGC && // TODO: Investigate why too slow
|
||||
!runtimeAssertionsPanic && // New allocator with assertions makes this test very slow
|
||||
(project.testTarget != "mingw_x64") // TODO: Fix on mingw.
|
||||
!runtimeAssertionsPanic // New allocator with assertions makes this test very slow
|
||||
source = "runtime/memory/stress_gc_allocations.kt"
|
||||
flags = ['-tr', '-opt-in=kotlin.native.internal.InternalForKotlinNative']
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ fun test() {
|
||||
if (Platform.memoryModel == MemoryModel.EXPERIMENTAL) {
|
||||
kotlin.native.runtime.GC.autotune = false
|
||||
kotlin.native.runtime.GC.targetHeapBytes = retainLimit
|
||||
kotlin.native.runtime.GC.pauseOnTargetHeapOverflow = true
|
||||
}
|
||||
|
||||
// On Linux, the child process might immediately commit the same amount of memory as the parent.
|
||||
|
||||
@@ -370,6 +370,7 @@ bitcode {
|
||||
headersDirs.from(files("src/gc/common/cpp", "src/mm/cpp", "src/main/cpp"))
|
||||
sourceSets {
|
||||
main {}
|
||||
test {}
|
||||
}
|
||||
|
||||
onlyIf { target.supportsThreads() }
|
||||
|
||||
@@ -59,29 +59,10 @@ struct ProcessWeaksTraits {
|
||||
|
||||
} // namespace
|
||||
|
||||
void gc::ConcurrentMarkAndSweep::ThreadData::Schedule() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
gc_.state_.schedule();
|
||||
}
|
||||
|
||||
void gc::ConcurrentMarkAndSweep::ThreadData::ScheduleAndWaitFullGC() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
auto scheduled_epoch = gc_.state_.schedule();
|
||||
gc_.state_.waitEpochFinished(scheduled_epoch);
|
||||
}
|
||||
|
||||
void gc::ConcurrentMarkAndSweep::ThreadData::ScheduleAndWaitFullGCWithFinalizers() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
auto scheduled_epoch = gc_.state_.schedule();
|
||||
gc_.state_.waitEpochFinalized(scheduled_epoch);
|
||||
}
|
||||
|
||||
void gc::ConcurrentMarkAndSweep::ThreadData::OnOOM(size_t size) noexcept {
|
||||
RuntimeLogDebug({kTagGC}, "Attempt to GC on OOM at size=%zu", size);
|
||||
ScheduleAndWaitFullGC();
|
||||
// TODO: This will print the log for "manual" scheduling. Fix this.
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinished();
|
||||
}
|
||||
|
||||
void gc::ConcurrentMarkAndSweep::ThreadData::OnSuspendForGC() noexcept {
|
||||
@@ -173,7 +154,7 @@ void gc::ConcurrentMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
|
||||
#endif
|
||||
|
||||
auto& scheduler = gcScheduler_;
|
||||
scheduler.gcData().OnPerformFullGC();
|
||||
scheduler.onGCStart();
|
||||
|
||||
state_.start(epoch);
|
||||
|
||||
@@ -222,7 +203,7 @@ void gc::ConcurrentMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
|
||||
finalizerQueue.TransferAllFrom(thread.gc().impl().alloc().ExtractFinalizerQueue());
|
||||
}
|
||||
#endif
|
||||
scheduler.gcData().UpdateAliveSetBytes(allocatedBytes());
|
||||
scheduler.onGCFinish(epoch, allocatedBytes());
|
||||
state_.finish(epoch);
|
||||
gcHandle.finalizersScheduled(finalizerQueue.size());
|
||||
gcHandle.finished();
|
||||
|
||||
@@ -78,13 +78,9 @@ public:
|
||||
|
||||
using Allocator = AllocatorWithGC<Allocator, ThreadData>;
|
||||
|
||||
explicit ThreadData(ConcurrentMarkAndSweep& gc, mm::ThreadData& threadData) noexcept : gc_(gc), threadData_(threadData) {}
|
||||
explicit ThreadData(ConcurrentMarkAndSweep& gc, mm::ThreadData& threadData) noexcept : threadData_(threadData) {}
|
||||
~ThreadData() = default;
|
||||
|
||||
void Schedule() noexcept;
|
||||
void ScheduleAndWaitFullGC() noexcept;
|
||||
void ScheduleAndWaitFullGCWithFinalizers() noexcept;
|
||||
|
||||
void OnOOM(size_t size) noexcept;
|
||||
|
||||
void OnSuspendForGC() noexcept;
|
||||
@@ -97,7 +93,6 @@ public:
|
||||
|
||||
private:
|
||||
friend ConcurrentMarkAndSweep;
|
||||
ConcurrentMarkAndSweep& gc_;
|
||||
mm::ThreadData& threadData_;
|
||||
std::atomic<bool> marking_;
|
||||
BarriersThreadData barriers_;
|
||||
@@ -135,8 +130,7 @@ public:
|
||||
alloc::Heap& heap() noexcept { return heap_; }
|
||||
#endif
|
||||
|
||||
int64_t Schedule() noexcept { return state_.schedule(); }
|
||||
void WaitFinalized(int64_t epoch) noexcept { state_.waitEpochFinalized(epoch); }
|
||||
GCStateHolder& state() noexcept { return state_; }
|
||||
|
||||
private:
|
||||
void PerformFullGC(int64_t epoch) noexcept;
|
||||
|
||||
@@ -250,7 +250,7 @@ TEST_P(ConcurrentMarkAndSweepTest, RootSet) {
|
||||
ASSERT_THAT(IsMarked(stack2.header()), false);
|
||||
ASSERT_THAT(IsMarked(stack3.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -296,7 +296,7 @@ TEST_P(ConcurrentMarkAndSweepTest, InterconnectedRootSet) {
|
||||
ASSERT_THAT(IsMarked(stack2.header()), false);
|
||||
ASSERT_THAT(IsMarked(stack3.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -320,7 +320,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjects) {
|
||||
ASSERT_THAT(IsMarked(object1.header()), false);
|
||||
ASSERT_THAT(IsMarked(object2.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
});
|
||||
@@ -337,7 +337,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectsWithFinalizers) {
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(object1.header()));
|
||||
EXPECT_CALL(finalizerHook(), Call(object2.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
});
|
||||
@@ -357,7 +357,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithFreeWeak) {
|
||||
ASSERT_THAT(weak1.get(), object1.header());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak1.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
});
|
||||
@@ -374,7 +374,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithHoldedWeak) {
|
||||
ASSERT_THAT(IsMarked(weak1.header()), false);
|
||||
ASSERT_THAT(weak1.get(), object1.header());
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(weak1.header(), stack.header()));
|
||||
EXPECT_THAT(IsMarked(weak1.header()), false);
|
||||
@@ -407,7 +407,7 @@ TEST_P(ConcurrentMarkAndSweepTest, ObjectReferencedFromRootSet) {
|
||||
ASSERT_THAT(IsMarked(object3.header()), false);
|
||||
ASSERT_THAT(IsMarked(object4.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -456,7 +456,7 @@ TEST_P(ConcurrentMarkAndSweepTest, ObjectsWithCycles) {
|
||||
ASSERT_THAT(IsMarked(object5.header()), false);
|
||||
ASSERT_THAT(IsMarked(object6.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -507,7 +507,7 @@ TEST_P(ConcurrentMarkAndSweepTest, ObjectsWithCyclesAndFinalizers) {
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(object5.header()));
|
||||
EXPECT_CALL(finalizerHook(), Call(object6.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -540,7 +540,7 @@ TEST_P(ConcurrentMarkAndSweepTest, ObjectsWithCyclesIntoRootSet) {
|
||||
ASSERT_THAT(IsMarked(object1.header()), false);
|
||||
ASSERT_THAT(IsMarked(object2.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), stack.header(), object1.header(), object2.header()));
|
||||
EXPECT_THAT(IsMarked(global.header()), false);
|
||||
@@ -584,8 +584,8 @@ TEST_P(ConcurrentMarkAndSweepTest, RunGCTwice) {
|
||||
ASSERT_THAT(IsMarked(object5.header()), false);
|
||||
ASSERT_THAT(IsMarked(object6.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -615,7 +615,7 @@ TEST_P(ConcurrentMarkAndSweepTest, PermanentObjects) {
|
||||
ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(global2.header()));
|
||||
EXPECT_THAT(IsMarked(global2.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global2.header()));
|
||||
EXPECT_THAT(IsMarked(global2.header()), false);
|
||||
@@ -635,7 +635,7 @@ TEST_P(ConcurrentMarkAndSweepTest, SameObjectInRootSet) {
|
||||
EXPECT_THAT(IsMarked(global.header()), false);
|
||||
EXPECT_THAT(IsMarked(object.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), object.header()));
|
||||
EXPECT_THAT(IsMarked(global.header()), false);
|
||||
@@ -742,7 +742,7 @@ TEST_P(ConcurrentMarkAndSweepTest, MultipleMutatorsCollect) {
|
||||
}));
|
||||
}
|
||||
|
||||
mm::GlobalData::Instance().gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
gcDone.store(true, std::memory_order_relaxed);
|
||||
|
||||
for (auto& future : gcFutures) {
|
||||
@@ -792,7 +792,7 @@ TEST_P(ConcurrentMarkAndSweepTest, MultipleMutatorsAllCollect) {
|
||||
|
||||
for (auto& mutator : mutators) {
|
||||
gcFutures.emplace_back(mutator.Execute([](mm::ThreadData& threadData, Mutator& mutator) {
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
// If GC starts before all thread executed line above, two gc will be run
|
||||
// So we temporary switch threads to native state and then return them back after all GC runs are done
|
||||
SwitchThreadState(mm::GetMemoryState(), kotlin::ThreadState::kNative);
|
||||
@@ -939,7 +939,7 @@ TEST_P(ConcurrentMarkAndSweepTest, CrossThreadReference) {
|
||||
}));
|
||||
}
|
||||
|
||||
mm::GlobalData::Instance().gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
gcDone.store(true, std::memory_order_relaxed);
|
||||
|
||||
for (auto& future : gcFutures) {
|
||||
@@ -1142,7 +1142,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithFreeWeakReversedOrder) {
|
||||
global1->field1 = object1_local.header();
|
||||
while (weak.load() == nullptr)
|
||||
;
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(object1_local.header(), weak.load()->header(), global1.header()));
|
||||
ASSERT_THAT(IsMarked(global1.header()), false);
|
||||
@@ -1153,7 +1153,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithFreeWeakReversedOrder) {
|
||||
global1->field1 = nullptr;
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak.load()->header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global1.header()));
|
||||
done = true;
|
||||
|
||||
@@ -19,18 +19,6 @@ gc::GC::ThreadData::ThreadData(GC& gc, mm::ThreadData& threadData) noexcept : im
|
||||
|
||||
gc::GC::ThreadData::~ThreadData() = default;
|
||||
|
||||
void gc::GC::ThreadData::Schedule() noexcept {
|
||||
impl_->gc().Schedule();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::ScheduleAndWaitFullGC() noexcept {
|
||||
impl_->gc().ScheduleAndWaitFullGC();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::ScheduleAndWaitFullGCWithFinalizers() noexcept {
|
||||
impl_->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::Publish() noexcept {
|
||||
#ifndef CUSTOM_ALLOCATOR
|
||||
impl_->extraObjectDataFactoryThreadQueue().Publish();
|
||||
@@ -144,11 +132,15 @@ ALWAYS_INLINE void gc::GC::processFieldInMark(void* state, ObjHeader* field) noe
|
||||
}
|
||||
|
||||
int64_t gc::GC::Schedule() noexcept {
|
||||
return impl_->gc().Schedule();
|
||||
return impl_->gc().state().schedule();
|
||||
}
|
||||
|
||||
void gc::GC::WaitFinished(int64_t epoch) noexcept {
|
||||
impl_->gc().state().waitEpochFinished(epoch);
|
||||
}
|
||||
|
||||
void gc::GC::WaitFinalizers(int64_t epoch) noexcept {
|
||||
impl_->gc().WaitFinalized(epoch);
|
||||
impl_->gc().state().waitEpochFinalized(epoch);
|
||||
}
|
||||
|
||||
bool gc::isMarked(ObjHeader* object) noexcept {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
#include "ExtraObjectData.hpp"
|
||||
#include "GCScheduler.hpp"
|
||||
@@ -34,10 +35,6 @@ public:
|
||||
|
||||
Impl& impl() noexcept { return *impl_; }
|
||||
|
||||
void Schedule() noexcept;
|
||||
void ScheduleAndWaitFullGC() noexcept;
|
||||
void ScheduleAndWaitFullGCWithFinalizers() noexcept;
|
||||
|
||||
void Publish() noexcept;
|
||||
void ClearForTests() noexcept;
|
||||
|
||||
@@ -73,10 +70,10 @@ public:
|
||||
static void processArrayInMark(void* state, ArrayHeader* array) noexcept;
|
||||
static void processFieldInMark(void* state, ObjHeader* field) noexcept;
|
||||
|
||||
// TODO: These should be moved into the scheduler.
|
||||
// TODO: These should exist only in the scheduler.
|
||||
int64_t Schedule() noexcept;
|
||||
void WaitFinished(int64_t epoch) noexcept;
|
||||
void WaitFinalizers(int64_t epoch) noexcept;
|
||||
void ScheduleAndWaitFullGCWithFinalizers() noexcept { WaitFinalizers(Schedule()); }
|
||||
|
||||
static const size_t objectDataSize;
|
||||
static bool SweepObject(void* objectData) noexcept;
|
||||
|
||||
@@ -19,18 +19,6 @@ gc::GC::ThreadData::ThreadData(GC& gc, mm::ThreadData& threadData) noexcept : im
|
||||
|
||||
gc::GC::ThreadData::~ThreadData() = default;
|
||||
|
||||
void gc::GC::ThreadData::Schedule() noexcept {
|
||||
impl_->gc().Schedule();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::ScheduleAndWaitFullGC() noexcept {
|
||||
impl_->gc().ScheduleAndWaitFullGC();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::ScheduleAndWaitFullGCWithFinalizers() noexcept {
|
||||
impl_->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::Publish() noexcept {
|
||||
#ifndef CUSTOM_ALLOCATOR
|
||||
impl_->extraObjectDataFactoryThreadQueue().Publish();
|
||||
@@ -131,6 +119,9 @@ ALWAYS_INLINE void gc::GC::processFieldInMark(void* state, ObjHeader* field) noe
|
||||
int64_t gc::GC::Schedule() noexcept {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void gc::GC::WaitFinished(int64_t epoch) noexcept {}
|
||||
|
||||
void gc::GC::WaitFinalizers(int64_t epoch) noexcept {}
|
||||
|
||||
bool gc::isMarked(ObjHeader* object) noexcept {
|
||||
|
||||
@@ -39,10 +39,6 @@ public:
|
||||
ThreadData() noexcept {}
|
||||
~ThreadData() = default;
|
||||
|
||||
void Schedule() noexcept {}
|
||||
void ScheduleAndWaitFullGC() noexcept {}
|
||||
void ScheduleAndWaitFullGCWithFinalizers() noexcept {}
|
||||
|
||||
void OnOOM(size_t size) noexcept {}
|
||||
|
||||
Allocator CreateAllocator() noexcept { return Allocator(); }
|
||||
|
||||
@@ -19,18 +19,6 @@ gc::GC::ThreadData::ThreadData(GC& gc, mm::ThreadData& threadData) noexcept : im
|
||||
|
||||
gc::GC::ThreadData::~ThreadData() = default;
|
||||
|
||||
void gc::GC::ThreadData::Schedule() noexcept {
|
||||
impl_->gc().Schedule();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::ScheduleAndWaitFullGC() noexcept {
|
||||
impl_->gc().ScheduleAndWaitFullGC();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::ScheduleAndWaitFullGCWithFinalizers() noexcept {
|
||||
impl_->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
}
|
||||
|
||||
void gc::GC::ThreadData::Publish() noexcept {
|
||||
#ifndef CUSTOM_ALLOCATOR
|
||||
impl_->extraObjectDataFactoryThreadQueue().Publish();
|
||||
@@ -140,11 +128,15 @@ ALWAYS_INLINE void gc::GC::processFieldInMark(void* state, ObjHeader* field) noe
|
||||
}
|
||||
|
||||
int64_t gc::GC::Schedule() noexcept {
|
||||
return impl_->gc().Schedule();
|
||||
return impl_->gc().state().schedule();
|
||||
}
|
||||
|
||||
void gc::GC::WaitFinished(int64_t epoch) noexcept {
|
||||
impl_->gc().state().waitEpochFinished(epoch);
|
||||
}
|
||||
|
||||
void gc::GC::WaitFinalizers(int64_t epoch) noexcept {
|
||||
impl_->gc().WaitFinalized(epoch);
|
||||
impl_->gc().state().waitEpochFinalized(epoch);
|
||||
}
|
||||
|
||||
bool gc::isMarked(ObjHeader* object) noexcept {
|
||||
|
||||
@@ -58,29 +58,10 @@ struct ProcessWeaksTraits {
|
||||
|
||||
} // namespace
|
||||
|
||||
void gc::SameThreadMarkAndSweep::ThreadData::Schedule() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
gc_.state_.schedule();
|
||||
}
|
||||
|
||||
void gc::SameThreadMarkAndSweep::ThreadData::ScheduleAndWaitFullGC() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
auto scheduled_epoch = gc_.state_.schedule();
|
||||
gc_.state_.waitEpochFinished(scheduled_epoch);
|
||||
}
|
||||
|
||||
void gc::SameThreadMarkAndSweep::ThreadData::ScheduleAndWaitFullGCWithFinalizers() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
auto scheduled_epoch = gc_.state_.schedule();
|
||||
gc_.state_.waitEpochFinalized(scheduled_epoch);
|
||||
}
|
||||
|
||||
void gc::SameThreadMarkAndSweep::ThreadData::OnOOM(size_t size) noexcept {
|
||||
RuntimeLogDebug({kTagGC}, "Attempt to GC on OOM at size=%zu", size);
|
||||
ScheduleAndWaitFullGC();
|
||||
// TODO: This will print the log for "manual" scheduling. Fix this.
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinished();
|
||||
}
|
||||
|
||||
#ifdef CUSTOM_ALLOCATOR
|
||||
@@ -142,7 +123,7 @@ void gc::SameThreadMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
|
||||
gcHandle.threadsAreSuspended();
|
||||
|
||||
auto& scheduler = gcScheduler_;
|
||||
scheduler.gcData().OnPerformFullGC();
|
||||
scheduler.onGCStart();
|
||||
|
||||
state_.start(epoch);
|
||||
|
||||
@@ -175,7 +156,7 @@ void gc::SameThreadMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
|
||||
auto finalizerQueue = heap_.Sweep(gcHandle);
|
||||
#endif
|
||||
|
||||
scheduler.gcData().UpdateAliveSetBytes(allocatedBytes());
|
||||
scheduler.onGCFinish(epoch, allocatedBytes());
|
||||
|
||||
mm::ResumeThreads();
|
||||
gcHandle.threadsAreResumed();
|
||||
|
||||
@@ -79,20 +79,14 @@ public:
|
||||
using ObjectData = SameThreadMarkAndSweep::ObjectData;
|
||||
using Allocator = AllocatorWithGC<Allocator, ThreadData>;
|
||||
|
||||
ThreadData(SameThreadMarkAndSweep& gc, mm::ThreadData& threadData) noexcept : gc_(gc) {}
|
||||
ThreadData(SameThreadMarkAndSweep& gc, mm::ThreadData& threadData) noexcept {}
|
||||
~ThreadData() = default;
|
||||
|
||||
void Schedule() noexcept;
|
||||
void ScheduleAndWaitFullGC() noexcept;
|
||||
void ScheduleAndWaitFullGCWithFinalizers() noexcept;
|
||||
|
||||
void OnOOM(size_t size) noexcept;
|
||||
|
||||
Allocator CreateAllocator() noexcept { return Allocator(gc::Allocator(), *this); }
|
||||
|
||||
private:
|
||||
|
||||
SameThreadMarkAndSweep& gc_;
|
||||
};
|
||||
|
||||
using Allocator = ThreadData::Allocator;
|
||||
@@ -118,8 +112,7 @@ public:
|
||||
void StopFinalizerThreadIfRunning() noexcept;
|
||||
bool FinalizersThreadIsRunning() noexcept;
|
||||
|
||||
int64_t Schedule() noexcept { return state_.schedule(); }
|
||||
void WaitFinalized(int64_t epoch) noexcept { state_.waitEpochFinalized(epoch); }
|
||||
GCStateHolder& state() noexcept { return state_; }
|
||||
|
||||
#ifdef CUSTOM_ALLOCATOR
|
||||
alloc::Heap& heap() noexcept { return heap_; }
|
||||
|
||||
@@ -246,7 +246,7 @@ TEST_F(SameThreadMarkAndSweepTest, RootSet) {
|
||||
ASSERT_THAT(IsMarked(stack2.header()), false);
|
||||
ASSERT_THAT(IsMarked(stack3.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -292,7 +292,7 @@ TEST_F(SameThreadMarkAndSweepTest, InterconnectedRootSet) {
|
||||
ASSERT_THAT(IsMarked(stack2.header()), false);
|
||||
ASSERT_THAT(IsMarked(stack3.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -316,7 +316,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjects) {
|
||||
ASSERT_THAT(IsMarked(object1.header()), false);
|
||||
ASSERT_THAT(IsMarked(object2.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
});
|
||||
@@ -333,7 +333,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectsWithFinalizers) {
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(object1.header()));
|
||||
EXPECT_CALL(finalizerHook(), Call(object2.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
});
|
||||
@@ -353,7 +353,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithFreeWeak) {
|
||||
ASSERT_THAT(weak1.get(), object1.header());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak1.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
});
|
||||
@@ -370,7 +370,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithHoldedWeak) {
|
||||
ASSERT_THAT(IsMarked(weak1.header()), false);
|
||||
ASSERT_THAT(weak1.get(), object1.header());
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(weak1.header(), stack.header()));
|
||||
EXPECT_THAT(IsMarked(weak1.header()), false);
|
||||
@@ -403,7 +403,7 @@ TEST_F(SameThreadMarkAndSweepTest, ObjectReferencedFromRootSet) {
|
||||
ASSERT_THAT(IsMarked(object3.header()), false);
|
||||
ASSERT_THAT(IsMarked(object4.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -452,7 +452,7 @@ TEST_F(SameThreadMarkAndSweepTest, ObjectsWithCycles) {
|
||||
ASSERT_THAT(IsMarked(object5.header()), false);
|
||||
ASSERT_THAT(IsMarked(object6.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -503,7 +503,7 @@ TEST_F(SameThreadMarkAndSweepTest, ObjectsWithCyclesAndFinalizers) {
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(object5.header()));
|
||||
EXPECT_CALL(finalizerHook(), Call(object6.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -536,7 +536,7 @@ TEST_F(SameThreadMarkAndSweepTest, ObjectsWithCyclesIntoRootSet) {
|
||||
ASSERT_THAT(IsMarked(object1.header()), false);
|
||||
ASSERT_THAT(IsMarked(object2.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), stack.header(), object1.header(), object2.header()));
|
||||
EXPECT_THAT(IsMarked(global.header()), false);
|
||||
@@ -580,8 +580,8 @@ TEST_F(SameThreadMarkAndSweepTest, RunGCTwice) {
|
||||
ASSERT_THAT(IsMarked(object5.header()), false);
|
||||
ASSERT_THAT(IsMarked(object6.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(
|
||||
Alive(threadData),
|
||||
@@ -611,7 +611,7 @@ TEST_F(SameThreadMarkAndSweepTest, PermanentObjects) {
|
||||
ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(global2.header()));
|
||||
EXPECT_THAT(IsMarked(global2.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global2.header()));
|
||||
EXPECT_THAT(IsMarked(global2.header()), false);
|
||||
@@ -631,7 +631,7 @@ TEST_F(SameThreadMarkAndSweepTest, SameObjectInRootSet) {
|
||||
EXPECT_THAT(IsMarked(global.header()), false);
|
||||
EXPECT_THAT(IsMarked(object.header()), false);
|
||||
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), object.header()));
|
||||
EXPECT_THAT(IsMarked(global.header()), false);
|
||||
@@ -738,7 +738,7 @@ TEST_F(SameThreadMarkAndSweepTest, MultipleMutatorsCollect) {
|
||||
}));
|
||||
}
|
||||
|
||||
mm::GlobalData::Instance().gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
gcDone.store(true, std::memory_order_relaxed);
|
||||
|
||||
for (auto& future : gcFutures) {
|
||||
@@ -788,7 +788,7 @@ TEST_F(SameThreadMarkAndSweepTest, MultipleMutatorsAllCollect) {
|
||||
|
||||
for (auto& mutator : mutators) {
|
||||
gcFutures.emplace_back(mutator.Execute([](mm::ThreadData& threadData, Mutator& mutator) {
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
// If GC starts before all thread executed line above, two gc will be run
|
||||
// So we temporary switch threads to native state and then return them back after all GC runs are done
|
||||
SwitchThreadState(mm::GetMemoryState(), kotlin::ThreadState::kNative);
|
||||
@@ -935,7 +935,7 @@ TEST_F(SameThreadMarkAndSweepTest, CrossThreadReference) {
|
||||
}));
|
||||
}
|
||||
|
||||
mm::GlobalData::Instance().gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
gcDone.store(true, std::memory_order_relaxed);
|
||||
|
||||
for (auto& future : gcFutures) {
|
||||
@@ -1138,7 +1138,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithFreeWeakReversedOrder) {
|
||||
global1->field1 = object1_local.header();
|
||||
while (weak.load() == nullptr)
|
||||
;
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(object1_local.header(), weak.load()->header(), global1.header()));
|
||||
ASSERT_THAT(IsMarked(global1.header()), false);
|
||||
@@ -1149,7 +1149,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithFreeWeakReversedOrder) {
|
||||
global1->field1 = nullptr;
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak.load()->header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global1.header()));
|
||||
done = true;
|
||||
|
||||
@@ -13,12 +13,57 @@
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
gcScheduler::GCScheduler::GCScheduler() noexcept :
|
||||
gcData_(std_support::make_unique<internal::GCSchedulerDataAdaptive<steady_clock>>(config_, []() noexcept {
|
||||
gcScheduler::GCScheduler::ThreadData::Impl::Impl(GCScheduler& scheduler, mm::ThreadData& thread) noexcept :
|
||||
scheduler_(scheduler.impl().impl()), mutatorAssists_(scheduler_.mutatorAssists(), thread) {}
|
||||
|
||||
gcScheduler::GCScheduler::ThreadData::ThreadData(gcScheduler::GCScheduler& scheduler, mm::ThreadData& thread) noexcept :
|
||||
impl_(std_support::make_unique<Impl>(scheduler, thread)) {}
|
||||
|
||||
gcScheduler::GCScheduler::ThreadData::~ThreadData() = default;
|
||||
|
||||
gcScheduler::GCScheduler::Impl::Impl(gcScheduler::GCSchedulerConfig& config) noexcept :
|
||||
impl_(config, []() noexcept {
|
||||
// This call acquires a lock, but the lock are always short-lived,
|
||||
// so we ignore thread state switching to avoid recursive safe points.
|
||||
CallsCheckerIgnoreGuard guard;
|
||||
mm::GlobalData::Instance().gc().Schedule();
|
||||
})) {}
|
||||
return mm::GlobalData::Instance().gc().Schedule();
|
||||
}) {}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::safePoint() noexcept {}
|
||||
gcScheduler::GCScheduler::GCScheduler() noexcept : impl_(std_support::make_unique<Impl>(config_)) {}
|
||||
|
||||
gcScheduler::GCScheduler::~GCScheduler() = default;
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::ThreadData::safePoint() noexcept {
|
||||
impl().mutatorAssists().safePoint();
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::schedule() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
impl().impl().schedule();
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::scheduleAndWaitFinished() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
auto epoch = impl().impl().schedule();
|
||||
NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
|
||||
mm::GlobalData::Instance().gc().WaitFinished(epoch);
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::scheduleAndWaitFinalized() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
auto epoch = impl().impl().schedule();
|
||||
NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
|
||||
mm::GlobalData::Instance().gc().WaitFinalizers(epoch);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::setAllocatedBytes(size_t bytes) noexcept {
|
||||
impl().impl().setAllocatedBytes(bytes);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::onGCStart() noexcept {
|
||||
impl().impl().onGCStart();
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::onGCFinish(int64_t epoch, size_t aliveBytes) noexcept {
|
||||
impl().impl().onGCFinish(epoch, aliveBytes);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,38 @@
|
||||
#include "GlobalData.hpp"
|
||||
#include "HeapGrowthController.hpp"
|
||||
#include "Logging.hpp"
|
||||
#include "MutatorAssists.hpp"
|
||||
#include "RegularIntervalPacer.hpp"
|
||||
#include "RepeatedTimer.hpp"
|
||||
#include "SafePoint.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
|
||||
namespace kotlin::gcScheduler::internal {
|
||||
namespace kotlin::gcScheduler {
|
||||
|
||||
namespace internal {
|
||||
template <typename Clock>
|
||||
class GCSchedulerDataAdaptive;
|
||||
}
|
||||
|
||||
class GCScheduler::ThreadData::Impl : private Pinned {
|
||||
public:
|
||||
Impl(GCScheduler& scheduler, mm::ThreadData& thread) noexcept;
|
||||
|
||||
internal::GCSchedulerDataAdaptive<steady_clock>& scheduler() noexcept { return scheduler_; }
|
||||
|
||||
internal::MutatorAssists::ThreadData& mutatorAssists() noexcept { return mutatorAssists_; }
|
||||
|
||||
private:
|
||||
internal::GCSchedulerDataAdaptive<steady_clock>& scheduler_;
|
||||
internal::MutatorAssists::ThreadData mutatorAssists_;
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
template <typename Clock>
|
||||
class GCSchedulerDataAdaptive : public GCSchedulerData {
|
||||
class GCSchedulerDataAdaptive {
|
||||
public:
|
||||
GCSchedulerDataAdaptive(GCSchedulerConfig& config, std::function<void()> scheduleGC) noexcept :
|
||||
GCSchedulerDataAdaptive(GCSchedulerConfig& config, std::function<int64_t()> scheduleGC) noexcept :
|
||||
config_(config),
|
||||
scheduleGC_(std::move(scheduleGC)),
|
||||
appStateTracking_(mm::GlobalData::Instance().appStateTracking()),
|
||||
@@ -32,34 +55,70 @@ public:
|
||||
}
|
||||
if (regularIntervalPacer_.NeedsGC()) {
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by timer");
|
||||
scheduleGC_();
|
||||
schedule();
|
||||
}
|
||||
}) {
|
||||
RuntimeLogInfo({kTagGC}, "Adaptive GC scheduler initialized");
|
||||
}
|
||||
|
||||
void OnPerformFullGC() noexcept override {
|
||||
heapGrowthController_.OnPerformFullGC();
|
||||
void onGCStart() noexcept {
|
||||
regularIntervalPacer_.OnPerformFullGC();
|
||||
timer_.restart(config_.regularGcInterval());
|
||||
}
|
||||
|
||||
void UpdateAliveSetBytes(size_t bytes) noexcept override { heapGrowthController_.UpdateAliveSetBytes(bytes); }
|
||||
|
||||
void SetAllocatedBytes(size_t bytes) noexcept override {
|
||||
if (heapGrowthController_.SetAllocatedBytes(bytes)) {
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by allocation");
|
||||
scheduleGC_();
|
||||
void setAllocatedBytes(size_t bytes) noexcept {
|
||||
auto boundary = heapGrowthController_.boundaryForHeapSize(bytes);
|
||||
switch (boundary) {
|
||||
case HeapGrowthController::MemoryBoundary::kNone:
|
||||
return;
|
||||
case HeapGrowthController::MemoryBoundary::kTrigger:
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by allocation");
|
||||
schedule();
|
||||
return;
|
||||
case HeapGrowthController::MemoryBoundary::kTarget:
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by allocation");
|
||||
auto epoch = schedule();
|
||||
RuntimeLogWarning({kTagGC}, "Pausing the mutators");
|
||||
mutatorAssists_.requestAssists(epoch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void onGCFinish(int64_t epoch, size_t bytes) noexcept {
|
||||
heapGrowthController_.updateBoundaries(bytes);
|
||||
// Must wait for all mutators to be released. GC thread cannot continue.
|
||||
// This is the contract between GC and mutators. With regular native state
|
||||
// each mutator must check that GC is not doing something. Here GC must check
|
||||
// that each mutator has done all it needs.
|
||||
mutatorAssists_.completeEpoch(epoch, [](mm::ThreadData& threadData) noexcept -> MutatorAssists::ThreadData& {
|
||||
return threadData.gcScheduler().impl().mutatorAssists();
|
||||
});
|
||||
}
|
||||
|
||||
int64_t schedule() noexcept { return scheduleGC_(); }
|
||||
|
||||
MutatorAssists& mutatorAssists() noexcept { return mutatorAssists_; }
|
||||
|
||||
private:
|
||||
GCSchedulerConfig& config_;
|
||||
std::function<void()> scheduleGC_;
|
||||
std::function<int64_t()> scheduleGC_;
|
||||
mm::AppStateTracking& appStateTracking_;
|
||||
HeapGrowthController heapGrowthController_;
|
||||
RegularIntervalPacer<Clock> regularIntervalPacer_;
|
||||
RepeatedTimer<Clock> timer_;
|
||||
MutatorAssists mutatorAssists_;
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler::internal
|
||||
} // namespace internal
|
||||
|
||||
class GCScheduler::Impl : private Pinned {
|
||||
public:
|
||||
explicit Impl(GCSchedulerConfig& config) noexcept;
|
||||
|
||||
internal::GCSchedulerDataAdaptive<steady_clock>& impl() noexcept { return impl_; }
|
||||
|
||||
private:
|
||||
internal::GCSchedulerDataAdaptive<steady_clock> impl_;
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler
|
||||
|
||||
@@ -20,18 +20,19 @@ namespace {
|
||||
|
||||
class MutatorThread : private Pinned {
|
||||
public:
|
||||
explicit MutatorThread(gcScheduler::GCSchedulerData& scheduler) : executor_([&scheduler] { return Context{scheduler}; }) {}
|
||||
explicit MutatorThread(gcScheduler::internal::GCSchedulerDataAdaptive<test_support::manual_clock>& scheduler) :
|
||||
executor_([&scheduler] { return Context{scheduler}; }) {}
|
||||
|
||||
std::future<void> SetAllocatedBytes(size_t bytes) {
|
||||
return executor_.execute([&, bytes] {
|
||||
auto& context = executor_.context();
|
||||
context.scheduler.SetAllocatedBytes(bytes);
|
||||
context.scheduler.setAllocatedBytes(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Context {
|
||||
gcScheduler::GCSchedulerData& scheduler;
|
||||
gcScheduler::internal::GCSchedulerDataAdaptive<test_support::manual_clock>& scheduler;
|
||||
};
|
||||
|
||||
SingleThreadExecutor<Context> executor_;
|
||||
@@ -53,24 +54,26 @@ public:
|
||||
return mutators_[mutator]->SetAllocatedBytes(allocatedBytes);
|
||||
}
|
||||
|
||||
void OnPerformFullGC() { scheduler_.OnPerformFullGC(); }
|
||||
void OnPerformFullGC() { scheduler_.onGCStart(); }
|
||||
|
||||
void UpdateAliveSetBytes(size_t bytes) {
|
||||
void onGCFinish(int64_t epoch, size_t bytes) {
|
||||
allocatedBytes_.store(bytes);
|
||||
scheduler_.UpdateAliveSetBytes(bytes);
|
||||
scheduler_.onGCFinish(epoch, bytes);
|
||||
}
|
||||
|
||||
testing::MockFunction<void()>& scheduleGC() { return scheduleGC_; }
|
||||
testing::MockFunction<int64_t()>& scheduleGC() { return scheduleGC_; }
|
||||
|
||||
template <typename Duration>
|
||||
void advance_time(Duration duration) {
|
||||
test_support::manual_clock::sleep_for(duration);
|
||||
}
|
||||
|
||||
int64_t assistsRequested() noexcept { return scheduler_.mutatorAssists().assistsRequested(std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
std::atomic<size_t> allocatedBytes_ = 0;
|
||||
std_support::vector<std_support::unique_ptr<MutatorThread>> mutators_;
|
||||
testing::MockFunction<void()> scheduleGC_;
|
||||
testing::MockFunction<int64_t()> scheduleGC_;
|
||||
gcScheduler::internal::GCSchedulerDataAdaptive<test_support::manual_clock> scheduler_;
|
||||
};
|
||||
|
||||
@@ -88,34 +91,77 @@ TEST_F(AdaptiveSchedulerTest, CollectOnTargetHeapReached) {
|
||||
config.regularGcIntervalMicroseconds = 10;
|
||||
config.autoTune = false;
|
||||
config.targetHeapBytes = (mutatorsCount + 1) * 10;
|
||||
config.heapTriggerCoefficient = 0.9;
|
||||
config.setMutatorAssists(true);
|
||||
GCSchedulerDataTestApi<mutatorsCount> schedulerTestApi(config);
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).Times(0);
|
||||
std_support::vector<std::future<void>> futures;
|
||||
for (int i = 0; i < mutatorsCount; ++i) {
|
||||
futures.push_back(schedulerTestApi.Allocate(i, 10));
|
||||
futures.push_back(schedulerTestApi.Allocate(i, 9));
|
||||
}
|
||||
for (auto& future : futures) {
|
||||
future.get();
|
||||
}
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call());
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).WillOnce(testing::Return(1));
|
||||
schedulerTestApi.Allocate(0, 9).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
EXPECT_THAT(schedulerTestApi.assistsRequested(), 0);
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(0);
|
||||
schedulerTestApi.onGCFinish(1, 0);
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).Times(0);
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call());
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).WillOnce(testing::Return(2));
|
||||
schedulerTestApi.Allocate(0, mutatorsCount * 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
EXPECT_THAT(schedulerTestApi.assistsRequested(), 2);
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(0);
|
||||
schedulerTestApi.onGCFinish(2, 0);
|
||||
}
|
||||
|
||||
TEST_F(AdaptiveSchedulerTest, CollectOnTargetHeapReachedWithoutAssists) {
|
||||
constexpr int mutatorsCount = kDefaultThreadCount;
|
||||
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.regularGcIntervalMicroseconds = 10;
|
||||
config.autoTune = false;
|
||||
config.targetHeapBytes = (mutatorsCount + 1) * 10;
|
||||
config.heapTriggerCoefficient = 0.9;
|
||||
config.setMutatorAssists(false);
|
||||
GCSchedulerDataTestApi<mutatorsCount> schedulerTestApi(config);
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).Times(0);
|
||||
std_support::vector<std::future<void>> futures;
|
||||
for (int i = 0; i < mutatorsCount; ++i) {
|
||||
futures.push_back(schedulerTestApi.Allocate(i, 9));
|
||||
}
|
||||
for (auto& future : futures) {
|
||||
future.get();
|
||||
}
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).WillOnce(testing::Return(1));
|
||||
schedulerTestApi.Allocate(0, 9).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
EXPECT_THAT(schedulerTestApi.assistsRequested(), 0);
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.onGCFinish(1, 0);
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).Times(0);
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).WillOnce(testing::Return(2));
|
||||
schedulerTestApi.Allocate(0, mutatorsCount * 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
EXPECT_THAT(schedulerTestApi.assistsRequested(), 0);
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.onGCFinish(2, 0);
|
||||
}
|
||||
|
||||
TEST_F(AdaptiveSchedulerTest, CollectOnTimeoutReached) {
|
||||
@@ -135,7 +181,7 @@ TEST_F(AdaptiveSchedulerTest, CollectOnTimeoutReached) {
|
||||
test_support::manual_clock::waitForPending(test_support::manual_clock::now() + microseconds(10));
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(0);
|
||||
schedulerTestApi.onGCFinish(1, 0);
|
||||
}
|
||||
|
||||
TEST_F(AdaptiveSchedulerTest, FullTimeoutAfterLastGC) {
|
||||
@@ -155,7 +201,7 @@ TEST_F(AdaptiveSchedulerTest, FullTimeoutAfterLastGC) {
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(0);
|
||||
schedulerTestApi.onGCFinish(1, 0);
|
||||
|
||||
// pending should restart to be 10us since the previous collection without scheduling another GC.
|
||||
EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).Times(0);
|
||||
@@ -176,7 +222,7 @@ TEST_F(AdaptiveSchedulerTest, DoNotTuneTargetHeap) {
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(10);
|
||||
schedulerTestApi.onGCFinish(1, 10);
|
||||
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 10);
|
||||
}
|
||||
@@ -197,7 +243,7 @@ TEST_F(AdaptiveSchedulerTest, TuneTargetHeap) {
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(10);
|
||||
schedulerTestApi.onGCFinish(1, 10);
|
||||
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 20);
|
||||
|
||||
@@ -206,7 +252,7 @@ TEST_F(AdaptiveSchedulerTest, TuneTargetHeap) {
|
||||
schedulerTestApi.Allocate(0, 10).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(20);
|
||||
schedulerTestApi.onGCFinish(2, 20);
|
||||
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 40);
|
||||
|
||||
@@ -215,7 +261,7 @@ TEST_F(AdaptiveSchedulerTest, TuneTargetHeap) {
|
||||
schedulerTestApi.Allocate(0, 40).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(60);
|
||||
schedulerTestApi.onGCFinish(3, 60);
|
||||
|
||||
// But we will keep the 50, which means we will trigger GC every allocation, until alive set falls down
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 50);
|
||||
@@ -225,7 +271,7 @@ TEST_F(AdaptiveSchedulerTest, TuneTargetHeap) {
|
||||
schedulerTestApi.Allocate(0, 0).get();
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(60);
|
||||
schedulerTestApi.onGCFinish(4, 60);
|
||||
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 50);
|
||||
|
||||
@@ -234,7 +280,7 @@ TEST_F(AdaptiveSchedulerTest, TuneTargetHeap) {
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
// Dropping to 40
|
||||
schedulerTestApi.UpdateAliveSetBytes(40);
|
||||
schedulerTestApi.onGCFinish(5, 40);
|
||||
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 50);
|
||||
|
||||
@@ -244,7 +290,7 @@ TEST_F(AdaptiveSchedulerTest, TuneTargetHeap) {
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
// Dropping to 1
|
||||
schedulerTestApi.UpdateAliveSetBytes(1);
|
||||
schedulerTestApi.onGCFinish(6, 1);
|
||||
|
||||
// But the minimum is set to 5.
|
||||
EXPECT_THAT(config.targetHeapBytes.load(), 5);
|
||||
@@ -283,5 +329,5 @@ TEST_F(AdaptiveSchedulerTest, DoNotCollectOnTimerInBackground) {
|
||||
test_support::manual_clock::waitForPending(test_support::manual_clock::now() + microseconds(10));
|
||||
testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC());
|
||||
schedulerTestApi.OnPerformFullGC();
|
||||
schedulerTestApi.UpdateAliveSetBytes(0);
|
||||
schedulerTestApi.onGCFinish(1, 0);
|
||||
}
|
||||
|
||||
@@ -13,14 +13,56 @@
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
gcScheduler::GCScheduler::GCScheduler() noexcept :
|
||||
gcData_(std_support::make_unique<internal::GCSchedulerDataAggressive>(config_, []() noexcept {
|
||||
gcScheduler::GCScheduler::ThreadData::Impl::Impl(GCScheduler& scheduler, mm::ThreadData& thread) noexcept :
|
||||
scheduler_(scheduler.impl().impl()), mutatorAssists_(scheduler_.mutatorAssists(), thread) {}
|
||||
|
||||
gcScheduler::GCScheduler::ThreadData::ThreadData(gcScheduler::GCScheduler& gcScheduler, mm::ThreadData& thread) noexcept :
|
||||
impl_(std_support::make_unique<Impl>(gcScheduler, thread)) {}
|
||||
|
||||
gcScheduler::GCScheduler::ThreadData::~ThreadData() = default;
|
||||
|
||||
gcScheduler::GCScheduler::Impl::Impl(gcScheduler::GCSchedulerConfig& config) noexcept :
|
||||
impl_(config, []() noexcept {
|
||||
// This call acquires a lock, but the lock are always short-lived,
|
||||
// so we ignore thread state switching to avoid recursive safe points.
|
||||
CallsCheckerIgnoreGuard guard;
|
||||
mm::GlobalData::Instance().gc().Schedule();
|
||||
})) {}
|
||||
return mm::GlobalData::Instance().gc().Schedule();
|
||||
}) {}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::safePoint() noexcept {
|
||||
static_cast<internal::GCSchedulerDataAggressive&>(gcData()).safePoint();
|
||||
gcScheduler::GCScheduler::GCScheduler() noexcept : impl_(std_support::make_unique<Impl>(config_)) {}
|
||||
|
||||
gcScheduler::GCScheduler::~GCScheduler() = default;
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::ThreadData::safePoint() noexcept {
|
||||
impl().mutatorAssists().safePoint();
|
||||
impl().scheduler().safePoint();
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::schedule() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
impl().impl().schedule();
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::scheduleAndWaitFinished() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
auto epoch = impl().impl().schedule();
|
||||
NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
|
||||
mm::GlobalData::Instance().gc().WaitFinished(epoch);
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::scheduleAndWaitFinalized() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
auto epoch = impl().impl().schedule();
|
||||
NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
|
||||
mm::GlobalData::Instance().gc().WaitFinalizers(epoch);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::setAllocatedBytes(size_t bytes) noexcept {
|
||||
impl().impl().setAllocatedBytes(bytes);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::onGCStart() noexcept {}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::onGCFinish(int64_t epoch, size_t aliveBytes) noexcept {
|
||||
impl().impl().onGCFinish(epoch, aliveBytes);
|
||||
}
|
||||
|
||||
@@ -12,44 +12,101 @@
|
||||
#include "GCSchedulerConfig.hpp"
|
||||
#include "HeapGrowthController.hpp"
|
||||
#include "Logging.hpp"
|
||||
#include "MutatorAssists.hpp"
|
||||
#include "SafePoint.hpp"
|
||||
#include "SafePointTracker.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
|
||||
namespace kotlin::gcScheduler::internal {
|
||||
namespace kotlin::gcScheduler {
|
||||
|
||||
namespace internal {
|
||||
class GCSchedulerDataAggressive;
|
||||
}
|
||||
|
||||
class GCScheduler::ThreadData::Impl : private Pinned {
|
||||
public:
|
||||
Impl(GCScheduler& scheduler, mm::ThreadData& thread) noexcept;
|
||||
|
||||
internal::GCSchedulerDataAggressive& scheduler() noexcept { return scheduler_; }
|
||||
|
||||
internal::MutatorAssists::ThreadData& mutatorAssists() noexcept { return mutatorAssists_; }
|
||||
|
||||
private:
|
||||
internal::GCSchedulerDataAggressive& scheduler_;
|
||||
internal::MutatorAssists::ThreadData mutatorAssists_;
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
// The slowpath will trigger GC if this thread didn't meet this safepoint/allocation site before.
|
||||
class GCSchedulerDataAggressive : public GCSchedulerData {
|
||||
class GCSchedulerDataAggressive {
|
||||
public:
|
||||
GCSchedulerDataAggressive(GCSchedulerConfig& config, std::function<void()> scheduleGC) noexcept :
|
||||
GCSchedulerDataAggressive(GCSchedulerConfig& config, std::function<int64_t()> scheduleGC) noexcept :
|
||||
scheduleGC_(std::move(scheduleGC)), heapGrowthController_(config) {
|
||||
RuntimeLogInfo({kTagGC}, "Aggressive GC scheduler initialized");
|
||||
}
|
||||
|
||||
void OnPerformFullGC() noexcept override { heapGrowthController_.OnPerformFullGC(); }
|
||||
void UpdateAliveSetBytes(size_t bytes) noexcept override { heapGrowthController_.UpdateAliveSetBytes(bytes); }
|
||||
void SetAllocatedBytes(size_t bytes) noexcept override {
|
||||
void setAllocatedBytes(size_t bytes) noexcept {
|
||||
// Still checking allocations: with a long running loop all safepoints
|
||||
// might be "met", so that's the only trigger to not run out of memory.
|
||||
if (heapGrowthController_.SetAllocatedBytes(bytes)) {
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by allocation");
|
||||
scheduleGC_();
|
||||
} else {
|
||||
safePoint();
|
||||
auto boundary = heapGrowthController_.boundaryForHeapSize(bytes);
|
||||
switch (boundary) {
|
||||
case HeapGrowthController::MemoryBoundary::kNone:
|
||||
safePoint();
|
||||
return;
|
||||
case HeapGrowthController::MemoryBoundary::kTrigger:
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by allocation");
|
||||
schedule();
|
||||
return;
|
||||
case HeapGrowthController::MemoryBoundary::kTarget:
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by allocation");
|
||||
auto epoch = schedule();
|
||||
RuntimeLogWarning({kTagGC}, "Pausing the mutators");
|
||||
mutatorAssists_.requestAssists(epoch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void safePoint() noexcept {
|
||||
if (safePointTracker_.registerCurrentSafePoint(1)) {
|
||||
RuntimeLogDebug({kTagGC}, "Scheduling GC by safepoint");
|
||||
scheduleGC_();
|
||||
schedule();
|
||||
}
|
||||
}
|
||||
|
||||
void onGCFinish(int64_t epoch, size_t aliveBytes) noexcept {
|
||||
heapGrowthController_.updateBoundaries(aliveBytes);
|
||||
// Must wait for all mutators to be released. GC thread cannot continue.
|
||||
// This is the contract between GC and mutators. With regular native state
|
||||
// each mutator must check that GC is not doing something. Here GC must check
|
||||
// that each mutator has done all it needs.
|
||||
mutatorAssists_.completeEpoch(epoch, [](mm::ThreadData& threadData) noexcept -> MutatorAssists::ThreadData& {
|
||||
return threadData.gcScheduler().impl().mutatorAssists();
|
||||
});
|
||||
}
|
||||
|
||||
int64_t schedule() noexcept { return scheduleGC_(); }
|
||||
|
||||
MutatorAssists& mutatorAssists() noexcept { return mutatorAssists_; }
|
||||
|
||||
private:
|
||||
std::function<void()> scheduleGC_;
|
||||
std::function<int64_t()> scheduleGC_;
|
||||
HeapGrowthController heapGrowthController_;
|
||||
SafePointTracker<> safePointTracker_;
|
||||
mm::SafePointActivator safePointActivator_;
|
||||
MutatorAssists mutatorAssists_;
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler::internal
|
||||
} // namespace internal
|
||||
|
||||
class GCScheduler::Impl : private Pinned {
|
||||
public:
|
||||
explicit Impl(GCSchedulerConfig& config) noexcept;
|
||||
|
||||
internal::GCSchedulerDataAggressive& impl() noexcept { return impl_; }
|
||||
|
||||
private:
|
||||
internal::GCSchedulerDataAggressive impl_;
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler
|
||||
|
||||
@@ -27,18 +27,18 @@ using namespace kotlin;
|
||||
TEST(AggressiveSchedulerTest, TriggerGCOnUniqueSafePoint) {
|
||||
SKIP_ON_WINDOWS();
|
||||
[]() OPTNONE {
|
||||
testing::MockFunction<void()> scheduleGC;
|
||||
testing::MockFunction<int64_t()> scheduleGC;
|
||||
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
gcScheduler::internal::GCSchedulerDataAggressive scheduler(config, scheduleGC.AsStdFunction());
|
||||
|
||||
EXPECT_CALL(scheduleGC, Call()).Times(1);
|
||||
EXPECT_CALL(scheduleGC, Call()).WillOnce(testing::Return(0));
|
||||
for (int i = 0; i < 10; i++) {
|
||||
scheduler.safePoint();
|
||||
}
|
||||
testing::Mock::VerifyAndClearExpectations(&scheduleGC);
|
||||
|
||||
EXPECT_CALL(scheduleGC, Call()).Times(1);
|
||||
EXPECT_CALL(scheduleGC, Call()).WillOnce(testing::Return(1));
|
||||
scheduler.safePoint();
|
||||
testing::Mock::VerifyAndClearExpectations(&scheduleGC);
|
||||
}();
|
||||
@@ -47,20 +47,34 @@ TEST(AggressiveSchedulerTest, TriggerGCOnUniqueSafePoint) {
|
||||
TEST(AggressiveSchedulerTest, TriggerGCOnAllocationThreshold) {
|
||||
SKIP_ON_WINDOWS();
|
||||
[]() OPTNONE {
|
||||
testing::MockFunction<void()> scheduleGC;
|
||||
testing::MockFunction<int64_t()> scheduleGC;
|
||||
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.autoTune = false;
|
||||
config.targetHeapBytes = 10;
|
||||
config.heapTriggerCoefficient = 0.9;
|
||||
gcScheduler::internal::GCSchedulerDataAggressive scheduler(config, scheduleGC.AsStdFunction());
|
||||
|
||||
int i = 0;
|
||||
// We trigger GC on the first iteration, when the unique allocation point is faced,
|
||||
// on the second to last iteration when weak target heap size is reached,
|
||||
// and on the last iteration when target heap size is reached.
|
||||
EXPECT_CALL(scheduleGC, Call()).WillOnce([&i]() { EXPECT_THAT(i, 0); }).WillOnce([&i]() { EXPECT_THAT(i, 9); });
|
||||
EXPECT_CALL(scheduleGC, Call())
|
||||
.WillOnce([&i]() {
|
||||
EXPECT_THAT(i, 0);
|
||||
return 0;
|
||||
})
|
||||
.WillOnce([&i]() {
|
||||
EXPECT_THAT(i, 8);
|
||||
return 1;
|
||||
})
|
||||
.WillOnce([&i]() {
|
||||
EXPECT_THAT(i, 9);
|
||||
return 2;
|
||||
});
|
||||
|
||||
for (; i < 10; i++) {
|
||||
scheduler.SetAllocatedBytes(i + 1);
|
||||
scheduler.setAllocatedBytes(i + 1);
|
||||
}
|
||||
testing::Mock::VerifyAndClearExpectations(&scheduleGC);
|
||||
}();
|
||||
|
||||
@@ -14,45 +14,59 @@
|
||||
#include "Utils.hpp"
|
||||
#include "std_support/Memory.hpp"
|
||||
|
||||
namespace kotlin::gcScheduler {
|
||||
|
||||
namespace test_support {
|
||||
class GCSchedulerThreadDataTestApi;
|
||||
namespace kotlin::mm {
|
||||
class ThreadData;
|
||||
}
|
||||
|
||||
class GCSchedulerThreadData;
|
||||
|
||||
class GCSchedulerData {
|
||||
public:
|
||||
virtual ~GCSchedulerData() = default;
|
||||
|
||||
// The protocol is: after the scheduler schedules the GC, the GC eventually calls `OnPerformFullGC`
|
||||
// when the collection has started, followed by `UpdateAliveSetBytes` when the marking has finished.
|
||||
// TODO: Consider returning a sort of future from the scheduleGC, and listen to it instead.
|
||||
|
||||
// Always called by the GC thread.
|
||||
virtual void OnPerformFullGC() noexcept = 0;
|
||||
|
||||
// Always called by the GC thread.
|
||||
virtual void UpdateAliveSetBytes(size_t bytes) noexcept = 0;
|
||||
|
||||
// Called by different mutator threads.
|
||||
virtual void SetAllocatedBytes(size_t bytes) noexcept = 0;
|
||||
};
|
||||
namespace kotlin::gcScheduler {
|
||||
|
||||
class GCScheduler : private Pinned {
|
||||
public:
|
||||
class Impl;
|
||||
|
||||
class ThreadData : private Pinned {
|
||||
public:
|
||||
class Impl;
|
||||
|
||||
ThreadData(GCScheduler&, mm::ThreadData&) noexcept;
|
||||
~ThreadData();
|
||||
|
||||
Impl& impl() noexcept { return *impl_; }
|
||||
|
||||
void safePoint() noexcept;
|
||||
|
||||
private:
|
||||
std_support::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
GCScheduler() noexcept;
|
||||
~GCScheduler();
|
||||
|
||||
Impl& impl() noexcept { return *impl_; }
|
||||
|
||||
GCSchedulerConfig& config() noexcept { return config_; }
|
||||
GCSchedulerData& gcData() noexcept { return *gcData_; }
|
||||
|
||||
// Should be called on encountering a safepoint.
|
||||
void safePoint() noexcept;
|
||||
// Called by different mutator threads.
|
||||
void setAllocatedBytes(size_t bytes) noexcept;
|
||||
|
||||
// Can be called by any thread.
|
||||
void schedule() noexcept;
|
||||
|
||||
// Can be called by any thread.
|
||||
void scheduleAndWaitFinished() noexcept;
|
||||
|
||||
// Can be called by any thread.
|
||||
void scheduleAndWaitFinalized() noexcept;
|
||||
|
||||
// Always called by the GC thread.
|
||||
void onGCStart() noexcept;
|
||||
|
||||
// Called by the GC thread only.
|
||||
void onGCFinish(int64_t epoch, size_t aliveBytes) noexcept;
|
||||
|
||||
private:
|
||||
GCSchedulerConfig config_;
|
||||
std_support::unique_ptr<GCSchedulerData> gcData_;
|
||||
std_support::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler
|
||||
|
||||
@@ -13,22 +13,63 @@ namespace kotlin::gcScheduler {
|
||||
|
||||
// NOTE: When changing default values, reflect them in GC.kt as well.
|
||||
struct GCSchedulerConfig {
|
||||
enum class MutatorAssists {
|
||||
kDefault,
|
||||
kEnable,
|
||||
kDisable,
|
||||
};
|
||||
|
||||
std::atomic<bool> autoTune = true;
|
||||
// The target interval between collections when Kotlin code is idle. GC will be triggered
|
||||
// by timer no sooner than this value and no later than twice this value since the previous collection.
|
||||
std::atomic<int64_t> regularGcIntervalMicroseconds = 10 * 1000 * 1000;
|
||||
// How many object bytes must be in the heap to trigger collection. Autotunes when autoTune is true.
|
||||
// GC will try to keep object bytes under this amount. If object bytes have
|
||||
// become bigger than this value, and `mutatorAssists` are enabled the GC will
|
||||
// stop the world and wait until current epoch finishes.
|
||||
// Adapts after each GC epoch when `autoTune = true`.
|
||||
std::atomic<int64_t> targetHeapBytes = 1024 * 1024;
|
||||
// The rate at which targetHeapBytes changes when autoTune = true. Concretely: if after the collection
|
||||
// N object bytes remain in the heap, the next targetHeapBytes will be N / targetHeapUtilization capped
|
||||
// between minHeapBytes and maxHeapBytes.
|
||||
// The rate at which `targetHeapBytes` changes when `autoTune = true`. Concretely: if after the collection
|
||||
// `N` object bytes remain in the heap, the next `targetHeapBytes` will be `N / targetHeapUtilization` capped
|
||||
// between `minHeapBytes` and `maxHeapBytes`.
|
||||
std::atomic<double> targetHeapUtilization = 0.5;
|
||||
// The minimum value of targetHeapBytes for autoTune = true
|
||||
// The minimum value of `targetHeapBytes` for `autoTune = true`
|
||||
std::atomic<int64_t> minHeapBytes = 1024 * 1024;
|
||||
// The maximum value of targetHeapBytes for autoTune = true
|
||||
// The maximum value of `targetHeapBytes` for `autoTune = true`
|
||||
std::atomic<int64_t> maxHeapBytes = std::numeric_limits<int64_t>::max();
|
||||
// GC will be triggered when object bytes reach `heapTriggerCoefficient * targetHeapBytes`.
|
||||
std::atomic<double> heapTriggerCoefficient = 0.9;
|
||||
// See `mutatorAssists()`.
|
||||
std::atomic<std::underlying_type_t<MutatorAssists>> mutatorAssistsImpl =
|
||||
static_cast<std::underlying_type_t<MutatorAssists>>(MutatorAssists::kDefault);
|
||||
|
||||
std::chrono::microseconds regularGcInterval() const { return std::chrono::microseconds(regularGcIntervalMicroseconds.load()); }
|
||||
|
||||
// Whether mutators should stop and wait for GC to complete when
|
||||
// current object heap size is bigger than `targetHeapBytes`.
|
||||
// By default on, unless `autoTune = false` or `maxHeapBytes` is set.
|
||||
bool mutatorAssists() const noexcept {
|
||||
switch (static_cast<MutatorAssists>(mutatorAssistsImpl.load())) {
|
||||
case MutatorAssists::kDisable:
|
||||
return false;
|
||||
case MutatorAssists::kEnable:
|
||||
return true;
|
||||
case MutatorAssists::kDefault:
|
||||
// If after a GC epoch the alive set is more than maximum `targetHeapBytes`, the next GC will be
|
||||
// scheduled instantly and when the assists are turned on, the mutators would be immediately paused.
|
||||
// This will look like the program has hanged.
|
||||
// So, by default, disable assisting if `targetHeapBytes` has a non-infinite limit
|
||||
// (either `autoTune == false`, so `targetHeapBytes` is fixed; or `maxHeapBytes`
|
||||
// is lower than infinity).
|
||||
// TODO: Figure out what to do with OOMs.
|
||||
return autoTune.load() && maxHeapBytes.load() == std::numeric_limits<int64_t>::max();
|
||||
}
|
||||
}
|
||||
|
||||
// See `mutatorAssists()`.
|
||||
void setMutatorAssists(bool assist) noexcept {
|
||||
mutatorAssistsImpl.store(
|
||||
static_cast<std::underlying_type_t<MutatorAssists>>(assist ? MutatorAssists::kEnable : MutatorAssists::kDisable));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "GCSchedulerConfig.hpp"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
TEST(GCSchedulerConfigTest, DefaultMutatorAssists) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
EXPECT_TRUE(config.mutatorAssists());
|
||||
config.autoTune = false;
|
||||
EXPECT_FALSE(config.mutatorAssists());
|
||||
config.autoTune = true;
|
||||
ASSERT_TRUE(config.mutatorAssists());
|
||||
config.maxHeapBytes = 1024 * 1024 * 1024;
|
||||
EXPECT_FALSE(config.mutatorAssists());
|
||||
config.maxHeapBytes = std::numeric_limits<int64_t>::max();
|
||||
EXPECT_TRUE(config.mutatorAssists());
|
||||
}
|
||||
|
||||
TEST(GCSchedulerConfigTest, DisabledMutatorAssists) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.setMutatorAssists(false);
|
||||
EXPECT_FALSE(config.mutatorAssists());
|
||||
config.autoTune = false;
|
||||
EXPECT_FALSE(config.mutatorAssists());
|
||||
config.autoTune = true;
|
||||
ASSERT_FALSE(config.mutatorAssists());
|
||||
config.maxHeapBytes = 1024 * 1024 * 1024;
|
||||
EXPECT_FALSE(config.mutatorAssists());
|
||||
config.maxHeapBytes = std::numeric_limits<int64_t>::max();
|
||||
EXPECT_FALSE(config.mutatorAssists());
|
||||
}
|
||||
|
||||
TEST(GCSchedulerConfigTest, EnabledMutatorAssists) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.setMutatorAssists(true);
|
||||
EXPECT_TRUE(config.mutatorAssists());
|
||||
config.autoTune = false;
|
||||
EXPECT_TRUE(config.mutatorAssists());
|
||||
config.autoTune = true;
|
||||
ASSERT_TRUE(config.mutatorAssists());
|
||||
config.maxHeapBytes = 1024 * 1024 * 1024;
|
||||
EXPECT_TRUE(config.mutatorAssists());
|
||||
config.maxHeapBytes = std::numeric_limits<int64_t>::max();
|
||||
EXPECT_TRUE(config.mutatorAssists());
|
||||
}
|
||||
@@ -17,17 +17,35 @@ namespace kotlin::gcScheduler::internal {
|
||||
|
||||
class HeapGrowthController {
|
||||
public:
|
||||
explicit HeapGrowthController(GCSchedulerConfig& config) noexcept :
|
||||
config_(config), targetHeapBytes_(config.targetHeapBytes.load(std::memory_order_relaxed)) {}
|
||||
enum class MemoryBoundary {
|
||||
// Memory usage is low.
|
||||
kNone,
|
||||
// Memory usage is high, GC should be triggered.
|
||||
kTrigger,
|
||||
// Memory usage is critical, GC is running behind the mutators. Mutators should pause.
|
||||
kTarget,
|
||||
};
|
||||
|
||||
// Called by the mutators.
|
||||
// Returns true if needs GC.
|
||||
bool SetAllocatedBytes(size_t totalAllocatedBytes) noexcept { return totalAllocatedBytes >= targetHeapBytes_; }
|
||||
explicit HeapGrowthController(GCSchedulerConfig& config) noexcept :
|
||||
config_(config), targetHeapBytes_(config.targetHeapBytes.load(std::memory_order_relaxed)) {
|
||||
triggerHeapBytes_ = targetHeapBytes_ * config_.heapTriggerCoefficient.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Can be called by any thread.
|
||||
MemoryBoundary boundaryForHeapSize(size_t totalAllocatedBytes) noexcept {
|
||||
if (totalAllocatedBytes >= targetHeapBytes_) {
|
||||
return config_.mutatorAssists() ? MemoryBoundary::kTarget : MemoryBoundary::kTrigger;
|
||||
} else if (totalAllocatedBytes >= triggerHeapBytes_) {
|
||||
return MemoryBoundary::kTrigger;
|
||||
} else {
|
||||
return MemoryBoundary::kNone;
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the GC thread.
|
||||
void UpdateAliveSetBytes(size_t bytes) noexcept {
|
||||
void updateBoundaries(size_t aliveBytes) noexcept {
|
||||
if (config_.autoTune.load()) {
|
||||
double targetHeapBytes = static_cast<double>(bytes) / config_.targetHeapUtilization;
|
||||
double targetHeapBytes = static_cast<double>(aliveBytes) / config_.targetHeapUtilization;
|
||||
if (!std::isfinite(targetHeapBytes)) {
|
||||
// This shouldn't happen in practice: targetHeapUtilization is in (0, 1]. But in case it does, don't touch anything.
|
||||
return;
|
||||
@@ -35,6 +53,7 @@ public:
|
||||
double minHeapBytes = static_cast<double>(config_.minHeapBytes.load(std::memory_order_relaxed));
|
||||
double maxHeapBytes = static_cast<double>(config_.maxHeapBytes.load(std::memory_order_relaxed));
|
||||
targetHeapBytes = std::min(std::max(targetHeapBytes, minHeapBytes), maxHeapBytes);
|
||||
triggerHeapBytes_ = static_cast<size_t>(targetHeapBytes * config_.heapTriggerCoefficient.load(std::memory_order_relaxed));
|
||||
config_.targetHeapBytes.store(static_cast<int64_t>(targetHeapBytes), std::memory_order_relaxed);
|
||||
targetHeapBytes_ = static_cast<size_t>(targetHeapBytes);
|
||||
} else {
|
||||
@@ -42,14 +61,13 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void OnPerformFullGC() noexcept {
|
||||
// TODO: Need to protect against mutators that can overrun the GC thread.
|
||||
targetHeapBytes_ = std::numeric_limits<size_t>::max();
|
||||
}
|
||||
size_t targetHeapBytes() const noexcept { return targetHeapBytes_; }
|
||||
size_t triggerHeapBytes() const noexcept { return triggerHeapBytes_; }
|
||||
|
||||
private:
|
||||
GCSchedulerConfig& config_;
|
||||
size_t targetHeapBytes_ = 0;
|
||||
size_t triggerHeapBytes_ = 0;
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler::internal
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "HeapGrowthController.hpp"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
using gcScheduler::internal::HeapGrowthController;
|
||||
using MemoryBoundary = HeapGrowthController::MemoryBoundary;
|
||||
|
||||
TEST(HeapGrowthControllerTest, BoundariesWithAssists) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.targetHeapBytes = 10;
|
||||
config.heapTriggerCoefficient = 0.7;
|
||||
ASSERT_TRUE(config.mutatorAssists());
|
||||
|
||||
HeapGrowthController controller(config);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
for (size_t i = 0; i < 12; ++i) {
|
||||
auto expected = i < 7 ? MemoryBoundary::kNone : i < 10 ? MemoryBoundary::kTrigger : MemoryBoundary::kTarget;
|
||||
EXPECT_THAT(controller.boundaryForHeapSize(i), expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HeapGrowthControllerTest, BoundariesWithoutAssists) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.targetHeapBytes = 10;
|
||||
config.heapTriggerCoefficient = 0.7;
|
||||
config.setMutatorAssists(false);
|
||||
|
||||
HeapGrowthController controller(config);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
for (size_t i = 0; i < 12; ++i) {
|
||||
auto expected = i < 7 ? MemoryBoundary::kNone : MemoryBoundary::kTrigger;
|
||||
EXPECT_THAT(controller.boundaryForHeapSize(i), expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HeapGrowthControllerTest, NoTune) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.autoTune = false;
|
||||
config.targetHeapBytes = 10;
|
||||
config.heapTriggerCoefficient = 0.7;
|
||||
|
||||
HeapGrowthController controller(config);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
controller.updateBoundaries(0);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
controller.updateBoundaries(10);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
controller.updateBoundaries(10000);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
controller.updateBoundaries(std::numeric_limits<uint32_t>::max());
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
}
|
||||
|
||||
TEST(HeapGrowthControllerTest, Tune) {
|
||||
gcScheduler::GCSchedulerConfig config;
|
||||
config.autoTune = true;
|
||||
config.minHeapBytes = 10;
|
||||
config.maxHeapBytes = 1000;
|
||||
config.targetHeapBytes = 100;
|
||||
config.heapTriggerCoefficient = 0.7;
|
||||
config.targetHeapUtilization = 0.5;
|
||||
|
||||
HeapGrowthController controller(config);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 100);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 70);
|
||||
|
||||
controller.updateBoundaries(0);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 10);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 7);
|
||||
|
||||
controller.updateBoundaries(10);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 20);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 14);
|
||||
|
||||
controller.updateBoundaries(100);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 200);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 140);
|
||||
|
||||
controller.updateBoundaries(10000);
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 1000);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 700);
|
||||
|
||||
controller.updateBoundaries(std::numeric_limits<uint32_t>::max());
|
||||
EXPECT_THAT(controller.targetHeapBytes(), 1000);
|
||||
EXPECT_THAT(controller.triggerHeapBytes(), 700);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "MutatorAssists.hpp"
|
||||
|
||||
#include "CallsChecker.hpp"
|
||||
#include "KAssert.h"
|
||||
#include "Logging.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
void gcScheduler::internal::MutatorAssists::ThreadData::safePoint() noexcept {
|
||||
Epoch epoch = owner_.assistsEpoch_.load(std::memory_order_acquire);
|
||||
auto noNeedToWait = [this, epoch] { return owner_.completedEpoch_.load(std::memory_order_acquire) >= epoch; };
|
||||
if (noNeedToWait()) return;
|
||||
auto prevState = thread_.suspensionData().setStateNoSafePoint(ThreadState::kNative);
|
||||
RuntimeAssert(prevState == ThreadState::kRunnable, "Expected runnable state");
|
||||
startedWaiting_.store(epoch * 2, std::memory_order_release);
|
||||
{
|
||||
std::unique_lock guard(owner_.m_);
|
||||
RuntimeLogDebug({kTagGC}, "Thread is assisting for epoch %" PRId64, epoch);
|
||||
owner_.cv_.wait(guard, noNeedToWait);
|
||||
RuntimeLogDebug({kTagGC}, "Thread has assisted for epoch %" PRId64, epoch);
|
||||
}
|
||||
startedWaiting_.store(epoch * 2 + 1, std::memory_order_release);
|
||||
// Not doing a safe point. We're a safe point.
|
||||
prevState = thread_.suspensionData().setStateNoSafePoint(ThreadState::kRunnable);
|
||||
RuntimeAssert(prevState == ThreadState::kNative, "Expected native state");
|
||||
}
|
||||
|
||||
bool gcScheduler::internal::MutatorAssists::ThreadData::completedEpoch(Epoch epoch) const noexcept {
|
||||
auto [waitingEpoch, isWaiting] = startedWaiting(std::memory_order_acquire);
|
||||
if (waitingEpoch > epoch)
|
||||
// Waiting for an epoch bigger than `epoch` => `epoch` is done here.
|
||||
return true;
|
||||
return !isWaiting;
|
||||
}
|
||||
|
||||
void gcScheduler::internal::MutatorAssists::requestAssists(Epoch epoch) noexcept {
|
||||
RuntimeLogDebug({kTagGC}, "Requesting assists for epoch %" PRId64, epoch);
|
||||
CallsCheckerIgnoreGuard guard;
|
||||
std::unique_lock lockGuard(m_);
|
||||
if (assistsEpoch_.load(std::memory_order_relaxed) >= epoch) {
|
||||
return;
|
||||
}
|
||||
assistsEpoch_.store(epoch, std::memory_order_release);
|
||||
if (completedEpoch_.load(std::memory_order_relaxed) >= epoch) {
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeLogDebug({kTagGC}, "Enabling assists for epoch %" PRId64, epoch);
|
||||
if (!safePointActivator_) {
|
||||
safePointActivator_ = mm::SafePointActivator();
|
||||
}
|
||||
}
|
||||
|
||||
void gcScheduler::internal::MutatorAssists::markEpochCompleted(Epoch epoch) noexcept {
|
||||
RuntimeLogDebug({kTagGC}, "Disabling assists for epoch %" PRId64, epoch);
|
||||
{
|
||||
std::unique_lock guard(m_);
|
||||
auto previousEpoch = completedEpoch_.exchange(epoch, std::memory_order_release);
|
||||
RuntimeAssert(
|
||||
previousEpoch == epoch - 1, "Epochs must be increasing by 1. Previous: %" PRId64 ". Setting: %" PRId64, previousEpoch,
|
||||
epoch);
|
||||
if (epoch >= assistsEpoch_.load(std::memory_order_relaxed)) {
|
||||
safePointActivator_ = std::nullopt;
|
||||
}
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "SafePoint.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
#if KONAN_WINDOWS
|
||||
#include "ConditionVariable.hpp"
|
||||
#else
|
||||
#include <condition_variable>
|
||||
#endif
|
||||
|
||||
namespace kotlin::gcScheduler::internal {
|
||||
|
||||
/**
|
||||
* Coordinating mutator assistance to the GC.
|
||||
*
|
||||
* Currently assisting only by pausing threads. So assisting means not creating
|
||||
* more work for the GC thread.
|
||||
*
|
||||
* Threads (both mutators and any other) can call `requestAssists(epoch)` for any
|
||||
* `epoch` at any time.
|
||||
*
|
||||
* If the current GC epoch is greater than `epoch`, the mutators should ignore
|
||||
* the request to assist.
|
||||
*
|
||||
* Otherwise the mutators must wait in the native state
|
||||
* until the GC thread calls `completeEpoch(epoch)` for epoch >= `epoch`.
|
||||
*
|
||||
* The GC thread shall call `completeEpoch(epoch)` once it is done with the epoch,
|
||||
* and it shall wait for all mutators assisting `epoch` (or lower) to continue.
|
||||
*/
|
||||
class MutatorAssists : private Pinned {
|
||||
public:
|
||||
using Epoch = int64_t;
|
||||
|
||||
class ThreadData : private Pinned {
|
||||
public:
|
||||
ThreadData(MutatorAssists& owner, mm::ThreadData& thread) noexcept : owner_(owner), thread_(thread) {}
|
||||
|
||||
void safePoint() noexcept;
|
||||
|
||||
std::pair<Epoch, bool> startedWaiting(std::memory_order ordering) const noexcept {
|
||||
auto value = startedWaiting_.load(ordering);
|
||||
auto waitingEpoch = value / 2;
|
||||
bool isWaiting = value % 2 == 0;
|
||||
return {waitingEpoch, isWaiting};
|
||||
}
|
||||
|
||||
private:
|
||||
friend class MutatorAssists;
|
||||
|
||||
bool completedEpoch(Epoch epoch) const noexcept;
|
||||
|
||||
MutatorAssists& owner_;
|
||||
mm::ThreadData& thread_;
|
||||
// Contains epoch * 2. The lower bit is 1, if completed waiting.
|
||||
std::atomic<int64_t> startedWaiting_ = 1;
|
||||
};
|
||||
|
||||
// Request all `kRunnable` mutators to start assisting GC for epoch `epoch`.
|
||||
// Can be called multiple times per `epoch`, and `epoch` may point to the past.
|
||||
void requestAssists(Epoch epoch) noexcept;
|
||||
|
||||
// Should be called by GC, when it completed epoch `epoch`.
|
||||
// The call blocks waiting for all assisting mutators to finish assisting `epoch`.
|
||||
// `f` is a map from `mm::ThreadData&` to `MutatorAssists::ThreadData&`.
|
||||
// Can only be called once per `epoch`, and `epoch` must be increasing
|
||||
// by exactly 1 every time.
|
||||
template <typename F>
|
||||
void completeEpoch(Epoch epoch, F&& f) noexcept {
|
||||
markEpochCompleted(epoch);
|
||||
mm::ThreadRegistry::Instance().waitAllThreads(
|
||||
[f = std::forward<F>(f), epoch](mm::ThreadData& threadData) noexcept { return f(threadData).completedEpoch(epoch); });
|
||||
}
|
||||
|
||||
Epoch assistsRequested(std::memory_order order) noexcept { return assistsEpoch_.load(order); }
|
||||
|
||||
private:
|
||||
void markEpochCompleted(Epoch epoch) noexcept;
|
||||
|
||||
std::atomic<Epoch> assistsEpoch_ = 0;
|
||||
std::atomic<Epoch> completedEpoch_ = 0;
|
||||
std::optional<mm::SafePointActivator> safePointActivator_;
|
||||
std::mutex m_;
|
||||
#if KONAN_WINDOWS
|
||||
// winpthreads being weird. Using this implementation of condvar means that assisting mutators will spin for the entire duration of the
|
||||
// GC. This is fine: reaching assisting state should be rare and this state exists to ward of "memory leaks", and additionally assists
|
||||
// can be disabled.
|
||||
ConditionVariableSpin cv_;
|
||||
#else
|
||||
std::condition_variable cv_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler::internal
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "MutatorAssists.hpp"
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <sstream>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "SafePoint.hpp"
|
||||
#include "TestSupport.hpp"
|
||||
#include "std_support/Map.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
using gcScheduler::internal::MutatorAssists;
|
||||
using Epoch = MutatorAssists::Epoch;
|
||||
|
||||
class MutatorAssistsTest : public ::testing::Test {
|
||||
public:
|
||||
class Mutator {
|
||||
public:
|
||||
template <typename F>
|
||||
Mutator(MutatorAssistsTest& owner, F&& f) noexcept :
|
||||
owner_(owner), thread_([f = std::forward<F>(f), this]() noexcept {
|
||||
ScopedMemoryInit memory;
|
||||
{
|
||||
std::unique_lock guard(initializedMutex_);
|
||||
threadData_ = memory.memoryState()->GetThreadData();
|
||||
assists_.emplace(owner_.assists_, *threadData_);
|
||||
}
|
||||
owner_.registerMutator(*this);
|
||||
initialized_.notify_one();
|
||||
f(*this);
|
||||
}) {
|
||||
std::unique_lock guard(initializedMutex_);
|
||||
initialized_.wait(guard, [this] { return threadData_ && assists_.has_value(); });
|
||||
}
|
||||
|
||||
~Mutator() {
|
||||
thread_.join();
|
||||
owner_.unregisterMutator(*this);
|
||||
}
|
||||
|
||||
mm::ThreadData& threadData() noexcept { return *threadData_; }
|
||||
MutatorAssists::ThreadData& assists() noexcept { return *assists_; }
|
||||
|
||||
private:
|
||||
friend MutatorAssistsTest;
|
||||
|
||||
MutatorAssistsTest& owner_;
|
||||
std::condition_variable initialized_;
|
||||
std::mutex initializedMutex_;
|
||||
mm::ThreadData* threadData_;
|
||||
std::optional<MutatorAssists::ThreadData> assists_;
|
||||
ScopedThread thread_;
|
||||
};
|
||||
|
||||
void requestAssists(Epoch epoch) noexcept { assists_.requestAssists(epoch); }
|
||||
|
||||
void completeEpoch(Epoch epoch) noexcept {
|
||||
assists_.completeEpoch(epoch, [this](mm::ThreadData& threadData) noexcept -> MutatorAssists::ThreadData& {
|
||||
return getMutator(threadData).assists();
|
||||
});
|
||||
}
|
||||
|
||||
void safePoint() noexcept {
|
||||
if (!mm::test_support::safePointsAreActive()) return;
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
getMutator(*threadData).assists().safePoint();
|
||||
}
|
||||
|
||||
private:
|
||||
void registerMutator(Mutator& mutator) noexcept {
|
||||
std::unique_lock guard(mutatorMapMutex_);
|
||||
auto [_, inserted] = mutatorMap_.insert(std::make_pair(&mutator.threadData(), &mutator));
|
||||
RuntimeAssert(inserted, "Mutator was already inserted");
|
||||
}
|
||||
|
||||
void unregisterMutator(Mutator& mutator) noexcept {
|
||||
std::unique_lock guard(mutatorMapMutex_);
|
||||
auto count = mutatorMap_.erase(&mutator.threadData());
|
||||
RuntimeAssert(count == 1, "Mutator must be in the map");
|
||||
}
|
||||
|
||||
Mutator& getMutator(mm::ThreadData& threadData) noexcept {
|
||||
std::shared_lock guard(mutatorMapMutex_);
|
||||
auto it = mutatorMap_.find(&threadData);
|
||||
RuntimeAssert(it != mutatorMap_.end(), "Mutator must be in the map");
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
MutatorAssists assists_;
|
||||
RWSpinLock<MutexThreadStateHandling::kIgnore> mutatorMapMutex_;
|
||||
std_support::map<mm::ThreadData*, Mutator*> mutatorMap_;
|
||||
};
|
||||
|
||||
TEST_F(MutatorAssistsTest, EnableSafePointsWhenRequestingAssists) {
|
||||
ASSERT_FALSE(mm::test_support::safePointsAreActive());
|
||||
requestAssists(1);
|
||||
EXPECT_TRUE(mm::test_support::safePointsAreActive());
|
||||
completeEpoch(1);
|
||||
EXPECT_FALSE(mm::test_support::safePointsAreActive());
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, EnableSafePointsWithNestedRequest) {
|
||||
ASSERT_FALSE(mm::test_support::safePointsAreActive());
|
||||
requestAssists(1);
|
||||
ASSERT_TRUE(mm::test_support::safePointsAreActive());
|
||||
requestAssists(2);
|
||||
EXPECT_TRUE(mm::test_support::safePointsAreActive());
|
||||
completeEpoch(1);
|
||||
EXPECT_TRUE(mm::test_support::safePointsAreActive());
|
||||
completeEpoch(2);
|
||||
EXPECT_FALSE(mm::test_support::safePointsAreActive());
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, StressEnableSafePointsByMutators) {
|
||||
constexpr Epoch epochsCount = 4;
|
||||
std::array<std::atomic<bool>, epochsCount> enabled = {false};
|
||||
std::atomic<bool> canStart = false;
|
||||
std::atomic<bool> canStop = false;
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&, i](Mutator&) noexcept {
|
||||
while (!canStart.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
requestAssists((i % epochsCount) + 1);
|
||||
enabled[i % epochsCount].store(true, std::memory_order_relaxed);
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
safePoint();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ASSERT_FALSE(mm::test_support::safePointsAreActive());
|
||||
canStart.store(true, std::memory_order_relaxed);
|
||||
for (Epoch i = 0; i < epochsCount; ++i) {
|
||||
while (!enabled[i].load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
EXPECT_TRUE(mm::test_support::safePointsAreActive());
|
||||
completeEpoch(i + 1);
|
||||
}
|
||||
EXPECT_FALSE(mm::test_support::safePointsAreActive());
|
||||
canStop.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, Assist) {
|
||||
constexpr Epoch epochsCount = 4;
|
||||
std::array<std::atomic<bool>, epochsCount> canStart = {false};
|
||||
std::array<std::atomic<size_t>, epochsCount> started = {0};
|
||||
std::array<std::atomic<size_t>, epochsCount> finished = {0};
|
||||
std::atomic<Epoch> gcCompleted = 0;
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&](Mutator&) noexcept {
|
||||
for (Epoch epoch = 0; epoch < epochsCount; ++epoch) {
|
||||
while (!canStart[epoch].load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
started[epoch].fetch_add(1, std::memory_order_relaxed);
|
||||
safePoint();
|
||||
EXPECT_THAT(gcCompleted.load(std::memory_order_relaxed), epoch);
|
||||
finished[epoch].fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}));
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
EXPECT_THAT(m->threadData().state(), ThreadState::kRunnable);
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
for (Epoch epoch = 0; epoch < epochsCount; ++epoch) {
|
||||
requestAssists(epoch + 1);
|
||||
canStart[epoch].store(true, std::memory_order_relaxed);
|
||||
while (started[epoch].load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
while (!std::all_of(mutators.begin(), mutators.end(), [epoch](auto& m) noexcept {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
return waitingEpoch == epoch + 1 && waiting;
|
||||
})) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
gcCompleted.store(epoch, std::memory_order_relaxed);
|
||||
for (auto& m : mutators) {
|
||||
EXPECT_THAT(m->threadData().state(), ThreadState::kNative);
|
||||
// And already checked that all of them have started waiting for epoch.
|
||||
}
|
||||
completeEpoch(epoch + 1);
|
||||
while (finished[epoch].load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
if (epoch != epochsCount - 1) {
|
||||
EXPECT_THAT(m->threadData().state(), ThreadState::kRunnable);
|
||||
}
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, epoch + 1);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, AssistNoSync) {
|
||||
constexpr Epoch epochsCount = 10000;
|
||||
std::atomic<bool> canStop = false;
|
||||
std::atomic<size_t> finished = 0;
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&](Mutator&) noexcept {
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
safePoint();
|
||||
std::this_thread::yield();
|
||||
}
|
||||
finished.fetch_add(1, std::memory_order_relaxed);
|
||||
}));
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
for (Epoch epoch = 0; epoch < epochsCount; ++epoch) {
|
||||
requestAssists(epoch + 1);
|
||||
completeEpoch(epoch + 1);
|
||||
}
|
||||
canStop.store(true, std::memory_order_relaxed);
|
||||
while (finished.load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, testing::Le(epochsCount));
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, AssistWithNativeMutators) {
|
||||
constexpr Epoch epochsCount = 10000;
|
||||
std::atomic<bool> canStop = false;
|
||||
std::atomic<size_t> finished = 0;
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&, i](Mutator&) noexcept {
|
||||
if (i % 2 == 0) {
|
||||
ThreadStateGuard guard(ThreadState::kNative);
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
} else {
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
safePoint();
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
finished.fetch_add(1, std::memory_order_relaxed);
|
||||
}));
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
for (Epoch epoch = 0; epoch < epochsCount; ++epoch) {
|
||||
requestAssists(epoch + 1);
|
||||
completeEpoch(epoch + 1);
|
||||
}
|
||||
canStop.store(true, std::memory_order_relaxed);
|
||||
while (finished.load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, testing::Le(epochsCount));
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, AssistNoRequests) {
|
||||
constexpr Epoch epochsCount = 10000;
|
||||
std::atomic<bool> canStart = false;
|
||||
std::atomic<bool> canStop = false;
|
||||
std::atomic<size_t> started = 0;
|
||||
std::atomic<size_t> finished = 0;
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&](Mutator&) noexcept {
|
||||
while (!canStart.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
started.fetch_add(1, std::memory_order_relaxed);
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
safePoint();
|
||||
std::this_thread::yield();
|
||||
}
|
||||
finished.fetch_add(1, std::memory_order_relaxed);
|
||||
}));
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
canStart.store(true, std::memory_order_relaxed);
|
||||
for (Epoch epoch = 0; epoch < epochsCount; ++epoch) {
|
||||
completeEpoch(epoch + 1);
|
||||
}
|
||||
canStop.store(true, std::memory_order_relaxed);
|
||||
while (finished.load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, AssistRequestsByMutators) {
|
||||
constexpr Epoch epochsCount = 100;
|
||||
std::atomic<bool> canStart = false;
|
||||
std::atomic<bool> canStop = false;
|
||||
std::atomic<size_t> started = 0;
|
||||
std::atomic<size_t> finished = 0;
|
||||
std::atomic<Epoch> currentEpoch = 0;
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&, i](Mutator&) noexcept {
|
||||
while (!canStart.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
started.fetch_add(1, std::memory_order_relaxed);
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
if (i % 2 != 0) {
|
||||
auto epoch = currentEpoch.load(std::memory_order_relaxed);
|
||||
requestAssists(epoch + 1);
|
||||
}
|
||||
safePoint();
|
||||
std::this_thread::yield();
|
||||
}
|
||||
finished.fetch_add(1, std::memory_order_relaxed);
|
||||
}));
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
canStart.store(true, std::memory_order_relaxed);
|
||||
for (Epoch epoch = 0; epoch < epochsCount; ++epoch) {
|
||||
currentEpoch.store(epoch, std::memory_order_relaxed);
|
||||
completeEpoch(epoch + 1);
|
||||
}
|
||||
canStop.store(true, std::memory_order_relaxed);
|
||||
while (finished.load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, testing::Le(epochsCount));
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MutatorAssistsTest, AssistRequestsByMutatorsIntoTheFuture) {
|
||||
constexpr Epoch epochsCount = 100;
|
||||
std::atomic<bool> canStart = false;
|
||||
std::atomic<bool> canStop = false;
|
||||
std::atomic<size_t> started = 0;
|
||||
std::atomic<size_t> finished = 0;
|
||||
std::mutex mutexEpoch;
|
||||
Epoch scheduledEpoch = 0;
|
||||
Epoch currentEpoch = 0;
|
||||
auto scheduleGC = [&]() noexcept -> Epoch {
|
||||
std::unique_lock guard(mutexEpoch);
|
||||
if (scheduledEpoch > currentEpoch) return scheduledEpoch;
|
||||
scheduledEpoch = currentEpoch + 1;
|
||||
return scheduledEpoch;
|
||||
};
|
||||
std_support::vector<std_support::unique_ptr<Mutator>> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back(std_support::make_unique<Mutator>(*this, [&, i](Mutator&) noexcept {
|
||||
while (!canStart.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
started.fetch_add(1, std::memory_order_relaxed);
|
||||
while (!canStop.load(std::memory_order_relaxed)) {
|
||||
if (i % 2 != 0) {
|
||||
auto epoch = scheduleGC();
|
||||
requestAssists(epoch);
|
||||
}
|
||||
safePoint();
|
||||
std::this_thread::yield();
|
||||
}
|
||||
finished.fetch_add(1, std::memory_order_relaxed);
|
||||
}));
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, 0);
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
canStart.store(true, std::memory_order_relaxed);
|
||||
for (Epoch epoch = 1; epoch <= epochsCount; ++epoch) {
|
||||
{
|
||||
std::unique_lock guard(mutexEpoch);
|
||||
currentEpoch = epoch;
|
||||
EXPECT_THAT(currentEpoch, testing::Ge(scheduledEpoch));
|
||||
}
|
||||
completeEpoch(epoch);
|
||||
}
|
||||
canStop.store(true, std::memory_order_relaxed);
|
||||
completeEpoch(epochsCount + 1); // The last GC.
|
||||
while (finished.load(std::memory_order_relaxed) < mutators.size()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
for (auto& m : mutators) {
|
||||
auto [waitingEpoch, waiting] = m->assists().startedWaiting(std::memory_order_relaxed);
|
||||
EXPECT_THAT(waitingEpoch, testing::Le(epochsCount + 1));
|
||||
EXPECT_FALSE(waiting);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,46 @@
|
||||
|
||||
#include "GCSchedulerImpl.hpp"
|
||||
|
||||
#include "CallsChecker.hpp"
|
||||
#include "GC.hpp"
|
||||
#include "GlobalData.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
gcScheduler::GCScheduler::GCScheduler() noexcept : gcData_(std_support::make_unique<internal::GCSchedulerDataManual>()) {}
|
||||
gcScheduler::GCScheduler::ThreadData::ThreadData(gcScheduler::GCScheduler&, mm::ThreadData&) noexcept :
|
||||
impl_(std_support::make_unique<Impl>()) {}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::safePoint() noexcept {}
|
||||
gcScheduler::GCScheduler::ThreadData::~ThreadData() = default;
|
||||
|
||||
gcScheduler::GCScheduler::GCScheduler() noexcept : impl_(std_support::make_unique<Impl>()) {}
|
||||
|
||||
gcScheduler::GCScheduler::~GCScheduler() = default;
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::ThreadData::safePoint() noexcept {}
|
||||
|
||||
void gcScheduler::GCScheduler::schedule() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
mm::GlobalData::Instance().gc().Schedule();
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::scheduleAndWaitFinished() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
CallsCheckerIgnoreGuard guard;
|
||||
auto& gc = mm::GlobalData::Instance().gc();
|
||||
auto epoch = gc.Schedule();
|
||||
NativeOrUnregisteredThreadGuard stateGuard(/* reentrant = */ true);
|
||||
gc.WaitFinished(epoch);
|
||||
}
|
||||
|
||||
void gcScheduler::GCScheduler::scheduleAndWaitFinalized() noexcept {
|
||||
RuntimeLogInfo({kTagGC}, "Scheduling GC manually");
|
||||
CallsCheckerIgnoreGuard guard;
|
||||
auto& gc = mm::GlobalData::Instance().gc();
|
||||
auto epoch = gc.Schedule();
|
||||
NativeOrUnregisteredThreadGuard stateGuard(/* reentrant = */ true);
|
||||
gc.WaitFinalizers(epoch);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::setAllocatedBytes(size_t bytes) noexcept {}
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::onGCStart() noexcept {}
|
||||
ALWAYS_INLINE void gcScheduler::GCScheduler::onGCFinish(int64_t epoch, size_t aliveBytes) noexcept {}
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
|
||||
#include "Logging.hpp"
|
||||
|
||||
namespace kotlin::gcScheduler::internal {
|
||||
namespace kotlin::gcScheduler {
|
||||
|
||||
class GCSchedulerDataManual : public GCSchedulerData {
|
||||
class GCScheduler::Impl : private Pinned {
|
||||
public:
|
||||
GCSchedulerDataManual() noexcept { RuntimeLogInfo({kTagGC}, "Manual GC scheduler initialized"); }
|
||||
|
||||
void OnPerformFullGC() noexcept override {}
|
||||
void UpdateAliveSetBytes(size_t bytes) noexcept override {}
|
||||
void SetAllocatedBytes(size_t bytes) noexcept override {}
|
||||
Impl() noexcept { RuntimeLogInfo({kTagGC}, "Manual GC scheduler initialized"); }
|
||||
};
|
||||
|
||||
} // namespace kotlin::gcScheduler::internal
|
||||
class GCScheduler::ThreadData::Impl : private Pinned {};
|
||||
|
||||
} // namespace kotlin::gcScheduler
|
||||
|
||||
@@ -177,7 +177,6 @@ public object GC {
|
||||
* When Kotlin code is not allocating enough to trigger GC, the GC scheduler uses timer to drive collection.
|
||||
* Timer-triggered collection will happen roughly in [regularGCInterval] .. 2 * [regularGCInterval] since
|
||||
* any previous collection.
|
||||
* Unused with on-safepoints GC scheduler.
|
||||
*
|
||||
* Default: 10 seconds
|
||||
*
|
||||
@@ -193,8 +192,9 @@ public object GC {
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of heap available for Kotlin objects. When Kotlin objects overflow this heap,
|
||||
* the garbage collection is requested. Automatically adjusts when [autotune] is true:
|
||||
* Total amount of heap available for Kotlin objects. The GC tries to schedule execution
|
||||
* so that Kotlin heap doesn't overflow this heap.
|
||||
* Automatically adjusts when [autotune] is true:
|
||||
* after each collection the [targetHeapBytes] is set to heapBytes / [targetHeapUtilization] and
|
||||
* capped between [minHeapBytes] and [maxHeapBytes], where heapBytes is heap usage after the garbage
|
||||
* is collected.
|
||||
@@ -265,6 +265,30 @@ public object GC {
|
||||
setMaxHeapBytes(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* The GC is scheduled when Kotlin heap overflows [heapTriggerCoefficient] * [targetHeapBytes].
|
||||
*
|
||||
* Default: 0.9
|
||||
*
|
||||
* @throws [IllegalArgumentException] when value is outside (0, 1] interval.
|
||||
*/
|
||||
var heapTriggerCoefficient: Double
|
||||
get() = getHeapTriggerCoefficient()
|
||||
set(value) {
|
||||
require(value > 0 && value <= 1) { "heapTriggerCoefficient must be in (0, 1] interval: $value" }
|
||||
setHeapTriggerCoefficient(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, the GC will pause Kotlin threads when Kotlin heap overflows [targetHeapBytes]
|
||||
* and will resume them only after current GC is done.
|
||||
*
|
||||
* Default: true, unless [autotune] is false or [maxHeapBytes] is less than [Long.MAX_VALUE].
|
||||
*/
|
||||
var pauseOnTargetHeapOverflow: Boolean
|
||||
get() = getPauseOnTargetHeapOverflow()
|
||||
set(value) = setPauseOnTargetHeapOverflow(value)
|
||||
|
||||
/**
|
||||
* Deprecated and unused. Always returns null.
|
||||
*
|
||||
@@ -356,4 +380,16 @@ public object GC {
|
||||
|
||||
@GCUnsafeCall("Kotlin_native_internal_GC_setMaxHeapBytes")
|
||||
private external fun setMaxHeapBytes(value: Long)
|
||||
|
||||
@GCUnsafeCall("Kotlin_native_internal_GC_getHeapTriggerCoefficient")
|
||||
private external fun getHeapTriggerCoefficient(): Double
|
||||
|
||||
@GCUnsafeCall("Kotlin_native_internal_GC_setHeapTriggerCoefficient")
|
||||
private external fun setHeapTriggerCoefficient(value: Double)
|
||||
|
||||
@GCUnsafeCall("Kotlin_native_internal_GC_getPauseOnTargetHeapOverflow")
|
||||
private external fun getPauseOnTargetHeapOverflow(): Boolean
|
||||
|
||||
@GCUnsafeCall("Kotlin_native_internal_GC_setPauseOnTargetHeapOverflow")
|
||||
private external fun setPauseOnTargetHeapOverflow(value: Boolean)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ extern "C" void DeinitMemory(MemoryState* state, bool destroyRuntime) {
|
||||
auto* node = mm::FromMemoryState(state);
|
||||
if (destroyRuntime) {
|
||||
ThreadStateGuard guard(state, ThreadState::kRunnable);
|
||||
node->Get()->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
// TODO: Why not just destruct `GC` object and its thread data counterpart entirely?
|
||||
mm::GlobalData::Instance().gc().StopFinalizerThreadIfRunning();
|
||||
}
|
||||
@@ -300,15 +300,11 @@ extern "C" RUNTIME_NOTHROW void GC_CollectorCallback(void* worker) {
|
||||
}
|
||||
|
||||
extern "C" void Kotlin_native_internal_GC_collect(ObjHeader*) {
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
AssertThreadState(threadData, ThreadState::kRunnable);
|
||||
threadData->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
}
|
||||
|
||||
extern "C" void Kotlin_native_internal_GC_schedule(ObjHeader*) {
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
AssertThreadState(threadData, ThreadState::kRunnable);
|
||||
threadData->gc().Schedule();
|
||||
mm::GlobalData::Instance().gcScheduler().schedule();
|
||||
}
|
||||
|
||||
extern "C" void Kotlin_native_internal_GC_collectCyclic(ObjHeader*) {
|
||||
@@ -421,6 +417,23 @@ extern "C" void Kotlin_native_internal_GC_setMinHeapBytes(ObjHeader*, KLong valu
|
||||
mm::GlobalData::Instance().gcScheduler().config().minHeapBytes = value;
|
||||
}
|
||||
|
||||
extern "C" KDouble Kotlin_native_internal_GC_getHeapTriggerCoefficient(ObjHeader*) {
|
||||
return mm::GlobalData::Instance().gcScheduler().config().heapTriggerCoefficient.load();
|
||||
}
|
||||
|
||||
extern "C" void Kotlin_native_internal_GC_setHeapTriggerCoefficient(ObjHeader*, KDouble value) {
|
||||
RuntimeAssert(value > 0 && value <= 1, "Must be handled by the caller");
|
||||
mm::GlobalData::Instance().gcScheduler().config().heapTriggerCoefficient = value;
|
||||
}
|
||||
|
||||
extern "C" KBoolean Kotlin_native_internal_GC_getPauseOnTargetHeapOverflow(ObjHeader*) {
|
||||
return mm::GlobalData::Instance().gcScheduler().config().mutatorAssists();
|
||||
}
|
||||
|
||||
extern "C" void Kotlin_native_internal_GC_setPauseOnTargetHeapOverflow(ObjHeader*, KBoolean value) {
|
||||
mm::GlobalData::Instance().gcScheduler().config().setMutatorAssists(value);
|
||||
}
|
||||
|
||||
extern "C" OBJ_GETTER(Kotlin_native_internal_GC_detectCycles, ObjHeader*) {
|
||||
// TODO: Remove when legacy MM is gone.
|
||||
RETURN_OBJ(nullptr);
|
||||
@@ -453,9 +466,7 @@ extern "C" void Kotlin_Any_share(ObjHeader* thiz) {
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW void PerformFullGC(MemoryState* memory) {
|
||||
auto* threadData = memory->GetThreadData();
|
||||
AssertThreadState(threadData, ThreadState::kRunnable);
|
||||
threadData->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
mm::GlobalData::Instance().gcScheduler().scheduleAndWaitFinalized();
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW bool ClearSubgraphReferences(ObjHeader* root, bool checked) {
|
||||
@@ -623,5 +634,5 @@ RUNTIME_NOTHROW extern "C" void DisposeRegularWeakReferenceImpl(ObjHeader* weakR
|
||||
}
|
||||
|
||||
void kotlin::OnMemoryAllocation(size_t totalAllocatedBytes) noexcept {
|
||||
mm::GlobalData::Instance().gcScheduler().gcData().SetAllocatedBytes(totalAllocatedBytes);
|
||||
mm::GlobalData::Instance().gcScheduler().setAllocatedBytes(totalAllocatedBytes);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ void safePointActionImpl(mm::ThreadData& threadData) noexcept {
|
||||
RuntimeAssert(!recursion, "Recursive safepoint");
|
||||
AutoReset guard(&recursion, true);
|
||||
|
||||
mm::GlobalData::Instance().gcScheduler().safePoint();
|
||||
threadData.gcScheduler().safePoint();
|
||||
threadData.gc().safePoint();
|
||||
threadData.suspensionData().suspendIfRequested();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
threadId_(threadId),
|
||||
globalsThreadQueue_(GlobalsRegistry::Instance()),
|
||||
specialRefRegistry_(SpecialRefRegistry::instance()),
|
||||
gcScheduler_(GlobalData::Instance().gcScheduler(), *this),
|
||||
gc_(GlobalData::Instance().gc(), *this),
|
||||
suspensionData_(ThreadState::kNative, *this) {}
|
||||
|
||||
@@ -53,6 +54,8 @@ public:
|
||||
|
||||
std_support::vector<std::pair<ObjHeader**, ObjHeader*>>& initializingSingletons() noexcept { return initializingSingletons_; }
|
||||
|
||||
gcScheduler::GCScheduler::ThreadData& gcScheduler() noexcept { return gcScheduler_; }
|
||||
|
||||
gc::GC::ThreadData& gc() noexcept { return gc_; }
|
||||
|
||||
ThreadSuspensionData& suspensionData() { return suspensionData_; }
|
||||
@@ -76,6 +79,7 @@ private:
|
||||
ThreadLocalStorage tls_;
|
||||
SpecialRefRegistry::ThreadQueue specialRefRegistry_;
|
||||
ShadowStack shadowStack_;
|
||||
gcScheduler::GCScheduler::ThreadData gcScheduler_;
|
||||
gc::GC::ThreadData gc_;
|
||||
std_support::vector<std::pair<ObjHeader**, ObjHeader*>> initializingSingletons_;
|
||||
ThreadSuspensionData suspensionData_;
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
ThreadState state() noexcept { return state_; }
|
||||
|
||||
ThreadState setState(ThreadState newState) noexcept;
|
||||
ThreadState setStateNoSafePoint(ThreadState newState) noexcept { return state_.exchange(newState, std::memory_order_acq_rel); }
|
||||
|
||||
bool suspended() noexcept { return suspended_; }
|
||||
bool suspendedOrNative() noexcept { return suspended() || state() == kotlin::ThreadState::kNative; }
|
||||
|
||||
Reference in New Issue
Block a user