GC API for lua (#43)

Almost old gc api, but:
rebased on new master
GC::State -> enabled_ flag
release lua state as soon as possible
This commit is contained in:
Ilia 2017-04-16 14:08:24 +03:00 committed by mihacooper
parent 6d20b0ff03
commit 64628c1757
12 changed files with 188 additions and 112 deletions

View File

@ -7,13 +7,12 @@
namespace effil {
GarbageCollector::GarbageCollector()
: state_(GCState::Idle)
GC::GC()
: enabled_(true)
, lastCleanup_(0)
, step_(200) {}
GCObject* GarbageCollector::get(GCObjectHandle handle) {
std::lock_guard<std::mutex> g(lock_);
GCObject* GC::findObject(GCObjectHandle handle) {
auto it = objects_.find(handle);
if (it == objects_.end()) {
DEBUG << "Null handle " << handle << std::endl;
@ -22,21 +21,16 @@ GCObject* GarbageCollector::get(GCObjectHandle handle) {
return it->second.get();
}
bool GarbageCollector::has(GCObjectHandle handle) const {
bool GC::has(GCObjectHandle handle) const {
std::lock_guard<std::mutex> g(lock_);
return objects_.find(handle) != objects_.end();
}
// Here is the naive tri-color marking
// garbage collecting algorithm implementation.
void GarbageCollector::cleanup() {
void GC::collect() {
std::lock_guard<std::mutex> g(lock_);
if (state_ == GCState::Stopped)
return;
assert(state_ != GCState::Running);
state_ = GCState::Running;
std::vector<GCObjectHandle> grey;
std::map<GCObjectHandle, std::shared_ptr<GCObject>> black;
@ -59,30 +53,44 @@ void GarbageCollector::cleanup() {
// Sweep phase
objects_ = std::move(black);
state_ = GCState::Idle;
lastCleanup_.store(0);
}
size_t GarbageCollector::size() const {
size_t GC::size() const {
std::lock_guard<std::mutex> g(lock_);
return objects_.size();
}
void GarbageCollector::stop() {
size_t GC::count() {
std::lock_guard<std::mutex> g(lock_);
assert(state_ == GCState::Idle || state_ == GCState::Stopped);
state_ = GCState::Stopped;
return objects_.size();
}
void GarbageCollector::resume() {
std::lock_guard<std::mutex> g(lock_);
assert(state_ == GCState::Idle || state_ == GCState::Stopped);
state_ = GCState::Idle;
}
GarbageCollector& getGC() {
static GarbageCollector pool;
GC& GC::instance() {
static GC pool;
return pool;
}
sol::table GC::getLuaApi(sol::state_view& lua) {
sol::table api = lua.create_table_with();
api["collect"] = [=] {
instance().collect();
};
api["pause"] = [] { instance().pause(); };
api["resume"] = [] { instance().resume(); };
api["enabled"] = [] { return instance().enabled(); };
api["step"] = [](sol::optional<int> newStep){
auto previous = instance().step();
if (newStep) {
REQUIRE(*newStep <= 0) << "gc.step have to be > 0";
instance().step(static_cast<size_t>(*newStep));
}
return previous;
};
api["count"] = [] {
return instance().count();
};
return api;
}
} // effil

View File

@ -2,6 +2,8 @@
#include "spin-mutex.h"
#include <sol.hpp>
#include <mutex>
#include <map>
#include <set>
@ -32,18 +34,17 @@ protected:
std::shared_ptr<std::set<GCObjectHandle>> refs_;
};
enum class GCState { Idle, Running, Stopped };
class GarbageCollector {
class GC {
public:
GarbageCollector();
~GarbageCollector() = default;
GC();
~GC() = default;
// This method is used to create all managed objects.
template <typename ObjectType, typename... Args>
ObjectType create(Args&&... args) {
if (lastCleanup_.fetch_add(1) == step_)
cleanup();
if (enabled_ && lastCleanup_.fetch_add(1) == step_)
collect();
auto object = std::make_shared<ObjectType>(std::forward<Args>(args)...);
std::lock_guard<std::mutex> g(lock_);
@ -51,28 +52,39 @@ public:
return *object;
}
GCObject* get(GCObjectHandle handle);
template <typename ObjectType>
ObjectType get(GCObjectHandle handle) {
std::lock_guard<std::mutex> g(lock_);
// TODO: add dynamic cast to check?
return *static_cast<ObjectType*>(findObject(handle));
}
bool has(GCObjectHandle handle) const;
void cleanup();
void collect();
size_t size() const;
void stop();
void resume();
void pause() { enabled_ = false; };
void resume() { enabled_ = true; };
size_t step() const { return step_; }
void step(size_t newStep) { step_ = newStep; }
GCState state() const { return state_; }
bool enabled() { return enabled_; };
size_t count();
static GC& instance();
static sol::table getLuaApi(sol::state_view& lua);
private:
mutable std::mutex lock_;
GCState state_;
bool enabled_;
std::atomic<size_t> lastCleanup_;
size_t step_;
std::map<GCObjectHandle, std::shared_ptr<GCObject>> objects_;
private:
GarbageCollector(GarbageCollector&&) = delete;
GarbageCollector(const GarbageCollector&) = delete;
GCObject* findObject(GCObjectHandle handle);
private:
GC(GC&&) = delete;
GC(const GC&) = delete;
};
GarbageCollector& getGC();
} // effil

View File

@ -18,9 +18,11 @@ sol::object createThread(const sol::this_state& lua,
return sol::make_object(lua, std::make_shared<Thread>(path, cpath, stepwise, step, function, args));
}
sol::object createTable(sol::this_state lua) { return sol::make_object(lua, getGC().create<SharedTable>()); }
sol::object createTable(sol::this_state lua) {
return sol::make_object(lua, GC::instance().create<SharedTable>());
}
SharedTable globalTable = getGC().create<SharedTable>();
SharedTable globalTable = GC::instance().create<SharedTable>();
} // namespace
@ -39,7 +41,9 @@ extern "C" int luaopen_libeffil(lua_State* L) {
"size", SharedTable::luaSize,
"setmetatable", SharedTable::luaSetMetatable,
"getmetatable", SharedTable::luaGetMetatable,
"G", sol::make_object(lua, globalTable)
"G", sol::make_object(lua, globalTable),
"getmetatable", SharedTable::luaGetMetatable,
"gc", GC::getLuaApi(lua)
);
sol::stack::push(lua, publicApi);
return 1;

View File

@ -104,7 +104,7 @@ sol::object SharedTable::rawGet(const sol::stack_object& luaKey, sol::this_state
{ \
std::unique_lock<SpinMutex> lock(data_->lock); \
if (data_->metatable != GCNull) { \
SharedTable tableHolder = *static_cast<SharedTable*>(getGC().get(data_->metatable)); \
auto tableHolder = GC::instance().get<SharedTable>(data_->metatable); \
lock.unlock(); \
sol::function handler = tableHolder.get(createStoredObject(methodName), state); \
if (handler.valid()) { \
@ -160,7 +160,7 @@ void SharedTable::luaNewIndex(const sol::stack_object& luaKey, const sol::stack_
{
std::unique_lock<SpinMutex> lock(data_->lock);
if (data_->metatable != GCNull) {
SharedTable tableHolder = *static_cast<SharedTable*>(getGC().get(data_->metatable));
auto tableHolder = GC::instance().get<SharedTable>(data_->metatable);
lock.unlock();
sol::function handler = tableHolder.get(createStoredObject("__newindex"), state);
if (handler.valid()) {
@ -180,7 +180,8 @@ sol::object SharedTable::luaIndex(const sol::stack_object& luaKey, sol::this_sta
StoredArray SharedTable::luaCall(sol::this_state state, const sol::variadic_args& args) {
std::unique_lock<SpinMutex> lock(data_->lock);
if (data_->metatable != GCNull) {
sol::function handler = static_cast<SharedTable*>(getGC().get(data_->metatable))->get(createStoredObject(std::string("__call")), state);
auto metatable = GC::instance().get<SharedTable>(data_->metatable);
sol::function handler = metatable.get(createStoredObject(std::string("__call")), state);
lock.unlock();
if (handler.valid()) {
StoredArray storedResults;
@ -280,7 +281,7 @@ SharedTable SharedTable::luaSetMetatable(SharedTable& stable, const sol::stack_o
sol::object SharedTable::luaGetMetatable(const SharedTable& stable, sol::this_state state) {
std::lock_guard<SpinMutex> lock(stable.data_->lock);
return stable.data_->metatable == GCNull ? sol::nil :
sol::make_object(state, *static_cast<SharedTable*>(getGC().get(stable.data_->metatable)));
sol::make_object(state, GC::instance().get<SharedTable>(stable.data_->metatable));
}
sol::object SharedTable::luaRawGet(const SharedTable& stable, const sol::stack_object& key, sol::this_state state) {

View File

@ -79,7 +79,7 @@ public:
TableHolder(const SolType& luaObject) {
assert(luaObject.template is<SharedTable>());
handle_ = luaObject.template as<SharedTable>().handle();
assert(getGC().has(handle_));
assert(GC::instance().has(handle_));
}
TableHolder(GCObjectHandle handle)
@ -90,7 +90,7 @@ public:
}
sol::object unpack(sol::this_state state) const final {
return sol::make_object(state, *static_cast<SharedTable*>(getGC().get(handle_)));
return sol::make_object(state, GC::instance().get<SharedTable>(handle_));
}
GCObjectHandle gcHandle() const override { return handle_; }
@ -115,7 +115,7 @@ StoredObject makeStoredObject(sol::object luaObject, SolTableToShared& visited)
auto st = std::find_if(visited.begin(), visited.end(), comparator);
if (st == std::end(visited)) {
SharedTable table = getGC().create<SharedTable>();
SharedTable table = GC::instance().create<SharedTable>();
visited.emplace_back(std::make_pair(luaTable, table.handle()));
dumpTable(&table, luaTable, visited);
return createStoredObject(table.handle());
@ -160,7 +160,7 @@ StoredObject fromSolObject(const SolObject& luaObject) {
sol::table luaTable = luaObject;
// Tables pool is used to store tables.
// Right now not defiantly clear how ownership between states works.
SharedTable table = getGC().create<SharedTable>();
SharedTable table = GC::instance().create<SharedTable>();
SolTableToShared visited{{luaTable, table.handle()}};
// Let's dump table and all subtables

View File

@ -54,7 +54,6 @@ class ThreadHandle {
public:
const bool managed;
sol::state lua;
Status status;
StoredArray result;
@ -68,8 +67,9 @@ public:
ThreadHandle(bool isManaged)
: managed(isManaged)
, status(Status::Running)
, command_(Command::Run) {
luaL_openlibs(lua);
, command_(Command::Run)
, lua_(std::make_unique<sol::state>()) {
luaL_openlibs(*lua_);
}
Command command() const { return command_; }
@ -81,9 +81,18 @@ public:
command_ = cmd;
}
sol::state& lua() {
assert(lua_);
return *lua_;
}
void destroyLua() { lua_.reset(); }
private:
SpinMutex commandMutex_;
Command command_;
std::unique_ptr<sol::state> lua_;
};
namespace {
@ -112,13 +121,8 @@ void luaHook(lua_State*, lua_Debug*) {
class ScopeGuard {
public:
ScopeGuard(const std::function<void()>& f)
: f_(f) {
}
~ScopeGuard() {
f_();
}
ScopeGuard(const std::function<void()>& f) : f_(f) {}
~ScopeGuard() { f_(); }
private:
std::function<void()> f_;
@ -130,6 +134,9 @@ void runThread(std::shared_ptr<ThreadHandle> handle,
ScopeGuard reportComplete([=](){
DEBUG << "Finished " << std::endl;
// Let's destroy accociated state
// to release all resources as soon as possible
handle->destroyLua();
handle->completion.notify();
});
@ -137,10 +144,10 @@ void runThread(std::shared_ptr<ThreadHandle> handle,
thisThreadHandle = handle.get();
try {
sol::function userFuncObj = loadString(handle->lua, strFunction);
sol::function userFuncObj = loadString(handle->lua(), strFunction);
sol::function_result results = userFuncObj(std::move(arguments));
(void)results; // just leave all returns on the stack
sol::variadic_args args(handle->lua, -lua_gettop(handle->lua));
sol::variadic_args args(handle->lua(), -lua_gettop(handle->lua()));
for (const auto& iter : args) {
StoredObject store = createStoredObject(iter.get<sol::object>());
handle->result.emplace_back(std::move(store));
@ -193,12 +200,12 @@ Thread::Thread(const std::string& path,
const sol::variadic_args& variadicArgs)
: handle_(std::make_shared<ThreadHandle>(managed)) {
handle_->lua["package"]["path"] = path;
handle_->lua["package"]["cpath"] = cpath;
handle_->lua.script("require 'effil'");
handle_->lua()["package"]["path"] = path;
handle_->lua()["package"]["cpath"] = cpath;
handle_->lua().script("require 'effil'");
if (managed)
lua_sethook(handle_->lua, luaHook, LUA_MASKCOUNT, step);
lua_sethook(handle_->lua(), luaHook, LUA_MASKCOUNT, step);
std::string strFunction = dumpFunction(function);

View File

@ -23,6 +23,7 @@ local api = {
setmetatable = capi.setmetatable,
getmetatable = capi.getmetatable,
G = capi.G,
gc = capi.gc
}
local function run_thread(config, f, ...)

View File

@ -12,10 +12,10 @@ TEST(gc, GCObject) {
GCObject o1;
EXPECT_EQ(o1.instances(), (size_t)1);
GCObject o2 = getGC().create<GCObject>();
GCObject o2 = GC::instance().create<GCObject>();
EXPECT_EQ(o2.instances(), (size_t)2);
GCObject o3 = getGC().create<GCObject>();
GCObject o3 = GC::instance().create<GCObject>();
GCObject o4(o3);
GCObject o5(o4);
EXPECT_EQ(o5.instances(), o3.instances());
@ -25,18 +25,15 @@ TEST(gc, GCObject) {
}
TEST(gc, collect) {
getGC().cleanup();
ASSERT_EQ(getGC().size(), (size_t)1);
GC::instance().collect();
ASSERT_EQ(GC::instance().size(), (size_t)1);
{
GCObject o1 = getGC().create<GCObject>();
;
GCObject o2 = getGC().create<GCObject>();
;
GCObject o1 = GC::instance().create<GCObject>();
GCObject o2 = GC::instance().create<GCObject>();
}
EXPECT_EQ(getGC().size(), (size_t)3);
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)1);
EXPECT_EQ(GC::instance().size(), (size_t)3);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)1);
}
namespace {
@ -44,26 +41,27 @@ namespace {
struct Dummy : public GCObject {
void add(GCObjectHandle ref) { refs_->insert(ref); }
};
}
TEST(gc, withRefs) {
getGC().cleanup();
GC::instance().collect();
{
Dummy root = getGC().create<Dummy>();
Dummy root = GC::instance().create<Dummy>();
{
Dummy orphan = getGC().create<Dummy>();
Dummy orphan = GC::instance().create<Dummy>();
for (size_t i = 0; i < 3; i++) {
Dummy child = getGC().create<Dummy>();
Dummy child = GC::instance().create<Dummy>();
root.add(child.handle());
}
}
EXPECT_EQ(getGC().size(), (size_t)6);
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)5);
EXPECT_EQ(GC::instance().size(), (size_t)6);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)5);
}
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)1);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)1);
}
TEST(gc, autoCleanup) {
@ -73,59 +71,59 @@ TEST(gc, autoCleanup) {
for (size_t i = 0; i < 5; i++)
threads.emplace_back([=] {
for (size_t i = 0; i < objectsPerThread; i++)
getGC().create<GCObject>();
GC::instance().create<GCObject>();
});
for (auto& thread : threads)
thread.join();
EXPECT_LT(getGC().size(), getGC().step());
EXPECT_LT(GC::instance().size(), GC::instance().step());
}
TEST(gc, gcInLuaState) {
sol::state lua;
bootstrapState(lua);
lua["st"] = getGC().create<SharedTable>();
lua["st"] = GC::instance().create<SharedTable>();
lua.script(R"(
for i=1,1000 do
st[i] = {"Wow"}
end
)");
EXPECT_EQ(getGC().size(), (size_t)1002);
EXPECT_EQ(GC::instance().size(), (size_t)1002);
lua.script(R"(
for i=1,1000 do
st[i] = nil
end
)");
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)2);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)2);
}
TEST(gc, cycles) {
{
sol::state lua;
bootstrapState(lua);
getGC().cleanup();
GC::instance().collect();
lua["st"] = getGC().create<SharedTable>();
lua["st"] = GC::instance().create<SharedTable>();
lua.script(R"(
st.parent = {}
st.parent.child = { ref = st.parent }
st[4] = { one = 1 }
st[5] = { flag = true }
)");
EXPECT_EQ(getGC().size(), (size_t)6);
EXPECT_EQ(GC::instance().size(), (size_t)6);
lua.script("st.parent = nil");
lua.collect_garbage();
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)4);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)4);
}
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)1);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)1);
}
TEST(gc, multipleStates) {
@ -135,12 +133,12 @@ TEST(gc, multipleStates) {
bootstrapState(lua2);
{
SharedTable st = getGC().create<SharedTable>();
SharedTable st = GC::instance().create<SharedTable>();
lua1["st"] = st;
lua2["st"] = st;
}
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)2);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)2);
lua1.script(R"(
st.men = { name = "John", age = 22 }
@ -152,13 +150,13 @@ st.men.car = st.car
st.men.cat = st.cat
st.men.fish = st.fish
)");
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)6);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)6);
lua2.script("copy = { st.men } st = nil");
lua1.script("st = nil");
lua1.collect_garbage();
lua2.collect_garbage();
getGC().cleanup();
EXPECT_EQ(getGC().size(), (size_t)5);
GC::instance().collect();
EXPECT_EQ(GC::instance().size(), (size_t)5);
}

View File

@ -129,9 +129,9 @@ TEST(sharedTable, playingWithSharedTables) {
sol::state lua;
bootstrapState(lua);
lua["recursive"] = getGC().create<SharedTable>();
lua["st1"] = getGC().create<SharedTable>();
lua["st2"] = getGC().create<SharedTable>();
lua["recursive"] = GC::instance().create<SharedTable>();
lua["st1"] = GC::instance().create<SharedTable>();
lua["st2"] = GC::instance().create<SharedTable>();
lua.script(R"(
st1.proxy = st2

40
tests/lua/gc.lua Normal file
View File

@ -0,0 +1,40 @@
local effil = require "effil"
local gc = effil.gc
TestGC = { tearDown = tearDown }
function TestGC:testCleanup()
collectgarbage()
gc.collect()
test.assertEquals(gc.count(), 1)
for i = 0, 10000 do
local tmp = effil.table()
end
collectgarbage()
gc.collect()
test.assertEquals(gc.count(), 1)
end
function TestGC:testDisableGC()
local nobjects = 10000
collectgarbage()
gc.collect()
test.assertEquals(gc.count(), 1)
gc.pause()
test.assertFalse(gc.enabled())
for i = 1, nobjects do
local tmp = effil.table()
end
test.assertEquals(gc.count(), nobjects + 1)
collectgarbage()
gc.collect()
test.assertEquals(gc.count(), 1)
gc.resume()
end

View File

@ -31,6 +31,7 @@ effil = require 'effil'
require 'test_utils'
require 'thread'
require 'shared_table'
require 'gc'
-- Hack tests functions to print when test starts
for suite_name, suite in pairs(_G) do

View File

@ -46,6 +46,10 @@ end
function tearDown()
collectgarbage()
effil.gc.collect()
-- effil.G is always present
-- thus, gc has one object
test.assertEquals(effil.gc.count(), 1)
end
function make_test_with_param(test_suite, test_case_pattern, ...)