From 6fa65e4729bbd59b47cc50d240d94f0b4cc4361a Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Mon, 14 Nov 2022 00:12:48 +0100 Subject: [PATCH 1/5] Track memory and CPU usage per script in LuaUtil::LuaState --- apps/openmw/mwlua/asyncbindings.cpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 4 + apps/openmw_test_suite/lua/test_async.cpp | 19 ++- apps/openmw_test_suite/lua/test_l10n.cpp | 2 +- .../lua/test_scriptscontainer.cpp | 3 +- apps/openmw_test_suite/lua/test_storage.cpp | 6 +- components/lua/luastate.cpp | 112 ++++++++++++++++-- components/lua/luastate.hpp | 88 +++++++++++++- components/lua/scriptscontainer.cpp | 103 +++++++++++++--- components/lua/scriptscontainer.hpp | 30 +++-- 10 files changed, 319 insertions(+), 50 deletions(-) diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index 481cce74c9..e850ef41e4 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -63,7 +63,7 @@ namespace MWLua = [](const LuaUtil::Callback& callback, sol::variadic_args va) { return callback.call(sol::as_args(va)); }; auto initializer = [](sol::table hiddenData) { - LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]; + LuaUtil::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]; return AsyncPackageId{ id.mContainer, id.mIndex, hiddenData }; }; return sol::make_object(context.mLua->sol(), initializer); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index a6e55ed3e9..e3b073f670 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -139,6 +139,10 @@ namespace MWLua mWorldView.update(); + mGlobalScripts.CPUusageNextFrame(); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->CPUusageNextFrame(); + std::vector globalEvents = std::move(mGlobalEvents); std::vector localEvents = std::move(mLocalEvents); mGlobalEvents = std::vector(); diff --git a/apps/openmw_test_suite/lua/test_async.cpp b/apps/openmw_test_suite/lua/test_async.cpp index c1e1fd6c86..6a167ef33d 100644 --- a/apps/openmw_test_suite/lua/test_async.cpp +++ b/apps/openmw_test_suite/lua/test_async.cpp @@ -15,41 +15,40 @@ namespace { void SetUp() override { - mLua.open_libraries(sol::lib::coroutine); - mLua["callback"] = [&](sol::protected_function fn) -> LuaUtil::Callback { - sol::table hiddenData(mLua, sol::create); - hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = sol::table(mLua, sol::create); + mLua.sol()["callback"] = [&](sol::protected_function fn) -> LuaUtil::Callback { + sol::table hiddenData(mLua.sol(), sol::create); + hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; return LuaUtil::Callback{ std::move(fn), hiddenData }; }; - mLua["pass"] = [this](LuaUtil::Callback callback) { mCb = callback; }; + mLua.sol()["pass"] = [this](LuaUtil::Callback callback) { mCb = callback; }; } - sol::state mLua; + LuaUtil::LuaState mLua{ nullptr, nullptr }; LuaUtil::Callback mCb; }; TEST_F(LuaCoroutineCallbackTest, CoroutineCallbacks) { internal::CaptureStdout(); - mLua.safe_script(R"X( + mLua.sol().safe_script(R"X( local s = 'test' coroutine.wrap(function() pass(callback(function(v) print(s) end)) end)() )X"); - mLua.collect_garbage(); + mLua.sol().collect_garbage(); mCb.call(); EXPECT_THAT(internal::GetCapturedStdout(), "test\n"); } TEST_F(LuaCoroutineCallbackTest, ErrorInCoroutineCallbacks) { - mLua.safe_script(R"X( + mLua.sol().safe_script(R"X( coroutine.wrap(function() pass(callback(function() error('COROUTINE CALLBACK') end)) end)() )X"); - mLua.collect_garbage(); + mLua.sol().collect_garbage(); EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK"); } } diff --git a/apps/openmw_test_suite/lua/test_l10n.cpp b/apps/openmw_test_suite/lua/test_l10n.cpp index 67415ee947..57844dfaec 100644 --- a/apps/openmw_test_suite/lua/test_l10n.cpp +++ b/apps/openmw_test_suite/lua/test_l10n.cpp @@ -82,9 +82,9 @@ you_have_arrows: "Arrows count: {count}" TEST_F(LuaL10nTest, L10n) { - internal::CaptureStdout(); LuaUtil::LuaState lua{ mVFS.get(), &mCfg }; sol::state& l = lua.sol(); + internal::CaptureStdout(); l10n::Manager l10nManager(mVFS.get()); l10nManager.setPreferredLocales({ "de", "en" }); EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n"); diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 92d28f72fc..3a43db9a67 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -448,8 +448,7 @@ CUSTOM, PLAYER: useInterface.lua { LuaUtil::Callback callback{ mLua.sol()["print"], mLua.newTable() }; callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua"; - callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] - = LuaUtil::ScriptsContainer::ScriptId{ nullptr, 0 }; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{ nullptr, 0 }; testing::internal::CaptureStdout(); callback.call(1.5); diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp index a340e0c930..8766b1b975 100644 --- a/apps/openmw_test_suite/lua/test_storage.cpp +++ b/apps/openmw_test_suite/lua/test_storage.cpp @@ -17,7 +17,9 @@ namespace TEST(LuaUtilStorageTest, Basic) { - sol::state mLua; + // Note: LuaUtil::Callback can be used only if Lua is initialized via LuaUtil::LuaState + LuaUtil::LuaState luaState{ nullptr, nullptr }; + sol::state& mLua = luaState.sol(); LuaUtil::LuaStorage::initLuaBindings(mLua); LuaUtil::LuaStorage storage(mLua); @@ -30,7 +32,7 @@ namespace callbackCalls.push_back(section + "_*"); }), sol::table(mLua, sol::create) }; - callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = "fakeId"; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; mLua["mutable"] = storage.getMutableSection("test"); mLua["ro"] = storage.getReadOnlySection("test"); diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index bada1868fc..d11ac116ce 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -10,6 +10,8 @@ #include #include +#include "scriptscontainer.hpp" + namespace LuaUtil { @@ -50,10 +52,98 @@ namespace LuaUtil "tonumber", "tostring", "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable" }; static const std::string safePackages[] = { "coroutine", "math", "string", "table" }; - LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) - : mConf(conf) + static constexpr int64_t countHookStep = 2000; + + void LuaState::countHook(lua_State* L, lua_Debug* ar) + { + LuaState* THIS; + (void)lua_getallocf(L, reinterpret_cast(&THIS)); + if (!THIS->mActiveScriptId.mContainer) + return; + THIS->mActiveScriptId.mContainer->addCPUusage(THIS->mActiveScriptId.mIndex, countHookStep); + THIS->mCurrentCallInstructionCounter += countHookStep; + if (THIS->mSettings.mInstructionLimit > 0 + && THIS->mCurrentCallInstructionCounter > THIS->mSettings.mInstructionLimit) + { + lua_pushstring(L, + "Lua CPU usage exceeded, probably an infinite loop in a script. " + "To change the limit set \"[Lua] instruction limit per call\" in settings.cfg"); + lua_error(L); + } + } + + void* LuaState::trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize) + { + LuaState* THIS = static_cast(ud); + const uint64_t smallAllocSize = THIS->mSettings.mSmallAllocMaxSize; + const uint64_t memoryLimit = THIS->mSettings.mMemoryLimit; + + if (!ptr) + osize = 0; + int64_t smallAllocDelta = 0, bigAllocDelta = 0; + if (osize <= smallAllocSize) + smallAllocDelta -= osize; + else + bigAllocDelta -= osize; + if (nsize <= smallAllocSize) + smallAllocDelta += nsize; + else + bigAllocDelta += nsize; + + if (bigAllocDelta > 0 && memoryLimit > 0 && THIS->mTotalMemoryUsage + nsize - osize > memoryLimit) + { + Log(Debug::Error) << "Lua realloc " << osize << "->" << nsize + << " is blocked because Lua memory limit (configurable in settings.cfg) is exceeded"; + return nullptr; + } + THIS->mTotalMemoryUsage += smallAllocDelta + bigAllocDelta; + THIS->mSmallAllocMemoryUsage += smallAllocDelta; + + void* newPtr = nullptr; + if (nsize == 0) + free(ptr); + else + newPtr = realloc(ptr, nsize); + + if (bigAllocDelta != 0) + { + auto it = osize > smallAllocSize ? THIS->mBigAllocOwners.find(ptr) : THIS->mBigAllocOwners.end(); + ScriptId id; + if (it != THIS->mBigAllocOwners.end()) + { + if (it->second.mContainer) + id = ScriptId{ *it->second.mContainer, it->second.mScriptIndex }; + if (ptr != newPtr || nsize <= smallAllocSize) + THIS->mBigAllocOwners.erase(it); + } + else if (bigAllocDelta > 0) + { + id = THIS->mActiveScriptId; + bigAllocDelta = nsize; + } + if (id.mContainer) + { + if (static_cast(id.mIndex) >= THIS->mMemoryUsage.size()) + THIS->mMemoryUsage.resize(id.mIndex + 1); + THIS->mMemoryUsage[id.mIndex] += bigAllocDelta; + id.mContainer->addMemoryUsage(id.mIndex, bigAllocDelta); + if (newPtr && nsize > smallAllocSize) + THIS->mBigAllocOwners.emplace(newPtr, AllocOwner{ id.mContainer->mThis, id.mIndex }); + } + } + + return newPtr; + } + + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf, const LuaStateSettings& settings) + : mSettings(settings) + , mLua(sol::default_at_panic, &trackingAllocator, this) + , mConf(conf) , mVFS(vfs) { + lua_sethook(mLua.lua_state(), &countHook, LUA_MASKCOUNT, countHookStep); + Log(Debug::Verbose) << "Initializing LuaUtil::LuaState"; + mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::bit32, sol::lib::string, sol::lib::table, sol::lib::os, sol::lib::debug); @@ -196,9 +286,15 @@ namespace LuaUtil env["_G"] = env; env[sol::metatable_key]["__metatable"] = false; - auto maybeRunLoader = [&hiddenData](const sol::object& package) -> sol::object { + ScriptId scriptId; + if (hiddenData.is()) + scriptId = hiddenData.as() + .get>(ScriptsContainer::sScriptIdKey) + .value_or(ScriptId{}); + + auto maybeRunLoader = [&hiddenData, scriptId](const sol::object& package) -> sol::object { if (package.is()) - return call(package.as(), hiddenData); + return call(scriptId, package.as(), hiddenData); else return package; }; @@ -207,19 +303,19 @@ namespace LuaUtil loaded[key] = maybeRunLoader(value); for (const auto& [key, value] : packages) loaded[key] = maybeRunLoader(value); - env["require"] = [this, env, loaded, hiddenData](std::string_view packageName) mutable { + env["require"] = [this, env, loaded, hiddenData, scriptId](std::string_view packageName) mutable { sol::object package = loaded[packageName]; if (package != sol::nil) return package; sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS)); sol::set_environment(env, packageLoader); - package = call(packageLoader, packageName); + package = call(scriptId, packageLoader, packageName); loaded[packageName] = package; return package; }; sol::set_environment(env, script); - return call(script); + return call(scriptId, script); } sol::environment LuaState::newInternalLibEnvironment() @@ -233,7 +329,7 @@ namespace LuaUtil return loaded[module]; sol::protected_function initializer = loadInternalLib(module); sol::set_environment(env, initializer); - loaded[module] = call(initializer, module); + loaded[module] = call({}, initializer, module); return loaded[module]; }; return env; diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 7baf3f4839..bda9ad38a4 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -19,6 +19,21 @@ namespace LuaUtil std::string getLuaVersion(); + class ScriptsContainer; + struct ScriptId + { + ScriptsContainer* mContainer = nullptr; + int mIndex; // index in LuaUtil::ScriptsConfiguration + }; + + struct LuaStateSettings + { + uint64_t mInstructionLimit = 0; // 0 is unlimited + uint64_t mMemoryLimit = 0; // 0 is unlimited + uint64_t mSmallAllocMaxSize = 1024 * 1024; // big default value efficiently disables memory tracking + bool mLogMemoryUsage = false; + }; + // Holds Lua state. // Provides additional features: // - Load scripts from the virtual filesystem; @@ -34,7 +49,11 @@ namespace LuaUtil class LuaState { public: - explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf); + explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf, + const LuaStateSettings& settings = LuaStateSettings{}); + LuaState(const LuaState&) = delete; + LuaState(LuaState&&) = delete; + ~LuaState(); // Returns underlying sol::state. @@ -86,12 +105,42 @@ namespace LuaUtil sol::function loadFromVFS(const std::string& path); sol::environment newInternalLibEnvironment(); + uint64_t getTotalMemoryUsage() const { return mTotalMemoryUsage; } + uint64_t getSmallAllocMemoryUsage() const { return mSmallAllocMemoryUsage; } + uint64_t getMemoryUsageByScriptIndex(unsigned id) const + { + return id < mMemoryUsage.size() ? mMemoryUsage[id] : 0; + } + + const LuaStateSettings& getSettings() const { return mSettings; } + private: static sol::protected_function_result throwIfError(sol::protected_function_result&&); template friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args); + template + friend sol::protected_function_result call( + ScriptId scriptId, const sol::protected_function& fn, Args&&... args); sol::function loadScriptAndCache(const std::string& path); + static void countHook(lua_State* L, lua_Debug* ar); + static void* trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize); + + struct AllocOwner + { + std::shared_ptr mContainer; + int mScriptIndex; + }; + + const LuaStateSettings mSettings; + + // Needed to track resource usage per script, must be initialized before mLua. + ScriptId mActiveScriptId; + uint64_t mCurrentCallInstructionCounter = 0; + std::map mBigAllocOwners; + uint64_t mTotalMemoryUsage = 0; + uint64_t mSmallAllocMemoryUsage = 0; + std::vector mMemoryUsage; sol::state mLua; const ScriptsConfiguration* mConf; @@ -102,21 +151,52 @@ namespace LuaUtil std::vector mLibSearchPaths; }; - // Should be used for every call of every Lua function. - // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 + // LuaUtil::call should be used for every call of every Lua function. + // 1) It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 + // 2) When called with ScriptId it tracks resource usage (scriptId refers to the script that is responsible for this + // call). + template sol::protected_function_result call(const sol::protected_function& fn, Args&&... args) { try { - return LuaState::throwIfError(fn(std::forward(args)...)); + auto res = LuaState::throwIfError(fn(std::forward(args)...)); + return res; + } + catch (std::exception&) + { + throw; + } + catch (...) + { + throw std::runtime_error("Unknown error"); + } + } + + // Lua must be initialized through LuaUtil::LuaState, otherwise this function will segfault. + template + sol::protected_function_result call(ScriptId scriptId, const sol::protected_function& fn, Args&&... args) + { + LuaState* luaState; + (void)lua_getallocf(fn.lua_state(), reinterpret_cast(&luaState)); + assert(luaState->mActiveScriptId.mContainer == nullptr && "recursive call of LuaUtil::call(scriptId, ...) ?"); + luaState->mActiveScriptId = scriptId; + luaState->mCurrentCallInstructionCounter = 0; + try + { + auto res = LuaState::throwIfError(fn(std::forward(args)...)); + luaState->mActiveScriptId = {}; + return res; } catch (std::exception&) { + luaState->mActiveScriptId = {}; throw; } catch (...) { + luaState->mActiveScriptId = {}; throw std::runtime_error("Unknown error"); } } diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 057fcd66f9..40a1919fbe 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -18,6 +18,7 @@ namespace LuaUtil ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix) , mLua(*lua) + , mThis(std::make_shared(this)) { registerEngineHandlers({ &mUpdateHandlers }); mPublicInterfaces = sol::table(lua->sol(), sol::create); @@ -74,6 +75,16 @@ namespace LuaUtil script.mHiddenData[sScriptIdKey] = ScriptId{ this, scriptId }; script.mHiddenData[sScriptDebugNameKey] = debugName; script.mPath = path; + script.mStats.mCPUusage = 0; + + const auto oldMemoryUsageIt = mRemovedScriptsMemoryUsage.find(scriptId); + if (oldMemoryUsageIt != mRemovedScriptsMemoryUsage.end()) + { + script.mStats.mMemoryUsage = oldMemoryUsageIt->second; + mRemovedScriptsMemoryUsage.erase(oldMemoryUsageIt); + } + else + script.mStats.mMemoryUsage = 0; try { @@ -146,8 +157,10 @@ namespace LuaUtil } catch (std::exception& e) { - mScripts[scriptId].mHiddenData[sScriptIdKey] = sol::nil; - mScripts.erase(scriptId); + auto iter = mScripts.find(scriptId); + iter->second.mHiddenData[sScriptIdKey] = sol::nil; + mRemovedScriptsMemoryUsage[scriptId] = iter->second.mStats.mMemoryUsage; + mScripts.erase(iter); Log(Debug::Error) << "Can't start " << debugName << "; " << e.what(); return false; } @@ -162,6 +175,7 @@ namespace LuaUtil if (script.mInterface) removeInterface(scriptId, script); script.mHiddenData[sScriptIdKey] = sol::nil; + mRemovedScriptsMemoryUsage[scriptId] = script.mStats.mMemoryUsage; mScripts.erase(scriptIter); for (auto& [_, handlers] : mEngineHandlers) removeHandler(handlers->mList, scriptId); @@ -192,7 +206,7 @@ namespace LuaUtil { try { - LuaUtil::call(*script.mOnOverride, *prev->mInterface); + LuaUtil::call({ this, scriptId }, *script.mOnOverride, *prev->mInterface); } catch (std::exception& e) { @@ -203,7 +217,7 @@ namespace LuaUtil { try { - LuaUtil::call(*next->mOnOverride, *script.mInterface); + LuaUtil::call({ this, nextId }, *next->mOnOverride, *script.mInterface); } catch (std::exception& e) { @@ -242,7 +256,7 @@ namespace LuaUtil prevInterface = *prev->mInterface; try { - LuaUtil::call(*next->mOnOverride, prevInterface); + LuaUtil::call({ this, nextId }, *next->mOnOverride, prevInterface); } catch (std::exception& e) { @@ -298,16 +312,17 @@ namespace LuaUtil EventHandlerList& list = it->second; for (int i = list.size() - 1; i >= 0; --i) { + const Handler& h = list[i]; try { - sol::object res = LuaUtil::call(list[i].mFn, data); + sol::object res = LuaUtil::call({ this, h.mScriptId }, h.mFn, data); if (res != sol::nil && !res.as()) break; // Skip other handlers if 'false' was returned. } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) << "] eventHandler[" - << eventName << "] failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(h.mScriptId) << "] eventHandler[" << eventName + << "] failed. " << e.what(); } } } @@ -322,7 +337,7 @@ namespace LuaUtil { try { - LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer)); + LuaUtil::call({ this, scriptId }, onInit, deserialize(mLua.sol(), data, mSerializer)); } catch (std::exception& e) { @@ -358,7 +373,7 @@ namespace LuaUtil { try { - sol::object state = LuaUtil::call(*script.mOnSave); + sol::object state = LuaUtil::call({ this, scriptId }, *script.mOnSave); savedScript.mData = serialize(state, mSerializer); } catch (std::exception& e) @@ -421,7 +436,7 @@ namespace LuaUtil { sol::object state = deserialize(mLua.sol(), scriptInfo.mSavedData->mData, mSavedDataDeserializer); sol::object initializationData = deserialize(mLua.sol(), scriptInfo.mInitData, mSerializer); - LuaUtil::call(*onLoad, state, initializationData); + LuaUtil::call({ this, scriptId }, *onLoad, state, initializationData); } catch (std::exception& e) { @@ -464,14 +479,18 @@ namespace LuaUtil { for (auto& [_, script] : mScripts) script.mHiddenData[sScriptIdKey] = sol::nil; + *mThis = nullptr; } // Note: shouldn't be called from destructor because mEngineHandlers has pointers on // external objects that are already removed during child class destruction. void ScriptsContainer::removeAllScripts() { - for (auto& [_, script] : mScripts) + for (auto& [id, script] : mScripts) + { script.mHiddenData[sScriptIdKey] = sol::nil; + mRemovedScriptsMemoryUsage[id] = script.mStats.mMemoryUsage; + } mScripts.clear(); for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); @@ -540,12 +559,12 @@ namespace LuaUtil auto it = script.mRegisteredCallbacks.find(callbackName); if (it == script.mRegisteredCallbacks.end()) throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); - LuaUtil::call(it->second, t.mArg); + LuaUtil::call({ this, t.mScriptId }, it->second, t.mArg); } else { int64_t id = std::get(t.mCallback); - LuaUtil::call(script.mTemporaryCallbacks.at(id)); + LuaUtil::call({ this, t.mScriptId }, script.mTemporaryCallbacks.at(id)); script.mTemporaryCallbacks.erase(id); } } @@ -571,4 +590,60 @@ namespace LuaUtil updateTimerQueue(mGameTimersQueue, gameTime); } + static constexpr float CPUusageAvgCoef = 1.0 / 30; // averaging over approximately 30 frames + + void ScriptsContainer::CPUusageNextFrame() + { + for (auto& [scriptId, script] : mScripts) + { + // The averaging formula is: averageValue = averageValue * (1-c) + newValue * c + script.mStats.mCPUusage *= 1 - CPUusageAvgCoef; + if (script.mStats.mCPUusage < 5) + script.mStats.mCPUusage = 0; // speeding up converge to zero if newValue is zero + } + } + + void ScriptsContainer::addCPUusage(int scriptId, int64_t CPUusage) + { + auto it = mScripts.find(scriptId); + if (it != mScripts.end()) + it->second.mStats.mCPUusage += CPUusage * CPUusageAvgCoef; + } + + void ScriptsContainer::addMemoryUsage(int scriptId, int64_t memoryDelta) + { + int64_t* usage; + auto it = mScripts.find(scriptId); + if (it != mScripts.end()) + usage = &it->second.mStats.mMemoryUsage; + else + { + auto [rIt, _] = mRemovedScriptsMemoryUsage.emplace(scriptId, 0); + usage = &rIt->second; + } + *usage += memoryDelta; + + if (mLua.getSettings().mLogMemoryUsage) + { + int64_t after = *usage; + int64_t before = after - memoryDelta; + // Logging only if one of the most significant bits of used memory size was changed. + // Otherwise it is too verbose. + if ((before ^ after) * 8 > after) + Log(Debug::Verbose) << mNamePrefix << "[" << scriptPath(scriptId) << "] memory usage " << before + << " -> " << after; + } + } + + void ScriptsContainer::collectStats(std::vector& stats) const + { + stats.resize(mLua.getConfiguration().size()); + for (auto& [id, script] : mScripts) + { + stats[id].mCPUusage += script.mStats.mCPUusage; + stats[id].mMemoryUsage += script.mStats.mMemoryUsage; + } + for (auto& [id, mem] : mRemovedScriptsMemoryUsage) + stats[id].mMemoryUsage += mem; + } } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 5fdbb5c1ef..6b9df70ac3 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -69,11 +69,6 @@ namespace LuaUtil // Present in mHiddenData even after removal of the script from ScriptsContainer. constexpr static std::string_view sScriptDebugNameKey = "_name"; - struct ScriptId - { - ScriptsContainer* mContainer; - int mIndex; // index in LuaUtil::ScriptsConfiguration - }; using TimerType = ESM::LuaTimer::Type; // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` @@ -151,6 +146,16 @@ namespace LuaUtil // because they can not be stored in saves. I.e. loading a saved game will not fully restore the state. void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::main_protected_function callback); + // Informs that new frame is started. Needed to track CPU usage per frame. + void CPUusageNextFrame(); + + struct ScriptStats + { + float mCPUusage = 0; // averaged number of Lua instructions per frame + int64_t mMemoryUsage = 0; // bytes + }; + void collectStats(std::vector& stats) const; + protected: struct Handler { @@ -178,7 +183,7 @@ namespace LuaUtil { try { - LuaUtil::call(handler.mFn, args...); + LuaUtil::call({ this, handler.mScriptId }, handler.mFn, args...); } catch (std::exception& e) { @@ -206,6 +211,7 @@ namespace LuaUtil std::map mRegisteredCallbacks; std::map mTemporaryCallbacks; std::string mPath; + ScriptStats mStats; }; struct Timer { @@ -220,6 +226,10 @@ namespace LuaUtil }; using EventHandlerList = std::vector; + friend class LuaState; + void addCPUusage(int scriptId, int64_t CPUusage); + void addMemoryUsage(int scriptId, int64_t memoryDelta); + // Add to container without calling onInit/onLoad. bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); @@ -252,6 +262,9 @@ namespace LuaUtil std::vector mSimulationTimersQueue; std::vector mGameTimersQueue; int64_t mTemporaryCallbackCounter = 0; + + std::map mRemovedScriptsMemoryUsage; + std::shared_ptr mThis; // used by LuaState to track ownership of memory allocations }; // Wrapper for a Lua function. @@ -267,8 +280,9 @@ namespace LuaUtil template sol::object call(Args&&... args) const { - if (isValid()) - return LuaUtil::call(mFunc, std::forward(args)...); + sol::optional scriptId = mHiddenData[ScriptsContainer::sScriptIdKey]; + if (scriptId.has_value()) + return LuaUtil::call(scriptId.value(), mFunc, std::forward(args)...); else Log(Debug::Debug) << "Ignored callback to the removed script " << mHiddenData.get(ScriptsContainer::sScriptDebugNameKey); From 02a9069a0ed6461ba32f547d508e0e396ecb9a9c Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sat, 26 Nov 2022 21:25:18 +0100 Subject: [PATCH 2/5] Lua Profiler --- apps/openmw/mwbase/luamanager.hpp | 2 + apps/openmw/mwbase/windowmanager.hpp | 1 + apps/openmw/mwgui/console.hpp | 1 + apps/openmw/mwgui/debugwindow.cpp | 36 ++++++- apps/openmw/mwgui/debugwindow.hpp | 2 + apps/openmw/mwgui/windowmanagerimp.cpp | 5 + apps/openmw/mwgui/windowmanagerimp.hpp | 1 + apps/openmw/mwlua/luamanagerimp.cpp | 97 ++++++++++++++++++- apps/openmw/mwlua/luamanagerimp.hpp | 3 +- .../source/reference/modding/settings/lua.rst | 61 ++++++++++++ files/data/l10n/DebugMenu/de.yaml | 1 + files/data/l10n/DebugMenu/en.yaml | 1 + files/data/l10n/DebugMenu/ru.yaml | 1 + files/settings-default.cfg | 13 +++ 14 files changed, 216 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 12abd91909..6885e93c19 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -100,6 +100,8 @@ namespace MWBase virtual void handleConsoleCommand( const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) = 0; + + virtual std::string formatResourceUsageStats() const = 0; }; } diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 77d8de6c48..8b60de6919 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -159,6 +159,7 @@ namespace MWBase virtual void updateSpellWindow() = 0; virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; + virtual MWWorld::Ptr getConsoleSelectedObject() const = 0; virtual void setConsoleMode(const std::string& mode) = 0; static constexpr std::string_view sConsoleColor_Default = "#FFFFFF"; diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 97c4c3e749..306c7f63f1 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -23,6 +23,7 @@ namespace MWGui public: /// Set the implicit object for script execution void setSelectedObject(const MWWorld::Ptr& object); + MWWorld::Ptr getSelectedObject() const { return mPtr; } MyGUI::EditBox* mCommandLine; MyGUI::EditBox* mHistory; diff --git a/apps/openmw/mwgui/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp index 24d161f6bd..fe2fed02bb 100644 --- a/apps/openmw/mwgui/debugwindow.cpp +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -8,6 +8,9 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" + #include #ifndef BT_NO_PROFILE @@ -106,6 +109,12 @@ namespace MWGui = itemLV->createWidgetReal("LogEdit", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Stretch); mLogView->setEditReadOnly(true); + MyGUI::TabItem* itemLuaProfiler = mTabControl->addItem("Lua Profiler"); + itemLuaProfiler->setCaptionWithReplacing(" #{DebugMenu:LuaProfiler} "); + mLuaProfiler = itemLuaProfiler->createWidgetReal( + "LogEdit", MyGUI::FloatCoord(0, 0, 1, 1), MyGUI::Align::Stretch); + mLuaProfiler->setEditReadOnly(true); + #ifndef BT_NO_PROFILE MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); item->setCaptionWithReplacing(" #{DebugMenu:PhysicsProfiler} "); @@ -206,6 +215,16 @@ namespace MWGui mLogView->setVScrollPosition(scrollPos); } + void DebugWindow::updateLuaProfile() + { + if (mLuaProfiler->isTextSelection()) + return; + + size_t previousPos = mLuaProfiler->getVScrollPosition(); + mLuaProfiler->setCaption(MWBase::Environment::get().getLuaManager()->formatResourceUsageStats()); + mLuaProfiler->setVScrollPosition(std::min(previousPos, mLuaProfiler->getVScrollRange() - 1)); + } + void DebugWindow::updateBulletProfile() { #ifndef BT_NO_PROFILE @@ -229,9 +248,18 @@ namespace MWGui return; timer = 0.25; - if (mTabControl->getIndexSelected() == 0) - updateLogView(); - else - updateBulletProfile(); + switch (mTabControl->getIndexSelected()) + { + case 0: + updateLogView(); + break; + case 1: + updateLuaProfile(); + break; + case 2: + updateBulletProfile(); + break; + default:; + } } } diff --git a/apps/openmw/mwgui/debugwindow.hpp b/apps/openmw/mwgui/debugwindow.hpp index 9b8711137a..7e65353c12 100644 --- a/apps/openmw/mwgui/debugwindow.hpp +++ b/apps/openmw/mwgui/debugwindow.hpp @@ -17,10 +17,12 @@ namespace MWGui private: void updateLogView(); + void updateLuaProfile(); void updateBulletProfile(); MyGUI::TabControl* mTabControl; MyGUI::EditBox* mLogView; + MyGUI::EditBox* mLuaProfiler; MyGUI::EditBox* mBulletProfilerEdit; }; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index fd46d2fff7..8b49417e7e 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -2158,6 +2158,11 @@ namespace MWGui mConsole->setSelectedObject(object); } + MWWorld::Ptr WindowManager::getConsoleSelectedObject() const + { + return mConsole->getSelectedObject(); + } + void WindowManager::printToConsole(const std::string& msg, std::string_view color) { mConsole->print(msg, color); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 773f670d1e..6ffc617675 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -188,6 +188,7 @@ namespace MWGui void updateSpellWindow() override; void setConsoleSelectedObject(const MWWorld::Ptr& object) override; + MWWorld::Ptr getConsoleSelectedObject() const override; void printToConsole(const std::string& msg, std::string_view color) override; void setConsoleMode(const std::string& mode) override; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index e3b073f670..d36bc9f92f 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -37,8 +37,16 @@ namespace MWLua { + static LuaUtil::LuaStateSettings createLuaStateSettings() + { + return { .mInstructionLimit = Settings::Manager::getUInt64("instruction limit per call", "Lua"), + .mMemoryLimit = Settings::Manager::getUInt64("memory limit", "Lua"), + .mSmallAllocMaxSize = Settings::Manager::getUInt64("small alloc max size", "Lua"), + .mLogMemoryUsage = Settings::Manager::getBool("log memory usage", "Lua") }; + } + LuaManager::LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir) - : mLua(vfs, &mConfiguration) + : mLua(vfs, &mConfiguration, createLuaStateSettings()) , mUiResourceManager(vfs) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); @@ -605,9 +613,90 @@ namespace MWLua mActionQueue.push_back(std::make_unique(&mLua, std::move(action), name)); } - void LuaManager::reportStats(unsigned int frameNumber, osg::Stats& stats) + void LuaManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const { - const sol::state_view state(mLua.sol()); - stats.setAttribute(frameNumber, "Lua UsedMemory", state.memory_used()); + stats.setAttribute(frameNumber, "Lua UsedMemory", mLua.getTotalMemoryUsage()); + } + + std::string LuaManager::formatResourceUsageStats() const + { + std::stringstream out; + + static const uint64_t smallAllocSize = Settings::Manager::getUInt64("small alloc max size", "Lua"); + out << "Total memory usage: " << mLua.getTotalMemoryUsage() << "\n"; + out << "small alloc max size = " << smallAllocSize << " (section [Lua] in settings.cfg)\n"; + out << "Smaller values give more information for the profiler, but increase performance overhead.\n"; + out << " Memory allocations <= " << smallAllocSize << " bytes: " << mLua.getSmallAllocMemoryUsage() + << " (not tracked)\n"; + out << " Memory allocations > " << smallAllocSize + << " bytes: " << mLua.getTotalMemoryUsage() - mLua.getSmallAllocMemoryUsage() << " (see the table below)\n"; + out << "\n"; + + using Stats = LuaUtil::ScriptsContainer::ScriptStats; + + std::vector activeStats; + mGlobalScripts.collectStats(activeStats); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->collectStats(activeStats); + + std::vector selectedStats; + MWWorld::Ptr selectedPtr = MWBase::Environment::get().getWindowManager()->getConsoleSelectedObject(); + LocalScripts* selectedScripts = nullptr; + if (!selectedPtr.isEmpty()) + { + selectedScripts = selectedPtr.getRefData().getLuaScripts(); + if (selectedScripts) + selectedScripts->collectStats(selectedStats); + out << "Profiled object (selected in the in-game console): " << ptrToString(selectedPtr) << "\n"; + } + else + out << "No selected object. Use the in-game console to select an object for detailed profile.\n"; + out << "\n"; + + constexpr int nameW = 50; + constexpr int valueW = 12; + + out << std::left; + out << " " << std::setw(nameW + 2) << "*** Resource usage per script"; + out << std::right; + out << std::setw(valueW) << "CPU"; + out << std::setw(valueW) << "memory"; + out << std::setw(valueW) << "memory"; + out << std::setw(valueW) << "CPU"; + out << std::setw(valueW) << "memory"; + out << "\n"; + out << std::left << " " << std::setw(nameW + 2) << "[name]" << std::right; + out << std::setw(valueW) << "[all]"; + out << std::setw(valueW) << "[active]"; + out << std::setw(valueW) << "[inactive]"; + out << std::setw(valueW * 2) << "[for selected object]"; + out << "\n"; + + for (size_t i = 0; i < mConfiguration.size(); ++i) + { + bool isGlobal = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sGlobal; + + out << std::left; + out << " " << std::setw(nameW) << mConfiguration[i].mScriptPath; + if (mConfiguration[i].mScriptPath.size() > nameW) + out << "\n " << std::setw(nameW) << ""; // if path is too long, break line + out << std::right; + out << std::setw(valueW) << static_cast(activeStats[i].mCPUusage); + out << std::setw(valueW) << activeStats[i].mMemoryUsage; + out << std::setw(valueW) << mLua.getMemoryUsageByScriptIndex(i) - activeStats[i].mMemoryUsage; + + if (isGlobal) + out << std::setw(valueW * 2) << "NA (global script)"; + else if (selectedPtr.isEmpty()) + out << std::setw(valueW * 2) << "NA (not selected) "; + else if (!selectedScripts || !selectedScripts->hasScript(i)) + out << std::setw(valueW) << "-" << std::setw(valueW) << selectedStats[i].mMemoryUsage; + else + out << std::setw(valueW) << static_cast(selectedStats[i].mCPUusage) << std::setw(valueW) + << selectedStats[i].mMemoryUsage; + out << "\n"; + } + + return out.str(); } } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index c599c95a53..a4037593da 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -122,7 +122,8 @@ namespace MWLua bool isProcessingInputEvents() const { return mProcessingInputEvents; } - void reportStats(unsigned int frameNumber, osg::Stats& stats); + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; + std::string formatResourceUsageStats() const override; private: void initConfiguration(); diff --git a/docs/source/reference/modding/settings/lua.rst b/docs/source/reference/modding/settings/lua.rst index 4433067952..b0fd88fe30 100644 --- a/docs/source/reference/modding/settings/lua.rst +++ b/docs/source/reference/modding/settings/lua.rst @@ -26,3 +26,64 @@ If one, a separate thread is used. Values >1 are not yet supported. This setting can only be configured by editing the settings configuration file. + +lua profiler +------------ + +:Type: boolean +:Range: True/False +:Default: True + +Enables Lua profiler. + +This setting can only be configured by editing the settings configuration file. + +small alloc max size +-------------------- + +:Type: integer +:Range: >= 0 +:Default: 1024 + +No ownership tracking for memory allocations below or equal this size (in bytes). +This setting is used only if ``lua profiler = true``. +With the default value (1024) the lua profiler will show almost no memory usage because allocation more than 1KB are rare. +Decrease the value of this setting (e.g. set it to 64) to have better memory tracking by the cost of higher overhead. + +This setting can only be configured by editing the settings configuration file. + +memory limit +------------ + +:Type: integer +:Range: > 0 +:Default: 2147483648 (2GB) + +Memory limit for Lua runtime (only if ``lua profiler = true``). If exceeded then only small allocations are allowed. +Small allocations are always allowed, so e.g. Lua console can function. + +This setting can only be configured by editing the settings configuration file. + +log memory usage +---------------- + +:Type: boolean +:Range: True/False +:Default: False + +Print debug info about memory usage (only if ``lua profiler = true``). + +This setting can only be configured by editing the settings configuration file. + +instruction limit per call +-------------------------- + +:Type: integer +:Range: > 1000 +:Default: 100000000 + +The maximal number of Lua instructions per function call (only if ``lua profiler = true``). +If exceeded (e.g. because of an infinite loop) the function will be terminated. + +This setting can only be configured by editing the settings configuration file. + diff --git a/files/data/l10n/DebugMenu/de.yaml b/files/data/l10n/DebugMenu/de.yaml index dd796db8cc..2ff4abd89f 100644 --- a/files/data/l10n/DebugMenu/de.yaml +++ b/files/data/l10n/DebugMenu/de.yaml @@ -1,3 +1,4 @@ DebugWindow: "Debug" LogViewer: "Protokollansicht" +LuaProfiler: "Lua-Profiler" PhysicsProfiler: "Physik-Profiler" diff --git a/files/data/l10n/DebugMenu/en.yaml b/files/data/l10n/DebugMenu/en.yaml index af3b31aacd..6f76d147c6 100644 --- a/files/data/l10n/DebugMenu/en.yaml +++ b/files/data/l10n/DebugMenu/en.yaml @@ -1,3 +1,4 @@ DebugWindow: "Debug" LogViewer: "Log Viewer" +LuaProfiler: "Lua Profiler" PhysicsProfiler: "Physics Profiler" diff --git a/files/data/l10n/DebugMenu/ru.yaml b/files/data/l10n/DebugMenu/ru.yaml index afa8bdb904..0cd7131705 100644 --- a/files/data/l10n/DebugMenu/ru.yaml +++ b/files/data/l10n/DebugMenu/ru.yaml @@ -1,3 +1,4 @@ DebugWindow: "Меню отладки" LogViewer: "Журнал логов" +LuaProfiler: "Профилировщик Луа" PhysicsProfiler: "Профилировщик физики" diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 3fc46ca981..0cefc3047f 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1141,6 +1141,19 @@ lua debug = false # If zero, Lua scripts are processed in the main thread. lua num threads = 1 +# No ownership tracking for allocations below or equal this size. +small alloc max size = 1024 + +# Memory limit for Lua runtime. If exceeded then only small allocations are allowed. Small allocations are always allowed, so e.g. Lua console can function. +# Default value is 2GB. +memory limit = 2147483648 + +# Print debug info about memory usage. +log memory usage = false + +# The maximal number of Lua instructions per function call. If exceeded (e.g. because of an infinite loop) the function will be terminated. +instruction limit per call = 100000000 + [Stereo] # Enable/disable stereo view. This setting is ignored in VR. stereo enabled = false From 55db95d4cfe5cedb1951fa09ea3c9bc2f0458974 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 29 Nov 2022 18:16:06 +0100 Subject: [PATCH 3/5] Update Lua profiler; add ability to run OpenMW with old LuaJit that doesn't allow custom allocator (Lua profiler will be disabled in this case) --- apps/openmw/mwlua/eventqueue.cpp | 5 +- apps/openmw/mwlua/eventqueue.hpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 71 +++++++--- apps/openmw/mwlua/objectbindings.cpp | 2 +- apps/openmw_test_suite/lua/test_l10n.cpp | 5 +- apps/openmw_test_suite/lua/test_storage.cpp | 4 +- components/lua/l10n.cpp | 3 +- components/lua/l10n.hpp | 2 +- components/lua/luastate.cpp | 144 +++++++++++--------- components/lua/luastate.hpp | 69 +++++++--- components/lua/scriptscontainer.cpp | 18 +-- components/lua/scriptscontainer.hpp | 8 +- components/lua/utilpackage.cpp | 3 +- components/lua/utilpackage.hpp | 3 +- files/settings-default.cfg | 12 +- 15 files changed, 221 insertions(+), 130 deletions(-) diff --git a/apps/openmw/mwlua/eventqueue.cpp b/apps/openmw/mwlua/eventqueue.cpp index cb952e4e3c..8c84d234d3 100644 --- a/apps/openmw/mwlua/eventqueue.cpp +++ b/apps/openmw/mwlua/eventqueue.cpp @@ -20,8 +20,9 @@ namespace MWLua saveLuaBinaryData(esm, event.mEventData); } - void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue& globalEvents, LocalEventQueue& localEvents, - const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer) + void loadEvents(sol::state_view& lua, ESM::ESMReader& esm, GlobalEventQueue& globalEvents, + LocalEventQueue& localEvents, const std::map& contentFileMapping, + const LuaUtil::UserdataSerializer* serializer) { while (esm.isNextSub("LUAE")) { diff --git a/apps/openmw/mwlua/eventqueue.hpp b/apps/openmw/mwlua/eventqueue.hpp index 273a4c2ad7..8854aa12a5 100644 --- a/apps/openmw/mwlua/eventqueue.hpp +++ b/apps/openmw/mwlua/eventqueue.hpp @@ -35,7 +35,7 @@ namespace MWLua using GlobalEventQueue = std::vector; using LocalEventQueue = std::vector; - void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&, + void loadEvents(sol::state_view& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&, const std::map& contentFileMapping, const LuaUtil::UserdataSerializer* serializer); void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue&, const LocalEventQueue&); } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index d36bc9f92f..220655fbb0 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -39,6 +39,8 @@ namespace MWLua static LuaUtil::LuaStateSettings createLuaStateSettings() { + if (!Settings::Manager::getBool("lua profiler", "Lua")) + LuaUtil::LuaState::disableProfiler(); return { .mInstructionLimit = Settings::Manager::getUInt64("instruction limit per call", "Lua"), .mMemoryLimit = Settings::Manager::getUInt64("memory limit", "Lua"), .mSmallAllocMaxSize = Settings::Manager::getUInt64("small alloc max size", "Lua"), @@ -147,9 +149,9 @@ namespace MWLua mWorldView.update(); - mGlobalScripts.CPUusageNextFrame(); + mGlobalScripts.statsNextFrame(); for (LocalScripts* scripts : mActiveLocalScripts) - scripts->CPUusageNextFrame(); + scripts->statsNextFrame(); std::vector globalEvents = std::move(mGlobalEvents); std::vector localEvents = std::move(mLocalEvents); @@ -620,17 +622,39 @@ namespace MWLua std::string LuaManager::formatResourceUsageStats() const { + if (!LuaUtil::LuaState::isProfilerEnabled()) + return "Lua profiler is disabled"; + std::stringstream out; + constexpr int nameW = 50; + constexpr int valueW = 12; + + auto outMemSize = [&](int64_t bytes) { + constexpr int64_t limit = 10000; + out << std::right << std::setw(valueW - 3); + if (bytes < limit) + out << bytes << " B "; + else if (bytes < limit * 1024) + out << (bytes / 1024) << " KB"; + else if (bytes < limit * 1024 * 1024) + out << (bytes / (1024 * 1024)) << " MB"; + else + out << (bytes / (1024 * 1024 * 1024)) << " GB"; + }; + static const uint64_t smallAllocSize = Settings::Manager::getUInt64("small alloc max size", "Lua"); - out << "Total memory usage: " << mLua.getTotalMemoryUsage() << "\n"; + out << "Total memory usage:"; + outMemSize(mLua.getTotalMemoryUsage()); + out << "\n"; out << "small alloc max size = " << smallAllocSize << " (section [Lua] in settings.cfg)\n"; out << "Smaller values give more information for the profiler, but increase performance overhead.\n"; - out << " Memory allocations <= " << smallAllocSize << " bytes: " << mLua.getSmallAllocMemoryUsage() - << " (not tracked)\n"; - out << " Memory allocations > " << smallAllocSize - << " bytes: " << mLua.getTotalMemoryUsage() - mLua.getSmallAllocMemoryUsage() << " (see the table below)\n"; - out << "\n"; + out << " Memory allocations <= " << smallAllocSize << " bytes:"; + outMemSize(mLua.getSmallAllocMemoryUsage()); + out << " (not tracked)\n"; + out << " Memory allocations > " << smallAllocSize << " bytes:"; + outMemSize(mLua.getTotalMemoryUsage() - mLua.getSmallAllocMemoryUsage()); + out << " (see the table below)\n\n"; using Stats = LuaUtil::ScriptsContainer::ScriptStats; @@ -653,16 +677,22 @@ namespace MWLua out << "No selected object. Use the in-game console to select an object for detailed profile.\n"; out << "\n"; - constexpr int nameW = 50; - constexpr int valueW = 12; + out << "Legend\n"; + out << " ops: Averaged number of Lua instruction per frame;\n"; + out << " memory: Aggregated size of Lua allocations > " << smallAllocSize << " bytes;\n"; + out << " [all]: Sum over all instances of each script;\n"; + out << " [active]: Sum over all active (i.e. currently in scene) instances of each script;\n"; + out << " [inactive]: Sum over all inactive instances of each script;\n"; + out << " [for selected object]: Only for the object that is selected in the console;\n"; + out << "\n"; out << std::left; out << " " << std::setw(nameW + 2) << "*** Resource usage per script"; out << std::right; - out << std::setw(valueW) << "CPU"; + out << std::setw(valueW) << "ops"; out << std::setw(valueW) << "memory"; out << std::setw(valueW) << "memory"; - out << std::setw(valueW) << "CPU"; + out << std::setw(valueW) << "ops"; out << std::setw(valueW) << "memory"; out << "\n"; out << std::left << " " << std::setw(nameW + 2) << "[name]" << std::right; @@ -681,19 +711,24 @@ namespace MWLua if (mConfiguration[i].mScriptPath.size() > nameW) out << "\n " << std::setw(nameW) << ""; // if path is too long, break line out << std::right; - out << std::setw(valueW) << static_cast(activeStats[i].mCPUusage); - out << std::setw(valueW) << activeStats[i].mMemoryUsage; - out << std::setw(valueW) << mLua.getMemoryUsageByScriptIndex(i) - activeStats[i].mMemoryUsage; + out << std::setw(valueW) << static_cast(activeStats[i].mAvgInstructionCount); + outMemSize(activeStats[i].mMemoryUsage); + outMemSize(mLua.getMemoryUsageByScriptIndex(i) - activeStats[i].mMemoryUsage); if (isGlobal) out << std::setw(valueW * 2) << "NA (global script)"; else if (selectedPtr.isEmpty()) out << std::setw(valueW * 2) << "NA (not selected) "; else if (!selectedScripts || !selectedScripts->hasScript(i)) - out << std::setw(valueW) << "-" << std::setw(valueW) << selectedStats[i].mMemoryUsage; + { + out << std::setw(valueW) << "-"; + outMemSize(selectedStats[i].mMemoryUsage); + } else - out << std::setw(valueW) << static_cast(selectedStats[i].mCPUusage) << std::setw(valueW) - << selectedStats[i].mMemoryUsage; + { + out << std::setw(valueW) << static_cast(selectedStats[i].mAvgInstructionCount); + outMemSize(selectedStats[i].mMemoryUsage); + } out << "\n"; } diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 2b1d572575..c62c1b7b36 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -146,7 +146,7 @@ namespace MWLua void registerObjectList(const std::string& prefix, const Context& context) { using ListT = ObjectList; - sol::state& lua = context.mLua->sol(); + sol::state_view& lua = context.mLua->sol(); ObjectRegistry* registry = context.mWorldView->getObjectRegistry(); sol::usertype listT = lua.new_usertype(prefix + "ObjectList"); listT[sol::meta_function::to_string] diff --git a/apps/openmw_test_suite/lua/test_l10n.cpp b/apps/openmw_test_suite/lua/test_l10n.cpp index 57844dfaec..e5558e5010 100644 --- a/apps/openmw_test_suite/lua/test_l10n.cpp +++ b/apps/openmw_test_suite/lua/test_l10n.cpp @@ -15,7 +15,7 @@ namespace using namespace TestingOpenMW; template - T get(sol::state& lua, const std::string& luaCode) + T get(sol::state_view& lua, const std::string& luaCode) { return lua.safe_script("return " + luaCode).get(); } @@ -83,7 +83,7 @@ you_have_arrows: "Arrows count: {count}" TEST_F(LuaL10nTest, L10n) { LuaUtil::LuaState lua{ mVFS.get(), &mCfg }; - sol::state& l = lua.sol(); + sol::state_view& l = lua.sol(); internal::CaptureStdout(); l10n::Manager l10nManager(mVFS.get()); l10nManager.setPreferredLocales({ "de", "en" }); @@ -164,5 +164,4 @@ you_have_arrows: "Arrows count: {count}" l10nManager.setPreferredLocales({ "en" }); EXPECT_EQ(get(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!"); } - } diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp index 8766b1b975..270012c9b6 100644 --- a/apps/openmw_test_suite/lua/test_storage.cpp +++ b/apps/openmw_test_suite/lua/test_storage.cpp @@ -10,7 +10,7 @@ namespace using namespace testing; template - T get(sol::state& lua, std::string luaCode) + T get(sol::state_view& lua, std::string luaCode) { return lua.safe_script("return " + luaCode).get(); } @@ -19,7 +19,7 @@ namespace { // Note: LuaUtil::Callback can be used only if Lua is initialized via LuaUtil::LuaState LuaUtil::LuaState luaState{ nullptr, nullptr }; - sol::state& mLua = luaState.sol(); + sol::state_view& mLua = luaState.sol(); LuaUtil::LuaStorage::initLuaBindings(mLua); LuaUtil::LuaStorage storage(mLua); diff --git a/components/lua/l10n.cpp b/components/lua/l10n.cpp index d402cf9346..9bfad15ad8 100644 --- a/components/lua/l10n.cpp +++ b/components/lua/l10n.cpp @@ -46,8 +46,9 @@ namespace sol namespace LuaUtil { - sol::function initL10nLoader(sol::state& lua, l10n::Manager* manager) + sol::function initL10nLoader(lua_State* L, l10n::Manager* manager) { + sol::state_view lua(L); sol::usertype ctxDef = lua.new_usertype("L10nContext"); ctxDef[sol::meta_function::call] = [](const L10nContext& ctx, std::string_view key, sol::optional args) { diff --git a/components/lua/l10n.hpp b/components/lua/l10n.hpp index 68cb86050c..1fc3e17747 100644 --- a/components/lua/l10n.hpp +++ b/components/lua/l10n.hpp @@ -10,7 +10,7 @@ namespace l10n namespace LuaUtil { - sol::function initL10nLoader(sol::state& lua, l10n::Manager* manager); + sol::function initL10nLoader(lua_State*, l10n::Manager* manager); } #endif // COMPONENTS_LUA_L10N_H diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index d11ac116ce..3b4f5c9e2f 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -52,21 +52,24 @@ namespace LuaUtil "tonumber", "tostring", "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable" }; static const std::string safePackages[] = { "coroutine", "math", "string", "table" }; - static constexpr int64_t countHookStep = 2000; + static constexpr int64_t countHookStep = 1000; + + bool LuaState::sProfilerEnabled = true; void LuaState::countHook(lua_State* L, lua_Debug* ar) { - LuaState* THIS; - (void)lua_getallocf(L, reinterpret_cast(&THIS)); - if (!THIS->mActiveScriptId.mContainer) + LuaState* self; + (void)lua_getallocf(L, reinterpret_cast(&self)); + if (self->mActiveScriptIdStack.empty()) return; - THIS->mActiveScriptId.mContainer->addCPUusage(THIS->mActiveScriptId.mIndex, countHookStep); - THIS->mCurrentCallInstructionCounter += countHookStep; - if (THIS->mSettings.mInstructionLimit > 0 - && THIS->mCurrentCallInstructionCounter > THIS->mSettings.mInstructionLimit) + const ScriptId& activeScript = self->mActiveScriptIdStack.back(); + activeScript.mContainer->addInstructionCount(activeScript.mIndex, countHookStep); + self->mWatchdogInstructionCounter += countHookStep; + if (self->mSettings.mInstructionLimit > 0 + && self->mWatchdogInstructionCounter > self->mSettings.mInstructionLimit) { lua_pushstring(L, - "Lua CPU usage exceeded, probably an infinite loop in a script. " + "Lua instruction count exceeded, probably an infinite loop in a script. " "To change the limit set \"[Lua] instruction limit per call\" in settings.cfg"); lua_error(L); } @@ -74,9 +77,9 @@ namespace LuaUtil void* LuaState::trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize) { - LuaState* THIS = static_cast(ud); - const uint64_t smallAllocSize = THIS->mSettings.mSmallAllocMaxSize; - const uint64_t memoryLimit = THIS->mSettings.mMemoryLimit; + LuaState* self = static_cast(ud); + const uint64_t smallAllocSize = self->mSettings.mSmallAllocMaxSize; + const uint64_t memoryLimit = self->mSettings.mMemoryLimit; if (!ptr) osize = 0; @@ -90,14 +93,14 @@ namespace LuaUtil else bigAllocDelta += nsize; - if (bigAllocDelta > 0 && memoryLimit > 0 && THIS->mTotalMemoryUsage + nsize - osize > memoryLimit) + if (bigAllocDelta > 0 && memoryLimit > 0 && self->mTotalMemoryUsage + nsize - osize > memoryLimit) { Log(Debug::Error) << "Lua realloc " << osize << "->" << nsize << " is blocked because Lua memory limit (configurable in settings.cfg) is exceeded"; return nullptr; } - THIS->mTotalMemoryUsage += smallAllocDelta + bigAllocDelta; - THIS->mSmallAllocMemoryUsage += smallAllocDelta; + self->mTotalMemoryUsage += smallAllocDelta + bigAllocDelta; + self->mSmallAllocMemoryUsage += smallAllocDelta; void* newPtr = nullptr; if (nsize == 0) @@ -107,59 +110,83 @@ namespace LuaUtil if (bigAllocDelta != 0) { - auto it = osize > smallAllocSize ? THIS->mBigAllocOwners.find(ptr) : THIS->mBigAllocOwners.end(); + auto it = osize > smallAllocSize ? self->mBigAllocOwners.find(ptr) : self->mBigAllocOwners.end(); ScriptId id; - if (it != THIS->mBigAllocOwners.end()) + if (it != self->mBigAllocOwners.end()) { if (it->second.mContainer) id = ScriptId{ *it->second.mContainer, it->second.mScriptIndex }; if (ptr != newPtr || nsize <= smallAllocSize) - THIS->mBigAllocOwners.erase(it); + self->mBigAllocOwners.erase(it); } else if (bigAllocDelta > 0) { - id = THIS->mActiveScriptId; + if (!self->mActiveScriptIdStack.empty()) + id = self->mActiveScriptIdStack.back(); bigAllocDelta = nsize; } if (id.mContainer) { - if (static_cast(id.mIndex) >= THIS->mMemoryUsage.size()) - THIS->mMemoryUsage.resize(id.mIndex + 1); - THIS->mMemoryUsage[id.mIndex] += bigAllocDelta; + if (static_cast(id.mIndex) >= self->mMemoryUsage.size()) + self->mMemoryUsage.resize(id.mIndex + 1); + self->mMemoryUsage[id.mIndex] += bigAllocDelta; id.mContainer->addMemoryUsage(id.mIndex, bigAllocDelta); if (newPtr && nsize > smallAllocSize) - THIS->mBigAllocOwners.emplace(newPtr, AllocOwner{ id.mContainer->mThis, id.mIndex }); + self->mBigAllocOwners.emplace(newPtr, AllocOwner{ id.mContainer->mThis, id.mIndex }); } } return newPtr; } + lua_State* LuaState::createLuaRuntime(LuaState* luaState) + { + if (sProfilerEnabled) + { + Log(Debug::Info) << "Initializing LuaUtil::LuaState with profiler"; + lua_State* L = lua_newstate(&trackingAllocator, luaState); + if (L) + return L; + else + { + sProfilerEnabled = false; + Log(Debug::Error) + << "Failed to initialize LuaUtil::LuaState with custom allocator; disabling Lua profiler"; + } + } + Log(Debug::Info) << "Initializing LuaUtil::LuaState without profiler"; + lua_State* L = luaL_newstate(); + if (!L) + throw std::runtime_error("Can't create Lua runtime"); + return L; + } + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf, const LuaStateSettings& settings) : mSettings(settings) - , mLua(sol::default_at_panic, &trackingAllocator, this) + , mLuaHolder(createLuaRuntime(this)) + , mSol(mLuaHolder.get()) , mConf(conf) , mVFS(vfs) { - lua_sethook(mLua.lua_state(), &countHook, LUA_MASKCOUNT, countHookStep); - Log(Debug::Verbose) << "Initializing LuaUtil::LuaState"; + if (sProfilerEnabled) + lua_sethook(mLuaHolder.get(), &countHook, LUA_MASKCOUNT, countHookStep); - mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::bit32, sol::lib::string, + mSol.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::bit32, sol::lib::string, sol::lib::table, sol::lib::os, sol::lib::debug); - mLua["math"]["randomseed"](static_cast(std::time(nullptr))); - mLua["math"]["randomseed"] = [] {}; + mSol["math"]["randomseed"](static_cast(std::time(nullptr))); + mSol["math"]["randomseed"] = [] {}; - mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; }; + mSol["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; }; // Some fixes for compatibility between different Lua versions - if (mLua["unpack"] == sol::nil) - mLua["unpack"] = mLua["table"]["unpack"]; - else if (mLua["table"]["unpack"] == sol::nil) - mLua["table"]["unpack"] = mLua["unpack"]; + if (mSol["unpack"] == sol::nil) + mSol["unpack"] = mSol["table"]["unpack"]; + else if (mSol["table"]["unpack"] == sol::nil) + mSol["table"]["unpack"] = mSol["unpack"]; if (LUA_VERSION_NUM <= 501) { - mLua.script(R"( + mSol.script(R"( local _pairs = pairs local _ipairs = ipairs pairs = function(v) return (rawget(getmetatable(v) or {}, '__pairs') or _pairs)(v) end @@ -167,7 +194,7 @@ namespace LuaUtil )"); } - mLua.script(R"( + mSol.script(R"( local printToLog = function(...) local strs = {} for i = 1, select('#', ...) do @@ -212,31 +239,24 @@ namespace LuaUtil end )"); - mSandboxEnv = sol::table(mLua, sol::create); - mSandboxEnv["_VERSION"] = mLua["_VERSION"]; + mSandboxEnv = sol::table(mSol, sol::create); + mSandboxEnv["_VERSION"] = mSol["_VERSION"]; for (const std::string& s : safeFunctions) { - if (mLua[s] == sol::nil) + if (mSol[s] == sol::nil) throw std::logic_error("Lua function not found: " + s); - mSandboxEnv[s] = mLua[s]; + mSandboxEnv[s] = mSol[s]; } for (const std::string& s : safePackages) { - if (mLua[s] == sol::nil) + if (mSol[s] == sol::nil) throw std::logic_error("Lua package not found: " + s); - mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mLua[s]); + mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mSol[s]); } - mSandboxEnv["getmetatable"] = mLua["getSafeMetatable"]; + mSandboxEnv["getmetatable"] = mSol["getSafeMetatable"]; mCommonPackages["os"] = mSandboxEnv["os"] - = makeReadOnly(tableFromPairs({ { "date", mLua["os"]["date"] }, - { "difftime", mLua["os"]["difftime"] }, { "time", mLua["os"]["time"] } })); - } - - LuaState::~LuaState() - { - // Should be cleaned before destructing mLua. - mCommonPackages.clear(); - mSandboxEnv = sol::nil; + = makeReadOnly(tableFromPairs({ { "date", mSol["os"]["date"] }, + { "difftime", mSol["os"]["difftime"] }, { "time", mSol["os"]["time"] } })); } sol::table makeReadOnly(const sol::table& table, bool strictIndex) @@ -280,9 +300,9 @@ namespace LuaUtil { sol::protected_function script = loadScriptAndCache(path); - sol::environment env(mLua, sol::create, mSandboxEnv); + sol::environment env(mSol, sol::create, mSandboxEnv); std::string envName = namePrefix + "[" + path + "]:"; - env["print"] = mLua["printGen"](envName); + env["print"] = mSol["printGen"](envName); env["_G"] = env; env[sol::metatable_key]["__metatable"] = false; @@ -298,18 +318,18 @@ namespace LuaUtil else return package; }; - sol::table loaded(mLua, sol::create); + sol::table loaded(mSol, sol::create); for (const auto& [key, value] : mCommonPackages) loaded[key] = maybeRunLoader(value); for (const auto& [key, value] : packages) loaded[key] = maybeRunLoader(value); - env["require"] = [this, env, loaded, hiddenData, scriptId](std::string_view packageName) mutable { + env["require"] = [this, env, loaded, hiddenData](std::string_view packageName) mutable { sol::object package = loaded[packageName]; if (package != sol::nil) return package; sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS)); sol::set_environment(env, packageLoader); - package = call(scriptId, packageLoader, packageName); + package = call(packageLoader, packageName); loaded[packageName] = package; return package; }; @@ -320,8 +340,8 @@ namespace LuaUtil sol::environment LuaState::newInternalLibEnvironment() { - sol::environment env(mLua, sol::create, mSandboxEnv); - sol::table loaded(mLua, sol::create); + sol::environment env(mSol, sol::create, mSandboxEnv); + sol::table loaded(mSol, sol::create); for (const std::string& s : safePackages) loaded[s] = static_cast(mSandboxEnv[s]); env["require"] = [this, loaded, env](const std::string& module) mutable { @@ -347,7 +367,7 @@ namespace LuaUtil { auto iter = mCompiledScripts.find(path); if (iter != mCompiledScripts.end()) - return mLua.load(iter->second.as_string_view(), path, sol::load_mode::binary); + return mSol.load(iter->second.as_string_view(), path, sol::load_mode::binary); sol::function res = loadFromVFS(path); mCompiledScripts[path] = res.dump(); return res; @@ -356,7 +376,7 @@ namespace LuaUtil sol::function LuaState::loadFromVFS(const std::string& path) { std::string fileContent(std::istreambuf_iterator(*mVFS->get(path)), {}); - sol::load_result res = mLua.load(fileContent, path, sol::load_mode::text); + sol::load_result res = mSol.load(fileContent, path, sol::load_mode::text); if (!res.valid()) throw std::runtime_error("Lua error: " + res.get()); return res; @@ -365,7 +385,7 @@ namespace LuaUtil sol::function LuaState::loadInternalLib(std::string_view libName) { const auto path = packageNameToPath(libName, mLibSearchPaths); - sol::load_result res = mLua.load_file(Files::pathToUnicodeString(path), sol::load_mode::text); + sol::load_result res = mSol.load_file(Files::pathToUnicodeString(path), sol::load_mode::text); if (!res.valid()) throw std::runtime_error("Lua error: " + res.get()); return res; diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index bda9ad38a4..b0e173d67e 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -54,23 +54,21 @@ namespace LuaUtil LuaState(const LuaState&) = delete; LuaState(LuaState&&) = delete; - ~LuaState(); - // Returns underlying sol::state. - sol::state& sol() { return mLua; } + sol::state_view& sol() { return mSol; } // Can be used by a C++ function that is called from Lua to get the Lua traceback. // Makes no sense if called not from Lua code. // Note: It is a slow function, should be used for debug purposes only. - std::string debugTraceback() { return mLua["debug"]["traceback"]().get(); } + std::string debugTraceback() { return mSol["debug"]["traceback"]().get(); } // A shortcut to create a new Lua table. - sol::table newTable() { return sol::table(mLua, sol::create); } + sol::table newTable() { return sol::table(mSol, sol::create); } template sol::table tableFromPairs(std::initializer_list> list) { - sol::table res(mLua, sol::create); + sol::table res(mSol, sol::create); for (const auto& [k, v] : list) res[k] = v; return res; @@ -105,7 +103,7 @@ namespace LuaUtil sol::function loadFromVFS(const std::string& path); sol::environment newInternalLibEnvironment(); - uint64_t getTotalMemoryUsage() const { return mTotalMemoryUsage; } + uint64_t getTotalMemoryUsage() const { return mSol.memory_used(); } uint64_t getSmallAllocMemoryUsage() const { return mSmallAllocMemoryUsage; } uint64_t getMemoryUsageByScriptIndex(unsigned id) const { @@ -114,6 +112,10 @@ namespace LuaUtil const LuaStateSettings& getSettings() const { return mSettings; } + // Note: Lua profiler can not be re-enabled after disabling. + static void disableProfiler() { sProfilerEnabled = false; } + static bool isProfilerEnabled() { return sProfilerEnabled; } + private: static sol::protected_function_result throwIfError(sol::protected_function_result&&); template @@ -126,6 +128,8 @@ namespace LuaUtil static void countHook(lua_State* L, lua_Debug* ar); static void* trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize); + lua_State* createLuaRuntime(LuaState* luaState); + struct AllocOwner { std::shared_ptr mContainer; @@ -134,21 +138,43 @@ namespace LuaUtil const LuaStateSettings mSettings; - // Needed to track resource usage per script, must be initialized before mLua. - ScriptId mActiveScriptId; - uint64_t mCurrentCallInstructionCounter = 0; + // Needed to track resource usage per script, must be initialized before mLuaHolder. + std::vector mActiveScriptIdStack; + uint64_t mWatchdogInstructionCounter = 0; std::map mBigAllocOwners; uint64_t mTotalMemoryUsage = 0; uint64_t mSmallAllocMemoryUsage = 0; std::vector mMemoryUsage; - sol::state mLua; + class LuaStateHolder + { + public: + LuaStateHolder(lua_State* L) + : L(L) + { + sol::set_default_state(L); + } + ~LuaStateHolder() { lua_close(L); } + LuaStateHolder(const LuaStateHolder&) = delete; + LuaStateHolder(LuaStateHolder&&) = delete; + lua_State* get() { return L; } + + private: + lua_State* L; + }; + + // Must be declared before mSol and all sol-related objects. Then on exit it will be destructed the last. + LuaStateHolder mLuaHolder; + + sol::state_view mSol; const ScriptsConfiguration* mConf; sol::table mSandboxEnv; std::map mCompiledScripts; std::map mCommonPackages; const VFS::Manager* mVFS; std::vector mLibSearchPaths; + + static bool sProfilerEnabled; }; // LuaUtil::call should be used for every call of every Lua function. @@ -178,25 +204,30 @@ namespace LuaUtil template sol::protected_function_result call(ScriptId scriptId, const sol::protected_function& fn, Args&&... args) { - LuaState* luaState; - (void)lua_getallocf(fn.lua_state(), reinterpret_cast(&luaState)); - assert(luaState->mActiveScriptId.mContainer == nullptr && "recursive call of LuaUtil::call(scriptId, ...) ?"); - luaState->mActiveScriptId = scriptId; - luaState->mCurrentCallInstructionCounter = 0; + LuaState* luaState = nullptr; + if (LuaState::sProfilerEnabled && scriptId.mContainer) + { + (void)lua_getallocf(fn.lua_state(), reinterpret_cast(&luaState)); + luaState->mActiveScriptIdStack.push_back(scriptId); + luaState->mWatchdogInstructionCounter = 0; + } try { auto res = LuaState::throwIfError(fn(std::forward(args)...)); - luaState->mActiveScriptId = {}; + if (luaState) + luaState->mActiveScriptIdStack.pop_back(); return res; } catch (std::exception&) { - luaState->mActiveScriptId = {}; + if (luaState) + luaState->mActiveScriptIdStack.pop_back(); throw; } catch (...) { - luaState->mActiveScriptId = {}; + if (luaState) + luaState->mActiveScriptIdStack.pop_back(); throw std::runtime_error("Unknown error"); } } diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 40a1919fbe..6b6151b293 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -75,7 +75,7 @@ namespace LuaUtil script.mHiddenData[sScriptIdKey] = ScriptId{ this, scriptId }; script.mHiddenData[sScriptDebugNameKey] = debugName; script.mPath = path; - script.mStats.mCPUusage = 0; + script.mStats.mAvgInstructionCount = 0; const auto oldMemoryUsageIt = mRemovedScriptsMemoryUsage.find(scriptId); if (oldMemoryUsageIt != mRemovedScriptsMemoryUsage.end()) @@ -590,24 +590,24 @@ namespace LuaUtil updateTimerQueue(mGameTimersQueue, gameTime); } - static constexpr float CPUusageAvgCoef = 1.0 / 30; // averaging over approximately 30 frames + static constexpr float instructionCountAvgCoef = 1.0 / 30; // averaging over approximately 30 frames - void ScriptsContainer::CPUusageNextFrame() + void ScriptsContainer::statsNextFrame() { for (auto& [scriptId, script] : mScripts) { // The averaging formula is: averageValue = averageValue * (1-c) + newValue * c - script.mStats.mCPUusage *= 1 - CPUusageAvgCoef; - if (script.mStats.mCPUusage < 5) - script.mStats.mCPUusage = 0; // speeding up converge to zero if newValue is zero + script.mStats.mAvgInstructionCount *= 1 - instructionCountAvgCoef; + if (script.mStats.mAvgInstructionCount < 5) + script.mStats.mAvgInstructionCount = 0; // speeding up converge to zero if newValue is zero } } - void ScriptsContainer::addCPUusage(int scriptId, int64_t CPUusage) + void ScriptsContainer::addInstructionCount(int scriptId, int64_t instructionCount) { auto it = mScripts.find(scriptId); if (it != mScripts.end()) - it->second.mStats.mCPUusage += CPUusage * CPUusageAvgCoef; + it->second.mStats.mAvgInstructionCount += instructionCount * instructionCountAvgCoef; } void ScriptsContainer::addMemoryUsage(int scriptId, int64_t memoryDelta) @@ -640,7 +640,7 @@ namespace LuaUtil stats.resize(mLua.getConfiguration().size()); for (auto& [id, script] : mScripts) { - stats[id].mCPUusage += script.mStats.mCPUusage; + stats[id].mAvgInstructionCount += script.mStats.mAvgInstructionCount; stats[id].mMemoryUsage += script.mStats.mMemoryUsage; } for (auto& [id, mem] : mRemovedScriptsMemoryUsage) diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 6b9df70ac3..ebc7fd6d55 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -146,12 +146,12 @@ namespace LuaUtil // because they can not be stored in saves. I.e. loading a saved game will not fully restore the state. void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::main_protected_function callback); - // Informs that new frame is started. Needed to track CPU usage per frame. - void CPUusageNextFrame(); + // Informs that new frame is started. Needed to track Lua instruction count per frame. + void statsNextFrame(); struct ScriptStats { - float mCPUusage = 0; // averaged number of Lua instructions per frame + float mAvgInstructionCount = 0; // averaged number of Lua instructions per frame int64_t mMemoryUsage = 0; // bytes }; void collectStats(std::vector& stats) const; @@ -227,7 +227,7 @@ namespace LuaUtil using EventHandlerList = std::vector; friend class LuaState; - void addCPUusage(int scriptId, int64_t CPUusage); + void addInstructionCount(int scriptId, int64_t instructionCount); void addMemoryUsage(int scriptId, int64_t memoryDelta); // Add to container without calling onInit/onLoad. diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index 91f189337b..65431f3f6e 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -89,8 +89,9 @@ namespace LuaUtil } } - sol::table initUtilPackage(sol::state& lua) + sol::table initUtilPackage(lua_State* L) { + sol::state_view lua(L); sol::table util(lua, sol::create); // Lua bindings for Vec2 diff --git a/components/lua/utilpackage.hpp b/components/lua/utilpackage.hpp index de47710334..2b5da99346 100644 --- a/components/lua/utilpackage.hpp +++ b/components/lua/utilpackage.hpp @@ -34,8 +34,7 @@ namespace LuaUtil return { q }; } - sol::table initUtilPackage(sol::state&); - + sol::table initUtilPackage(lua_State*); } #endif // COMPONENTS_LUA_UTILPACKAGE_H diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 0cefc3047f..6f672a65b4 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1141,17 +1141,21 @@ lua debug = false # If zero, Lua scripts are processed in the main thread. lua num threads = 1 +# Enable Lua profiler +lua profiler = true + # No ownership tracking for allocations below or equal this size. small alloc max size = 1024 -# Memory limit for Lua runtime. If exceeded then only small allocations are allowed. Small allocations are always allowed, so e.g. Lua console can function. -# Default value is 2GB. +# Memory limit for Lua runtime (only if lua profiler = true). If exceeded then only small allocations are allowed. +# Small allocations are always allowed, so e.g. Lua console can function. Default value is 2GB. memory limit = 2147483648 -# Print debug info about memory usage. +# Print debug info about memory usage (only if lua profiler = true). log memory usage = false -# The maximal number of Lua instructions per function call. If exceeded (e.g. because of an infinite loop) the function will be terminated. +# The maximal number of Lua instructions per function call (only if lua profiler = true). +# If exceeded (e.g. because of an infinite loop) the function will be terminated. instruction limit per call = 100000000 [Stereo] From 7c36a7eb49ddbb1b027b6a382e93007d51751aad Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sat, 3 Dec 2022 16:56:23 +0100 Subject: [PATCH 4/5] Run Lua GC in every frame --- apps/openmw/mwlua/luamanagerimp.cpp | 4 ++++ docs/source/reference/modding/settings/lua.rst | 11 +++++++++++ files/settings-default.cfg | 3 +++ 3 files changed, 18 insertions(+) diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 220655fbb0..dce51454ec 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -132,6 +132,10 @@ namespace MWLua void LuaManager::update() { static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua"); + static const int gcStepCount = Settings::Manager::getInt("gc steps per frame", "Lua"); + if (gcStepCount > 0) + lua_gc(mLua.sol(), LUA_GCSTEP, gcStepCount); + if (mPlayer.isEmpty()) return; // The game is not started yet. diff --git a/docs/source/reference/modding/settings/lua.rst b/docs/source/reference/modding/settings/lua.rst index b0fd88fe30..1aca9fec0f 100644 --- a/docs/source/reference/modding/settings/lua.rst +++ b/docs/source/reference/modding/settings/lua.rst @@ -87,3 +87,14 @@ If exceeded (e.g. because of an infinite loop) the function will be terminated. This setting can only be configured by editing the settings configuration file. +gc steps per frame +------------------ + +:Type: integer +:Range: >= 0 +:Default: 100 + +Lua garbage collector steps per frame. The higher the value the more time Lua runtime can spend on freeing unused memory. + +This setting can only be configured by editing the settings configuration file. + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 6f672a65b4..6a4800b47a 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1158,6 +1158,9 @@ log memory usage = false # If exceeded (e.g. because of an infinite loop) the function will be terminated. instruction limit per call = 100000000 +# Lua garbage collector steps per frame. +gc steps per frame = 100 + [Stereo] # Enable/disable stereo view. This setting is ignored in VR. stereo enabled = false From 9902be0e974fa989c47eb8c52c57073af3a5c8b9 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Sat, 3 Dec 2022 17:01:01 +0100 Subject: [PATCH 5/5] Increment cache key for windows builds in .gitlab-ci.yml --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6df47b13a4..0c2aa136b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -455,7 +455,7 @@ macOS12_Xcode13: after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-v4 + key: ninja-v5 paths: - ccache - deps @@ -556,7 +556,7 @@ macOS12_Xcode13: after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-v4 + key: msbuild-v5 paths: - ccache - deps