feat: Add global bytecode cache for Eluna scripts (#293)

This commit is contained in:
iThorgrim
2025-08-29 14:27:57 +02:00
committed by GitHub
parent 6fd81ea0e6
commit 65af80f08d
4 changed files with 319 additions and 5 deletions

View File

@@ -37,6 +37,14 @@
# Below are a set of "standard" paths used by most package managers. # 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;" # "/usr/local/lib/lua/%s/?.so;/usr/lib/x86_64-linux-gnu/lua/%s/?.so;/usr/local/lib/lua/%s/loadall.so;"
# Default: "" # 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.Enabled = true
Eluna.TraceBack = false Eluna.TraceBack = false
@@ -44,6 +52,7 @@ Eluna.ScriptPath = "lua_scripts"
Eluna.PlayerAnnounceReload = false Eluna.PlayerAnnounceReload = false
Eluna.RequirePaths = "" Eluna.RequirePaths = ""
Eluna.RequireCPaths = "" Eluna.RequireCPaths = ""
Eluna.BytecodeCache = true
################################################################################################### ###################################################################################################
# LOGGING SYSTEM SETTINGS # LOGGING SYSTEM SETTINGS

View File

@@ -27,6 +27,10 @@ extern "C"
#define lua_load(L, buf_read, dec_buf, str, NULL) \ #define lua_load(L, buf_read, dec_buf, str, NULL) \
lua_load(L, buf_read, dec_buf, str) lua_load(L, buf_read, dec_buf, str)
#ifndef LUA_OK
#define LUA_OK 0
#endif
#if !defined LUAJIT_VERSION #if !defined LUAJIT_VERSION
void* luaL_testudata(lua_State* L, int index, const char* tname); void* luaL_testudata(lua_State* L, int index, const char* tname);
void luaL_setmetatable(lua_State* L, const char* tname); void luaL_setmetatable(lua_State* L, const char* tname);

View File

@@ -25,6 +25,11 @@
#define USING_BOOST #define USING_BOOST
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <fstream>
#include <vector>
#include <ctime>
#include <sys/stat.h>
#include <unordered_map>
extern "C" extern "C"
{ {
@@ -46,6 +51,11 @@ bool Eluna::reload = false;
bool Eluna::initialized = false; bool Eluna::initialized = false;
Eluna::LockType Eluna::lock; Eluna::LockType Eluna::lock;
// Global bytecode cache that survives Eluna reloads
static std::unordered_map<std::string, GlobalCacheEntry> globalBytecodeCache;
static std::unordered_map<std::string, std::time_t> timestampCache;
static std::mutex globalCacheMutex;
extern void RegisterFunctions(Eluna* E); extern void RegisterFunctions(Eluna* E);
void Eluna::Initialize() void Eluna::Initialize()
@@ -78,6 +88,9 @@ void Eluna::Uninitialize()
lua_scripts.clear(); lua_scripts.clear();
lua_extensions.clear(); lua_extensions.clear();
// Clear global cache on shutdown
ClearGlobalCache();
initialized = false; initialized = false;
} }
@@ -345,7 +358,7 @@ void Eluna::AddScriptPath(std::string filename, const std::string& fullpath)
filename = filename.substr(0, extDot); filename = filename.substr(0, extDot);
// check extension and add path to scripts to load // 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; return;
bool extension = ext == ".ext"; 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); 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<std::mutex> 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<BytecodeWriter*>(ud);
const uint8* bytes = static_cast<const uint8*>(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<std::mutex> 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<BytecodeWriter*>(ud);
const uint8* bytes = static_cast<const uint8*>(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<std::mutex> 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<const char*>(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<bool>("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<std::mutex> lock(globalCacheMutex);
auto it = globalBytecodeCache.find(filepath);
if (it != globalBytecodeCache.end() && !it->second.bytecode.empty())
{
result = luaL_loadbuffer(L, reinterpret_cast<const char*>(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<std::mutex> lock(globalCacheMutex);
globalBytecodeCache.clear();
timestampCache.clear();
ELUNA_LOG_INFO("[Eluna]: Global bytecode cache cleared");
}
void Eluna::ClearTimestampCache()
{
std::lock_guard<std::mutex> lock(globalCacheMutex);
timestampCache.clear();
}
size_t Eluna::GetGlobalCacheSize()
{
std::lock_guard<std::mutex> 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<char> 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 // Finds lua script files from given path (including subdirectories) and pushes them to scripts
void Eluna::GetScripts(std::string path) void Eluna::GetScripts(std::string path)
{ {
@@ -425,6 +660,13 @@ void Eluna::RunScripts()
uint32 oldMSTime = ElunaUtil::GetCurrTime(); uint32 oldMSTime = ElunaUtil::GetCurrTime();
uint32 count = 0; uint32 count = 0;
uint32 compiledCount = 0;
uint32 cachedCount = 0;
uint32 precompiledCount = 0;
bool cacheEnabled = eConfigMgr->GetOption<bool>("Eluna.BytecodeCache", true);
if (cacheEnabled)
ClearTimestampCache();
ScriptList scripts; ScriptList scripts;
lua_extensions.sort(ScriptPathComparator); lua_extensions.sort(ScriptPathComparator);
@@ -439,7 +681,7 @@ void Eluna::RunScripts()
luaL_getsubtable(L, -1, "loaded"); luaL_getsubtable(L, -1, "loaded");
// Stack: package, modules // Stack: package, modules
int modules = lua_gettop(L); 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 // Check that no duplicate names exist
if (loaded.find(it->filename) != loaded.end()) if (loaded.find(it->filename) != loaded.end())
@@ -462,8 +704,7 @@ void Eluna::RunScripts()
if (it->fileext == ".moon") if (it->fileext == ".moon")
{ {
std::string str = "return require('moonscript').loadfile([[" + it->filepath + "]])"; if (LoadScriptWithCache(L, it->filepath, true, &compiledCount, &cachedCount))
if (luaL_loadstring(L, str.c_str()) || lua_pcall(L, 0, LUA_MULTRET, 0))
{ {
// Stack: package, modules, errmsg // Stack: package, modules, errmsg
ELUNA_LOG_ERROR("[Eluna]: Error loading MoonScript `{}`", it->filepath); ELUNA_LOG_ERROR("[Eluna]: Error loading MoonScript `{}`", it->filepath);
@@ -472,6 +713,29 @@ void Eluna::RunScripts()
continue; 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 else
{ {
if (luaL_loadfile(L, it->filepath.c_str())) if (luaL_loadfile(L, it->filepath.c_str()))
@@ -505,7 +769,13 @@ void Eluna::RunScripts()
} }
// Stack: package, modules // Stack: package, modules
lua_pop(L, 2); 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(); OnLuaStateOpen();
} }

View File

@@ -26,6 +26,9 @@
#include "LootMgr.h" #include "LootMgr.h"
#include <mutex> #include <mutex>
#include <memory> #include <memory>
#include <vector>
#include <ctime>
#include <unordered_map>
extern "C" extern "C"
{ {
@@ -73,12 +76,28 @@ template<typename T> struct EventKey;
template<typename T> struct EntryKey; template<typename T> struct EntryKey;
template<typename T> struct UniqueObjectKey; template<typename T> struct UniqueObjectKey;
// Type definition for bytecode buffer
typedef std::vector<uint8> 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 struct LuaScript
{ {
std::string fileext; std::string fileext;
std::string filename; std::string filename;
std::string filepath; std::string filepath;
std::string modulepath; std::string modulepath;
LuaScript() {}
}; };
#define ELUNA_STATE_PTR "Eluna State Ptr" #define ELUNA_STATE_PTR "Eluna State Ptr"
@@ -150,6 +169,18 @@ private:
static void LoadScriptPaths(); static void LoadScriptPaths();
static void GetScripts(std::string path); static void GetScripts(std::string path);
static void AddScriptPath(std::string filename, const std::string& fullpath); 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 int StackTrace(lua_State *_L);
static void Report(lua_State* _L); static void Report(lua_State* _L);