Index: source/scriptinterface/ScriptContext.h =================================================================== --- source/scriptinterface/ScriptContext.h +++ source/scriptinterface/ScriptContext.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -60,12 +60,9 @@ * 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. + * This can and should be called quite regularly. The timeBudget paramater specifies the maximum time the GC is allowed to run. */ - void MaybeIncrementalGC(double delay); + void MaybeIncrementalGC(double timeBudget); void ShrinkingGC(); /** @@ -91,8 +88,8 @@ std::list m_Realms; int m_ContextSize; - int m_HeapGrowthBytesGCTrigger; - int m_LastGCBytes; + uint32_t m_HeapGrowthBytesGCTrigger; + uint32_t m_LastGCBytes; double m_LastGCCheck; }; Index: source/scriptinterface/ScriptContext.cpp =================================================================== --- source/scriptinterface/ScriptContext.cpp +++ source/scriptinterface/ScriptContext.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -20,12 +20,15 @@ #include "ScriptContext.h" #include "lib/alignment.h" +#include "lib/types.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptInterface.h" +#include + void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) { /** @@ -148,7 +151,7 @@ } #define GC_DEBUG_PRINT 0 -void ScriptContext::MaybeIncrementalGC(double delay) +void ScriptContext::MaybeIncrementalGC(double timeBudget) { PROFILE2("MaybeIncrementalGC"); @@ -160,18 +163,16 @@ // 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 int GCSliceTimeBudget = 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); + const uint32_t gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES); + const uint32_t gcMaxBytes = JS_GetGCParameter(m_cx, JSGC_MAX_BYTES); + const double gcPressure = static_cast(gcBytes) / gcMaxBytes; + double minimumTimeBugdget = 20; + if (gcPressure > 0.5) + // Add 50% minimum time budget for each 0.1 of pressure over 0.5. + minimumTimeBugdget *= 1 + ((gcPressure - 0.5) * 5); + timeBudget = std::max(minimumTimeBugdget, timeBudget); #if GC_DEBUG_PRINT std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; @@ -185,9 +186,9 @@ m_LastGCBytes = gcBytes; } - // Run an additional incremental GC slice if the currently running incremental GC isn't over yet + // 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 + // 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 @@ -195,56 +196,18 @@ 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", + " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n gcPressure: %f% ", gcBytes / 1024, m_LastGCBytes / 1024, - m_HeapGrowthBytesGCTrigger / 1024); + m_HeapGrowthBytesGCTrigger / 1024, + gcPressure); #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); - } - } - } + PrepareZonesForIncrementalGC(); + if (!JS::IsIncrementalGCInProgress(m_cx)) + JS::StartIncrementalGC(m_cx, GC_NORMAL, JS::GCReason::API, timeBudget); 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, GC_NORMAL, JS::GCReason::API, GCSliceTimeBudget); - else - JS::IncrementalGCSlice(m_cx, JS::GCReason::API, GCSliceTimeBudget); - } + JS::IncrementalGCSlice(m_cx, JS::GCReason::API, timeBudget); m_LastGCBytes = gcBytes; } } Index: source/simulation2/Simulation2.cpp =================================================================== --- source/simulation2/Simulation2.cpp +++ source/simulation2/Simulation2.cpp @@ -363,6 +363,8 @@ PROFILE3("sim update"); PROFILE2_ATTR("turn %d", (int)m_TurnNumber); + const auto turnRealLengthStart = std::chrono::system_clock::now(); + fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; /* @@ -492,20 +494,15 @@ } } - // Run the GC occasionally - // No delay because a lot of garbage accumulates in one turn and in non-visual replays there are - // much more turns in the same time than in normal games. - // Every 500 turns we run a shrinking GC, which decommits unused memory and frees all JIT code. - // Based on testing, this seems to be a good compromise between memory usage and performance. - // Also check the comment about gcPreserveCode in the ScriptInterface code and this forum topic: - // http://www.wildfiregames.com/forum/index.php?showtopic=18466&p=300323 - // + // Calculate how much time we have this thread idle for before the next turn is scheduled to start + // (TODO: we ought to schedule this for a frame where we're not // running the sim update, to spread the load) - if (m_TurnNumber % 500 == 0) - scriptInterface.GetContext()->ShrinkingGC(); - else - scriptInterface.GetContext()->MaybeIncrementalGC(0.0f); + + const std::chrono::duration turnRealLength = std::chrono::system_clock::now() - turnRealLengthStart; + // Reserve 10 ms for overhead, may be excessive / unneeded. + const double timeBudget = turnLength - 10 - static_cast(turnRealLength.count()); + scriptInterface.GetContext()->MaybeIncrementalGC(timeBudget); if (m_EnableOOSLog) DumpState();