Stop running shrinking gcs & simplify GC logic.

Shrinking GCs dump the JITted code, which leads to redundant recompilations, lowers performance, and makes profiling JS more difficult.
They may still happen if the runtime is at risk of OOM.

(cherry picked from commit af32d386b9)
Signed-off-by: Itms <itms@wildfiregames.com>
This commit is contained in:
Lancelot de Ferrière 2024-11-23 18:36:23 +01:00 committed by Itms
parent b78b666797
commit e4b604982a
No known key found for this signature in database
GPG key ID: C7E52BD14CE14E09
8 changed files with 96 additions and 122 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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.

View file

@ -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

View file

@ -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<SimulationCommand> commands;

View file

@ -80,12 +80,12 @@ void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const J
#endif
}
std::shared_ptr<ScriptContext> ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger)
std::shared_ptr<ScriptContext> ScriptContext::CreateContext(int contextSize, uint32_t heapGrowthBytesGCTrigger)
{
return std::make_shared<ScriptContext>(contextSize, heapGrowthBytesGCTrigger);
}
ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger):
ScriptContext::ScriptContext(int contextSize, uint32_t heapGrowthBytesGCTrigger):
m_JobQueue{std::make_unique<Script::JobQueue>()},
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;
}
}

View file

@ -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<ScriptContext> 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<JS::Realm*> 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,

View file

@ -489,20 +489,9 @@ void CSimulation2Impl::Update(int turnLength, const std::vector<SimulationComman
}
}
// 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);
scriptInterface.GetContext().MaybeIncrementalGC();
if (m_EnableOOSLog)
DumpState();