diff --git a/Data/Base.rte/Activities/AIBattle.lua b/Data/Base.rte/Activities/AIBattle.lua index fd33794a68..8a67fc1065 100644 --- a/Data/Base.rte/Activities/AIBattle.lua +++ b/Data/Base.rte/Activities/AIBattle.lua @@ -10,8 +10,10 @@ Human players can just watch, by leaving every team to the CPU, or take a team f function AIBattle:StartActivity() -- How long a team waits between its reinforcements, and how many units it can have before it stops getting more. - self.reinforcementInterval = 20000; - self.maxUnitsPerTeam = 8; + -- Setting the environment variable CCCP_AI_BATTLE_SCALE to a number multiplies both how often reinforcements come and how many units each team can have, for battles big enough to put the game under load. + local scale = tonumber(os.getenv("CCCP_AI_BATTLE_SCALE") or "") or 1; + self.reinforcementInterval = 20000 / scale; + self.maxUnitsPerTeam = math.floor(8 * scale); self.deliveryCreationHandler = require("Activities/Utility/DeliveryCreationHandler"); self.deliveryCreationHandler:Initialize(self); diff --git a/Source/Managers/LuaMan.cpp b/Source/Managers/LuaMan.cpp index 88bab187f0..7fe6bbd516 100644 --- a/Source/Managers/LuaMan.cpp +++ b/Source/Managers/LuaMan.cpp @@ -31,6 +31,7 @@ void LuaStateWrapper::Clear() { m_LastError.clear(); m_CurrentlyRunningScriptPath = ""; m_RunningAI = false; + m_HeapSizeAfterGarbageCollectionStep = 0; } void LuaStateWrapper::Initialize() { @@ -38,7 +39,7 @@ void LuaStateWrapper::Initialize() { luabind::open(m_State); tracy::LuaRegister(m_State); - // We do async GC, but we still keep the normal GC on so it can catch any big spikes or runaway allocs + // Lua's own garbage collector is left on until StepGarbageCollection takes over from it, so it still collects while scripts are being loaded. //lua_gc(m_State, LUA_GCSTOP, 0); const luaL_Reg libsToLoad[] = { @@ -890,6 +891,23 @@ void LuaStateWrapper::Update() { m_AddedRegisteredMOs.clear(); } +void LuaStateWrapper::StepGarbageCollection() { + std::lock_guard lock(m_Mutex); + + size_t heapSize = GetHeapSize(); + if (m_HeapSizeAfterGarbageCollectionStep != 0) { + // Lua's own collector does its step multiplier's worth of collecting for each kilobyte allocated, and a step of that many kilobytes does the same, so this keeps up with what was allocated since the last step just as it would have. + // With nothing allocated it still does a single step, so that a cycle always gets finished. + // Unlike Lua's own collector, it goes straight on from one cycle into the next rather than waiting for the heap to grow first. That wait goes by an estimate of the heap that leaves out what's still to be finalized, which there's no way to get from outside Lua, and going by the whole heap instead lets it grow without end. + size_t allocatedSinceLastStep = heapSize > m_HeapSizeAfterGarbageCollectionStep ? heapSize - m_HeapSizeAfterGarbageCollectionStep : 0; + lua_gc(m_State, LUA_GCSTEP, static_cast(allocatedSinceLastStep / 1024)); + } + + // Stop the Lua GC from triggering manually so we don't get spikes in work that the main thread may be waiting on + lua_gc(m_State, LUA_GCSTOP, 0); + m_HeapSizeAfterGarbageCollectionStep = GetHeapSize(); +} + void LuaStateWrapper::ClearScriptTimings() { m_ScriptTimings.clear(); } @@ -1005,6 +1023,11 @@ std::string LuaStateWrapper::DescribeLuaStack() { return stackDescription.str(); } +size_t LuaStateWrapper::GetHeapSize() { + // Lua only gives the heap size in whole kilobytes, plus the bytes left over from that, so they're put back together to get it in bytes. + return static_cast(lua_gc(m_State, LUA_GCCOUNT, 0)) * 1024 + static_cast(lua_gc(m_State, LUA_GCCOUNTB, 0)); +} + LuaMan::LuaMan() { Clear(); } @@ -1317,9 +1340,7 @@ void LuaMan::StartAsyncGarbageCollection() { m_GarbageCollectionTask.push_back( g_ThreadMan.GetPriorityThreadPool().submit([luaState]() { ZoneScopedN("Lua Garbage Collection"); - std::lock_guard lock(luaState->GetMutex()); - lua_gc(luaState->GetLuaState(), LUA_GCSTEP, 100); - lua_gc(luaState->GetLuaState(), LUA_GCSTOP, 0); + luaState->StepGarbageCollection(); })); } } diff --git a/Source/Managers/LuaMan.h b/Source/Managers/LuaMan.h index f7ee1fe8d6..544d66deb2 100644 --- a/Source/Managers/LuaMan.h +++ b/Source/Managers/LuaMan.h @@ -172,6 +172,10 @@ namespace RTE { /// Updates this Lua state. void Update(); + /// Does garbage collection on this Lua state, as much of it as Lua's own collector would have done for what was allocated since the last time this was called, and stops Lua's own collector from doing any in the meantime. + /// This way garbage collection only happens here, where it can be done alongside other work, but no more of it is done than is needed to keep up. + void StepGarbageCollection(); + /// Clears m_ScriptTimings. void ClearScriptTimings(); #pragma endregion @@ -265,6 +269,10 @@ namespace RTE { /// @return A string that describes the current state of the Lua stack. std::string DescribeLuaStack(); + /// Gets the size of this Lua state's heap. + /// @return The size of this Lua state's heap, in bytes. + size_t GetHeapSize(); + /// Clears all the member variables of this LuaStateWrapper, effectively resetting the members of this abstraction level only. void Clear(); @@ -287,6 +295,8 @@ namespace RTE { std::unordered_map m_ScriptTimings; //!< Internal map of script timings. + size_t m_HeapSizeAfterGarbageCollectionStep; //!< The size of this Lua state's heap, in bytes, at the end of the last StepGarbageCollection. 0 until it's first called. + // For determinism, every Lua state has it's own random number generator. RandomGenerator m_RandomGenerator; //!< The random number generator used for this lua state. RandomGenerator m_AIRandomGenerator; //!< The random number generator used for this lua state while it is running AI.