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 @@ -65,7 +65,7 @@ * 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. */ - void MaybeIncrementalGC(double delay); + void MaybeIncrementalGC(double timeBudget); void ShrinkingGC(); /** 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 @@ -148,7 +148,7 @@ } #define GC_DEBUG_PRINT 0 -void ScriptContext::MaybeIncrementalGC(double delay) +void ScriptContext::MaybeIncrementalGC(double timeBudget) { PROFILE2("MaybeIncrementalGC"); @@ -160,18 +160,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); + int gcMaxBytes = JS_GetGCParameter(m_cx, JSGC_MAX_BYTES); + float gcPressure = 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 + ((gcPressure - 0.5) * 5); + timeBudget = std::max(minimumTimeBugdget, timeBudget); #if GC_DEBUG_PRINT std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; @@ -195,56 +193,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); + 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); + + std::chrono::duration turnRealLength = std::chrono::system_clock::now() - turnRealLengthStart; + // reserve 10 ms for overhead + double timeBudget = turnLength - static_cast(turnRealLength.count() - 10); + scriptInterface.GetContext()->MaybeIncrementalGC(timeBudget); if (m_EnableOOSLog) DumpState();