/* Copyright (C) 2014 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * 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. * * 0 A.D. is distributed in the hope that it will be useful, * 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 * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptRuntime.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" void GCSliceCallbackHook(JSRuntime* UNUSED(rt), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) { /* * During non-incremental GC, the GC is bracketed by JSGC_CYCLE_BEGIN/END * callbacks. During an incremental GC, the sequence of callbacks is as * follows: * JSGC_CYCLE_BEGIN, JSGC_SLICE_END (first slice) * JSGC_SLICE_BEGIN, JSGC_SLICE_END (second slice) * ... * JSGC_SLICE_BEGIN, JSGC_CYCLE_END (last slice) */ if (progress == JS::GC_SLICE_BEGIN) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Start("GCSlice"); g_Profiler2.RecordRegionEnter("GCSlice"); } else if (progress == JS::GC_SLICE_END) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave("GCSlice"); } else if (progress == JS::GC_CYCLE_BEGIN) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Start("GCSlice"); g_Profiler2.RecordRegionEnter("GCSlice"); } else if (progress == JS::GC_CYCLE_END) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave("GCSlice"); } // 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"); const jschar* str = desc.formatMessage(rt); int len = 0; for(int i = 0; i < 10000; i++) { len++; if(!str[i]) break; } wchar_t outstring[len]; for(int i = 0; i < len; i++) { outstring[i] = (wchar_t)str[i]; } printf("---------------------------------------\n: %ls \n---------------------------------------\n", outstring); #endif } void ScriptRuntime::GCCallback(JSRuntime* UNUSED(rt), JSGCStatus status, void *data) { if (status == JSGC_END) reinterpret_cast(data)->GCCallbackMember(); } void ScriptRuntime::GCCallbackMember() { m_FinalizationListObjectIdCache.clear(); } void ScriptRuntime::AddDeferredFinalizationObject(const std::shared_ptr& obj) { m_FinalizationListObjectIdCache.push_back(obj); } bool ScriptRuntime::m_Initialized = false; ScriptRuntime::ScriptRuntime(shared_ptr parentRuntime, int runtimeSize, int heapGrowthBytesGCTrigger): m_LastGCBytes(0), m_LastGCCheck(0.0f), m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), m_RuntimeSize(runtimeSize) { if (!m_Initialized) { ENSURE(JS_Init()); m_Initialized = true; } JSRuntime* parentJSRuntime = parentRuntime ? parentRuntime->m_rt : nullptr; m_rt = JS_NewRuntime(runtimeSize, JS_USE_HELPER_THREADS, parentJSRuntime); ENSURE(m_rt); // TODO: error handling if (g_ScriptProfilingEnabled) { // Execute and call hooks are disabled if the runtime debug mode is disabled JS_SetRuntimeDebugMode(m_rt, true); // Profiler isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (CProfileManager::IsInitialised()) { JS_SetExecuteHook(m_rt, jshook_script, this); JS_SetCallHook(m_rt, jshook_function, this); } } } JS::SetGCSliceCallback(m_rt, GCSliceCallbackHook); JS_SetGCCallback(m_rt, ScriptRuntime::GCCallback, this); JS_SetGCParameter(m_rt, JSGC_MAX_MALLOC_BYTES, m_RuntimeSize); JS_SetGCParameter(m_rt, JSGC_MAX_BYTES, m_RuntimeSize); JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_INCREMENTAL); // The whole heap-growth mechanism seems to work only for non-incremental GCs. // We disable it to make it more clear if full GCs happen triggered by this JSAPI internal mechanism. JS_SetGCParameter(m_rt, JSGC_DYNAMIC_HEAP_GROWTH, false); m_dummyContext = JS_NewContext(m_rt, STACK_CHUNK_SIZE); ENSURE(m_dummyContext); } ScriptRuntime::~ScriptRuntime() { JS_DestroyContext(m_dummyContext); JS_SetGCCallback(m_rt, nullptr, nullptr); JS_DestroyRuntime(m_rt); ENSURE(m_FinalizationListObjectIdCache.empty() && "Leak: Removing callback while some objects still aren't finalized!"); } void ScriptRuntime::RegisterContext(JSContext* cx) { m_Contexts.push_back(cx); } void ScriptRuntime::UnRegisterContext(JSContext* cx) { m_Contexts.remove(cx); } #define GC_DEBUG_PRINT 0 void ScriptRuntime::MaybeIncrementalGC(double delay) { PROFILE2("MaybeIncrementalGC"); if (JS::IsIncrementalGCEnabled(m_rt)) { // 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. // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in. // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set). // While the sweeping is happening we already run scripts again and produce new garbage. const int GCSliceTimeBudget = 30; // Milliseconds an incremental slice is allowed to run // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute // the load well enough and low enough to make sure we don't run out of memory before we can start with the // sweeping. if (timer_Time() - m_LastGCCheck < delay) return; m_LastGCCheck = timer_Time(); int gcBytes = JS_GetGCParameter(m_rt, JSGC_BYTES); #if GC_DEBUG_PRINT std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; #endif if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) { #if GC_DEBUG_PRINT printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); #endif m_LastGCBytes = gcBytes; } // 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_rt) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) { #if GC_DEBUG_PRINT if (JS::IsIncrementalGCInProgress(m_rt)) 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); #endif // A hack to make sure we never exceed the runtime size because we can't collect the memory // fast enough. if(gcBytes > m_RuntimeSize / 2) { if (JS::IsIncrementalGCInProgress(m_rt)) { #if GC_DEBUG_PRINT printf("Finishing incremental GC because gcBytes > m_RuntimeSize / 2. \n"); #endif PrepareContextsForIncrementalGC(); JS::FinishIncrementalGC(m_rt, JS::gcreason::REFRESH_FRAME); } else { if (gcBytes > m_RuntimeSize * 0.75) { ShrinkingGC(); #if GC_DEBUG_PRINT printf("Running shrinking GC because gcBytes > m_RuntimeSize * 0.75. \n"); #endif } else { #if GC_DEBUG_PRINT printf("Running full GC because gcBytes > m_RuntimeSize / 2. \n"); #endif JS_GC(m_rt); } } } else { #if GC_DEBUG_PRINT if (!JS::IsIncrementalGCInProgress(m_rt)) printf("Starting incremental GC \n"); else printf("Running incremental GC slice \n"); #endif PrepareContextsForIncrementalGC(); JS::IncrementalGC(m_rt, JS::gcreason::REFRESH_FRAME, GCSliceTimeBudget); } m_LastGCBytes = gcBytes; } } } void ScriptRuntime::ShrinkingGC() { JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_COMPARTMENT); JS::PrepareForFullGC(m_rt); JS::ShrinkingGC(m_rt, JS::gcreason::REFRESH_FRAME); JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_INCREMENTAL); } void* ScriptRuntime::jshook_script(JSContext* UNUSED(cx), JSAbstractFramePtr UNUSED(fp), bool UNUSED(isConstructing), bool before, bool* UNUSED(ok), void* closure) { if (before) g_Profiler.StartScript("script invocation"); else g_Profiler.Stop(); return closure; } void* ScriptRuntime::jshook_function(JSContext* cx, JSAbstractFramePtr fp, bool UNUSED(isConstructing), bool before, bool* UNUSED(ok), void* closure) { JSAutoRequest rq(cx); if (!before) { g_Profiler.Stop(); return closure; } JS::RootedFunction fn(cx, fp.maybeFun()); if (!fn) { g_Profiler.StartScript("(function)"); return closure; } // Try to get the name of non-anonymous functions JS::RootedString name(cx, JS_GetFunctionId(fn)); if (name) { char* chars = JS_EncodeString(cx, name); if (chars) { g_Profiler.StartScript(StringFlyweight(chars).get().c_str()); JS_free(cx, chars); return closure; } } // No name - use fileName and line instead JS::AutoFilename fileName; unsigned lineno; JS::DescribeScriptedCaller(cx, &fileName, &lineno); std::stringstream ss; ss << "(" << fileName.get() << ":" << lineno << ")"; g_Profiler.StartScript(StringFlyweight(ss.str()).get().c_str()); return closure; } void ScriptRuntime::PrepareContextsForIncrementalGC() { for (JSContext* const& ctx : m_Contexts) JS::PrepareZoneForGC(js::GetCompartmentZone(js::GetContextCompartment(ctx))); }