Add RootSet to unify iterating across the entire rootset (#4685)

This commit is contained in:
Alexander Shabalin
2021-02-08 15:25:14 +03:00
committed by Vasily Levchenko
parent e2f573c26c
commit eedfea7f40
7 changed files with 418 additions and 10 deletions
@@ -80,4 +80,11 @@ extern "C" const int KonanNeedDebugInfo;
::internal::TODOImpl(CURRENT_SOURCE_LOCATION, ##__VA_ARGS__); \
} while (false)
// Use RuntimeFail() to unconditionally fail, signifying compiler/runtime bug.
// TODO: Consider using `CURRENT_SOURCE_LOCATION` when `KonanNeedDebugInfo` is `true`.
#define RuntimeFail(format, ...) \
do { \
RuntimeAssertFailed(nullptr, format, ##__VA_ARGS__); \
} while (false)
#endif // RUNTIME_ASSERT_H
@@ -26,6 +26,9 @@ public:
using Iterator = MultiSourceQueue<ObjHeader**>::Iterator;
GlobalsRegistry();
~GlobalsRegistry();
static GlobalsRegistry& Instance() noexcept;
void RegisterStorageForGlobal(mm::ThreadData* threadData, ObjHeader** location) noexcept;
@@ -41,11 +44,6 @@ public:
Iterable Iter() noexcept { return globals_.Iter(); }
private:
friend class GlobalData;
GlobalsRegistry();
~GlobalsRegistry();
// TODO: Add-only MultiSourceQueue can be made more efficient. Measure, if it's a problem.
MultiSourceQueue<ObjHeader**> globals_;
};
@@ -0,0 +1,149 @@
/*
* Copyright 2010-2021 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 "RootSet.hpp"
#include "KAssert.h"
#include "GlobalData.hpp"
#include "ThreadData.hpp"
using namespace kotlin;
mm::ThreadRootSet::Iterator::Iterator(begin_t, ThreadRootSet& owner) noexcept :
owner_(owner), phase_(Phase::kStack), stackIterator_(owner_.stack_.begin()) {
Init();
}
mm::ThreadRootSet::Iterator::Iterator(end_t, ThreadRootSet& owner) noexcept : owner_(owner), phase_(Phase::kDone) {}
ObjHeader*& mm::ThreadRootSet::Iterator::operator*() noexcept {
switch (phase_) {
case Phase::kStack:
return *stackIterator_;
case Phase::kTLS:
return **tlsIterator_;
case Phase::kDone:
RuntimeFail("Cannot dereference");
}
}
mm::ThreadRootSet::Iterator& mm::ThreadRootSet::Iterator::operator++() noexcept {
switch (phase_) {
case Phase::kStack:
++stackIterator_;
Init();
return *this;
case Phase::kTLS:
++tlsIterator_;
Init();
return *this;
case Phase::kDone:
return *this;
}
}
bool mm::ThreadRootSet::Iterator::operator==(const Iterator& rhs) const noexcept {
if (phase_ != rhs.phase_) {
return false;
}
switch (phase_) {
case Phase::kDone:
return true;
case Phase::kStack:
return stackIterator_ == rhs.stackIterator_;
case Phase::kTLS:
return tlsIterator_ == rhs.tlsIterator_;
}
}
void mm::ThreadRootSet::Iterator::Init() noexcept {
while (phase_ != Phase::kDone) {
switch (phase_) {
case Phase::kStack:
if (stackIterator_ != owner_.stack_.end()) return;
phase_ = Phase::kTLS;
tlsIterator_ = owner_.tls_.begin();
break;
case Phase::kTLS:
if (tlsIterator_ != owner_.tls_.end()) return;
phase_ = Phase::kDone;
break;
case Phase::kDone:
RuntimeFail("Impossible");
}
}
}
mm::GlobalRootSet::Iterator::Iterator(begin_t, GlobalRootSet& owner) noexcept :
owner_(owner), phase_(Phase::kGlobals), globalsIterator_(owner_.globalsIterable_.begin()) {
Init();
}
mm::GlobalRootSet::Iterator::Iterator(end_t, GlobalRootSet& owner) noexcept : owner_(owner), phase_(Phase::kDone) {}
ObjHeader*& mm::GlobalRootSet::Iterator::operator*() noexcept {
switch (phase_) {
case Phase::kGlobals:
return **globalsIterator_;
case Phase::kStableRefs:
return *stableRefsIterator_;
case Phase::kDone:
RuntimeFail("Cannot dereference");
}
}
mm::GlobalRootSet::Iterator& mm::GlobalRootSet::Iterator::operator++() noexcept {
switch (phase_) {
case Phase::kGlobals:
++globalsIterator_;
Init();
return *this;
case Phase::kStableRefs:
++stableRefsIterator_;
Init();
return *this;
case Phase::kDone:
return *this;
}
}
bool mm::GlobalRootSet::Iterator::operator==(const Iterator& rhs) const noexcept {
if (phase_ != rhs.phase_) {
return false;
}
switch (phase_) {
case Phase::kDone:
return true;
case Phase::kGlobals:
return globalsIterator_ == rhs.globalsIterator_;
case Phase::kStableRefs:
return stableRefsIterator_ == rhs.stableRefsIterator_;
}
}
void mm::GlobalRootSet::Iterator::Init() noexcept {
while (phase_ != Phase::kDone) {
switch (phase_) {
case Phase::kGlobals:
if (globalsIterator_ != owner_.globalsIterable_.end()) return;
phase_ = Phase::kStableRefs;
stableRefsIterator_ = owner_.stableRefsIterable_.begin();
break;
case Phase::kStableRefs:
if (stableRefsIterator_ != owner_.stableRefsIterable_.end()) return;
phase_ = Phase::kDone;
break;
case Phase::kDone:
RuntimeFail("Impossible");
}
}
}
mm::ThreadRootSet::ThreadRootSet(ThreadData& threadData) noexcept : ThreadRootSet(threadData.shadowStack(), threadData.tls()) {}
mm::GlobalRootSet::GlobalRootSet() noexcept :
GlobalRootSet(mm::GlobalData::Instance().globalsRegistry(), mm::GlobalData::Instance().stableRefRegistry()) {}
@@ -0,0 +1,123 @@
/*
* Copyright 2010-2021 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.
*/
#ifndef RUNTIME_MM_ROOT_SET_H
#define RUNTIME_MM_ROOT_SET_H
#include "GlobalsRegistry.hpp"
#include "ShadowStack.hpp"
#include "StableRefRegistry.hpp"
#include "ThreadLocalStorage.hpp"
struct ObjHeader;
namespace kotlin {
namespace mm {
class ThreadData;
class ThreadRootSet {
public:
class Iterator {
public:
struct begin_t {};
static constexpr inline begin_t begin = begin_t{};
struct end_t {};
static constexpr inline end_t end = end_t{};
Iterator(begin_t, ThreadRootSet& owner) noexcept;
Iterator(end_t, ThreadRootSet& owner) noexcept;
ObjHeader*& operator*() noexcept;
Iterator& operator++() noexcept;
bool operator==(const Iterator& rhs) const noexcept;
bool operator!=(const Iterator& rhs) const noexcept { return !(*this == rhs); }
private:
enum class Phase {
kStack,
kTLS,
kDone,
};
void Init() noexcept;
ThreadRootSet& owner_;
Phase phase_;
union {
ShadowStack::Iterator stackIterator_;
ThreadLocalStorage::Iterator tlsIterator_;
};
};
ThreadRootSet(ShadowStack& stack, ThreadLocalStorage& tls) noexcept : stack_(stack), tls_(tls) {}
explicit ThreadRootSet(ThreadData& threadData) noexcept;
Iterator begin() noexcept { return Iterator(Iterator::begin, *this); }
Iterator end() noexcept { return Iterator(Iterator::end, *this); }
private:
ShadowStack& stack_;
ThreadLocalStorage& tls_;
};
class GlobalRootSet {
public:
class Iterator {
public:
struct begin_t {};
static constexpr inline begin_t begin = begin_t{};
struct end_t {};
static constexpr inline end_t end = end_t{};
Iterator(begin_t, GlobalRootSet& owner) noexcept;
Iterator(end_t, GlobalRootSet& owner) noexcept;
ObjHeader*& operator*() noexcept;
Iterator& operator++() noexcept;
bool operator==(const Iterator& rhs) const noexcept;
bool operator!=(const Iterator& rhs) const noexcept { return !(*this == rhs); }
private:
enum class Phase {
kGlobals,
kStableRefs,
kDone,
};
void Init() noexcept;
GlobalRootSet& owner_;
Phase phase_;
union {
GlobalsRegistry::Iterator globalsIterator_;
StableRefRegistry::Iterator stableRefsIterator_;
};
};
GlobalRootSet(GlobalsRegistry& globalsRegistry, StableRefRegistry& stableRefRegistry) noexcept :
globalsIterable_(globalsRegistry.Iter()), stableRefsIterable_(stableRefRegistry.Iter()) {}
GlobalRootSet() noexcept;
Iterator begin() noexcept { return Iterator(Iterator::begin, *this); }
Iterator end() noexcept { return Iterator(Iterator::end, *this); }
private:
// TODO: These use separate locks, which is inefficient, and slightly dangerous. In practice it's
// fine, because this is the only place where these two locks are taken simultaneously.
GlobalsRegistry::Iterable globalsIterable_;
StableRefRegistry::Iterable stableRefsIterable_;
};
} // namespace mm
} // namespace kotlin
#endif // RUNTIME_MM_ROOT_SET_H
@@ -0,0 +1,126 @@
/*
* Copyright 2010-2021 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 "RootSet.hpp"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ShadowStack.hpp"
using namespace kotlin;
namespace {
// TODO: All the test helpers to create the rootset should be abstracted out.
template <size_t LocalsCount>
class StackEntry : private Pinned {
public:
static_assert(LocalsCount > 0, "Must have at least 1 object on stack");
explicit StackEntry(mm::ShadowStack& shadowStack) : shadowStack_(shadowStack), value_(make_unique<ObjHeader>()) {
// Fill `locals_` with some values.
for (size_t i = 0; i < LocalsCount; ++i) {
(*this)[i] = value_.get() + i;
}
shadowStack_.EnterFrame(data_.data(), 0, kTotalCount);
}
~StackEntry() { shadowStack_.LeaveFrame(data_.data(), 0, kTotalCount); }
ObjHeader*& operator[](size_t index) { return data_[kFrameOverlayCount + index]; }
private:
mm::ShadowStack& shadowStack_;
KStdUniquePtr<ObjHeader> value_;
// The following is what the compiler creates on the stack.
static inline constexpr int kFrameOverlayCount = sizeof(FrameOverlay) / sizeof(ObjHeader**);
static inline constexpr int kTotalCount = kFrameOverlayCount + LocalsCount;
std::array<ObjHeader*, kTotalCount> data_;
};
struct TLSKey {};
} // namespace
TEST(ThreadRootSetTest, Basic) {
mm::ShadowStack stack;
StackEntry<2> entry(stack);
TLSKey key;
mm::ThreadLocalStorage tls;
tls.AddRecord(&key, 3);
tls.Commit();
mm::ThreadRootSet iter(stack, tls);
KStdVector<ObjHeader*> actual;
for (auto& object : iter) {
actual.push_back(object);
}
EXPECT_THAT(actual, testing::ElementsAre(entry[0], entry[1], *tls.Lookup(&key, 0), *tls.Lookup(&key, 1), *tls.Lookup(&key, 2)));
}
TEST(ThreadRootSetTest, Empty) {
mm::ShadowStack stack;
mm::ThreadLocalStorage tls;
mm::ThreadRootSet iter(stack, tls);
KStdVector<ObjHeader*> actual;
for (auto& object : iter) {
actual.push_back(object);
}
EXPECT_THAT(actual, testing::IsEmpty());
}
TEST(GlobalRootSetTest, Basic) {
mm::GlobalsRegistry globals;
mm::GlobalsRegistry::ThreadQueue globalsProducer(globals);
ObjHeader* global1 = reinterpret_cast<ObjHeader*>(1);
ObjHeader* global2 = reinterpret_cast<ObjHeader*>(2);
globalsProducer.Insert(&global1);
globalsProducer.Insert(&global2);
mm::StableRefRegistry stableRefs;
mm::StableRefRegistry::ThreadQueue stableRefsProducer(stableRefs);
ObjHeader* stableRef1 = reinterpret_cast<ObjHeader*>(3);
ObjHeader* stableRef2 = reinterpret_cast<ObjHeader*>(4);
ObjHeader* stableRef3 = reinterpret_cast<ObjHeader*>(5);
stableRefsProducer.Insert(stableRef1);
stableRefsProducer.Insert(stableRef2);
stableRefsProducer.Insert(stableRef3);
globalsProducer.Publish();
stableRefsProducer.Publish();
mm::GlobalRootSet iter(globals, stableRefs);
KStdVector<ObjHeader*> actual;
for (auto& object : iter) {
actual.push_back(object);
}
EXPECT_THAT(actual, testing::ElementsAre(global1, global2, stableRef1, stableRef2, stableRef3));
}
TEST(GlobalRootSetTest, Empty) {
mm::GlobalsRegistry globals;
mm::StableRefRegistry stableRefs;
mm::GlobalRootSet iter(globals, stableRefs);
KStdVector<ObjHeader*> actual;
for (auto& object : iter) {
actual.push_back(object);
}
EXPECT_THAT(actual, testing::IsEmpty());
}
@@ -26,6 +26,9 @@ public:
using Iterator = MultiSourceQueue<ObjHeader*>::Iterator;
using Node = MultiSourceQueue<ObjHeader*>::Node;
StableRefRegistry();
~StableRefRegistry();
static StableRefRegistry& Instance() noexcept;
Node* RegisterStableRef(mm::ThreadData* threadData, ObjHeader* object) noexcept;
@@ -46,11 +49,6 @@ public:
Iterable Iter() noexcept { return stableRefs_.Iter(); }
private:
friend class GlobalData;
StableRefRegistry();
~StableRefRegistry();
// Current approach optimizes for creating and disposing of stable refs:
// * creation just enqueues ref, disposing either queues or deletes the ref immediately (if it still resides in the current queue).
// * when thread is stopped, it'll scan through the local queue (to mark that refs no longer reside in it) and push creation and
@@ -59,6 +59,13 @@ public:
GC::ThreadData& gc() noexcept { return gc_; }
void Publish() noexcept {
// TODO: These use separate locks, which is inefficient.
globalsThreadQueue_.Publish();
stableRefThreadQueue_.Publish();
objectFactoryThreadQueue_.Publish();
}
private:
const pthread_t threadId_;
GlobalsRegistry::ThreadQueue globalsThreadQueue_;