From 65af80f08d4519c7a6d6d3fc00a11522b5259b5b Mon Sep 17 00:00:00 2001 From: iThorgrim <125808072+iThorgrim@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:27:57 +0200 Subject: [PATCH] feat: Add global bytecode cache for Eluna scripts (#293) --- conf/mod_eluna.conf.dist | 9 ++ src/LuaEngine/ElunaCompat.h | 4 + src/LuaEngine/LuaEngine.cpp | 280 +++++++++++++++++++++++++++++++++++- src/LuaEngine/LuaEngine.h | 31 ++++ 4 files changed, 319 insertions(+), 5 deletions(-) diff --git a/conf/mod_eluna.conf.dist b/conf/mod_eluna.conf.dist index 3bf17a3..dea70a7 100644 --- a/conf/mod_eluna.conf.dist +++ b/conf/mod_eluna.conf.dist @@ -37,6 +37,14 @@ # Below are a set of "standard" paths used by most package managers. # "/usr/local/lib/lua/%s/?.so;/usr/lib/x86_64-linux-gnu/lua/%s/?.so;/usr/local/lib/lua/%s/loadall.so;" # Default: "" +# +# Eluna.BytecodeCache +# Description: Enable or disable bytecode caching for improved performance. +# When enabled, Lua/MoonScript files are compiled to bytecode and cached in memory. +# This significantly speeds up script reloading (.reload eluna). +# Cache is cleared only when files are modified or server restarts. +# Default: true - (enabled) +# false - (disabled) Eluna.Enabled = true Eluna.TraceBack = false @@ -44,6 +52,7 @@ Eluna.ScriptPath = "lua_scripts" Eluna.PlayerAnnounceReload = false Eluna.RequirePaths = "" Eluna.RequireCPaths = "" +Eluna.BytecodeCache = true ################################################################################################### # LOGGING SYSTEM SETTINGS diff --git a/src/LuaEngine/ElunaCompat.h b/src/LuaEngine/ElunaCompat.h index db6ef2a..68ecbfb 100644 --- a/src/LuaEngine/ElunaCompat.h +++ b/src/LuaEngine/ElunaCompat.h @@ -27,6 +27,10 @@ extern "C" #define lua_load(L, buf_read, dec_buf, str, NULL) \ lua_load(L, buf_read, dec_buf, str) + #ifndef LUA_OK + #define LUA_OK 0 + #endif + #if !defined LUAJIT_VERSION void* luaL_testudata(lua_State* L, int index, const char* tname); void luaL_setmetatable(lua_State* L, const char* tname); diff --git a/src/LuaEngine/LuaEngine.cpp b/src/LuaEngine/LuaEngine.cpp index 27dfa86..d78299f 100644 --- a/src/LuaEngine/LuaEngine.cpp +++ b/src/LuaEngine/LuaEngine.cpp @@ -25,6 +25,11 @@ #define USING_BOOST #include +#include +#include +#include +#include +#include extern "C" { @@ -46,6 +51,11 @@ bool Eluna::reload = false; bool Eluna::initialized = false; Eluna::LockType Eluna::lock; +// Global bytecode cache that survives Eluna reloads +static std::unordered_map globalBytecodeCache; +static std::unordered_map timestampCache; +static std::mutex globalCacheMutex; + extern void RegisterFunctions(Eluna* E); void Eluna::Initialize() @@ -78,6 +88,9 @@ void Eluna::Uninitialize() lua_scripts.clear(); lua_extensions.clear(); + // Clear global cache on shutdown + ClearGlobalCache(); + initialized = false; } @@ -345,7 +358,7 @@ void Eluna::AddScriptPath(std::string filename, const std::string& fullpath) filename = filename.substr(0, extDot); // check extension and add path to scripts to load - if (ext != ".lua" && ext != ".dll" && ext != ".so" && ext != ".ext" && ext !=".moon") + if (ext != ".lua" && ext != ".dll" && ext != ".so" && ext != ".ext" && ext !=".moon" && ext != ".out") return; bool extension = ext == ".ext"; @@ -361,6 +374,228 @@ void Eluna::AddScriptPath(std::string filename, const std::string& fullpath) ELUNA_LOG_DEBUG("[Eluna]: AddScriptPath add path `{}`", fullpath); } +std::time_t Eluna::GetFileModTime(const std::string& filepath) +{ + struct stat fileInfo; + if (stat(filepath.c_str(), &fileInfo) == 0) + return fileInfo.st_mtime; + return 0; +} + +std::time_t Eluna::GetFileModTimeWithCache(const std::string& filepath) +{ + auto it = timestampCache.find(filepath); + if (it != timestampCache.end()) + return it->second; + + std::time_t modTime = GetFileModTime(filepath); + timestampCache[filepath] = modTime; + return modTime; +} + +bool Eluna::CompileScriptToGlobalCache(const std::string& filepath) +{ + std::lock_guard lock(globalCacheMutex); + + lua_State* tempL = luaL_newstate(); + if (!tempL) + return false; + + int result = luaL_loadfile(tempL, filepath.c_str()); + if (result != LUA_OK) + { + lua_close(tempL); + return false; + } + + std::time_t modTime = GetFileModTime(filepath); + + auto& cacheEntry = globalBytecodeCache[filepath]; + cacheEntry.last_modified = modTime; + cacheEntry.filepath = filepath; + cacheEntry.bytecode.clear(); + cacheEntry.bytecode.reserve(1024); + + struct BytecodeWriter { + BytecodeBuffer* buffer; + static int writer(lua_State*, const void* p, size_t sz, void* ud) { + BytecodeWriter* w = static_cast(ud); + const uint8* bytes = static_cast(p); + w->buffer->insert(w->buffer->end(), bytes, bytes + sz); + return 0; + } + }; + + BytecodeWriter writer; + writer.buffer = &cacheEntry.bytecode; + + int dumpResult = lua_dump(tempL, BytecodeWriter::writer, &writer); + if (dumpResult != LUA_OK || cacheEntry.bytecode.empty()) + { + globalBytecodeCache.erase(filepath); + lua_close(tempL); + return false; + } + + lua_close(tempL); + return true; +} + +bool Eluna::CompileMoonScriptToGlobalCache(const std::string& filepath) +{ + std::lock_guard lock(globalCacheMutex); + + lua_State* tempL = luaL_newstate(); + if (!tempL) + return false; + + luaL_openlibs(tempL); + + std::string moonscriptLoader = "return require('moonscript').loadfile([[" + filepath + "]])"; + int result = luaL_loadstring(tempL, moonscriptLoader.c_str()); + if (result != LUA_OK) + { + lua_close(tempL); + return false; + } + + result = lua_pcall(tempL, 0, 1, 0); + if (result != LUA_OK) + { + lua_close(tempL); + return false; + } + + std::time_t modTime = GetFileModTime(filepath); + + auto& cacheEntry = globalBytecodeCache[filepath]; + cacheEntry.last_modified = modTime; + cacheEntry.filepath = filepath; + cacheEntry.bytecode.clear(); + cacheEntry.bytecode.reserve(2048); + + struct BytecodeWriter { + BytecodeBuffer* buffer; + static int writer(lua_State*, const void* p, size_t sz, void* ud) { + BytecodeWriter* w = static_cast(ud); + const uint8* bytes = static_cast(p); + w->buffer->insert(w->buffer->end(), bytes, bytes + sz); + return 0; + } + }; + + BytecodeWriter writer; + writer.buffer = &cacheEntry.bytecode; + + int dumpResult = lua_dump(tempL, BytecodeWriter::writer, &writer); + if (dumpResult != LUA_OK || cacheEntry.bytecode.empty()) + { + globalBytecodeCache.erase(filepath); + lua_close(tempL); + return false; + } + + lua_close(tempL); + return true; +} + +int Eluna::TryLoadFromGlobalCache(lua_State* L, const std::string& filepath) +{ + std::lock_guard lock(globalCacheMutex); + + auto it = globalBytecodeCache.find(filepath); + if (it == globalBytecodeCache.end() || it->second.bytecode.empty()) + return LUA_ERRFILE; + + std::time_t currentModTime = GetFileModTimeWithCache(filepath); + if (it->second.last_modified != currentModTime || currentModTime == 0) + return LUA_ERRFILE; + + return luaL_loadbuffer(L, reinterpret_cast(it->second.bytecode.data()), it->second.bytecode.size(), filepath.c_str()); +} + +int Eluna::LoadScriptWithCache(lua_State* L, const std::string& filepath, bool isMoonScript, uint32* compiledCount, uint32* cachedCount) +{ + bool cacheEnabled = eConfigMgr->GetOption("Eluna.BytecodeCache", true); + + if (cacheEnabled) + { + int result = TryLoadFromGlobalCache(L, filepath); + if (result == LUA_OK) + { + if (cachedCount) (*cachedCount)++; + return LUA_OK; + } + + bool compileSuccess = isMoonScript ? + CompileMoonScriptToGlobalCache(filepath) : + CompileScriptToGlobalCache(filepath); + + if (compileSuccess) + { + if (compiledCount) (*compiledCount)++; + std::lock_guard lock(globalCacheMutex); + auto it = globalBytecodeCache.find(filepath); + if (it != globalBytecodeCache.end() && !it->second.bytecode.empty()) + { + result = luaL_loadbuffer(L, reinterpret_cast(it->second.bytecode.data()), it->second.bytecode.size(), filepath.c_str()); + if (result == LUA_OK) + return LUA_OK; + } + } + } + + if (isMoonScript) + { + std::string str = "return require('moonscript').loadfile([[" + filepath + "]])"; + int result = luaL_loadstring(L, str.c_str()); + if (result != LUA_OK) + return result; + return lua_pcall(L, 0, LUA_MULTRET, 0); + } + else + { + return luaL_loadfile(L, filepath.c_str()); + } +} + +void Eluna::ClearGlobalCache() +{ + std::lock_guard lock(globalCacheMutex); + globalBytecodeCache.clear(); + timestampCache.clear(); + ELUNA_LOG_INFO("[Eluna]: Global bytecode cache cleared"); +} + +void Eluna::ClearTimestampCache() +{ + std::lock_guard lock(globalCacheMutex); + timestampCache.clear(); +} + +size_t Eluna::GetGlobalCacheSize() +{ + std::lock_guard lock(globalCacheMutex); + return globalBytecodeCache.size(); +} + +int Eluna::LoadCompiledScript(lua_State* L, const std::string& filepath) +{ + std::ifstream file(filepath, std::ios::binary); + if (!file.is_open()) + return LUA_ERRFILE; + + file.seekg(0, std::ios::end); + size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(fileSize); + file.read(buffer.data(), fileSize); + file.close(); + + return luaL_loadbuffer(L, buffer.data(), fileSize, filepath.c_str()); +} + // Finds lua script files from given path (including subdirectories) and pushes them to scripts void Eluna::GetScripts(std::string path) { @@ -425,6 +660,13 @@ void Eluna::RunScripts() uint32 oldMSTime = ElunaUtil::GetCurrTime(); uint32 count = 0; + uint32 compiledCount = 0; + uint32 cachedCount = 0; + uint32 precompiledCount = 0; + bool cacheEnabled = eConfigMgr->GetOption("Eluna.BytecodeCache", true); + + if (cacheEnabled) + ClearTimestampCache(); ScriptList scripts; lua_extensions.sort(ScriptPathComparator); @@ -439,7 +681,7 @@ void Eluna::RunScripts() luaL_getsubtable(L, -1, "loaded"); // Stack: package, modules int modules = lua_gettop(L); - for (ScriptList::const_iterator it = scripts.begin(); it != scripts.end(); ++it) + for (ScriptList::iterator it = scripts.begin(); it != scripts.end(); ++it) { // Check that no duplicate names exist if (loaded.find(it->filename) != loaded.end()) @@ -462,8 +704,7 @@ void Eluna::RunScripts() if (it->fileext == ".moon") { - std::string str = "return require('moonscript').loadfile([[" + it->filepath + "]])"; - if (luaL_loadstring(L, str.c_str()) || lua_pcall(L, 0, LUA_MULTRET, 0)) + if (LoadScriptWithCache(L, it->filepath, true, &compiledCount, &cachedCount)) { // Stack: package, modules, errmsg ELUNA_LOG_ERROR("[Eluna]: Error loading MoonScript `{}`", it->filepath); @@ -472,6 +713,29 @@ void Eluna::RunScripts() continue; } } + else if (it->fileext == ".out") + { + if (LoadCompiledScript(L, it->filepath)) + { + // Stack: package, modules, errmsg + ELUNA_LOG_ERROR("[Eluna]: Error loading compiled script `{}`", it->filepath); + Report(L); + // Stack: package, modules + continue; + } + precompiledCount++; + } + else if (it->fileext == ".lua" || it->fileext == ".ext") + { + if (LoadScriptWithCache(L, it->filepath, false, &compiledCount, &cachedCount)) + { + // Stack: package, modules, errmsg + ELUNA_LOG_ERROR("[Eluna]: Error loading `{}`", it->filepath); + Report(L); + // Stack: package, modules + continue; + } + } else { if (luaL_loadfile(L, it->filepath.c_str())) @@ -505,7 +769,13 @@ void Eluna::RunScripts() } // Stack: package, modules lua_pop(L, 2); - ELUNA_LOG_INFO("[Eluna]: Executed {} Lua scripts in {} ms", count, ElunaUtil::GetTimeDiff(oldMSTime)); + + std::string details = ""; + if (cacheEnabled && (compiledCount > 0 || cachedCount > 0 || precompiledCount > 0)) + { + details = fmt::format("({} compiled, {} cached, {} pre-compiled)", compiledCount, cachedCount, precompiledCount); + } + ELUNA_LOG_INFO("[Eluna]: Executed {} Lua scripts in {} ms {}", count, ElunaUtil::GetTimeDiff(oldMSTime), details); OnLuaStateOpen(); } diff --git a/src/LuaEngine/LuaEngine.h b/src/LuaEngine/LuaEngine.h index fdc8d88..1853b97 100644 --- a/src/LuaEngine/LuaEngine.h +++ b/src/LuaEngine/LuaEngine.h @@ -26,6 +26,9 @@ #include "LootMgr.h" #include #include +#include +#include +#include extern "C" { @@ -73,12 +76,28 @@ template struct EventKey; template struct EntryKey; template struct UniqueObjectKey; +// Type definition for bytecode buffer +typedef std::vector BytecodeBuffer; + +// Global bytecode cache entry +struct GlobalCacheEntry +{ + BytecodeBuffer bytecode; + std::time_t last_modified; + std::string filepath; + + GlobalCacheEntry() : last_modified(0) {} + GlobalCacheEntry(const BytecodeBuffer& code, std::time_t modTime, const std::string& path) + : bytecode(code), last_modified(modTime), filepath(path) {} +}; + struct LuaScript { std::string fileext; std::string filename; std::string filepath; std::string modulepath; + LuaScript() {} }; #define ELUNA_STATE_PTR "Eluna State Ptr" @@ -150,6 +169,18 @@ private: static void LoadScriptPaths(); static void GetScripts(std::string path); static void AddScriptPath(std::string filename, const std::string& fullpath); + static int LoadCompiledScript(lua_State* L, const std::string& filepath); + static std::time_t GetFileModTime(const std::string& filepath); + static std::time_t GetFileModTimeWithCache(const std::string& filepath); + + // Global cache management + static bool CompileScriptToGlobalCache(const std::string& filepath); + static bool CompileMoonScriptToGlobalCache(const std::string& filepath); + static int TryLoadFromGlobalCache(lua_State* L, const std::string& filepath); + static int LoadScriptWithCache(lua_State* L, const std::string& filepath, bool isMoonScript, uint32* compiledCount = nullptr, uint32* cachedCount = nullptr); + static void ClearGlobalCache(); + static void ClearTimestampCache(); + static size_t GetGlobalCacheSize(); static int StackTrace(lua_State *_L); static void Report(lua_State* _L);