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