From 635ad2c36c0f5801890f6c3d33140b023ef25d3a Mon Sep 17 00:00:00 2001 From: Causeless Date: Sun, 25 Oct 2026 08:43:50 +0000 Subject: [PATCH 1/5] Made the async Lua garbage collection only collect as much as is needed Every update, each Lua state did a garbage collection step of 100 KB, which LuaJIT works off at its step multiplier: about 200 KB of marking and sweeping per Lua state per update, whether or not the state had made any garbage. So every Lua state was always partway through a collection cycle, going over everything in it again every few updates, and the main thread waited on the slowest of them at the start of the next update. Each Lua state now does the collecting that Lua's own collector would have done for what it allocated since its last step, and at least a single step, so that cycles always get finished. It's still done alongside other work, and Lua's own collector is still kept from collecting in the middle of scripts. Co-Authored-By: Claude Opus 5.5 --- Source/Managers/LuaMan.cpp | 30 +++++++++++++++++++++++++----- Source/Managers/LuaMan.h | 10 ++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Source/Managers/LuaMan.cpp b/Source/Managers/LuaMan.cpp index f0911901e7..8402ac1e59 100644 --- a/Source/Managers/LuaMan.cpp +++ b/Source/Managers/LuaMan.cpp @@ -26,6 +26,7 @@ void LuaStateWrapper::Clear() { m_TempEntityVector.clear(); m_LastError.clear(); m_CurrentlyRunningScriptPath = ""; + m_HeapSizeAfterGarbageCollectionStep = 0; } void LuaStateWrapper::Initialize() { @@ -33,8 +34,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_gc(m_State, LUA_GCSTOP, 0); + // Lua's own garbage collector is left on until StepGarbageCollection takes over from it, so it still collects while scripts are being loaded. const luaL_Reg libsToLoad[] = { // Basic Lua libraries @@ -872,6 +872,24 @@ void LuaStateWrapper::Update() { m_AddedRegisteredMOs.clear(); } +void LuaStateWrapper::StepGarbageCollection() { + std::lock_guard lock(m_Mutex); + + // The first time, there's nothing to go by yet for how much was allocated, since Lua's own collector was what collected while scripts were being loaded. + 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)); + } + + // Lua's own collector would otherwise collect whenever an allocation took it over its threshold, in the middle of whatever script was running. + lua_gc(m_State, LUA_GCSTOP, 0); + m_HeapSizeAfterGarbageCollectionStep = GetHeapSize(); +} + void LuaStateWrapper::ClearScriptTimings() { m_ScriptTimings.clear(); } @@ -987,6 +1005,10 @@ std::string LuaStateWrapper::DescribeLuaStack() { return stackDescription.str(); } +size_t LuaStateWrapper::GetHeapSize() { + return static_cast(lua_gc(m_State, LUA_GCCOUNT, 0)) * 1024 + static_cast(lua_gc(m_State, LUA_GCCOUNTB, 0)); +} + LuaMan::LuaMan() { Clear(); } @@ -1299,9 +1321,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 1d2b3cbbbc..973457b483 100644 --- a/Source/Managers/LuaMan.h +++ b/Source/Managers/LuaMan.h @@ -152,6 +152,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 @@ -245,6 +249,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(); @@ -267,6 +275,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. }; From c04bfc547b03f6d9d78b052b742e95248a508e85 Mon Sep 17 00:00:00 2001 From: Causeless Date: Sun, 25 Oct 2026 08:55:14 +0000 Subject: [PATCH 2/5] Made the AI Battle's size settable, for battles that put the game under load Setting the environment variable CCCP_AI_BATTLE_SCALE to a number multiplies both how often each team gets reinforcements and how many units it can have. Co-Authored-By: Claude Opus 5.5 --- Data/Base.rte/Activities/AIBattle.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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); From 790a257b1c49a706dd7fee20318c12ae874cb34d Mon Sep 17 00:00:00 2001 From: Causeless Date: Sun, 25 Oct 2026 15:08:23 +0000 Subject: [PATCH 3/5] Reworded the garbage collection comments Co-Authored-By: Claude Opus 5.5 --- Source/Managers/LuaMan.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Managers/LuaMan.cpp b/Source/Managers/LuaMan.cpp index 8402ac1e59..ca7017713e 100644 --- a/Source/Managers/LuaMan.cpp +++ b/Source/Managers/LuaMan.cpp @@ -875,7 +875,6 @@ void LuaStateWrapper::Update() { void LuaStateWrapper::StepGarbageCollection() { std::lock_guard lock(m_Mutex); - // The first time, there's nothing to go by yet for how much was allocated, since Lua's own collector was what collected while scripts were being loaded. 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. @@ -885,7 +884,7 @@ void LuaStateWrapper::StepGarbageCollection() { lua_gc(m_State, LUA_GCSTEP, static_cast(allocatedSinceLastStep / 1024)); } - // Lua's own collector would otherwise collect whenever an allocation took it over its threshold, in the middle of whatever script was running. + // 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(); } From fab6bc1e7156f2e2ce813dd86ed3b49eb255dedb Mon Sep 17 00:00:00 2001 From: Causeless Date: Sun, 25 Oct 2026 15:09:28 +0000 Subject: [PATCH 4/5] Commented how GetHeapSize gets the heap size in bytes Co-Authored-By: Claude Opus 5.5 --- Source/Managers/LuaMan.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Managers/LuaMan.cpp b/Source/Managers/LuaMan.cpp index ca7017713e..d63ee7c43c 100644 --- a/Source/Managers/LuaMan.cpp +++ b/Source/Managers/LuaMan.cpp @@ -1005,6 +1005,7 @@ std::string LuaStateWrapper::DescribeLuaStack() { } 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)); } From 6dc592fff7dc7492a20cad6c0a82a31e808adc76 Mon Sep 17 00:00:00 2001 From: Causeless Date: Sun, 25 Oct 2026 15:11:15 +0000 Subject: [PATCH 5/5] Kept the commented-out LUA_GCSTOP below the comment on Lua's own garbage collector It shows what the comment is about leaving out. Co-Authored-By: Claude Opus 5.5 --- Source/Managers/LuaMan.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Managers/LuaMan.cpp b/Source/Managers/LuaMan.cpp index d63ee7c43c..3e640b5fd4 100644 --- a/Source/Managers/LuaMan.cpp +++ b/Source/Managers/LuaMan.cpp @@ -35,6 +35,7 @@ void LuaStateWrapper::Initialize() { tracy::LuaRegister(m_State); // 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[] = { // Basic Lua libraries