LCOV - code coverage report
Current view: top level - source/scriptinterface - ScriptContext.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 61 80 76.2 %
Date: 2023-01-19 00:18:29 Functions: 8 9 88.9 %

          Line data    Source code
       1             : /* Copyright (C) 2023 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "ScriptContext.h"
      21             : 
      22             : #include "lib/alignment.h"
      23             : #include "ps/GameSetup/Config.h"
      24             : #include "ps/Profile.h"
      25             : #include "scriptinterface/ScriptExtraHeaders.h"
      26             : #include "scriptinterface/ScriptEngine.h"
      27             : #include "scriptinterface/ScriptInterface.h"
      28             : 
      29        1820 : void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc))
      30             : {
      31             :     /**
      32             :      * From the GCAPI.h file:
      33             :      * > During GC, the GC is bracketed by GC_CYCLE_BEGIN/END callbacks. Each
      34             :      * > slice between those (whether an incremental or the sole non-incremental
      35             :      * > slice) is bracketed by GC_SLICE_BEGIN/GC_SLICE_END.
      36             :      * Thus, to safely monitor GCs, we need to profile SLICE_X calls.
      37             :      */
      38             : 
      39             : 
      40        1820 :     if (progress == JS::GC_SLICE_BEGIN)
      41             :     {
      42         455 :         if (CProfileManager::IsInitialised() && Threading::IsMainThread())
      43           0 :             g_Profiler.Start("GCSlice");
      44         455 :         g_Profiler2.RecordRegionEnter("GCSlice");
      45             :     }
      46        1365 :     else if (progress == JS::GC_SLICE_END)
      47             :     {
      48         455 :         if (CProfileManager::IsInitialised() && Threading::IsMainThread())
      49           0 :             g_Profiler.Stop();
      50         455 :         g_Profiler2.RecordRegionLeave();
      51             :     }
      52             : 
      53             :     // The following code can be used to print some information aobut garbage collection
      54             :     // Search for "Nonincremental reason" if there are problems running GC incrementally.
      55             :     #if 0
      56             :     if (progress == JS::GCProgress::GC_CYCLE_BEGIN)
      57             :         printf("starting cycle ===========================================\n");
      58             : 
      59             :     const char16_t* str = desc.formatMessage(cx);
      60             :     int len = 0;
      61             : 
      62             :     for(int i = 0; i < 10000; i++)
      63             :     {
      64             :         len++;
      65             :         if(!str[i])
      66             :             break;
      67             :     }
      68             : 
      69             :     wchar_t outstring[len];
      70             : 
      71             :     for(int i = 0; i < len; i++)
      72             :     {
      73             :         outstring[i] = (wchar_t)str[i];
      74             :     }
      75             : 
      76             :     printf("---------------------------------------\n: %ls \n---------------------------------------\n", outstring);
      77             :     #endif
      78        1820 : }
      79             : 
      80           1 : std::shared_ptr<ScriptContext> ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger)
      81             : {
      82           1 :     return std::make_shared<ScriptContext>(contextSize, heapGrowthBytesGCTrigger);
      83             : }
      84             : 
      85           1 : ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger):
      86             :     m_LastGCBytes(0),
      87             :     m_LastGCCheck(0.0f),
      88             :     m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger),
      89           1 :     m_ContextSize(contextSize)
      90             : {
      91           1 :     ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptContexts!");
      92             : 
      93           1 :     m_cx = JS_NewContext(contextSize);
      94           1 :     ENSURE(m_cx); // TODO: error handling
      95             : 
      96             :     // Set stack quota limits - JS scripts will stop with a "too much recursion" exception.
      97             :     // This seems to refer to the program's actual stack size, so it should be lower than the lowest common denominator
      98             :     // of the various stack sizes of supported OS.
      99             :     // From SM78's jsapi.h:
     100             :     // - "The stack quotas for each kind of code should be monotonically descending"
     101             :     // - "This function may only be called immediately after the runtime is initialized
     102             :     //   and before any code is executed and/or interrupts requested"
     103           1 :     JS_SetNativeStackQuota(m_cx, 950 * KiB, 900 * KiB, 850 * KiB);
     104             : 
     105           1 :     ENSURE(JS::InitSelfHostedCode(m_cx));
     106             : 
     107           1 :     JS::SetGCSliceCallback(m_cx, GCSliceCallbackHook);
     108             : 
     109           1 :     JS_SetGCParameter(m_cx, JSGC_MAX_BYTES, m_ContextSize);
     110           1 :     JS_SetGCParameter(m_cx, JSGC_INCREMENTAL_GC_ENABLED, true);
     111           1 :     JS_SetGCParameter(m_cx, JSGC_PER_ZONE_GC_ENABLED, false);
     112             : 
     113           1 :     JS_SetOffthreadIonCompilationEnabled(m_cx, true);
     114             : 
     115             :     // For GC debugging:
     116             :     // JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ);
     117             : 
     118           1 :     JS_SetContextPrivate(m_cx, nullptr);
     119             : 
     120           1 :     JS_SetGlobalJitCompilerOption(m_cx, JSJITCOMPILER_ION_ENABLE, 1);
     121           1 :     JS_SetGlobalJitCompilerOption(m_cx, JSJITCOMPILER_BASELINE_ENABLE, 1);
     122             : 
     123           1 :     JS::ContextOptionsRef(m_cx).setStrictMode(true);
     124             : 
     125           1 :     ScriptEngine::GetSingleton().RegisterContext(m_cx);
     126           1 : }
     127             : 
     128           2 : ScriptContext::~ScriptContext()
     129             : {
     130           1 :     ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be active (initialized and not yet shut down) when destroying a ScriptContext!");
     131             : 
     132           1 :     JS_DestroyContext(m_cx);
     133           1 :     ScriptEngine::GetSingleton().UnRegisterContext(m_cx);
     134           1 : }
     135             : 
     136         282 : void ScriptContext::RegisterRealm(JS::Realm* realm)
     137             : {
     138         282 :     ENSURE(realm);
     139         282 :     m_Realms.push_back(realm);
     140         282 : }
     141             : 
     142         282 : void ScriptContext::UnRegisterRealm(JS::Realm* realm)
     143             : {
     144             :     // Schedule the zone for GC, which will destroy the realm.
     145         282 :     if (JS::IsIncrementalGCInProgress(m_cx))
     146           0 :         JS::FinishIncrementalGC(m_cx, JS::GCReason::API);
     147         282 :     JS::PrepareZoneForGC(m_cx, js::GetRealmZone(realm));
     148         282 :     m_Realms.remove(realm);
     149         282 : }
     150             : 
     151             : #define GC_DEBUG_PRINT 0
     152           2 : void ScriptContext::MaybeIncrementalGC(double delay)
     153             : {
     154           3 :     PROFILE2("MaybeIncrementalGC");
     155             : 
     156           2 :     if (JS::IsIncrementalGCEnabled(m_cx))
     157             :     {
     158             :         // The idea is to get the heap size after a completed GC and trigger the next GC when the heap size has
     159             :         // reached m_LastGCBytes + X.
     160             :         // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in.
     161             :         // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set).
     162             :         // While the sweeping is happening we already run scripts again and produce new garbage.
     163             : 
     164           2 :         const int GCSliceTimeBudget = 30; // Milliseconds an incremental slice is allowed to run
     165             : 
     166             :         // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC
     167             :         // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute
     168             :         // the load well enough and low enough to make sure we don't run out of memory before we can start with the
     169             :         // sweeping.
     170           2 :         if (timer_Time() - m_LastGCCheck < delay)
     171           1 :             return;
     172             : 
     173           1 :         m_LastGCCheck = timer_Time();
     174             : 
     175           1 :         int gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES);
     176             : 
     177             : #if GC_DEBUG_PRINT
     178             :             std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl;
     179             : #endif
     180             : 
     181           1 :         if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0)
     182             :         {
     183             : #if GC_DEBUG_PRINT
     184             :             printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024);
     185             : #endif
     186           1 :             m_LastGCBytes = gcBytes;
     187             :         }
     188             : 
     189             :         // Run an additional incremental GC slice if the currently running incremental GC isn't over yet
     190             :         // ... or
     191             :         // start a new incremental GC if the JS heap size has grown enough for a GC to make sense
     192           1 :         if (JS::IsIncrementalGCInProgress(m_cx) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger))
     193             :         {
     194             : #if GC_DEBUG_PRINT
     195             :             if (JS::IsIncrementalGCInProgress(m_cx))
     196             :                 printf("An incremental GC cycle is in progress. \n");
     197             :             else
     198             :                 printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n"
     199             :                     "    JSGC_BYTES: %d KB \n    m_LastGCBytes: %d KB \n    m_HeapGrowthBytesGCTrigger: %d KB \n",
     200             :                     gcBytes / 1024,
     201             :                     m_LastGCBytes / 1024,
     202             :                     m_HeapGrowthBytesGCTrigger / 1024);
     203             : #endif
     204             : 
     205             :             // A hack to make sure we never exceed the context size because we can't collect the memory
     206             :             // fast enough.
     207           0 :             if (gcBytes > m_ContextSize / 2)
     208             :             {
     209           0 :                 if (JS::IsIncrementalGCInProgress(m_cx))
     210             :                 {
     211             : #if GC_DEBUG_PRINT
     212             :                     printf("Finishing incremental GC because gcBytes > m_ContextSize / 2. \n");
     213             : #endif
     214           0 :                     PrepareZonesForIncrementalGC();
     215           0 :                     JS::FinishIncrementalGC(m_cx, JS::GCReason::API);
     216             :                 }
     217             :                 else
     218             :                 {
     219           0 :                     if (gcBytes > m_ContextSize * 0.75)
     220             :                     {
     221           0 :                         ShrinkingGC();
     222             : #if GC_DEBUG_PRINT
     223             :                         printf("Running shrinking GC because gcBytes > m_ContextSize * 0.75. \n");
     224             : #endif
     225             :                     }
     226             :                     else
     227             :                     {
     228             : #if GC_DEBUG_PRINT
     229             :                         printf("Running full GC because gcBytes > m_ContextSize / 2. \n");
     230             : #endif
     231           0 :                         JS_GC(m_cx);
     232             :                     }
     233             :                 }
     234             :             }
     235             :             else
     236             :             {
     237             : #if GC_DEBUG_PRINT
     238             :                 if (!JS::IsIncrementalGCInProgress(m_cx))
     239             :                     printf("Starting incremental GC \n");
     240             :                 else
     241             :                     printf("Running incremental GC slice \n");
     242             : #endif
     243           0 :                 PrepareZonesForIncrementalGC();
     244           0 :                 if (!JS::IsIncrementalGCInProgress(m_cx))
     245           0 :                     JS::StartIncrementalGC(m_cx, JS::GCOptions::Normal, JS::GCReason::API, GCSliceTimeBudget);
     246             :                 else
     247           0 :                     JS::IncrementalGCSlice(m_cx, JS::GCReason::API, GCSliceTimeBudget);
     248             :             }
     249           0 :             m_LastGCBytes = gcBytes;
     250             :         }
     251             :     }
     252             : }
     253             : 
     254         454 : void ScriptContext::ShrinkingGC()
     255             : {
     256         454 :     JS_SetGCParameter(m_cx, JSGC_INCREMENTAL_GC_ENABLED, false);
     257         454 :     JS_SetGCParameter(m_cx, JSGC_PER_ZONE_GC_ENABLED, true);
     258         454 :     JS::PrepareForFullGC(m_cx);
     259         454 :     JS::NonIncrementalGC(m_cx, JS::GCOptions::Shrink, JS::GCReason::API);
     260         454 :     JS_SetGCParameter(m_cx, JSGC_INCREMENTAL_GC_ENABLED, true);
     261         454 :     JS_SetGCParameter(m_cx, JSGC_PER_ZONE_GC_ENABLED, false);
     262         454 : }
     263             : 
     264           0 : void ScriptContext::PrepareZonesForIncrementalGC() const
     265             : {
     266           0 :     for (JS::Realm* const& realm : m_Realms)
     267           0 :         JS::PrepareZoneForGC(m_cx, js::GetRealmZone(realm));
     268           0 : }

Generated by: LCOV version 1.13