1 | /* Copyright (C) 2021 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 | 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 | if (progress == JS::GC_SLICE_BEGIN)
|
---|
41 | {
|
---|
42 | if (CProfileManager::IsInitialised() && Threading::IsMainThread())
|
---|
43 | g_Profiler.Start("GCSlice");
|
---|
44 | g_Profiler2.RecordRegionEnter("GCSlice");
|
---|
45 | }
|
---|
46 | else if (progress == JS::GC_SLICE_END)
|
---|
47 | {
|
---|
48 | if (CProfileManager::IsInitialised() && Threading::IsMainThread())
|
---|
49 | g_Profiler.Stop();
|
---|
50 | 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 | }
|
---|
79 |
|
---|
80 | shared_ptr<ScriptContext> ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger)
|
---|
81 | {
|
---|
82 | return std::make_shared<ScriptContext>(contextSize, heapGrowthBytesGCTrigger);
|
---|
83 | }
|
---|
84 |
|
---|
85 | ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger):
|
---|
86 | m_LastGCBytes(0),
|
---|
87 | m_LastGCCheck(0.0f),
|
---|
88 | m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger),
|
---|
89 | m_ContextSize(contextSize)
|
---|
90 | {
|
---|
91 | ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptContexts!");
|
---|
92 |
|
---|
93 | m_cx = JS_NewContext(contextSize);
|
---|
94 | 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 | JS_SetNativeStackQuota(m_cx, 950 * KiB, 900 * KiB, 850 * KiB);
|
---|
104 |
|
---|
105 | ENSURE(JS::InitSelfHostedCode(m_cx));
|
---|
106 |
|
---|
107 | JS::SetGCSliceCallback(m_cx, GCSliceCallbackHook);
|
---|
108 |
|
---|
109 | JS_SetGCParameter(m_cx, JSGC_MAX_BYTES, m_ContextSize);
|
---|
110 | JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_INCREMENTAL);
|
---|
111 |
|
---|
112 | JS_SetOffthreadIonCompilationEnabled(m_cx, true);
|
---|
113 |
|
---|
114 | // For GC debugging:
|
---|
115 | // JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ);
|
---|
116 |
|
---|
117 | JS_SetContextPrivate(m_cx, nullptr);
|
---|
118 |
|
---|
119 | JS_SetGlobalJitCompilerOption(m_cx, JSJITCOMPILER_ION_ENABLE, 1);
|
---|
120 | JS_SetGlobalJitCompilerOption(m_cx, JSJITCOMPILER_BASELINE_ENABLE, 1);
|
---|
121 |
|
---|
122 | JS::ContextOptionsRef(m_cx).setStrictMode(true);
|
---|
123 |
|
---|
124 | ScriptEngine::GetSingleton().RegisterContext(m_cx);
|
---|
125 | }
|
---|
126 |
|
---|
127 | ScriptContext::~ScriptContext()
|
---|
128 | {
|
---|
129 | ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be active (initialized and not yet shut down) when destroying a ScriptContext!");
|
---|
130 |
|
---|
131 | JS_DestroyContext(m_cx);
|
---|
132 | ScriptEngine::GetSingleton().UnRegisterContext(m_cx);
|
---|
133 | }
|
---|
134 |
|
---|
135 | void ScriptContext::RegisterRealm(JS::Realm* realm)
|
---|
136 | {
|
---|
137 | ENSURE(realm);
|
---|
138 | m_Realms.push_back(realm);
|
---|
139 | }
|
---|
140 |
|
---|
141 | void ScriptContext::UnRegisterRealm(JS::Realm* realm)
|
---|
142 | {
|
---|
143 | // Schedule the zone for GC, which will destroy the realm.
|
---|
144 | if (JS::IsIncrementalGCInProgress(m_cx))
|
---|
145 | JS::FinishIncrementalGC(m_cx, JS::GCReason::API);
|
---|
146 | JS::PrepareZoneForGC(js::GetRealmZone(realm));
|
---|
147 | m_Realms.remove(realm);
|
---|
148 | }
|
---|
149 |
|
---|
150 | #define GC_DEBUG_PRINT 0
|
---|
151 | void ScriptContext::MaybeIncrementalGC(double delay)
|
---|
152 | {
|
---|
153 | PROFILE2("MaybeIncrementalGC");
|
---|
154 |
|
---|
155 | if (JS::IsIncrementalGCEnabled(m_cx))
|
---|
156 | {
|
---|
157 | // The idea is to get the heap size after a completed GC and trigger the next GC when the heap size has
|
---|
158 | // reached m_LastGCBytes + X.
|
---|
159 | // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in.
|
---|
160 | // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set).
|
---|
161 | // While the sweeping is happening we already run scripts again and produce new garbage.
|
---|
162 |
|
---|
163 | const int GCSliceTimeBudget = 30; // Milliseconds an incremental slice is allowed to run
|
---|
164 |
|
---|
165 | // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC
|
---|
166 | // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute
|
---|
167 | // the load well enough and low enough to make sure we don't run out of memory before we can start with the
|
---|
168 | // sweeping.
|
---|
169 | if (timer_Time() - m_LastGCCheck < delay)
|
---|
170 | return;
|
---|
171 |
|
---|
172 | m_LastGCCheck = timer_Time();
|
---|
173 |
|
---|
174 | int gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES);
|
---|
175 |
|
---|
176 | #if GC_DEBUG_PRINT
|
---|
177 | std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl;
|
---|
178 | #endif
|
---|
179 |
|
---|
180 | if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0)
|
---|
181 | {
|
---|
182 | #if GC_DEBUG_PRINT
|
---|
183 | printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024);
|
---|
184 | #endif
|
---|
185 | m_LastGCBytes = gcBytes;
|
---|
186 | }
|
---|
187 |
|
---|
188 | // Run an additional incremental GC slice if the currently running incremental GC isn't over yet
|
---|
189 | // ... or
|
---|
190 | // start a new incremental GC if the JS heap size has grown enough for a GC to make sense
|
---|
191 | if (JS::IsIncrementalGCInProgress(m_cx) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger))
|
---|
192 | {
|
---|
193 | #if GC_DEBUG_PRINT
|
---|
194 | if (JS::IsIncrementalGCInProgress(m_cx))
|
---|
195 | printf("An incremental GC cycle is in progress. \n");
|
---|
196 | else
|
---|
197 | printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n"
|
---|
198 | " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n",
|
---|
199 | gcBytes / 1024,
|
---|
200 | m_LastGCBytes / 1024,
|
---|
201 | m_HeapGrowthBytesGCTrigger / 1024);
|
---|
202 | #endif
|
---|
203 |
|
---|
204 | // A hack to make sure we never exceed the context size because we can't collect the memory
|
---|
205 | // fast enough.
|
---|
206 | if (gcBytes > m_ContextSize / 2)
|
---|
207 | {
|
---|
208 | if (JS::IsIncrementalGCInProgress(m_cx))
|
---|
209 | {
|
---|
210 | #if GC_DEBUG_PRINT
|
---|
211 | printf("Finishing incremental GC because gcBytes > m_ContextSize / 2. \n");
|
---|
212 | #endif
|
---|
213 | PrepareZonesForIncrementalGC();
|
---|
214 | JS::FinishIncrementalGC(m_cx, JS::GCReason::API);
|
---|
215 | }
|
---|
216 | else
|
---|
217 | {
|
---|
218 | if (gcBytes > m_ContextSize * 0.75)
|
---|
219 | {
|
---|
220 | ShrinkingGC();
|
---|
221 | #if GC_DEBUG_PRINT
|
---|
222 | printf("Running shrinking GC because gcBytes > m_ContextSize * 0.75. \n");
|
---|
223 | #endif
|
---|
224 | }
|
---|
225 | else
|
---|
226 | {
|
---|
227 | #if GC_DEBUG_PRINT
|
---|
228 | printf("Running full GC because gcBytes > m_ContextSize / 2. \n");
|
---|
229 | #endif
|
---|
230 | JS_GC(m_cx);
|
---|
231 | }
|
---|
232 | }
|
---|
233 | }
|
---|
234 | else
|
---|
235 | {
|
---|
236 | #if GC_DEBUG_PRINT
|
---|
237 | if (!JS::IsIncrementalGCInProgress(m_cx))
|
---|
238 | printf("Starting incremental GC \n");
|
---|
239 | else
|
---|
240 | printf("Running incremental GC slice \n");
|
---|
241 | #endif
|
---|
242 | PrepareZonesForIncrementalGC();
|
---|
243 | if (!JS::IsIncrementalGCInProgress(m_cx))
|
---|
244 | JS::StartIncrementalGC(m_cx, GC_NORMAL, JS::GCReason::API, GCSliceTimeBudget);
|
---|
245 | else
|
---|
246 | JS::IncrementalGCSlice(m_cx, JS::GCReason::API, GCSliceTimeBudget);
|
---|
247 | }
|
---|
248 | m_LastGCBytes = gcBytes;
|
---|
249 | }
|
---|
250 | }
|
---|
251 | }
|
---|
252 |
|
---|
253 | void ScriptContext::ShrinkingGC()
|
---|
254 | {
|
---|
255 | JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_ZONE);
|
---|
256 | JS::PrepareForFullGC(m_cx);
|
---|
257 | JS::NonIncrementalGC(m_cx, GC_SHRINK, JS::GCReason::API);
|
---|
258 | JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_INCREMENTAL);
|
---|
259 | }
|
---|
260 |
|
---|
261 | void ScriptContext::PrepareZonesForIncrementalGC() const
|
---|
262 | {
|
---|
263 | for (JS::Realm* const& realm : m_Realms)
|
---|
264 | JS::PrepareZoneForGC(js::GetRealmZone(realm));
|
---|
265 | }
|
---|