From 82a7c255a4111067b1e1a1a1d0bbd9c567faeb3b Mon Sep 17 00:00:00 2001 From: Ilari Liusvaara Date: Mon, 25 May 2015 23:44:51 +0300 Subject: [PATCH] Lua: Memory usage limit This allows limiting memory usage by Lua, which might very well be useful, given the behaviour of accumulating garbage in memory if one does not garbage-collect. --- include/library/lua-base.hpp | 72 ++++++++++++++++++++++++- include/library/lua-class.hpp | 6 ++- include/library/settingvar.hpp | 4 +- include/lua/lua.hpp | 20 ++++++- src/core/instance.cpp | 4 +- src/library/lua.cpp | 120 +++++++++++++++++++++++++++++++++++++++-- src/lua/core.cpp | 8 +++ src/lua/lua.cpp | 41 +++++++++++++- 8 files changed, 261 insertions(+), 14 deletions(-) diff --git a/include/library/lua-base.hpp b/include/library/lua-base.hpp index b9e028e3..df4b405a 100644 --- a/include/library/lua-base.hpp +++ b/include/library/lua-base.hpp @@ -220,6 +220,10 @@ public: */ void set_oom_handler(void (*oom)()) { oom_handler = oom ? oom : builtin_oom; } /** + * Set soft OOM handler. + */ + void set_soft_oom_handler(void (*oom)(int status)) { soft_oom_handler = oom ? oom : builtin_soft_oom; } +/** * Reset the state. */ void reset() throw(std::runtime_error, std::bad_alloc); @@ -239,6 +243,55 @@ public: */ int trampoline_upval(int val) { return lua_upvalueindex(trampoline_upvals + val); } /** + * Set value of interruptable flag. + * + * Parameter flag: The flag. + */ + void set_interruptable_flag(bool flag) + { + if(master) master->set_interruptable_flag(flag); else interruptable = flag; + } +/** + * Get interruptable flag. + */ + bool get_interruptable_flag() + { + if(master) return master->get_interruptable_flag(); else return interruptable; + } +/** + * Set memory limit. + */ + void set_memory_limit(size_t limit) + { + if(master) master->set_memory_limit(limit); else memory_limit = limit; + } +/** + * Get memory limit. + */ + size_t get_memory_limit() + { + if(master) return master->get_memory_limit(); else return memory_limit; + } +/** + * Get memory use. + */ + size_t get_memory_use() + { + if(master) return master->get_memory_use(); else return memory_use; + } +/** + * Charge against memory limit. + */ + bool charge_memory(size_t amount, bool release); +/** + * Execute function in interruptable mode. + * + * Parameter fn: The function to execute + * Parameter in: Number of slots to copy in. + * Parameter out: Number of slots to copy out. + */ + void run_interruptable(std::function fn, unsigned in, unsigned out); +/** * Get a string argument. * * Parameter argindex: The stack index. @@ -423,6 +476,7 @@ public: void* newuserdata(size_t size) { return lua_newuserdata(lua_handle, size); } int setmetatable(int index) { return lua_setmetatable(lua_handle, index); } int type(int index) { return lua_type(lua_handle, index); } + void replace(int index) { lua_replace(lua_handle, index); } int getmetatable(int index) { return lua_getmetatable(lua_handle, index); } int rawequal(int index1, int index2) { return lua_rawequal(lua_handle, index1, index2); } void* touserdata(int index) { return lua_touserdata(lua_handle, index); } @@ -462,7 +516,18 @@ public: void pushlstring(const char* s, size_t len) { lua_pushlstring(lua_handle, s, len); } void pushlstring(const std::string& s) { lua_pushlstring(lua_handle, s.c_str(), s.length()); } void pushlstring(const char32_t* s, size_t len) { pushlstring(utf8::to8(std::u32string(s, len))); } - int pcall(int nargs, int nresults, int errfunc) { return lua_pcall(lua_handle, nargs, nresults, errfunc); } + int pcall(int nargs, int nresults, int errfunc) + { + state* master_state = this; + while(master_state->master) master_state = master_state->master; + //Upon entry to protected mode, interruptable mode is always set, and it is restored on exit + //from protected mode. + bool old_interruptable = master_state->interruptable; + master_state->interruptable = true; + auto ret = lua_pcall(lua_handle, nargs, nresults, errfunc); + master_state->interruptable = old_interruptable; + return ret; + } int next(int index) { return lua_next(lua_handle, index); } int isnoneornil(int index) { return lua_isnoneornil(lua_handle, index); } void rawgeti(int index, int n) { lua_rawgeti(lua_handle, index, n); } @@ -479,9 +544,14 @@ private: void _pushnumber(lua_Number n) { return lua_pushnumber(lua_handle, n); } void _pushinteger(uint64_t n) { return LUA_INTEGER_POSTFIX(lua_push) (lua_handle, n); } static void builtin_oom(); + static void builtin_soft_oom(int status); static void* builtin_alloc(void* user, void* old, size_t olds, size_t news); void (*oom_handler)(); + void (*soft_oom_handler)(int status); state* master; + bool interruptable; + size_t memory_limit; + size_t memory_use; lua_State* lua_handle; state(state&); state& operator=(state&); diff --git a/include/library/lua-class.hpp b/include/library/lua-class.hpp index 2ff152d7..8d184c07 100644 --- a/include/library/lua-class.hpp +++ b/include/library/lua-class.hpp @@ -220,7 +220,11 @@ template class _class : public class_base template T* _create(state& _state, U... args) { size_t overcommit = T::overcommit(args...); - void* obj = _state.newuserdata(sizeof(T) + overcommit); + void* obj = NULL; + auto st = &_state; + _state.run_interruptable([st, overcommit, &obj]() { + obj = st->newuserdata(sizeof(T) + overcommit); + }, 0, 1); load_metatable(_state); _state.setmetatable(-2); T* _obj = reinterpret_cast(obj); diff --git a/include/library/settingvar.hpp b/include/library/settingvar.hpp index 1ef23079..aa161845 100644 --- a/include/library/settingvar.hpp +++ b/include/library/settingvar.hpp @@ -390,7 +390,7 @@ public: /** * Get setting. */ - valtype_t get() throw(std::bad_alloc) + valtype_t get() const throw(std::bad_alloc) { threads::arlock h(get_setting_lock()); return model::transform(value); @@ -398,7 +398,7 @@ public: /** * Get setting. */ - operator valtype_t() + operator valtype_t() const { return get(); } diff --git a/include/lua/lua.hpp b/include/lua/lua.hpp index dc6bb0eb..8691097c 100644 --- a/include/lua/lua.hpp +++ b/include/lua/lua.hpp @@ -10,6 +10,7 @@ #include "library/framebuffer.hpp" #include "library/lua-base.hpp" #include "library/lua-framebuffer.hpp" +#include "library/settingvar.hpp" namespace command { class group; } namespace keyboard { class key; } @@ -22,7 +23,7 @@ void quit_lua() throw(); struct lua_state { - lua_state(lua::state& _L, command::group& _command); + lua_state(lua::state& _L, command::group& _command, settingvar::group& settings); ~lua_state(); lua::state::callback_list* on_paint; @@ -102,6 +103,7 @@ struct lua_state lua::render_context* renderq_saved; lua::render_context* renderq_last; bool renderq_redirect; + void set_memory_limit(size_t max_mb); std::list startup_scripts; std::map watch_vars; @@ -119,6 +121,22 @@ private: command::_fnptr evalcmd; command::_fnptr evalcmd2; command::_fnptr runcmd; + struct _listener : public settingvar::listener + { + _listener(settingvar::group& group, lua_state& _obj) + : obj(_obj), grp(group) + { + group.add_listener(*this); + } + ~_listener() throw() + { + grp.remove_listener(*this); + } + void on_setting_change(settingvar::group& grp, const settingvar::base& val); + lua_state& obj; + settingvar::group& grp; + }; + _listener listener; }; #endif diff --git a/src/core/instance.cpp b/src/core/instance.cpp index 9777955d..d68bd19c 100644 --- a/src/core/instance.cpp +++ b/src/core/instance.cpp @@ -81,10 +81,10 @@ emulator_instance::emulator_instance() D.init(mlogic); D.init(slotcache, *mlogic, *command); D.init(memory); + D.init(settings); D.init(lua); - D.init(lua2, *lua, *command); + D.init(lua2, *lua, *command, *settings); D.init(mwatch, *memory, *project, *fbuf, *rom); - D.init(settings); D.init(jukebox, *settings, *command); D.init(setcache, *settings); D.init(audio); diff --git a/src/library/lua.cpp b/src/library/lua.cpp index 6a4d2889..135a8cd6 100644 --- a/src/library/lua.cpp +++ b/src/library/lua.cpp @@ -220,11 +220,16 @@ namespace state* lstate = reinterpret_cast(lua_touserdata(L, lua_upvalueindex(1))); void* _fn = lua_touserdata(L, lua_upvalueindex(2)); fnraw_t fn = (fnraw_t)_fn; + //The function is always run in non-set_interruptable mode. try { state _L(*lstate, L); - return fn(_L); + lstate->set_interruptable_flag(false); + int r = fn(_L); + lstate->set_interruptable_flag(true); + return r; } catch(std::exception& e) { lua_pushfstring(L, "%s", e.what()); + lstate->set_interruptable_flag(true); lua_error(L); } return 0; @@ -286,6 +291,24 @@ namespace { fun->register_state(L); } + + int run_interruptable_trampoline(lua_State* L) + { + //Be very careful with faults here! We are running in interruptable context. + static std::string err; + auto& fn = *(std::function*)lua_touserdata(L, -1); + int out = lua_tonumber(L, -2); + lua_pop(L, 2); //fn is passed as a pointer, so popping it is OK. + try { + fn(); + } catch(std::exception& e) { + err = e.what(); + //This can fault, so err is static. + lua_pushlstring(L, err.c_str(), err.length()); + lua_error(L); + } + return out; + } } state::state() throw(std::bad_alloc) @@ -293,6 +316,10 @@ state::state() throw(std::bad_alloc) master = NULL; lua_handle = NULL; oom_handler = builtin_oom; + soft_oom_handler = builtin_soft_oom; + interruptable = false; //Assume initially not interruptable. + memory_limit = (size_t)-1; //Unlimited. + memory_use = 0; } state::state(state& _master, lua_State* L) @@ -323,16 +350,54 @@ void state::builtin_oom() exit(1); } +void state::builtin_soft_oom(int status) +{ + if(status == 0) + std::cerr << "Lua: Memory limit exceeded, attempting to free memory..." << std::endl; + if(status < 0) + std::cerr << "Lua: Memory allocation still failed." << std::endl; + if(status > 0) + std::cerr << "Lua: Allocation successful after freeing some memory." << std::endl; +} + void* state::builtin_alloc(void* user, void* old, size_t olds, size_t news) { + void* m; + auto& st = *reinterpret_cast(user); if(news) { - void* m = realloc(old, news); - if(!m) - reinterpret_cast(user)->oom_handler(); + if(news > olds && !st.charge_memory(news - olds, false)) { + goto retry_allocation; + } + m = realloc(old, news); + if(!m && !st.get_interruptable_flag()) + st.oom_handler(); + if(!m) { + st.charge_memory(news - olds, true); //Undo commit. + goto retry_allocation; + } + if(news < olds) + st.charge_memory(olds - news, true); //Release memory. return m; - } else + } else { + st.charge_memory(olds, true); //Release memory. free(old); + } return NULL; +retry_allocation: + st.soft_oom_handler(0); + st.interruptable = false; //Give everything we got for the GC. + lua_gc(st.lua_handle, LUA_GCCOLLECT,0); //Do full cycle to try to free some memory. + st.interruptable = true; + if(!st.charge_memory(news - olds, false)) { //Try to see if memory can be allocated. + st.soft_oom_handler(-1); + return NULL; + } + m = realloc(old, news); + if(!m && news > olds) + st.charge_memory(news - olds, true); //Undo commit. + st.soft_oom_handler(m ? 1 : -1); + return m; + } void state::push_trampoline(int(*fn)(state& L), unsigned n_upvals) @@ -346,6 +411,51 @@ void state::push_trampoline(int(*fn)(state& L), unsigned n_upvals) lua_pushcclosure(lua_handle, lua_main_trampoline, trampoline_upvals + n_upvals); } +void state::run_interruptable(std::function fn, unsigned in, unsigned out) +{ + pushnumber(out); + pushlightuserdata(&fn); + pushcfunction(run_interruptable_trampoline); + insert(-(int)in - 3); + int r = pcall(in + 2, out, 0); + if(r == LUA_OK) { + //Nothing. + } else if(r == LUA_ERRRUN) { + throw std::runtime_error(tostring(-1)); + } else if(r == LUA_ERRMEM) { + throw std::runtime_error("Lua out of memory"); + } else if(r == LUA_ERRERR) { + throw std::runtime_error("Lua double fault"); +#ifdef LUA_ERRGCMM + } else if(r == LUA_ERRGCMM) { + throw std::runtime_error("Lua fault in garbage collector"); +#endif + } +} + +bool state::charge_memory(size_t amount, bool release) +{ + if(master) return master->charge_memory(amount, release); + if(release) { + if(memory_use > amount) + memory_use -= amount; + else + memory_use = 0; + return true; + } + if(!interruptable) { + //Give everything we got. + memory_use += amount; + return true; + } else { + //Check limit and refuse allocations too large. + if(memory_use + amount > memory_limit || memory_use + amount < amount) + return false; + memory_use += amount; + return true; + } +} + function::function(function_group& _group, const std::string& func) throw(std::bad_alloc) : group(_group) { diff --git a/src/lua/core.cpp b/src/lua/core.cpp index 7f24879b..7e766530 100644 --- a/src/lua/core.cpp +++ b/src/lua/core.cpp @@ -205,6 +205,13 @@ namespace return 0; } + int get_lua_memory_use(lua::state& L, lua::parameters& P) + { + L.pushnumber(L.get_memory_use()); + L.pushnumber(L.get_memory_limit()); + return 2; + } + int get_runmode(lua::state& L, lua::parameters& P) { auto& core = CORE(); @@ -231,6 +238,7 @@ namespace {"set_idle_timeout", set_idle_timeout}, {"set_timer_timeout", set_timer_timeout}, {"bus_address", bus_address}, + {"get_lua_memory_use", get_lua_memory_use}, {"memory.get_lag_flag", get_lag_flag}, {"memory.set_lag_flag", set_lag_flag}, {"gui.get_runmode", get_runmode}, diff --git a/src/lua/lua.cpp b/src/lua/lua.cpp index f6efea60..4440460e 100644 --- a/src/lua/lua.cpp +++ b/src/lua/lua.cpp @@ -37,6 +37,10 @@ lua::class_group lua_class_fileio; namespace { + typedef settingvar::model_int<32,1024> mb_model; + settingvar::supervariable SET_lua_maxmem(lsnes_setgrp, "lua-maxmem", + "Lua‣Maximum memory use (MB)", 128); + void pushpair(lua::state& L, std::string key, double value) { L.pushstring(key.c_str()); @@ -91,6 +95,16 @@ void push_keygroup_parameters(lua::state& L, keyboard::key& p) namespace { + void soft_oom(int status) + { + if(status == 0) + messages << "Lua: Memory limit exceeded, attempting to free memory..." << std::endl; + if(status < 0) + messages << "Lua: Memory allocation still failed." << std::endl; + if(status > 0) + messages << "Lua: Allocation successful after freeing some memory." << std::endl; + } + int push_keygroup_parameters2(lua::state& L, keyboard::key* p) { push_keygroup_parameters(L, *p); @@ -148,17 +162,32 @@ namespace } } -lua_state::lua_state(lua::state& _L, command::group& _command) +void lua_state::_listener::on_setting_change(settingvar::group& grp, const settingvar::base& val) +{ + if(val.get_iname() == "lua-maxmem") + obj.set_memory_limit(dynamic_cast*>(&val)->get()); +} + +void lua_state::set_memory_limit(size_t limit_mb) +{ + L.set_memory_limit(limit_mb << 20); +} + +lua_state::lua_state(lua::state& _L, command::group& _command, settingvar::group& settings) : L(_L), command(_command), resetcmd(command, CLUA::reset, [this]() { this->do_reset(); }), evalcmd(command, CLUA::eval, [this](const std::string& a) { this->do_eval_lua(a); }), evalcmd2(command, CLUA::eval2, [this](const std::string& a) { this->do_eval_lua(a); }), - runcmd(command, CLUA::run, [this](command::arg_filename a) { this->do_run_lua(a); }) + runcmd(command, CLUA::run, [this](command::arg_filename a) { this->do_run_lua(a); }), + listener(settings, *this) { requests_repaint = false; requests_subframe_paint = false; render_ctx = NULL; input_controllerdata = NULL; + //We can't read the value of lua maxmem setting here (it crashes), so just set default, it will be changed + //if needed. + L.set_memory_limit(1 << 27); idle_hook_time = 0x7EFFFFFFFFFFFFFFULL; timer_hook_time = 0x7EFFFFFFFFFFFFFFULL; @@ -374,6 +403,7 @@ void init_lua() throw() { auto& core = lsnes_instance; core.lua->set_oom_handler(OOM_panic); + core.lua->set_soft_oom_handler(soft_oom); try { core.lua->reset(); core.lua->add_function_group(lua_func_bit); @@ -516,6 +546,13 @@ bool lua_state::run_lua_fragment() throw(std::bad_alloc) L.pop(1); result = false; } +#ifdef LUA_ERRGCMM + if(r == LUA_ERRGCMM) { + messages << "Error running Lua hunk: Fault in garbage collector" << std::endl; + L.pop(1); + result = false; + } +#endif render_ctx = NULL; if(requests_repaint) { requests_repaint = false; -- 2.11.4.GIT