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 : }
|