mirror of
https://github.com/OpenMW/openmw.git
synced 2025-05-10 14:11:29 +00:00
Track memory and CPU usage per script in LuaUtil::LuaState
This commit is contained in:
parent
f4ac32efc9
commit
6fa65e4729
10 changed files with 319 additions and 50 deletions
|
@ -63,7 +63,7 @@ namespace MWLua
|
||||||
= [](const LuaUtil::Callback& callback, sol::variadic_args va) { return callback.call(sol::as_args(va)); };
|
= [](const LuaUtil::Callback& callback, sol::variadic_args va) { return callback.call(sol::as_args(va)); };
|
||||||
|
|
||||||
auto initializer = [](sol::table hiddenData) {
|
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 AsyncPackageId{ id.mContainer, id.mIndex, hiddenData };
|
||||||
};
|
};
|
||||||
return sol::make_object(context.mLua->sol(), initializer);
|
return sol::make_object(context.mLua->sol(), initializer);
|
||||||
|
|
|
@ -139,6 +139,10 @@ namespace MWLua
|
||||||
|
|
||||||
mWorldView.update();
|
mWorldView.update();
|
||||||
|
|
||||||
|
mGlobalScripts.CPUusageNextFrame();
|
||||||
|
for (LocalScripts* scripts : mActiveLocalScripts)
|
||||||
|
scripts->CPUusageNextFrame();
|
||||||
|
|
||||||
std::vector<GlobalEvent> globalEvents = std::move(mGlobalEvents);
|
std::vector<GlobalEvent> globalEvents = std::move(mGlobalEvents);
|
||||||
std::vector<LocalEvent> localEvents = std::move(mLocalEvents);
|
std::vector<LocalEvent> localEvents = std::move(mLocalEvents);
|
||||||
mGlobalEvents = std::vector<GlobalEvent>();
|
mGlobalEvents = std::vector<GlobalEvent>();
|
||||||
|
|
|
@ -15,41 +15,40 @@ namespace
|
||||||
{
|
{
|
||||||
void SetUp() override
|
void SetUp() override
|
||||||
{
|
{
|
||||||
mLua.open_libraries(sol::lib::coroutine);
|
mLua.sol()["callback"] = [&](sol::protected_function fn) -> LuaUtil::Callback {
|
||||||
mLua["callback"] = [&](sol::protected_function fn) -> LuaUtil::Callback {
|
sol::table hiddenData(mLua.sol(), sol::create);
|
||||||
sol::table hiddenData(mLua, sol::create);
|
hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{};
|
||||||
hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = sol::table(mLua, sol::create);
|
|
||||||
return LuaUtil::Callback{ std::move(fn), hiddenData };
|
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;
|
LuaUtil::Callback mCb;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(LuaCoroutineCallbackTest, CoroutineCallbacks)
|
TEST_F(LuaCoroutineCallbackTest, CoroutineCallbacks)
|
||||||
{
|
{
|
||||||
internal::CaptureStdout();
|
internal::CaptureStdout();
|
||||||
mLua.safe_script(R"X(
|
mLua.sol().safe_script(R"X(
|
||||||
local s = 'test'
|
local s = 'test'
|
||||||
coroutine.wrap(function()
|
coroutine.wrap(function()
|
||||||
pass(callback(function(v) print(s) end))
|
pass(callback(function(v) print(s) end))
|
||||||
end)()
|
end)()
|
||||||
)X");
|
)X");
|
||||||
mLua.collect_garbage();
|
mLua.sol().collect_garbage();
|
||||||
mCb.call();
|
mCb.call();
|
||||||
EXPECT_THAT(internal::GetCapturedStdout(), "test\n");
|
EXPECT_THAT(internal::GetCapturedStdout(), "test\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LuaCoroutineCallbackTest, ErrorInCoroutineCallbacks)
|
TEST_F(LuaCoroutineCallbackTest, ErrorInCoroutineCallbacks)
|
||||||
{
|
{
|
||||||
mLua.safe_script(R"X(
|
mLua.sol().safe_script(R"X(
|
||||||
coroutine.wrap(function()
|
coroutine.wrap(function()
|
||||||
pass(callback(function() error('COROUTINE CALLBACK') end))
|
pass(callback(function() error('COROUTINE CALLBACK') end))
|
||||||
end)()
|
end)()
|
||||||
)X");
|
)X");
|
||||||
mLua.collect_garbage();
|
mLua.sol().collect_garbage();
|
||||||
EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK");
|
EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,9 +82,9 @@ you_have_arrows: "Arrows count: {count}"
|
||||||
|
|
||||||
TEST_F(LuaL10nTest, L10n)
|
TEST_F(LuaL10nTest, L10n)
|
||||||
{
|
{
|
||||||
internal::CaptureStdout();
|
|
||||||
LuaUtil::LuaState lua{ mVFS.get(), &mCfg };
|
LuaUtil::LuaState lua{ mVFS.get(), &mCfg };
|
||||||
sol::state& l = lua.sol();
|
sol::state& l = lua.sol();
|
||||||
|
internal::CaptureStdout();
|
||||||
l10n::Manager l10nManager(mVFS.get());
|
l10n::Manager l10nManager(mVFS.get());
|
||||||
l10nManager.setPreferredLocales({ "de", "en" });
|
l10nManager.setPreferredLocales({ "de", "en" });
|
||||||
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n");
|
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n");
|
||||||
|
|
|
@ -448,8 +448,7 @@ CUSTOM, PLAYER: useInterface.lua
|
||||||
{
|
{
|
||||||
LuaUtil::Callback callback{ mLua.sol()["print"], mLua.newTable() };
|
LuaUtil::Callback callback{ mLua.sol()["print"], mLua.newTable() };
|
||||||
callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua";
|
callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua";
|
||||||
callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]
|
callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{ nullptr, 0 };
|
||||||
= LuaUtil::ScriptsContainer::ScriptId{ nullptr, 0 };
|
|
||||||
|
|
||||||
testing::internal::CaptureStdout();
|
testing::internal::CaptureStdout();
|
||||||
callback.call(1.5);
|
callback.call(1.5);
|
||||||
|
|
|
@ -17,7 +17,9 @@ namespace
|
||||||
|
|
||||||
TEST(LuaUtilStorageTest, Basic)
|
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::initLuaBindings(mLua);
|
||||||
LuaUtil::LuaStorage storage(mLua);
|
LuaUtil::LuaStorage storage(mLua);
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ namespace
|
||||||
callbackCalls.push_back(section + "_*");
|
callbackCalls.push_back(section + "_*");
|
||||||
}),
|
}),
|
||||||
sol::table(mLua, sol::create) };
|
sol::table(mLua, sol::create) };
|
||||||
callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = "fakeId";
|
callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{};
|
||||||
|
|
||||||
mLua["mutable"] = storage.getMutableSection("test");
|
mLua["mutable"] = storage.getMutableSection("test");
|
||||||
mLua["ro"] = storage.getReadOnlySection("test");
|
mLua["ro"] = storage.getReadOnlySection("test");
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
#include <components/files/conversion.hpp>
|
#include <components/files/conversion.hpp>
|
||||||
#include <components/vfs/manager.hpp>
|
#include <components/vfs/manager.hpp>
|
||||||
|
|
||||||
|
#include "scriptscontainer.hpp"
|
||||||
|
|
||||||
namespace LuaUtil
|
namespace LuaUtil
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -50,10 +52,98 @@ namespace LuaUtil
|
||||||
"tonumber", "tostring", "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable" };
|
"tonumber", "tostring", "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable" };
|
||||||
static const std::string safePackages[] = { "coroutine", "math", "string", "table" };
|
static const std::string safePackages[] = { "coroutine", "math", "string", "table" };
|
||||||
|
|
||||||
LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf)
|
static constexpr int64_t countHookStep = 2000;
|
||||||
: mConf(conf)
|
|
||||||
|
void LuaState::countHook(lua_State* L, lua_Debug* ar)
|
||||||
|
{
|
||||||
|
LuaState* THIS;
|
||||||
|
(void)lua_getallocf(L, reinterpret_cast<void**>(&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<LuaState*>(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<size_t>(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)
|
, 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,
|
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);
|
sol::lib::table, sol::lib::os, sol::lib::debug);
|
||||||
|
|
||||||
|
@ -196,9 +286,15 @@ namespace LuaUtil
|
||||||
env["_G"] = env;
|
env["_G"] = env;
|
||||||
env[sol::metatable_key]["__metatable"] = false;
|
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>())
|
if (package.is<sol::function>())
|
||||||
return call(package.as<sol::function>(), hiddenData);
|
return call(scriptId, package.as<sol::function>(), hiddenData);
|
||||||
else
|
else
|
||||||
return package;
|
return package;
|
||||||
};
|
};
|
||||||
|
@ -207,19 +303,19 @@ namespace LuaUtil
|
||||||
loaded[key] = maybeRunLoader(value);
|
loaded[key] = maybeRunLoader(value);
|
||||||
for (const auto& [key, value] : packages)
|
for (const auto& [key, value] : packages)
|
||||||
loaded[key] = maybeRunLoader(value);
|
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];
|
sol::object package = loaded[packageName];
|
||||||
if (package != sol::nil)
|
if (package != sol::nil)
|
||||||
return package;
|
return package;
|
||||||
sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS));
|
sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS));
|
||||||
sol::set_environment(env, packageLoader);
|
sol::set_environment(env, packageLoader);
|
||||||
package = call(packageLoader, packageName);
|
package = call(scriptId, packageLoader, packageName);
|
||||||
loaded[packageName] = package;
|
loaded[packageName] = package;
|
||||||
return package;
|
return package;
|
||||||
};
|
};
|
||||||
|
|
||||||
sol::set_environment(env, script);
|
sol::set_environment(env, script);
|
||||||
return call(script);
|
return call(scriptId, script);
|
||||||
}
|
}
|
||||||
|
|
||||||
sol::environment LuaState::newInternalLibEnvironment()
|
sol::environment LuaState::newInternalLibEnvironment()
|
||||||
|
@ -233,7 +329,7 @@ namespace LuaUtil
|
||||||
return loaded[module];
|
return loaded[module];
|
||||||
sol::protected_function initializer = loadInternalLib(module);
|
sol::protected_function initializer = loadInternalLib(module);
|
||||||
sol::set_environment(env, initializer);
|
sol::set_environment(env, initializer);
|
||||||
loaded[module] = call(initializer, module);
|
loaded[module] = call({}, initializer, module);
|
||||||
return loaded[module];
|
return loaded[module];
|
||||||
};
|
};
|
||||||
return env;
|
return env;
|
||||||
|
|
|
@ -19,6 +19,21 @@ namespace LuaUtil
|
||||||
|
|
||||||
std::string getLuaVersion();
|
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.
|
// Holds Lua state.
|
||||||
// Provides additional features:
|
// Provides additional features:
|
||||||
// - Load scripts from the virtual filesystem;
|
// - Load scripts from the virtual filesystem;
|
||||||
|
@ -34,7 +49,11 @@ namespace LuaUtil
|
||||||
class LuaState
|
class LuaState
|
||||||
{
|
{
|
||||||
public:
|
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();
|
~LuaState();
|
||||||
|
|
||||||
// Returns underlying sol::state.
|
// Returns underlying sol::state.
|
||||||
|
@ -86,12 +105,42 @@ namespace LuaUtil
|
||||||
sol::function loadFromVFS(const std::string& path);
|
sol::function loadFromVFS(const std::string& path);
|
||||||
sol::environment newInternalLibEnvironment();
|
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:
|
private:
|
||||||
static sol::protected_function_result throwIfError(sol::protected_function_result&&);
|
static sol::protected_function_result throwIfError(sol::protected_function_result&&);
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... 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);
|
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<ScriptsContainer*> 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<void*, AllocOwner> mBigAllocOwners;
|
||||||
|
uint64_t mTotalMemoryUsage = 0;
|
||||||
|
uint64_t mSmallAllocMemoryUsage = 0;
|
||||||
|
std::vector<int64_t> mMemoryUsage;
|
||||||
|
|
||||||
sol::state mLua;
|
sol::state mLua;
|
||||||
const ScriptsConfiguration* mConf;
|
const ScriptsConfiguration* mConf;
|
||||||
|
@ -102,14 +151,18 @@ namespace LuaUtil
|
||||||
std::vector<std::filesystem::path> mLibSearchPaths;
|
std::vector<std::filesystem::path> mLibSearchPaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Should be used for every call of every Lua function.
|
// LuaUtil::call 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
|
// 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>
|
template <typename... Args>
|
||||||
sol::protected_function_result call(const sol::protected_function& fn, Args&&... args)
|
sol::protected_function_result call(const sol::protected_function& fn, Args&&... args)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return LuaState::throwIfError(fn(std::forward<Args>(args)...));
|
auto res = LuaState::throwIfError(fn(std::forward<Args>(args)...));
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
catch (std::exception&)
|
catch (std::exception&)
|
||||||
{
|
{
|
||||||
|
@ -121,6 +174,33 @@ 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;
|
||||||
|
(void)lua_getallocf(fn.lua_state(), reinterpret_cast<void**>(&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>(args)...));
|
||||||
|
luaState->mActiveScriptId = {};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
catch (std::exception&)
|
||||||
|
{
|
||||||
|
luaState->mActiveScriptId = {};
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
luaState->mActiveScriptId = {};
|
||||||
|
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.
|
// getFieldOrNil(table, "a", "b", "c") returns table["a"]["b"]["c"] or nil if some of the fields doesn't exist.
|
||||||
template <class... Str>
|
template <class... Str>
|
||||||
sol::object getFieldOrNil(const sol::object& table, std::string_view first, const Str&... 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)
|
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix)
|
||||||
: mNamePrefix(namePrefix)
|
: mNamePrefix(namePrefix)
|
||||||
, mLua(*lua)
|
, mLua(*lua)
|
||||||
|
, mThis(std::make_shared<ScriptsContainer*>(this))
|
||||||
{
|
{
|
||||||
registerEngineHandlers({ &mUpdateHandlers });
|
registerEngineHandlers({ &mUpdateHandlers });
|
||||||
mPublicInterfaces = sol::table(lua->sol(), sol::create);
|
mPublicInterfaces = sol::table(lua->sol(), sol::create);
|
||||||
|
@ -74,6 +75,16 @@ namespace LuaUtil
|
||||||
script.mHiddenData[sScriptIdKey] = ScriptId{ this, scriptId };
|
script.mHiddenData[sScriptIdKey] = ScriptId{ this, scriptId };
|
||||||
script.mHiddenData[sScriptDebugNameKey] = debugName;
|
script.mHiddenData[sScriptDebugNameKey] = debugName;
|
||||||
script.mPath = path;
|
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
|
try
|
||||||
{
|
{
|
||||||
|
@ -146,8 +157,10 @@ namespace LuaUtil
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
mScripts[scriptId].mHiddenData[sScriptIdKey] = sol::nil;
|
auto iter = mScripts.find(scriptId);
|
||||||
mScripts.erase(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();
|
Log(Debug::Error) << "Can't start " << debugName << "; " << e.what();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -162,6 +175,7 @@ namespace LuaUtil
|
||||||
if (script.mInterface)
|
if (script.mInterface)
|
||||||
removeInterface(scriptId, script);
|
removeInterface(scriptId, script);
|
||||||
script.mHiddenData[sScriptIdKey] = sol::nil;
|
script.mHiddenData[sScriptIdKey] = sol::nil;
|
||||||
|
mRemovedScriptsMemoryUsage[scriptId] = script.mStats.mMemoryUsage;
|
||||||
mScripts.erase(scriptIter);
|
mScripts.erase(scriptIter);
|
||||||
for (auto& [_, handlers] : mEngineHandlers)
|
for (auto& [_, handlers] : mEngineHandlers)
|
||||||
removeHandler(handlers->mList, scriptId);
|
removeHandler(handlers->mList, scriptId);
|
||||||
|
@ -192,7 +206,7 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LuaUtil::call(*script.mOnOverride, *prev->mInterface);
|
LuaUtil::call({ this, scriptId }, *script.mOnOverride, *prev->mInterface);
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
|
@ -203,7 +217,7 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LuaUtil::call(*next->mOnOverride, *script.mInterface);
|
LuaUtil::call({ this, nextId }, *next->mOnOverride, *script.mInterface);
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
|
@ -242,7 +256,7 @@ namespace LuaUtil
|
||||||
prevInterface = *prev->mInterface;
|
prevInterface = *prev->mInterface;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LuaUtil::call(*next->mOnOverride, prevInterface);
|
LuaUtil::call({ this, nextId }, *next->mOnOverride, prevInterface);
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
|
@ -298,16 +312,17 @@ namespace LuaUtil
|
||||||
EventHandlerList& list = it->second;
|
EventHandlerList& list = it->second;
|
||||||
for (int i = list.size() - 1; i >= 0; --i)
|
for (int i = list.size() - 1; i >= 0; --i)
|
||||||
{
|
{
|
||||||
|
const Handler& h = list[i];
|
||||||
try
|
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>())
|
if (res != sol::nil && !res.as<bool>())
|
||||||
break; // Skip other handlers if 'false' was returned.
|
break; // Skip other handlers if 'false' was returned.
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) << "] eventHandler["
|
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(h.mScriptId) << "] eventHandler[" << eventName
|
||||||
<< eventName << "] failed. " << e.what();
|
<< "] failed. " << e.what();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -322,7 +337,7 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer));
|
LuaUtil::call({ this, scriptId }, onInit, deserialize(mLua.sol(), data, mSerializer));
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
|
@ -358,7 +373,7 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sol::object state = LuaUtil::call(*script.mOnSave);
|
sol::object state = LuaUtil::call({ this, scriptId }, *script.mOnSave);
|
||||||
savedScript.mData = serialize(state, mSerializer);
|
savedScript.mData = serialize(state, mSerializer);
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
|
@ -421,7 +436,7 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
sol::object state = deserialize(mLua.sol(), scriptInfo.mSavedData->mData, mSavedDataDeserializer);
|
sol::object state = deserialize(mLua.sol(), scriptInfo.mSavedData->mData, mSavedDataDeserializer);
|
||||||
sol::object initializationData = deserialize(mLua.sol(), scriptInfo.mInitData, mSerializer);
|
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)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
|
@ -464,14 +479,18 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
for (auto& [_, script] : mScripts)
|
for (auto& [_, script] : mScripts)
|
||||||
script.mHiddenData[sScriptIdKey] = sol::nil;
|
script.mHiddenData[sScriptIdKey] = sol::nil;
|
||||||
|
*mThis = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: shouldn't be called from destructor because mEngineHandlers has pointers on
|
// Note: shouldn't be called from destructor because mEngineHandlers has pointers on
|
||||||
// external objects that are already removed during child class destruction.
|
// external objects that are already removed during child class destruction.
|
||||||
void ScriptsContainer::removeAllScripts()
|
void ScriptsContainer::removeAllScripts()
|
||||||
{
|
{
|
||||||
for (auto& [_, script] : mScripts)
|
for (auto& [id, script] : mScripts)
|
||||||
|
{
|
||||||
script.mHiddenData[sScriptIdKey] = sol::nil;
|
script.mHiddenData[sScriptIdKey] = sol::nil;
|
||||||
|
mRemovedScriptsMemoryUsage[id] = script.mStats.mMemoryUsage;
|
||||||
|
}
|
||||||
mScripts.clear();
|
mScripts.clear();
|
||||||
for (auto& [_, handlers] : mEngineHandlers)
|
for (auto& [_, handlers] : mEngineHandlers)
|
||||||
handlers->mList.clear();
|
handlers->mList.clear();
|
||||||
|
@ -540,12 +559,12 @@ namespace LuaUtil
|
||||||
auto it = script.mRegisteredCallbacks.find(callbackName);
|
auto it = script.mRegisteredCallbacks.find(callbackName);
|
||||||
if (it == script.mRegisteredCallbacks.end())
|
if (it == script.mRegisteredCallbacks.end())
|
||||||
throw std::logic_error("Callback '" + callbackName + "' doesn't exist");
|
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
|
else
|
||||||
{
|
{
|
||||||
int64_t id = std::get<int64_t>(t.mCallback);
|
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);
|
script.mTemporaryCallbacks.erase(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -571,4 +590,60 @@ namespace LuaUtil
|
||||||
updateTimerQueue(mGameTimersQueue, gameTime);
|
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<ScriptStats>& 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,11 +69,6 @@ namespace LuaUtil
|
||||||
// Present in mHiddenData even after removal of the script from ScriptsContainer.
|
// Present in mHiddenData even after removal of the script from ScriptsContainer.
|
||||||
constexpr static std::string_view sScriptDebugNameKey = "_name";
|
constexpr static std::string_view sScriptDebugNameKey = "_name";
|
||||||
|
|
||||||
struct ScriptId
|
|
||||||
{
|
|
||||||
ScriptsContainer* mContainer;
|
|
||||||
int mIndex; // index in LuaUtil::ScriptsConfiguration
|
|
||||||
};
|
|
||||||
using TimerType = ESM::LuaTimer::Type;
|
using TimerType = ESM::LuaTimer::Type;
|
||||||
|
|
||||||
// `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print`
|
// `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.
|
// 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);
|
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<ScriptStats>& stats) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
struct Handler
|
struct Handler
|
||||||
{
|
{
|
||||||
|
@ -178,7 +183,7 @@ namespace LuaUtil
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LuaUtil::call(handler.mFn, args...);
|
LuaUtil::call({ this, handler.mScriptId }, handler.mFn, args...);
|
||||||
}
|
}
|
||||||
catch (std::exception& e)
|
catch (std::exception& e)
|
||||||
{
|
{
|
||||||
|
@ -206,6 +211,7 @@ namespace LuaUtil
|
||||||
std::map<std::string, sol::main_protected_function> mRegisteredCallbacks;
|
std::map<std::string, sol::main_protected_function> mRegisteredCallbacks;
|
||||||
std::map<int64_t, sol::main_protected_function> mTemporaryCallbacks;
|
std::map<int64_t, sol::main_protected_function> mTemporaryCallbacks;
|
||||||
std::string mPath;
|
std::string mPath;
|
||||||
|
ScriptStats mStats;
|
||||||
};
|
};
|
||||||
struct Timer
|
struct Timer
|
||||||
{
|
{
|
||||||
|
@ -220,6 +226,10 @@ namespace LuaUtil
|
||||||
};
|
};
|
||||||
using EventHandlerList = std::vector<Handler>;
|
using EventHandlerList = std::vector<Handler>;
|
||||||
|
|
||||||
|
friend class LuaState;
|
||||||
|
void addCPUusage(int scriptId, int64_t CPUusage);
|
||||||
|
void addMemoryUsage(int scriptId, int64_t memoryDelta);
|
||||||
|
|
||||||
// Add to container without calling onInit/onLoad.
|
// Add to container without calling onInit/onLoad.
|
||||||
bool addScript(int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& 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> mSimulationTimersQueue;
|
||||||
std::vector<Timer> mGameTimersQueue;
|
std::vector<Timer> mGameTimersQueue;
|
||||||
int64_t mTemporaryCallbackCounter = 0;
|
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.
|
// Wrapper for a Lua function.
|
||||||
|
@ -267,8 +280,9 @@ namespace LuaUtil
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
sol::object call(Args&&... args) const
|
sol::object call(Args&&... args) const
|
||||||
{
|
{
|
||||||
if (isValid())
|
sol::optional<ScriptId> scriptId = mHiddenData[ScriptsContainer::sScriptIdKey];
|
||||||
return LuaUtil::call(mFunc, std::forward<Args>(args)...);
|
if (scriptId.has_value())
|
||||||
|
return LuaUtil::call(scriptId.value(), mFunc, std::forward<Args>(args)...);
|
||||||
else
|
else
|
||||||
Log(Debug::Debug) << "Ignored callback to the removed script "
|
Log(Debug::Debug) << "Ignored callback to the removed script "
|
||||||
<< mHiddenData.get<std::string>(ScriptsContainer::sScriptDebugNameKey);
|
<< mHiddenData.get<std::string>(ScriptsContainer::sScriptDebugNameKey);
|
||||||
|
|
Loading…
Reference in a new issue