diff --git a/source/gui/GUIManager.cpp b/source/gui/GUIManager.cpp index 8fd8f4f7e1..ce4f363c9c 100644 --- a/source/gui/GUIManager.cpp +++ b/source/gui/GUIManager.cpp @@ -27,6 +27,7 @@ #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRuntime.h" CGUIManager* g_GUI = NULL; @@ -388,7 +389,7 @@ void CGUIManager::TickObjects() // We share the script runtime with everything else that runs in the same thread. // This call makes sure we trigger GC regularly even if the simulation is not running. - m_ScriptInterface->MaybeIncrementalRuntimeGC(1.0f); + m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(1.0f); // Save an immutable copy so iterators aren't invalidated by tick handlers PageStackType pageStack = m_PageStack; diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 467f2a6d9f..e3c4e3b056 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -28,6 +28,7 @@ #include "lib/external_libraries/enet.h" #include "ps/CLogger.h" #include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRuntime.h" #include "simulation2/Simulation2.h" #include "ps/ConfigDB.h" @@ -391,7 +392,7 @@ bool CNetServerWorker::RunStep() // (Do as little work as possible while the mutex is held open, // to avoid performance problems and deadlocks.) - m_ScriptInterface->MaybeIncrementalRuntimeGC(0.5f); + m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(0.5f); JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index d41b4f28a3..17555f85a1 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -18,6 +18,7 @@ #include "precompiled.h" #include "ScriptInterface.h" +#include "ScriptRuntime.h" // #include "DebuggingServer.h" // JS debugger temporarily disabled during the SpiderMonkey upgrade (check trac ticket #2348 for details) #include "ScriptStats.h" #include "AutoRooters.h" @@ -43,8 +44,6 @@ #include "valgrind.h" -#define STACK_CHUNK_SIZE 8192 - #include "scriptinterface/ScriptExtraHeaders.h" /** @@ -54,390 +53,6 @@ * directly accessing the underlying JS api. */ -//////////////////////////////////////////////////////////////// - -/** - * Abstraction around a SpiderMonkey JSRuntime. - * Each ScriptRuntime can be used to initialize several ScriptInterface - * contexts which can then share data, but a single ScriptRuntime should - * only be used on a single thread. - * - * (One means to share data between threads and runtimes is to create - * a ScriptInterface::StructuredClone.) - */ - - -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 -} - -class ScriptRuntime -{ -public: - ScriptRuntime(int runtimeSize, int heapGrowthBytesGCTrigger): - m_rooter(NULL), - m_LastGCBytes(0), - m_LastGCCheck(0.0f), - m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), - m_RuntimeSize(runtimeSize) - { - m_rt = JS_NewRuntime(runtimeSize, JS_USE_HELPER_THREADS); - - ENSURE(m_rt); // TODO: error handling - - JS_SetNativeStackQuota(m_rt, 128 * sizeof(size_t) * 1024); - if (g_ScriptProfilingEnabled) - { - // 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_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); - - JS_AddExtraGCRootsTracer(m_rt, jshook_trace, this); - - m_dummyContext = JS_NewContext(m_rt, STACK_CHUNK_SIZE); - ENSURE(m_dummyContext); - } - - #define GC_DEBUG_PRINT 0 - void 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 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 RegisterContext(JSContext* cx) - { - m_Contexts.push_back(cx); - } - - void UnRegisterContext(JSContext* cx) - { - m_Contexts.remove(cx); - } - - ~ScriptRuntime() - { - JS_RemoveExtraGCRootsTracer(m_rt, jshook_trace, this); - JS_DestroyContext(m_dummyContext); - JS_DestroyRuntime(m_rt); - } - - JSRuntime* m_rt; - AutoGCRooter* m_rooter; - -private: - - // Workaround for: https://bugzilla.mozilla.org/show_bug.cgi?id=890243 - JSContext* m_dummyContext; - - int m_RuntimeSize; - int m_HeapGrowthBytesGCTrigger; - int m_LastGCBytes; - double m_LastGCCheck; - - void PrepareContextsForIncrementalGC() - { - for (std::list::iterator itr = m_Contexts.begin(); itr != m_Contexts.end(); itr++) - { - JS::PrepareZoneForGC(js::GetCompartmentZone(js::GetContextCompartment(*itr))); - } - } - - std::list m_Contexts; - - - static void* jshook_script(JSContext* UNUSED(cx), JSAbstractFramePtr UNUSED(fp), bool UNUSED(isConstructing), JSBool before, JSBool* UNUSED(ok), void* closure) - { - if (before) - g_Profiler.StartScript("script invocation"); - else - g_Profiler.Stop(); - - return closure; - } - - // To profile scripts usefully, we use a call hook that's called on every enter/exit, - // and need to find the function name. But most functions are anonymous so we make do - // with filename plus line number instead. - // Computing the names is fairly expensive, and we need to return an interned char* - // for the profiler to hold a copy of, so we use boost::flyweight to construct interned - // strings per call location. - - // Identifies a location in a script - struct ScriptLocation - { - JSContext* cx; - JSScript* script; - jsbytecode* pc; - - bool operator==(const ScriptLocation& b) const - { - return cx == b.cx && script == b.script && pc == b.pc; - } - - friend std::size_t hash_value(const ScriptLocation& loc) - { - std::size_t seed = 0; - boost::hash_combine(seed, loc.cx); - boost::hash_combine(seed, loc.script); - boost::hash_combine(seed, loc.pc); - return seed; - } - }; - - // Computes and stores the name of a location in a script - struct ScriptLocationName - { - ScriptLocationName(const ScriptLocation& loc) - { - JSContext* cx = loc.cx; - JSScript* script = loc.script; - jsbytecode* pc = loc.pc; - - std::string filename = JS_GetScriptFilename(cx, script); - size_t slash = filename.rfind('/'); - if (slash != filename.npos) - filename = filename.substr(slash+1); - - uint line = JS_PCToLineNumber(cx, script, pc); - - std::stringstream ss; - ss << "(" << filename << ":" << line << ")"; - name = ss.str(); - } - - std::string name; - }; - - // Flyweight types (with no_locking because the call hooks are only used in the - // main thread, and no_tracking because we mustn't delete values the profiler is - // using and it's not going to waste much memory) - typedef boost::flyweight< - std::string, - boost::flyweights::no_tracking, - boost::flyweights::no_locking - > StringFlyweight; - typedef boost::flyweight< - boost::flyweights::key_value, - boost::flyweights::no_tracking, - boost::flyweights::no_locking - > LocFlyweight; - - static void* jshook_function(JSContext* cx, JSAbstractFramePtr fp, bool UNUSED(isConstructing), JSBool before, JSBool* UNUSED(ok), void* closure) - { - if (!before) - { - g_Profiler.Stop(); - return closure; - } - - JSFunction* fn = fp.maybeFun(); - if (!fn) - { - g_Profiler.StartScript("(function)"); - return closure; - } - - // Try to get the name of non-anonymous functions - JSString* name = 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 - compute from the location instead - JSScript* script; - uint lineno; - JS_DescribeScriptedCaller(cx, &script, &lineno); - ENSURE(script == fp.script()); - ScriptLocation loc = { cx, fp.script(), JS_LineNumberToPC(cx, script, lineno) }; - g_Profiler.StartScript(LocFlyweight(loc).get().name.c_str()); - - return closure; - } - - static void jshook_trace(JSTracer* trc, void* data) - { - ScriptRuntime* m = static_cast(data); - - if (m->m_rooter) - m->m_rooter->Trace(trc); - } -}; - -shared_ptr ScriptInterface::CreateRuntime(int runtimeSize, int heapGrowthBytesGCTrigger) -{ - return shared_ptr(new ScriptRuntime(runtimeSize, heapGrowthBytesGCTrigger)); -} - -//////////////////////////////////////////////////////////////// struct ScriptInterface_impl { @@ -1227,6 +842,11 @@ bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& cod return ok; } +shared_ptr ScriptInterface::CreateRuntime(int runtimeSize, int heapGrowthBytesGCTrigger) +{ + return shared_ptr(new ScriptRuntime(runtimeSize, heapGrowthBytesGCTrigger)); +} + bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::wstring& code) { JSAutoRequest rq(m->m_cx); @@ -1477,11 +1097,6 @@ void ScriptInterface::DumpHeap() fprintf(stderr, "# Bytes allocated after GC: %u\n", JS_GetGCParameter(GetJSRuntime(), JSGC_BYTES)); } -void ScriptInterface::MaybeIncrementalRuntimeGC(double delay) -{ - m->m_runtime->MaybeIncrementalGC(delay); -} - void ScriptInterface::MaybeGC() { JS_MaybeGC(m->m_cx); diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h index b96ba316ff..0126feea23 100644 --- a/source/scriptinterface/ScriptInterface.h +++ b/source/scriptinterface/ScriptInterface.h @@ -18,14 +18,15 @@ #ifndef INCLUDED_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE -#include -#include -#include +#include + +#include "lib/file/vfs/vfs_path.h" #include "ScriptTypes.h" #include "ScriptVal.h" - #include "ps/Errors.h" +#include "ps/Profile.h" + ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); @@ -44,12 +45,6 @@ ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); -#include "lib/file/vfs/vfs_path.h" -#include "ps/Profile.h" -#include "ps/utf16string.h" - -#include - class AutoGCRooter; // Set the maximum number of function arguments that can be handled @@ -353,18 +348,6 @@ public: */ void MaybeGC(); - /** - * MaybeIncrementalRuntimeGC tries to determine whether a runtime-wide garbage collection would free up enough memory to - * be worth the amount of time it would take. It does this with our own logic and NOT some predefined JSAPI logic because - * such functionality currently isn't available out of the box. - * It does incremental GC which means it will collect one slice each time it's called until the garbage collection is done. - * This can and should be called quite regularly. The delay parameter allows you to specify a minimum time since the last GC - * in seconds (the delay should be a fraction of a second in most cases though). - * It will only start a new incremental GC or another GC slice if this time is exceeded. The user of this function is - * responsible for ensuring that GC can run with a small enough delay to get done with the work. - */ - void MaybeIncrementalRuntimeGC(double delay); - /** * Triggers a full non-incremental garbage collection immediately. That should only be required in special cases and normally * you should try to use MaybeIncrementalRuntimeGC instead. diff --git a/source/scriptinterface/ScriptRuntime.cpp b/source/scriptinterface/ScriptRuntime.cpp new file mode 100644 index 0000000000..287e6ca53d --- /dev/null +++ b/source/scriptinterface/ScriptRuntime.cpp @@ -0,0 +1,303 @@ +/* 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 "AutoRooters.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 +} + +ScriptRuntime::ScriptRuntime(int runtimeSize, int heapGrowthBytesGCTrigger): + m_rooter(NULL), + m_LastGCBytes(0), + m_LastGCCheck(0.0f), + m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), + m_RuntimeSize(runtimeSize) +{ + m_rt = JS_NewRuntime(runtimeSize, JS_USE_HELPER_THREADS); + + ENSURE(m_rt); // TODO: error handling + + JS_SetNativeStackQuota(m_rt, 128 * sizeof(size_t) * 1024); + if (g_ScriptProfilingEnabled) + { + // 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_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); + + JS_AddExtraGCRootsTracer(m_rt, jshook_trace, this); + + m_dummyContext = JS_NewContext(m_rt, STACK_CHUNK_SIZE); + ENSURE(m_dummyContext); +} + +ScriptRuntime::~ScriptRuntime() +{ + JS_RemoveExtraGCRootsTracer(m_rt, jshook_trace, this); + JS_DestroyContext(m_dummyContext); + JS_DestroyRuntime(m_rt); +} + +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 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::jshook_script(JSContext* UNUSED(cx), JSAbstractFramePtr UNUSED(fp), bool UNUSED(isConstructing), JSBool before, JSBool* 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), JSBool before, JSBool* UNUSED(ok), void* closure) +{ + if (!before) + { + g_Profiler.Stop(); + return closure; + } + + JSFunction* fn = fp.maybeFun(); + if (!fn) + { + g_Profiler.StartScript("(function)"); + return closure; + } + + // Try to get the name of non-anonymous functions + JSString* name = 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 - compute from the location instead + JSScript* script; + uint lineno; + JS_DescribeScriptedCaller(cx, &script, &lineno); + ENSURE(script == fp.script()); + ScriptLocation loc = { cx, fp.script(), JS_LineNumberToPC(cx, script, lineno) }; + g_Profiler.StartScript(LocFlyweight(loc).get().name.c_str()); + + return closure; +} + +void ScriptRuntime::jshook_trace(JSTracer* trc, void* data) +{ + ScriptRuntime* m = static_cast(data); + + if (m->m_rooter) + m->m_rooter->Trace(trc); +} + +void ScriptRuntime::PrepareContextsForIncrementalGC() +{ + for (std::list::iterator itr = m_Contexts.begin(); itr != m_Contexts.end(); itr++) + { + JS::PrepareZoneForGC(js::GetCompartmentZone(js::GetContextCompartment(*itr))); + } +} \ No newline at end of file diff --git a/source/scriptinterface/ScriptRuntime.h b/source/scriptinterface/ScriptRuntime.h new file mode 100644 index 0000000000..345607d045 --- /dev/null +++ b/source/scriptinterface/ScriptRuntime.h @@ -0,0 +1,160 @@ +/* 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 . + */ + +#ifndef INCLUDED_SCRIPTRUNTIME +#define INCLUDED_SCRIPTRUNTIME + +#include +#include +#include +#include + +#include "ScriptTypes.h" +#include "ScriptExtraHeaders.h" + +#define STACK_CHUNK_SIZE 8192 + +class AutoGCRooter; + +/** + * Abstraction around a SpiderMonkey JSRuntime. + * Each ScriptRuntime can be used to initialize several ScriptInterface + * contexts which can then share data, but a single ScriptRuntime should + * only be used on a single thread. + * + * (One means to share data between threads and runtimes is to create + * a ScriptInterface::StructuredClone.) + */ + +class ScriptRuntime +{ +public: + ScriptRuntime(int runtimeSize, int heapGrowthBytesGCTrigger); + ~ScriptRuntime(); + + /** + * MaybeIncrementalRuntimeGC tries to determine whether a runtime-wide garbage collection would free up enough memory to + * be worth the amount of time it would take. It does this with our own logic and NOT some predefined JSAPI logic because + * such functionality currently isn't available out of the box. + * It does incremental GC which means it will collect one slice each time it's called until the garbage collection is done. + * This can and should be called quite regularly. The delay parameter allows you to specify a minimum time since the last GC + * in seconds (the delay should be a fraction of a second in most cases though). + * It will only start a new incremental GC or another GC slice if this time is exceeded. The user of this function is + * responsible for ensuring that GC can run with a small enough delay to get done with the work. + */ + void MaybeIncrementalGC(double delay); + + void RegisterContext(JSContext* cx); + void UnRegisterContext(JSContext* cx); + + JSRuntime* m_rt; + AutoGCRooter* m_rooter; + +private: + + void PrepareContextsForIncrementalGC(); + + // Workaround for: https://bugzilla.mozilla.org/show_bug.cgi?id=890243 + JSContext* m_dummyContext; + + std::list m_Contexts; + + int m_RuntimeSize; + int m_HeapGrowthBytesGCTrigger; + int m_LastGCBytes; + double m_LastGCCheck; + + static void* jshook_script(JSContext* UNUSED(cx), JSAbstractFramePtr UNUSED(fp), + bool UNUSED(isConstructing), JSBool before, + JSBool* UNUSED(ok), void* closure); + + static void* jshook_function(JSContext* cx, JSAbstractFramePtr fp, + bool UNUSED(isConstructing), JSBool before, + JSBool* UNUSED(ok), void* closure); + + static void jshook_trace(JSTracer* trc, void* data); + + // To profile scripts usefully, we use a call hook that's called on every enter/exit, + // and need to find the function name. But most functions are anonymous so we make do + // with filename plus line number instead. + // Computing the names is fairly expensive, and we need to return an interned char* + // for the profiler to hold a copy of, so we use boost::flyweight to construct interned + // strings per call location. + + // Identifies a location in a script + struct ScriptLocation + { + JSContext* cx; + JSScript* script; + jsbytecode* pc; + + bool operator==(const ScriptLocation& b) const + { + return cx == b.cx && script == b.script && pc == b.pc; + } + + friend std::size_t hash_value(const ScriptLocation& loc) + { + std::size_t seed = 0; + boost::hash_combine(seed, loc.cx); + boost::hash_combine(seed, loc.script); + boost::hash_combine(seed, loc.pc); + return seed; + } + }; + + // Computes and stores the name of a location in a script + struct ScriptLocationName + { + ScriptLocationName(const ScriptLocation& loc) + { + JSContext* cx = loc.cx; + JSScript* script = loc.script; + jsbytecode* pc = loc.pc; + + std::string filename = JS_GetScriptFilename(cx, script); + size_t slash = filename.rfind('/'); + if (slash != filename.npos) + filename = filename.substr(slash+1); + + uint line = JS_PCToLineNumber(cx, script, pc); + + std::stringstream ss; + ss << "(" << filename << ":" << line << ")"; + name = ss.str(); + } + + std::string name; + }; + + // Flyweight types (with no_locking because the call hooks are only used in the + // main thread, and no_tracking because we mustn't delete values the profiler is + // using and it's not going to waste much memory) + typedef boost::flyweight< + std::string, + boost::flyweights::no_tracking, + boost::flyweights::no_locking + > StringFlyweight; + typedef boost::flyweight< + boost::flyweights::key_value, + boost::flyweights::no_tracking, + boost::flyweights::no_locking + > LocFlyweight; + +}; + +#endif // INCLUDED_SCRIPTRUNTIME \ No newline at end of file diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index e18af5a202..6fb40be0f9 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -20,6 +20,7 @@ #include "Simulation2.h" #include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRuntime.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" @@ -453,7 +454,7 @@ void CSimulation2Impl::Update(int turnLength, const std::vectorMaybeIncrementalGC(0.0f); if (m_EnableOOSLog) DumpState();