source: ps/trunk/source/scriptinterface/ScriptContext.cpp@ 25280

Last change on this file since 25280 was 25280, checked in by wraitii, 3 years ago

Set a stack quota for JS scripts to prevent crashes from infinite loops.

Infinite loop will instead trigger JS exceptions, which will make error reports much nicer.

Differential Revision: https://code.wildfiregames.com/D3851

  • Property svn:eol-style set to native
File size: 8.5 KB
Line 
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
29void 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
80shared_ptr<ScriptContext> ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger)
81{
82 return std::make_shared<ScriptContext>(contextSize, heapGrowthBytesGCTrigger);
83}
84
85ScriptContext::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
127ScriptContext::~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
135void ScriptContext::RegisterRealm(JS::Realm* realm)
136{
137 ENSURE(realm);
138 m_Realms.push_back(realm);
139}
140
141void 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
151void 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
253void 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
261void ScriptContext::PrepareZonesForIncrementalGC() const
262{
263 for (JS::Realm* const& realm : m_Realms)
264 JS::PrepareZoneForGC(js::GetRealmZone(realm));
265}
Note: See TracBrowser for help on using the repository browser.