diff --git a/source/gui/GUIManager.cpp b/source/gui/GUIManager.cpp index 8399b4b328..5cd752810e 100644 --- a/source/gui/GUIManager.cpp +++ b/source/gui/GUIManager.cpp @@ -370,7 +370,7 @@ void CGUIManager::TickObjects() // We share the script context with everything else that runs in the same thread. // This call makes sure we trigger GC regularly even if the simulation is not running. - m_ScriptContext.MaybeIncrementalGC(1.0f); + m_ScriptContext.MaybeIncrementalGC(); // Save an immutable copy so iterators aren't invalidated by tick handlers PageStackType pageStack = m_PageStack; diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 2d1aaaf758..5c3374c548 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -424,7 +424,7 @@ bool CNetServerWorker::RunStep() // (Do as little work as possible while the mutex is held open, // to avoid performance problems and deadlocks.) - m_ScriptInterface->GetContext().MaybeIncrementalGC(0.5f); + m_ScriptInterface->GetContext().MaybeIncrementalGC(); ScriptRequest rq(m_ScriptInterface); diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index acd736a978..cb56d30a58 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -43,6 +43,7 @@ #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" @@ -331,6 +332,11 @@ PSRETURN CGame::ReallyStartGame() // all be invisible) Interpolate(0, 0); + // Run a shrinking GC to reset the memory before starting the game proper. + // This also clears JIT code, which seems like a good idea as the game init + // might have different patterns to the game itself. + m_Simulation2->GetScriptInterface().GetContext().ShrinkingGC(); + m_GameStarted = true; // Preload resources to avoid blinking on a first game frame. diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 5d4d38e7b8..21c6218615 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -535,7 +535,7 @@ bool Init(const CmdLineArgs& args, int flags) // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; - const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; + const int heapGrowthBytesGCTrigger = 12 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); // On the first Init (INIT_MODS), check for command-line arguments diff --git a/source/ps/Replay.cpp b/source/ps/Replay.cpp index 1cc9c26fe1..6c71f7de7d 100644 --- a/source/ps/Replay.cpp +++ b/source/ps/Replay.cpp @@ -197,7 +197,7 @@ void CReplayPlayer::Replay(const bool serializationtest, const int rejointesttur g_ProfileViewer.AddRootTable(g_ScriptStatsTable); const int contextSize = 384 * 1024 * 1024; - const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; + const int heapGrowthBytesGCTrigger = 12 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); std::vector commands; diff --git a/source/scriptinterface/ScriptContext.cpp b/source/scriptinterface/ScriptContext.cpp index 5f285781a4..29303ebc1f 100644 --- a/source/scriptinterface/ScriptContext.cpp +++ b/source/scriptinterface/ScriptContext.cpp @@ -80,12 +80,12 @@ void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const J #endif } -std::shared_ptr ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger) +std::shared_ptr ScriptContext::CreateContext(int contextSize, uint32_t heapGrowthBytesGCTrigger) { return std::make_shared(contextSize, heapGrowthBytesGCTrigger); } -ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): +ScriptContext::ScriptContext(int contextSize, uint32_t heapGrowthBytesGCTrigger): m_JobQueue{std::make_unique()}, m_ContextSize{contextSize}, m_HeapGrowthBytesGCTrigger{heapGrowthBytesGCTrigger} @@ -112,6 +112,14 @@ ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): JS_SetGCParameter(m_cx, JSGC_INCREMENTAL_GC_ENABLED, true); JS_SetGCParameter(m_cx, JSGC_PER_ZONE_GC_ENABLED, false); + // Attempt to turn off Spidermonkey-led GCs. + JS_SetGCParameter(m_cx, JSGC_HEAP_GROWTH_FACTOR, contextSize); + JS_SetGCParameter(m_cx, JSGC_LOW_FREQUENCY_HEAP_GROWTH, 300); + JS_SetGCParameter(m_cx, JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH, 500); + JS_SetGCParameter(m_cx, JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH, 300); + JS_SetGCParameter(m_cx, JSGC_SLICE_TIME_BUDGET_MS, 10); + + JS_SetOffthreadIonCompilationEnabled(m_cx, true); // For GC debugging: @@ -168,105 +176,71 @@ void ScriptContext::UnRegisterRealm(JS::Realm* realm) } #define GC_DEBUG_PRINT 0 -void ScriptContext::MaybeIncrementalGC(double delay) +void ScriptContext::MaybeIncrementalGC() { PROFILE2("MaybeIncrementalGC"); - if (JS::IsIncrementalGCEnabled(m_cx)) + if (!JS::IsIncrementalGCEnabled(m_cx)) + return; + + // The idea is to get the heap size after a completed GC and trigger the next GC + // when the heap size has reached m_LastGCBytes + X. + // Spidermonkey allocates memory arenas of 4KB for JS heap data. + // At the end of a GC, any such arena that became empty is freed. + // On shrinking GCs, spidermonkey further defragments the arenas, which effectively frees more memory but costs time. + // In practice, shrinking GCs also dump JITted code and the defragmentation is not worth it for 0 A.D. + // The regular GCs also free quite a bit of memory anyways, and non-full arenas get used for new objects. + + const uint32_t gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES); + +#if GC_DEBUG_PRINT + printf("gcBytes: %i KB, last of %i KB\n", gcBytes / 1024, m_LastGCBytes / 1024); +#endif + + // The memory freeing happens mostly in the background, so we can't rely on the value on the last incremental slice. + // To fix that, just remember a 'minimum' value. + if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) { - // The idea is to get the heap size after a completed GC and trigger the next GC when the heap size has - // reached m_LastGCBytes + X. - // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in. - // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set). - // While the sweeping is happening we already run scripts again and produce new garbage. - - const js::SliceBudget GCSliceTimeBudget = js::SliceBudget(js::TimeBudget(30)); // Milliseconds an incremental slice is allowed to run - - // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC - // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute - // the load well enough and low enough to make sure we don't run out of memory before we can start with the - // sweeping. - if (timer_Time() - m_LastGCCheck < delay) - return; - - m_LastGCCheck = timer_Time(); - - int gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES); - #if GC_DEBUG_PRINT - std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; + printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); +#endif + m_LastGCBytes = gcBytes; + } + + // Run an additional incremental GC slice if the currently running incremental GC isn't over yet + // ... or + // start a new incremental GC if the JS heap size has grown enough for a GC to make sense + if (JS::IsIncrementalGCInProgress(m_cx) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) + { +#if GC_DEBUG_PRINT + if (JS::IsIncrementalGCInProgress(m_cx)) + printf("An incremental GC cycle is in progress. \n"); + else + printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n" + " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n", + gcBytes / 1024, + m_LastGCBytes / 1024, + m_HeapGrowthBytesGCTrigger / 1024); #endif - if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) - { #if GC_DEBUG_PRINT - printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); -#endif - m_LastGCBytes = gcBytes; - } - - // Run an additional incremental GC slice if the currently running incremental GC isn't over yet - // ... or - // start a new incremental GC if the JS heap size has grown enough for a GC to make sense - if (JS::IsIncrementalGCInProgress(m_cx) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) - { -#if GC_DEBUG_PRINT - if (JS::IsIncrementalGCInProgress(m_cx)) - printf("An incremental GC cycle is in progress. \n"); - else - printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n" - " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n", - gcBytes / 1024, - m_LastGCBytes / 1024, - m_HeapGrowthBytesGCTrigger / 1024); + if (!JS::IsIncrementalGCInProgress(m_cx)) + printf("Starting incremental GC \n"); + else + printf("Running incremental GC slice \n"); #endif - // A hack to make sure we never exceed the context size because we can't collect the memory - // fast enough. - if (gcBytes > m_ContextSize / 2) - { - if (JS::IsIncrementalGCInProgress(m_cx)) - { -#if GC_DEBUG_PRINT - printf("Finishing incremental GC because gcBytes > m_ContextSize / 2. \n"); -#endif - PrepareZonesForIncrementalGC(); - JS::FinishIncrementalGC(m_cx, JS::GCReason::API); - } - else - { - if (gcBytes > m_ContextSize * 0.75) - { - ShrinkingGC(); -#if GC_DEBUG_PRINT - printf("Running shrinking GC because gcBytes > m_ContextSize * 0.75. \n"); -#endif - } - else - { -#if GC_DEBUG_PRINT - printf("Running full GC because gcBytes > m_ContextSize / 2. \n"); -#endif - JS_GC(m_cx); - } - } - } - else - { -#if GC_DEBUG_PRINT - if (!JS::IsIncrementalGCInProgress(m_cx)) - printf("Starting incremental GC \n"); - else - printf("Running incremental GC slice \n"); -#endif - PrepareZonesForIncrementalGC(); - if (!JS::IsIncrementalGCInProgress(m_cx)) - JS::StartIncrementalGC(m_cx, JS::GCOptions::Normal, JS::GCReason::API, GCSliceTimeBudget); - else - JS::IncrementalGCSlice(m_cx, JS::GCReason::API, GCSliceTimeBudget); - } - m_LastGCBytes = gcBytes; - } + // There is a tradeoff between this time and the number of frames we must run GCs on, but overall we should prioritize smooth framerates. + const js::SliceBudget GCSliceTimeBudget = js::SliceBudget(js::TimeBudget(6)); // Milliseconds an incremental slice is allowed to run. SM respects this fairly well. + + PrepareZonesForIncrementalGC(); + if (!JS::IsIncrementalGCInProgress(m_cx)) + JS::StartIncrementalGC(m_cx, JS::GCOptions::Normal, JS::GCReason::API, GCSliceTimeBudget); + else + JS::IncrementalGCSlice(m_cx, JS::GCReason::API, GCSliceTimeBudget); + + // Reset this here so that the minimum gets cleared. + m_LastGCBytes = gcBytes; } } diff --git a/source/scriptinterface/ScriptContext.h b/source/scriptinterface/ScriptContext.h index c1b08058f2..017e75273f 100644 --- a/source/scriptinterface/ScriptContext.h +++ b/source/scriptinterface/ScriptContext.h @@ -25,7 +25,7 @@ // Those are minimal defaults. The runtime for the main game is larger and GCs upon a larger growth. constexpr int DEFAULT_CONTEXT_SIZE = 16 * 1024 * 1024; -constexpr int DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024; +constexpr uint32_t DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024; namespace Script { @@ -45,7 +45,7 @@ class JobQueue; class ScriptContext { public: - ScriptContext(int contextSize, int heapGrowthBytesGCTrigger); + ScriptContext(int contextSize, uint32_t heapGrowthBytesGCTrigger); ~ScriptContext(); /** @@ -57,20 +57,26 @@ public: */ static std::shared_ptr CreateContext( int contextSize = DEFAULT_CONTEXT_SIZE, - int heapGrowthBytesGCTrigger = DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER); + uint32_t heapGrowthBytesGCTrigger = DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER); /** - * MaybeIncrementalGC tries to determine whether a context-wide garbage collection would free up enough memory to - * be worth the amount of time it would take. It does this with our own logic and NOT some predefined JSAPI logic because - * such functionality currently isn't available out of the box. - * It does incremental GC which means it will collect one slice each time it's called until the garbage collection is done. - * This can and should be called quite regularly. The delay parameter allows you to specify a minimum time since the last GC - * in seconds (the delay should be a fraction of a second in most cases though). - * It will only start a new incremental GC or another GC slice if this time is exceeded. The user of this function is - * responsible for ensuring that GC can run with a small enough delay to get done with the work. + * MaybeIncrementalGC checks if running a GC is worth the time that will take. + * The logic is custom as Spidermonkey tends to assume 'idle time' will exist, + * which is a thing in websites but not really in 0 A.D. + * This can have a few behaviours: + * - doing nothing + * - starting a new incremental GC + * - running a GC slice + * - finishing the incremental GC + * For details, check the SM doc in e.g. GC.cpp and GCapi.cpp + */ + void MaybeIncrementalGC(); + + /** + * Does a non-incremental, shrinking GC. + * A shrinking GC dumps JIT code and tries to defragment memory. */ - void MaybeIncrementalGC(double delay); void ShrinkingGC(); /** @@ -105,9 +111,8 @@ private: std::list m_Realms; int m_ContextSize; - int m_HeapGrowthBytesGCTrigger; - int m_LastGCBytes{0}; - double m_LastGCCheck{0.0}; + uint32_t m_HeapGrowthBytesGCTrigger; + uint32_t m_LastGCBytes{0}; }; // Using a global object for the context is a workaround until Simulation, AI, etc, diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index 9c43445d3f..0aac3e7e9a 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -489,20 +489,9 @@ void CSimulation2Impl::Update(int turnLength, const std::vector