2025-05-27 00:22:17 -07:00
/* Copyright (C) 2025 Wildfire Games.
2023-12-02 16:30:12 -08:00
* This file is part of 0 A . D .
2014-11-13 03:19:28 -08:00
*
2023-12-02 16:30:12 -08:00
* 0 A . D . is free software : you can redistribute it and / or modify
2014-11-13 03:19:28 -08:00
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 2 of the License , or
* ( at your option ) any later version .
*
2023-12-02 16:30:12 -08:00
* 0 A . D . is distributed in the hope that it will be useful ,
2014-11-13 03:19:28 -08:00
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
2023-12-02 16:30:12 -08:00
* along with 0 A . D . If not , see < http : //www.gnu.org/licenses/>.
2014-11-13 03:19:28 -08:00
*/
# include "precompiled.h"
2020-11-14 02:57:50 -08:00
# include "ScriptContext.h"
2014-11-13 03:19:28 -08:00
2021-04-16 09:49:18 -07:00
# include "lib/alignment.h"
2014-11-13 03:19:28 -08:00
# include "ps/GameSetup/Config.h"
# include "ps/Profile.h"
2025-01-15 11:38:37 -08:00
# include "scriptinterface/ModuleLoader.h"
2024-07-08 12:07:04 -07:00
# include "scriptinterface/Promises.h"
2020-11-30 01:03:20 -08:00
# include "scriptinterface/ScriptExtraHeaders.h"
2016-08-06 08:41:59 -07:00
# include "scriptinterface/ScriptEngine.h"
2020-11-14 00:46:32 -08:00
# include "scriptinterface/ScriptInterface.h"
2014-11-13 03:19:28 -08:00
2025-04-05 00:33:47 -07:00
# include "js/friend/PerformanceHint.h"
2025-01-15 11:38:37 -08:00
# include "js/Modules.h"
2025-04-05 00:33:47 -07:00
2020-11-18 06:39:04 -08:00
void GCSliceCallbackHook ( JSContext * UNUSED ( cx ) , JS : : GCProgress progress , const JS : : GCDescription & UNUSED ( desc ) )
2014-11-13 03:19:28 -08:00
{
2020-11-24 07:47:03 -08:00
/**
* From the GCAPI . h file :
* > During GC , the GC is bracketed by GC_CYCLE_BEGIN / END callbacks . Each
* > slice between those ( whether an incremental or the sole non - incremental
* > slice ) is bracketed by GC_SLICE_BEGIN / GC_SLICE_END .
* Thus , to safely monitor GCs , we need to profile SLICE_X calls .
*/
2014-11-13 03:19:28 -08:00
if ( progress = = JS : : GC_SLICE_BEGIN )
{
2021-01-10 00:39:54 -08:00
if ( CProfileManager : : IsInitialised ( ) & & Threading : : IsMainThread ( ) )
2014-11-13 03:19:28 -08:00
g_Profiler . Start ( " GCSlice " ) ;
g_Profiler2 . RecordRegionEnter ( " GCSlice " ) ;
}
else if ( progress = = JS : : GC_SLICE_END )
{
2021-01-10 00:39:54 -08:00
if ( CProfileManager : : IsInitialised ( ) & & Threading : : IsMainThread ( ) )
2014-11-13 03:19:28 -08:00
g_Profiler . Stop ( ) ;
2020-11-24 07:47:03 -08:00
g_Profiler2 . RecordRegionLeave ( ) ;
2014-11-13 03:19:28 -08:00
}
// The following code can be used to print some information aobut garbage collection
// Search for "Nonincremental reason" if there are problems running GC incrementally.
#if 0
if ( progress = = JS : : GCProgress : : GC_CYCLE_BEGIN )
printf ( " starting cycle =========================================== \n " ) ;
2025-05-27 00:22:17 -07:00
//const char16_t* str = desc.formatMessage(cx);
const char16_t * str = desc . formatSliceMessage ( cx ) ;
2014-11-13 03:19:28 -08:00
int len = 0 ;
2016-11-23 06:09:58 -08:00
2014-11-13 03:19:28 -08:00
for ( int i = 0 ; i < 10000 ; i + + )
{
len + + ;
if ( ! str [ i ] )
break ;
}
2016-11-23 06:09:58 -08:00
2014-11-13 03:19:28 -08:00
wchar_t outstring [ len ] ;
2016-11-23 06:09:58 -08:00
2014-11-13 03:19:28 -08:00
for ( int i = 0 ; i < len ; i + + )
{
outstring [ i ] = ( wchar_t ) str [ i ] ;
}
2016-11-23 06:09:58 -08:00
2014-11-13 03:19:28 -08:00
printf ( " --------------------------------------- \n : %ls \n --------------------------------------- \n " , outstring ) ;
2025-05-27 00:22:17 -07:00
const uint32_t gcBytes = JS_GetGCParameter ( cx , JSGC_BYTES ) ;
printf ( " gcBytes: %i KB \n " , gcBytes / 1024 ) ;
if ( progress = = JS : : GCProgress : : GC_SLICE_END )
printf ( " ending cycle =========================================== \n " ) ;
2014-11-13 03:19:28 -08:00
# endif
}
2024-11-23 09:36:23 -08:00
std : : shared_ptr < ScriptContext > ScriptContext : : CreateContext ( int contextSize , uint32_t heapGrowthBytesGCTrigger )
2020-11-12 01:34:40 -08:00
{
2021-02-13 15:53:40 -08:00
return std : : make_shared < ScriptContext > ( contextSize , heapGrowthBytesGCTrigger ) ;
2020-11-12 01:34:40 -08:00
}
2024-11-23 09:36:23 -08:00
ScriptContext : : ScriptContext ( int contextSize , uint32_t heapGrowthBytesGCTrigger ) :
2024-07-08 12:07:04 -07:00
m_JobQueue { std : : make_unique < Script : : JobQueue > ( ) } ,
m_ContextSize { contextSize } ,
m_HeapGrowthBytesGCTrigger { heapGrowthBytesGCTrigger }
2014-11-13 03:19:28 -08:00
{
2020-11-14 02:57:50 -08:00
ENSURE ( ScriptEngine : : IsInitialised ( ) & & " The ScriptEngine must be initialized before constructing any ScriptContexts! " ) ;
2014-11-13 03:19:28 -08:00
2020-12-06 06:03:02 -08:00
m_cx = JS_NewContext ( contextSize ) ;
2020-11-18 06:39:04 -08:00
ENSURE ( m_cx ) ; // TODO: error handling
2021-04-16 09:49:18 -07:00
// Set stack quota limits - JS scripts will stop with a "too much recursion" exception.
// This seems to refer to the program's actual stack size, so it should be lower than the lowest common denominator
// of the various stack sizes of supported OS.
// From SM78's jsapi.h:
// - "The stack quotas for each kind of code should be monotonically descending"
// - "This function may only be called immediately after the runtime is initialized
// and before any code is executed and/or interrupts requested"
JS_SetNativeStackQuota ( m_cx , 950 * KiB , 900 * KiB , 850 * KiB ) ;
2020-11-18 06:39:04 -08:00
ENSURE ( JS : : InitSelfHostedCode ( m_cx ) ) ;
2014-11-13 03:19:28 -08:00
2020-11-18 06:39:04 -08:00
JS : : SetGCSliceCallback ( m_cx , GCSliceCallbackHook ) ;
2016-08-02 09:58:30 -07:00
2020-11-18 06:39:04 -08:00
JS_SetGCParameter ( m_cx , JSGC_MAX_BYTES , m_ContextSize ) ;
2023-01-10 09:06:47 -08:00
JS_SetGCParameter ( m_cx , JSGC_INCREMENTAL_GC_ENABLED , true ) ;
JS_SetGCParameter ( m_cx , JSGC_PER_ZONE_GC_ENABLED , false ) ;
2020-11-14 00:46:32 -08:00
2025-05-27 00:22:17 -07:00
// Set a low time budget to avoid lag spikes, but allow any number of last ditch GCs
// to avoid OOM errors.
2025-05-27 23:46:11 -07:00
JS_SetGCParameter ( m_cx , JSGC_SLICE_TIME_BUDGET_MS , 6 ) ;
2025-05-27 00:22:17 -07:00
JS_SetGCParameter ( m_cx , JSGC_MIN_LAST_DITCH_GC_PERIOD , 0 ) ;
2024-11-23 09:36:23 -08:00
2020-11-18 06:39:04 -08:00
JS_SetOffthreadIonCompilationEnabled ( m_cx , true ) ;
2020-11-14 00:46:32 -08:00
// For GC debugging:
// JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ);
JS_SetContextPrivate ( m_cx , nullptr ) ;
2020-11-18 06:39:04 -08:00
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_ION_ENABLE , 1 ) ;
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_BASELINE_ENABLE , 1 ) ;
2020-11-14 00:46:32 -08:00
2023-06-14 00:44:23 -07:00
// Turn off Spectre mitigations - this is a huge speedup on JS code, particularly JS -> C++ calls.
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_SPECTRE_JIT_TO_CXX_CALLS , 0 ) ;
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_SPECTRE_INDEX_MASKING , 0 ) ;
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_SPECTRE_VALUE_MASKING , 0 ) ;
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_SPECTRE_STRING_MITIGATIONS , 0 ) ;
JS_SetGlobalJitCompilerOption ( m_cx , JSJITCOMPILER_SPECTRE_OBJECT_MITIGATIONS , 0 ) ;
2025-04-05 00:33:47 -07:00
// Workaround to turn off nursery size heuristic.
// See https://gitea.wildfiregames.com/0ad/0ad/issues/7714 for details.
js : : gc : : SetPerformanceHint ( m_cx , js : : gc : : PerformanceHint : : InPageLoad ) ;
2020-11-14 02:57:50 -08:00
ScriptEngine : : GetSingleton ( ) . RegisterContext ( m_cx ) ;
2024-07-08 12:07:04 -07:00
JS : : SetJobQueue ( m_cx , m_JobQueue . get ( ) ) ;
2024-07-22 01:40:33 -07:00
JS : : SetPromiseRejectionTrackerCallback ( m_cx , & Script : : UnhandledRejectedPromise ) ;
2025-01-15 11:38:37 -08:00
2025-01-15 13:08:06 -08:00
JSRuntime * runtime { JS_GetRuntime ( m_cx ) } ;
2025-01-15 13:15:08 -08:00
JS : : SetModuleMetadataHook ( runtime , & Script : : ModuleLoader : : MetadataHook ) ;
2025-01-15 13:08:06 -08:00
JS : : SetModuleResolveHook ( runtime , & Script : : ModuleLoader : : ResolveHook ) ;
JS : : SetModuleDynamicImportHook ( runtime , & Script : : ModuleLoader : : DynamicImportHook ) ;
2014-11-13 03:19:28 -08:00
}
2020-11-14 02:57:50 -08:00
ScriptContext : : ~ ScriptContext ( )
2014-11-13 03:19:28 -08:00
{
2020-11-14 02:57:50 -08:00
ENSURE ( ScriptEngine : : IsInitialised ( ) & & " The ScriptEngine must be active (initialized and not yet shut down) when destroying a ScriptContext! " ) ;
2025-01-15 13:08:06 -08:00
JSRuntime * runtime { JS_GetRuntime ( m_cx ) } ;
JS : : SetModuleDynamicImportHook ( runtime , nullptr ) ;
JS : : SetModuleResolveHook ( runtime , nullptr ) ;
2025-01-15 13:15:08 -08:00
JS : : SetModuleMetadataHook ( runtime , nullptr ) ;
2025-01-15 11:38:37 -08:00
2025-04-05 00:33:47 -07:00
// Switch back to normal performance mode to avoid assertion in debug mode.
js : : gc : : SetPerformanceHint ( m_cx , js : : gc : : PerformanceHint : : Normal ) ;
2020-11-14 00:46:32 -08:00
JS_DestroyContext ( m_cx ) ;
2020-11-14 02:57:50 -08:00
ScriptEngine : : GetSingleton ( ) . UnRegisterContext ( m_cx ) ;
2014-11-13 03:19:28 -08:00
}
2020-11-30 01:03:20 -08:00
void ScriptContext : : RegisterRealm ( JS : : Realm * realm )
2014-11-13 03:19:28 -08:00
{
2020-11-30 01:03:20 -08:00
ENSURE ( realm ) ;
m_Realms . push_back ( realm ) ;
2014-11-13 03:19:28 -08:00
}
2020-11-30 01:03:20 -08:00
void ScriptContext : : UnRegisterRealm ( JS : : Realm * realm )
2014-11-13 03:19:28 -08:00
{
2020-12-14 00:51:29 -08:00
// Schedule the zone for GC, which will destroy the realm.
if ( JS : : IsIncrementalGCInProgress ( m_cx ) )
JS : : FinishIncrementalGC ( m_cx , JS : : GCReason : : API ) ;
2023-01-10 09:06:47 -08:00
JS : : PrepareZoneForGC ( m_cx , js : : GetRealmZone ( realm ) ) ;
2020-11-30 01:03:20 -08:00
m_Realms . remove ( realm ) ;
2014-11-13 03:19:28 -08:00
}
# define GC_DEBUG_PRINT 0
2024-11-23 09:36:23 -08:00
void ScriptContext : : MaybeIncrementalGC ( )
2014-11-13 03:19:28 -08:00
{
PROFILE2 ( " MaybeIncrementalGC " ) ;
2016-11-23 06:09:58 -08:00
2024-11-23 09:36:23 -08:00
if ( ! JS : : IsIncrementalGCEnabled ( m_cx ) )
return ;
2016-11-23 06:09:58 -08:00
2024-11-23 09:36:23 -08:00
// 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.
2016-11-23 06:09:58 -08:00
2024-11-23 09:36:23 -08:00
const uint32_t gcBytes = JS_GetGCParameter ( m_cx , JSGC_BYTES ) ;
2016-11-23 06:09:58 -08:00
2014-11-13 03:19:28 -08:00
# if GC_DEBUG_PRINT
2024-11-23 09:36:23 -08:00
printf ( " gcBytes: %i KB, last of %i KB \n " , gcBytes / 1024 , m_LastGCBytes / 1024 ) ;
2014-11-13 03:19:28 -08:00
# endif
2016-11-23 06:09:58 -08:00
2024-11-23 09:36:23 -08:00
// 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 )
{
2014-11-13 03:19:28 -08:00
# if GC_DEBUG_PRINT
2024-11-23 09:36:23 -08:00
printf ( " Setting m_LastGCBytes: %d KB \n " , gcBytes / 1024 ) ;
2014-11-13 03:19:28 -08:00
# endif
2024-11-23 09:36:23 -08:00
m_LastGCBytes = gcBytes ;
}
2016-11-23 06:09:58 -08:00
2024-11-23 09:36:23 -08:00
// 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 ) )
{
2015-01-24 06:46:52 -08:00
# if GC_DEBUG_PRINT
2024-11-23 09:36:23 -08:00
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 ) ;
2015-01-24 06:46:52 -08:00
# endif
2024-11-23 09:36:23 -08:00
2014-11-13 03:19:28 -08:00
# if GC_DEBUG_PRINT
2024-11-23 09:36:23 -08:00
if ( ! JS : : IsIncrementalGCInProgress ( m_cx ) )
printf ( " Starting incremental GC \n " ) ;
else
printf ( " Running incremental GC slice \n " ) ;
2014-11-13 03:19:28 -08:00
# endif
2024-11-23 09:36:23 -08:00
// 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 ;
2014-11-13 03:19:28 -08:00
}
}
2020-11-14 02:57:50 -08:00
void ScriptContext : : ShrinkingGC ( )
2015-01-24 06:46:52 -08:00
{
2023-01-10 09:06:47 -08:00
JS_SetGCParameter ( m_cx , JSGC_INCREMENTAL_GC_ENABLED , false ) ;
JS_SetGCParameter ( m_cx , JSGC_PER_ZONE_GC_ENABLED , true ) ;
2020-11-18 06:39:04 -08:00
JS : : PrepareForFullGC ( m_cx ) ;
2023-01-10 09:06:47 -08:00
JS : : NonIncrementalGC ( m_cx , JS : : GCOptions : : Shrink , JS : : GCReason : : API ) ;
JS_SetGCParameter ( m_cx , JSGC_INCREMENTAL_GC_ENABLED , true ) ;
JS_SetGCParameter ( m_cx , JSGC_PER_ZONE_GC_ENABLED , false ) ;
2015-01-24 06:46:52 -08:00
}
2024-07-08 12:07:04 -07:00
void ScriptContext : : RunJobs ( )
{
m_JobQueue - > runJobs ( m_cx ) ;
}
2020-11-30 01:03:20 -08:00
void ScriptContext : : PrepareZonesForIncrementalGC ( ) const
2014-11-13 03:19:28 -08:00
{
2020-11-30 01:03:20 -08:00
for ( JS : : Realm * const & realm : m_Realms )
2023-01-10 09:06:47 -08:00
JS : : PrepareZoneForGC ( m_cx , js : : GetRealmZone ( realm ) ) ;
2015-01-24 06:46:52 -08:00
}