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 @@ -18,10 +18,12 @@ #ifndef INCLUDED_SCRIPTCONTEXT #define INCLUDED_SCRIPTCONTEXT +#include "lib/types.h" #include "ScriptTypes.h" #include "ScriptExtraHeaders.h" #include +#include // 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; @@ -60,12 +62,12 @@ * 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); + /** + * ShrinkingGC runs a full stop-the-world GC. Used only in testing. + */ void ShrinkingGC(); /** @@ -91,8 +93,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)) { /** @@ -85,8 +88,7 @@ ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): m_LastGCBytes(0), m_LastGCCheck(0.0f), - m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), - m_ContextSize(contextSize) + m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger) { ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptContexts!"); @@ -106,7 +108,7 @@ JS::SetGCSliceCallback(m_cx, GCSliceCallbackHook); - JS_SetGCParameter(m_cx, JSGC_MAX_BYTES, m_ContextSize); + JS_SetGCParameter(m_cx, JSGC_MAX_BYTES, contextSize); JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_INCREMENTAL); JS_SetOffthreadIonCompilationEnabled(m_cx, true); @@ -148,7 +150,7 @@ } #define GC_DEBUG_PRINT 0 -void ScriptContext::MaybeIncrementalGC(double delay) +void ScriptContext::MaybeIncrementalGC(double timeBudget) { PROFILE2("MaybeIncrementalGC"); @@ -160,18 +162,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.0; + if (gcPressure > 0.5) + // Add 50% minimum time budget for each 0.1 of pressure over 0.5. + minimumTimeBugdget *= 1.0 + ((gcPressure - 0.5) * 5.0); + timeBudget = std::max(minimumTimeBugdget, timeBudget); #if GC_DEBUG_PRINT std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; @@ -185,9 +185,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 +195,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 double turnRealLengthStart = timer_Time(); + 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); + + double turnRealLength = timer_Time() - turnRealLengthStart; + // Reserve 10 ms for overhead, may be excessive / unneeded. + const double timeBudget = turnLength - 10 - turnRealLength; + scriptInterface.GetContext()->MaybeIncrementalGC(timeBudget); if (m_EnableOOSLog) DumpState();