Index: source/main.cpp =================================================================== --- source/main.cpp +++ source/main.cpp @@ -271,6 +271,23 @@ * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ +inline static double TimeTillNextFrame(double fpsLimit) +{ + return 1000.0 / fpsLimit - + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - lastFrameTime + ).count(); +} + +inline static void Wait(double durationMiliseconds) +{ + double endMiliseconds = timer_Time() + durationMiliseconds; + g_ScriptContext->MaybeIncrementalGC(durationMiliseconds); + double remainingTime = endMiliseconds - timer_Time(); + if (remainingTime > 0) + SDL_Delay(remainingTime); +} + inline static void LimitFPS() { if (g_VideoMode.IsVSyncEnabled()) @@ -283,12 +300,7 @@ if (fpsLimit < 20.0 || fpsLimit >= 360.0) return; - double wait = 1000.0 / fpsLimit - - std::chrono::duration_cast( - std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; - - if (wait > 0.0) - SDL_Delay(wait); + Wait(TimeTillNextFrame(fpsLimit)); lastFrameTime = std::chrono::high_resolution_clock::now(); } @@ -388,7 +400,7 @@ PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored - SDL_Delay(10); + Wait(10); } // this scans for changed files/directories and reloads them, thus @@ -455,6 +467,8 @@ g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); + double realTurnStart = timer_Time(); + if (g_NetClient) g_NetClient->Poll(); @@ -463,6 +477,10 @@ if (g_Game->GetTurnManager()->Update(DEFAULT_TURN_LENGTH, 1)) debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH); + double timeElapsed = timer_Time() - realTurnStart; + double remainingTime = DEFAULT_TURN_LENGTH - timeElapsed; + if (remainingTime > 0 ) + g_ScriptContext->MaybeIncrementalGC(remainingTime); g_Profiler.Frame(); if (g_Game->IsGameFinished()) 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 @@ -22,10 +22,11 @@ #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; -constexpr int DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024; +constexpr u32 DEFAULT_CONTEXT_SIZE = 16 * 1024 * 1024; +constexpr u32 DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024; /** * Abstraction around a SpiderMonkey JSContext. @@ -40,7 +41,7 @@ class ScriptContext { public: - ScriptContext(int contextSize, int heapGrowthBytesGCTrigger); + ScriptContext(u32 contextSize, u32 heapGrowthBytesGCTrigger); ~ScriptContext(); /** @@ -51,8 +52,8 @@ * @param heapGrowthBytesGCTrigger Size in bytes of cumulated allocations after which a GC will be triggered */ static std::shared_ptr CreateContext( - int contextSize = DEFAULT_CONTEXT_SIZE, - int heapGrowthBytesGCTrigger = DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER); + u32 contextSize = DEFAULT_CONTEXT_SIZE, + u32 heapGrowthBytesGCTrigger = DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER); /** @@ -60,12 +61,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 timeBudgetMilliseconds); + /** + * ShrinkingGC runs a full stop-the-world GC. Used only in testing. + */ void ShrinkingGC(); /** @@ -90,10 +91,8 @@ void PrepareZonesForIncrementalGC() const; std::list m_Realms; - int m_ContextSize; - int m_HeapGrowthBytesGCTrigger; - int m_LastGCBytes; - double m_LastGCCheck; + u32 m_HeapGrowthBytesGCTrigger; + u32 m_LastGCBytes; }; // Using a global object for the context is a workaround until Simulation, AI, etc, 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 @@ -19,6 +19,7 @@ #include "ScriptContext.h" +#include "lib/debug.h" #include "lib/alignment.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" @@ -26,6 +27,8 @@ #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptInterface.h" +#include + void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) { /** @@ -77,16 +80,14 @@ #endif } -std::shared_ptr ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger) +std::shared_ptr ScriptContext::CreateContext(u32 contextSize, u32 heapGrowthBytesGCTrigger) { return std::make_shared(contextSize, heapGrowthBytesGCTrigger); } -ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): +ScriptContext::ScriptContext(u32 contextSize, u32 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 +107,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,103 +149,63 @@ } #define GC_DEBUG_PRINT 0 -void ScriptContext::MaybeIncrementalGC(double delay) +void ScriptContext::MaybeIncrementalGC(double timeBudgetMilliseconds) { PROFILE2("MaybeIncrementalGC"); if (JS::IsIncrementalGCEnabled(m_cx)) { // 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. + // reached m_LastGCBytes + HeapGrowthBytesGCTrigger. + // 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 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 u32 gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES); + const u32 gcMaxBytes = JS_GetGCParameter(m_cx, JSGC_MAX_BYTES); + const double gcPressure = static_cast(gcBytes) / gcMaxBytes; + if (gcPressure > 0.5) + { + double minimumTimeBudgetMilliseconds = 20.0; + // Add 50% minimum time budget for each 0.1 of pressure over 0.5. + minimumTimeBudgetMilliseconds *= 1.0 + ((gcPressure - 0.5) * 5.0); + timeBudgetMilliseconds = std::max(minimumTimeBudgetMilliseconds, timeBudgetMilliseconds); + } #if GC_DEBUG_PRINT - std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; + debug_printf("gcBytes: %u KB\n", gcBytes / 1024); #endif if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) { #if GC_DEBUG_PRINT - printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); + debug_printf("Setting m_LastGCBytes: %u KB \n", gcBytes / 1024); #endif 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 - if (JS::IsIncrementalGCInProgress(m_cx) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) + // Start a new incremental GC if the JS heap size has grown enough for a GC to make sense. + const bool gcInProgress = JS::IsIncrementalGCInProgress(m_cx); + if (gcInProgress || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) { #if GC_DEBUG_PRINT - if (JS::IsIncrementalGCInProgress(m_cx)) - printf("An incremental GC cycle is in progress. \n"); + if (gcInProgress) + debug_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", + debug_printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n" + " JSGC_BYTES: %u KB \n m_LastGCBytes: %u KB \n m_HeapGrowthBytesGCTrigger: %u KB \n gcPressure: %lf% ", 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 (!gcInProgress) + JS::StartIncrementalGC(m_cx, GC_NORMAL, JS::GCReason::API, timeBudgetMilliseconds); 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, timeBudgetMilliseconds); m_LastGCBytes = gcBytes; } } Index: source/simulation2/Simulation2.cpp =================================================================== --- source/simulation2/Simulation2.cpp +++ source/simulation2/Simulation2.cpp @@ -492,21 +492,6 @@ } } - // 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 - // - // (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); - if (m_EnableOOSLog) DumpState();