mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-02-19 12:40:49 +00:00
Merge branch 'lua_profiler' into 'master'
Lua profiler See merge request OpenMW/openmw!2523
This commit is contained in:
commit
1d55be8214
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -8,6 +8,9 @@
|
||||
#include <components/debug/debugging.hpp>
|
||||
#include <components/settings/settings.hpp>
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/luamanager.hpp"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#ifndef BT_NO_PROFILE
|
||||
@ -106,6 +109,12 @@ namespace MWGui
|
||||
= itemLV->createWidgetReal<MyGUI::EditBox>("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<MyGUI::EditBox>(
|
||||
"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:;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -2159,6 +2159,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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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<int, int>& contentFileMapping, const LuaUtil::UserdataSerializer* serializer)
|
||||
void loadEvents(sol::state_view& lua, ESM::ESMReader& esm, GlobalEventQueue& globalEvents,
|
||||
LocalEventQueue& localEvents, const std::map<int, int>& contentFileMapping,
|
||||
const LuaUtil::UserdataSerializer* serializer)
|
||||
{
|
||||
while (esm.isNextSub("LUAE"))
|
||||
{
|
||||
|
@ -35,7 +35,7 @@ namespace MWLua
|
||||
using GlobalEventQueue = std::vector<GlobalEvent>;
|
||||
using LocalEventQueue = std::vector<LocalEvent>;
|
||||
|
||||
void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&,
|
||||
void loadEvents(sol::state_view& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&,
|
||||
const std::map<int, int>& contentFileMapping, const LuaUtil::UserdataSerializer* serializer);
|
||||
void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue&, const LocalEventQueue&);
|
||||
}
|
||||
|
@ -38,8 +38,18 @@
|
||||
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"),
|
||||
.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();
|
||||
@ -123,6 +133,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.
|
||||
|
||||
@ -140,6 +154,10 @@ namespace MWLua
|
||||
|
||||
mWorldView.update();
|
||||
|
||||
mGlobalScripts.statsNextFrame();
|
||||
for (LocalScripts* scripts : mActiveLocalScripts)
|
||||
scripts->statsNextFrame();
|
||||
|
||||
std::vector<GlobalEvent> globalEvents = std::move(mGlobalEvents);
|
||||
std::vector<LocalEvent> localEvents = std::move(mLocalEvents);
|
||||
mGlobalEvents = std::vector<GlobalEvent>();
|
||||
@ -602,9 +620,123 @@ namespace MWLua
|
||||
mActionQueue.push_back(std::make_unique<FunctionAction>(&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
|
||||
{
|
||||
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:";
|
||||
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:";
|
||||
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;
|
||||
|
||||
std::vector<Stats> activeStats;
|
||||
mGlobalScripts.collectStats(activeStats);
|
||||
for (LocalScripts* scripts : mActiveLocalScripts)
|
||||
scripts->collectStats(activeStats);
|
||||
|
||||
std::vector<Stats> 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";
|
||||
|
||||
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) << "ops";
|
||||
out << std::setw(valueW) << "memory";
|
||||
out << std::setw(valueW) << "memory";
|
||||
out << std::setw(valueW) << "ops";
|
||||
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<int64_t>(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) << "-";
|
||||
outMemSize(selectedStats[i].mMemoryUsage);
|
||||
}
|
||||
else
|
||||
{
|
||||
out << std::setw(valueW) << static_cast<int64_t>(selectedStats[i].mAvgInstructionCount);
|
||||
outMemSize(selectedStats[i].mMemoryUsage);
|
||||
}
|
||||
out << "\n";
|
||||
}
|
||||
|
||||
return out.str();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -146,7 +146,7 @@ namespace MWLua
|
||||
void registerObjectList(const std::string& prefix, const Context& context)
|
||||
{
|
||||
using ListT = ObjectList<ObjectT>;
|
||||
sol::state& lua = context.mLua->sol();
|
||||
sol::state_view& lua = context.mLua->sol();
|
||||
ObjectRegistry* registry = context.mWorldView->getObjectRegistry();
|
||||
sol::usertype<ListT> listT = lua.new_usertype<ListT>(prefix + "ObjectList");
|
||||
listT[sol::meta_function::to_string]
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace
|
||||
using namespace TestingOpenMW;
|
||||
|
||||
template <typename T>
|
||||
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<T>();
|
||||
}
|
||||
@ -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();
|
||||
sol::state_view& l = lua.sol();
|
||||
internal::CaptureStdout();
|
||||
l10n::Manager l10nManager(mVFS.get());
|
||||
l10nManager.setPreferredLocales({ "de", "en" });
|
||||
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n");
|
||||
@ -164,5 +164,4 @@ you_have_arrows: "Arrows count: {count}"
|
||||
l10nManager.setPreferredLocales({ "en" });
|
||||
EXPECT_EQ(get<std::string>(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -10,14 +10,16 @@ namespace
|
||||
using namespace testing;
|
||||
|
||||
template <typename T>
|
||||
T get(sol::state& lua, std::string luaCode)
|
||||
T get(sol::state_view& lua, std::string luaCode)
|
||||
{
|
||||
return lua.safe_script("return " + luaCode).get<T>();
|
||||
}
|
||||
|
||||
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_view& 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");
|
||||
|
@ -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<L10nContext> ctxDef = lua.new_usertype<L10nContext>("L10nContext");
|
||||
ctxDef[sol::meta_function::call]
|
||||
= [](const L10nContext& ctx, std::string_view key, sol::optional<sol::table> args) {
|
||||
|
@ -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
|
||||
|
@ -10,6 +10,8 @@
|
||||
#include <components/files/conversion.hpp>
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
#include "scriptscontainer.hpp"
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
@ -50,26 +52,141 @@ 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 = 1000;
|
||||
|
||||
bool LuaState::sProfilerEnabled = true;
|
||||
|
||||
void LuaState::countHook(lua_State* L, lua_Debug* ar)
|
||||
{
|
||||
LuaState* self;
|
||||
(void)lua_getallocf(L, reinterpret_cast<void**>(&self));
|
||||
if (self->mActiveScriptIdStack.empty())
|
||||
return;
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
void* LuaState::trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize)
|
||||
{
|
||||
LuaState* self = static_cast<LuaState*>(ud);
|
||||
const uint64_t smallAllocSize = self->mSettings.mSmallAllocMaxSize;
|
||||
const uint64_t memoryLimit = self->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 && 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;
|
||||
}
|
||||
self->mTotalMemoryUsage += smallAllocDelta + bigAllocDelta;
|
||||
self->mSmallAllocMemoryUsage += smallAllocDelta;
|
||||
|
||||
void* newPtr = nullptr;
|
||||
if (nsize == 0)
|
||||
free(ptr);
|
||||
else
|
||||
newPtr = realloc(ptr, nsize);
|
||||
|
||||
if (bigAllocDelta != 0)
|
||||
{
|
||||
auto it = osize > smallAllocSize ? self->mBigAllocOwners.find(ptr) : self->mBigAllocOwners.end();
|
||||
ScriptId id;
|
||||
if (it != self->mBigAllocOwners.end())
|
||||
{
|
||||
if (it->second.mContainer)
|
||||
id = ScriptId{ *it->second.mContainer, it->second.mScriptIndex };
|
||||
if (ptr != newPtr || nsize <= smallAllocSize)
|
||||
self->mBigAllocOwners.erase(it);
|
||||
}
|
||||
else if (bigAllocDelta > 0)
|
||||
{
|
||||
if (!self->mActiveScriptIdStack.empty())
|
||||
id = self->mActiveScriptIdStack.back();
|
||||
bigAllocDelta = nsize;
|
||||
}
|
||||
if (id.mContainer)
|
||||
{
|
||||
if (static_cast<size_t>(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)
|
||||
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)
|
||||
, mLuaHolder(createLuaRuntime(this))
|
||||
, mSol(mLuaHolder.get())
|
||||
, mConf(conf)
|
||||
, mVFS(vfs)
|
||||
{
|
||||
mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::bit32, sol::lib::string,
|
||||
if (sProfilerEnabled)
|
||||
lua_sethook(mLuaHolder.get(), &countHook, LUA_MASKCOUNT, countHookStep);
|
||||
|
||||
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<unsigned>(std::time(nullptr)));
|
||||
mLua["math"]["randomseed"] = [] {};
|
||||
mSol["math"]["randomseed"](static_cast<unsigned>(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
|
||||
@ -77,7 +194,7 @@ namespace LuaUtil
|
||||
)");
|
||||
}
|
||||
|
||||
mLua.script(R"(
|
||||
mSol.script(R"(
|
||||
local printToLog = function(...)
|
||||
local strs = {}
|
||||
for i = 1, select('#', ...) do
|
||||
@ -122,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<std::string_view, sol::function>({ { "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<std::string_view, sol::function>({ { "date", mSol["os"]["date"] },
|
||||
{ "difftime", mSol["os"]["difftime"] }, { "time", mSol["os"]["time"] } }));
|
||||
}
|
||||
|
||||
sol::table makeReadOnly(const sol::table& table, bool strictIndex)
|
||||
@ -190,19 +300,25 @@ 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;
|
||||
|
||||
auto maybeRunLoader = [&hiddenData](const sol::object& package) -> sol::object {
|
||||
ScriptId scriptId;
|
||||
if (hiddenData.is<sol::table>())
|
||||
scriptId = hiddenData.as<sol::table>()
|
||||
.get<sol::optional<ScriptId>>(ScriptsContainer::sScriptIdKey)
|
||||
.value_or(ScriptId{});
|
||||
|
||||
auto maybeRunLoader = [&hiddenData, scriptId](const sol::object& package) -> sol::object {
|
||||
if (package.is<sol::function>())
|
||||
return call(package.as<sol::function>(), hiddenData);
|
||||
return call(scriptId, package.as<sol::function>(), hiddenData);
|
||||
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)
|
||||
@ -219,13 +335,13 @@ namespace LuaUtil
|
||||
};
|
||||
|
||||
sol::set_environment(env, script);
|
||||
return call(script);
|
||||
return call(scriptId, script);
|
||||
}
|
||||
|
||||
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<sol::object>(mSandboxEnv[s]);
|
||||
env["require"] = [this, loaded, env](const std::string& module) mutable {
|
||||
@ -233,7 +349,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;
|
||||
@ -251,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;
|
||||
@ -260,7 +376,7 @@ namespace LuaUtil
|
||||
sol::function LuaState::loadFromVFS(const std::string& path)
|
||||
{
|
||||
std::string fileContent(std::istreambuf_iterator<char>(*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<std::string>());
|
||||
return res;
|
||||
@ -269,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<std::string>());
|
||||
return res;
|
||||
|
@ -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,24 +49,26 @@ namespace LuaUtil
|
||||
class LuaState
|
||||
{
|
||||
public:
|
||||
explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf);
|
||||
~LuaState();
|
||||
explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf,
|
||||
const LuaStateSettings& settings = LuaStateSettings{});
|
||||
LuaState(const LuaState&) = delete;
|
||||
LuaState(LuaState&&) = delete;
|
||||
|
||||
// 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>(); }
|
||||
std::string debugTraceback() { return mSol["debug"]["traceback"]().get<std::string>(); }
|
||||
|
||||
// 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 <typename Key, typename Value>
|
||||
sol::table tableFromPairs(std::initializer_list<std::pair<Key, Value>> list)
|
||||
{
|
||||
sol::table res(mLua, sol::create);
|
||||
sol::table res(mSol, sol::create);
|
||||
for (const auto& [k, v] : list)
|
||||
res[k] = v;
|
||||
return res;
|
||||
@ -86,30 +103,92 @@ namespace LuaUtil
|
||||
sol::function loadFromVFS(const std::string& path);
|
||||
sol::environment newInternalLibEnvironment();
|
||||
|
||||
uint64_t getTotalMemoryUsage() const { return mSol.memory_used(); }
|
||||
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; }
|
||||
|
||||
// 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 <typename... Args>
|
||||
friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args);
|
||||
template <typename... Args>
|
||||
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);
|
||||
|
||||
sol::state mLua;
|
||||
lua_State* createLuaRuntime(LuaState* luaState);
|
||||
|
||||
struct AllocOwner
|
||||
{
|
||||
std::shared_ptr<ScriptsContainer*> mContainer;
|
||||
int mScriptIndex;
|
||||
};
|
||||
|
||||
const LuaStateSettings mSettings;
|
||||
|
||||
// Needed to track resource usage per script, must be initialized before mLuaHolder.
|
||||
std::vector<ScriptId> mActiveScriptIdStack;
|
||||
uint64_t mWatchdogInstructionCounter = 0;
|
||||
std::map<void*, AllocOwner> mBigAllocOwners;
|
||||
uint64_t mTotalMemoryUsage = 0;
|
||||
uint64_t mSmallAllocMemoryUsage = 0;
|
||||
std::vector<int64_t> mMemoryUsage;
|
||||
|
||||
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<std::string, sol::bytecode> mCompiledScripts;
|
||||
std::map<std::string, sol::object> mCommonPackages;
|
||||
const VFS::Manager* mVFS;
|
||||
std::vector<std::filesystem::path> mLibSearchPaths;
|
||||
|
||||
static bool sProfilerEnabled;
|
||||
};
|
||||
|
||||
// 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 <typename... Args>
|
||||
sol::protected_function_result call(const sol::protected_function& fn, Args&&... args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return LuaState::throwIfError(fn(std::forward<Args>(args)...));
|
||||
auto res = LuaState::throwIfError(fn(std::forward<Args>(args)...));
|
||||
return res;
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
@ -121,6 +200,38 @@ namespace LuaUtil
|
||||
}
|
||||
}
|
||||
|
||||
// Lua must be initialized through LuaUtil::LuaState, otherwise this function will segfault.
|
||||
template <typename... Args>
|
||||
sol::protected_function_result call(ScriptId scriptId, const sol::protected_function& fn, Args&&... args)
|
||||
{
|
||||
LuaState* luaState = nullptr;
|
||||
if (LuaState::sProfilerEnabled && scriptId.mContainer)
|
||||
{
|
||||
(void)lua_getallocf(fn.lua_state(), reinterpret_cast<void**>(&luaState));
|
||||
luaState->mActiveScriptIdStack.push_back(scriptId);
|
||||
luaState->mWatchdogInstructionCounter = 0;
|
||||
}
|
||||
try
|
||||
{
|
||||
auto res = LuaState::throwIfError(fn(std::forward<Args>(args)...));
|
||||
if (luaState)
|
||||
luaState->mActiveScriptIdStack.pop_back();
|
||||
return res;
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
if (luaState)
|
||||
luaState->mActiveScriptIdStack.pop_back();
|
||||
throw;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (luaState)
|
||||
luaState->mActiveScriptIdStack.pop_back();
|
||||
throw std::runtime_error("Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
// getFieldOrNil(table, "a", "b", "c") returns table["a"]["b"]["c"] or nil if some of the fields doesn't exist.
|
||||
template <class... Str>
|
||||
sol::object getFieldOrNil(const sol::object& table, std::string_view first, const Str&... str)
|
||||
|
@ -18,6 +18,7 @@ namespace LuaUtil
|
||||
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix)
|
||||
: mNamePrefix(namePrefix)
|
||||
, mLua(*lua)
|
||||
, mThis(std::make_shared<ScriptsContainer*>(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.mAvgInstructionCount = 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<bool>())
|
||||
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<int64_t>(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 instructionCountAvgCoef = 1.0 / 30; // averaging over approximately 30 frames
|
||||
|
||||
void ScriptsContainer::statsNextFrame()
|
||||
{
|
||||
for (auto& [scriptId, script] : mScripts)
|
||||
{
|
||||
// The averaging formula is: averageValue = averageValue * (1-c) + newValue * c
|
||||
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::addInstructionCount(int scriptId, int64_t instructionCount)
|
||||
{
|
||||
auto it = mScripts.find(scriptId);
|
||||
if (it != mScripts.end())
|
||||
it->second.mStats.mAvgInstructionCount += instructionCount * instructionCountAvgCoef;
|
||||
}
|
||||
|
||||
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<ScriptStats>& stats) const
|
||||
{
|
||||
stats.resize(mLua.getConfiguration().size());
|
||||
for (auto& [id, script] : mScripts)
|
||||
{
|
||||
stats[id].mAvgInstructionCount += script.mStats.mAvgInstructionCount;
|
||||
stats[id].mMemoryUsage += script.mStats.mMemoryUsage;
|
||||
}
|
||||
for (auto& [id, mem] : mRemovedScriptsMemoryUsage)
|
||||
stats[id].mMemoryUsage += mem;
|
||||
}
|
||||
}
|
||||
|
@ -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 Lua instruction count per frame.
|
||||
void statsNextFrame();
|
||||
|
||||
struct ScriptStats
|
||||
{
|
||||
float mAvgInstructionCount = 0; // averaged number of Lua instructions per frame
|
||||
int64_t mMemoryUsage = 0; // bytes
|
||||
};
|
||||
void collectStats(std::vector<ScriptStats>& 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<std::string, sol::main_protected_function> mRegisteredCallbacks;
|
||||
std::map<int64_t, sol::main_protected_function> mTemporaryCallbacks;
|
||||
std::string mPath;
|
||||
ScriptStats mStats;
|
||||
};
|
||||
struct Timer
|
||||
{
|
||||
@ -220,6 +226,10 @@ namespace LuaUtil
|
||||
};
|
||||
using EventHandlerList = std::vector<Handler>;
|
||||
|
||||
friend class LuaState;
|
||||
void addInstructionCount(int scriptId, int64_t instructionCount);
|
||||
void addMemoryUsage(int scriptId, int64_t memoryDelta);
|
||||
|
||||
// Add to container without calling onInit/onLoad.
|
||||
bool addScript(int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& onLoad);
|
||||
|
||||
@ -252,6 +262,9 @@ namespace LuaUtil
|
||||
std::vector<Timer> mSimulationTimersQueue;
|
||||
std::vector<Timer> mGameTimersQueue;
|
||||
int64_t mTemporaryCallbackCounter = 0;
|
||||
|
||||
std::map<int, int64_t> mRemovedScriptsMemoryUsage;
|
||||
std::shared_ptr<ScriptsContainer*> mThis; // used by LuaState to track ownership of memory allocations
|
||||
};
|
||||
|
||||
// Wrapper for a Lua function.
|
||||
@ -267,8 +280,9 @@ namespace LuaUtil
|
||||
template <typename... Args>
|
||||
sol::object call(Args&&... args) const
|
||||
{
|
||||
if (isValid())
|
||||
return LuaUtil::call(mFunc, std::forward<Args>(args)...);
|
||||
sol::optional<ScriptId> scriptId = mHiddenData[ScriptsContainer::sScriptIdKey];
|
||||
if (scriptId.has_value())
|
||||
return LuaUtil::call(scriptId.value(), mFunc, std::forward<Args>(args)...);
|
||||
else
|
||||
Log(Debug::Debug) << "Ignored callback to the removed script "
|
||||
<< mHiddenData.get<std::string>(ScriptsContainer::sScriptDebugNameKey);
|
||||
|
@ -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
|
||||
|
@ -34,8 +34,7 @@ namespace LuaUtil
|
||||
return { q };
|
||||
}
|
||||
|
||||
sol::table initUtilPackage(sol::state&);
|
||||
|
||||
sol::table initUtilPackage(lua_State*);
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_UTILPACKAGE_H
|
||||
|
@ -26,3 +26,75 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
DebugWindow: "Debug"
|
||||
LogViewer: "Protokollansicht"
|
||||
LuaProfiler: "Lua-Profiler"
|
||||
PhysicsProfiler: "Physik-Profiler"
|
||||
|
@ -1,3 +1,4 @@
|
||||
DebugWindow: "Debug"
|
||||
LogViewer: "Log Viewer"
|
||||
LuaProfiler: "Lua Profiler"
|
||||
PhysicsProfiler: "Physics Profiler"
|
||||
|
@ -1,3 +1,4 @@
|
||||
DebugWindow: "Меню отладки"
|
||||
LogViewer: "Журнал логов"
|
||||
LuaProfiler: "Профилировщик Луа"
|
||||
PhysicsProfiler: "Профилировщик физики"
|
||||
|
@ -1144,6 +1144,26 @@ 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 (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 (only if lua profiler = true).
|
||||
log memory usage = false
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user