diff options
Diffstat (limited to 'dom/workers/RuntimeService.cpp')
-rw-r--r-- | dom/workers/RuntimeService.cpp | 2424 |
1 files changed, 2424 insertions, 0 deletions
diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp new file mode 100644 index 0000000000..71f6d5e1ac --- /dev/null +++ b/dom/workers/RuntimeService.cpp @@ -0,0 +1,2424 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RuntimeService.h" + +#include "nsContentSecurityUtils.h" +#include "nsIContentSecurityPolicy.h" +#include "mozilla/dom/Document.h" +#include "nsIObserverService.h" +#include "nsIScriptContext.h" +#include "nsIStreamTransportService.h" +#include "nsISupportsPriority.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIXULRuntime.h" +#include "nsPIDOMWindow.h" + +#include <algorithm> +#include "mozilla/ipc/BackgroundChild.h" +#include "GeckoProfiler.h" +#include "js/experimental/CTypes.h" // JS::CTypesActivityType, JS::SetCTypesActivityCallback +#include "jsfriendapi.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/ContextOptions.h" +#include "js/LocaleSensitive.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/AtomList.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/EventTargetBinding.h" +#include "mozilla/dom/FetchUtil.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/PerformanceService.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/Monitor.h" +#include "nsContentUtils.h" +#include "nsCycleCollector.h" +#include "nsDOMJSUtils.h" +#include "nsISupportsImpl.h" +#include "nsLayoutStatics.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsXPCOM.h" +#include "nsXPCOMPrivate.h" +#include "OSFileConstants.h" +#include "xpcpublic.h" + +#if defined(XP_MACOSX) +# include "nsMacUtilsImpl.h" +#endif + +#include "Principal.h" +#include "WorkerDebuggerManager.h" +#include "WorkerError.h" +#include "WorkerLoadInfo.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "WorkerThread.h" +#include "prsystem.h" + +#ifdef DEBUG +# include "nsICookieJarSettings.h" +#endif + +#define WORKERS_SHUTDOWN_TOPIC "web-workers-shutdown" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +using namespace workerinternals; + +namespace workerinternals { + +// The size of the worker runtime heaps in bytes. May be changed via pref. +#define WORKER_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024 + +// The size of the worker JS allocation threshold in MB. May be changed via +// pref. +#define WORKER_DEFAULT_ALLOCATION_THRESHOLD 30 + +// Half the size of the actual C stack, to be safe. +#define WORKER_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024 + +// The maximum number of hardware concurrency, overridable via pref. +#define MAX_HARDWARE_CONCURRENCY 8 + +// The maximum number of threads to use for workers, overridable via pref. +#define MAX_WORKERS_PER_DOMAIN 512 + +static_assert(MAX_WORKERS_PER_DOMAIN >= 1, + "We should allow at least one worker per domain."); + +// The number of seconds that idle threads can hang around before being killed. +#define IDLE_THREAD_TIMEOUT_SEC 30 + +// The maximum number of threads that can be idle at one time. +#define MAX_IDLE_THREADS 20 + +#define PREF_WORKERS_PREFIX "dom.workers." +#define PREF_WORKERS_MAX_PER_DOMAIN PREF_WORKERS_PREFIX "maxPerDomain" +#define PREF_WORKERS_MAX_HARDWARE_CONCURRENCY "dom.maxHardwareConcurrency" + +#define GC_REQUEST_OBSERVER_TOPIC "child-gc-request" +#define CC_REQUEST_OBSERVER_TOPIC "child-cc-request" +#define MEMORY_PRESSURE_OBSERVER_TOPIC "memory-pressure" +#define LOW_MEMORY_DATA "low-memory" +#define LOW_MEMORY_ONGOING_DATA "low-memory-ongoing" +#define MEMORY_PRESSURE_STOP_OBSERVER_TOPIC "memory-pressure-stop" + +// Prefixes for observing preference changes. +#define PREF_JS_OPTIONS_PREFIX "javascript.options." +#define PREF_WORKERS_OPTIONS_PREFIX PREF_WORKERS_PREFIX "options." +#define PREF_MEM_OPTIONS_PREFIX "mem." +#define PREF_GCZEAL "gcZeal" + +static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID); + +namespace { + +const uint32_t kNoIndex = uint32_t(-1); + +uint32_t gMaxWorkersPerDomain = MAX_WORKERS_PER_DOMAIN; +uint32_t gMaxHardwareConcurrency = MAX_HARDWARE_CONCURRENCY; + +// Does not hold an owning reference. +RuntimeService* gRuntimeService = nullptr; + +// Only true during the call to Init. +bool gRuntimeServiceDuringInit = false; + +class LiteralRebindingCString : public nsDependentCString { + public: + template <int N> + void RebindLiteral(const char (&aStr)[N]) { + Rebind(aStr, N - 1); + } +}; + +template <typename T> +struct PrefTraits; + +template <> +struct PrefTraits<bool> { + typedef bool PrefValueType; + + static const PrefValueType kDefaultValue = false; + + static inline PrefValueType Get(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetBool(aPref); + } + + static inline bool Exists(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetType(aPref) == nsIPrefBranch::PREF_BOOL; + } +}; + +template <> +struct PrefTraits<int32_t> { + typedef int32_t PrefValueType; + + static inline PrefValueType Get(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetInt(aPref); + } + + static inline bool Exists(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetType(aPref) == nsIPrefBranch::PREF_INT; + } +}; + +template <typename T> +T GetWorkerPref(const nsACString& aPref, + const T aDefault = PrefTraits<T>::kDefaultValue, + bool* aPresent = nullptr) { + AssertIsOnMainThread(); + + typedef PrefTraits<T> PrefHelper; + + T result; + bool present = true; + + nsAutoCString prefName; + prefName.AssignLiteral(PREF_WORKERS_OPTIONS_PREFIX); + prefName.Append(aPref); + + if (PrefHelper::Exists(prefName.get())) { + result = PrefHelper::Get(prefName.get()); + } else { + prefName.AssignLiteral(PREF_JS_OPTIONS_PREFIX); + prefName.Append(aPref); + + if (PrefHelper::Exists(prefName.get())) { + result = PrefHelper::Get(prefName.get()); + } else { + result = aDefault; + present = false; + } + } + + if (aPresent) { + *aPresent = present; + } + return result; +} + +void LoadContextOptions(const char* aPrefName, void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + if (!rts) { + // May be shutting down, just bail. + return; + } + + const nsDependentCString prefName(aPrefName); + + // Several other pref branches will get included here so bail out if there is + // another callback that will handle this change. + if (StringBeginsWith( + prefName, + nsLiteralCString(PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX)) || + StringBeginsWith( + prefName, nsLiteralCString( + PREF_WORKERS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX))) { + return; + } + +#ifdef JS_GC_ZEAL + if (prefName.EqualsLiteral(PREF_JS_OPTIONS_PREFIX PREF_GCZEAL) || + prefName.EqualsLiteral(PREF_WORKERS_OPTIONS_PREFIX PREF_GCZEAL)) { + return; + } +#endif + + // Context options. + JS::ContextOptions contextOptions; + contextOptions + .setAsmJS(GetWorkerPref<bool>("asmjs"_ns)) +#ifdef FUZZING + .setFuzzing(GetWorkerPref<bool>("fuzzing.enabled"_ns)) +#endif + .setWasm(GetWorkerPref<bool>("wasm"_ns)) + .setWasmForTrustedPrinciples( + GetWorkerPref<bool>("wasm_trustedprincipals"_ns)) + .setWasmBaseline(GetWorkerPref<bool>("wasm_baselinejit"_ns)) +#ifdef ENABLE_WASM_CRANELIFT + .setWasmCranelift(GetWorkerPref<bool>("wasm_optimizingjit"_ns)) +#else + .setWasmIon(GetWorkerPref<bool>("wasm_optimizingjit"_ns)) +#endif + .setWasmReftypes(GetWorkerPref<bool>("wasm_reftypes"_ns)) +#ifdef ENABLE_WASM_MULTI_VALUE + .setWasmMultiValue(GetWorkerPref<bool>("wasm_multi_value"_ns)) +#endif +#ifdef ENABLE_WASM_SIMD + .setWasmSimd(GetWorkerPref<bool>("wasm_simd"_ns)) +#endif +#ifdef ENABLE_WASM_FUNCTION_REFERENCES + .setWasmFunctionReferences( + GetWorkerPref<bool>("wasm_function_references"_ns)) +#endif +#ifdef ENABLE_WASM_GC + .setWasmGc(GetWorkerPref<bool>("wasm_gc"_ns)) +#endif + .setWasmVerbose(GetWorkerPref<bool>("wasm_verbose"_ns)) + .setThrowOnAsmJSValidationFailure( + GetWorkerPref<bool>("throw_on_asmjs_validation_failure"_ns)) + .setSourcePragmas(GetWorkerPref<bool>("source_pragmas"_ns)) + .setAsyncStack(GetWorkerPref<bool>("asyncstack"_ns)) + .setAsyncStackCaptureDebuggeeOnly( + GetWorkerPref<bool>("asyncstack_capture_debuggee_only"_ns)) + .setPrivateClassFields( + GetWorkerPref<bool>("experimental.private_fields"_ns)) + .setPrivateClassMethods( + GetWorkerPref<bool>("experimental.private_methods"_ns)) + .setTopLevelAwait(GetWorkerPref<bool>("experimental.top_level_await"_ns)); + + nsCOMPtr<nsIXULRuntime> xr = do_GetService("@mozilla.org/xre/runtime;1"); + if (xr) { + bool safeMode = false; + xr->GetInSafeMode(&safeMode); + if (safeMode) { + contextOptions.disableOptionsForSafeMode(); + } + } + + RuntimeService::SetDefaultContextOptions(contextOptions); + + if (rts) { + rts->UpdateAllWorkerContextOptions(); + } +} + +#ifdef JS_GC_ZEAL +void LoadGCZealOptions(const char* /* aPrefName */, void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + if (!rts) { + // May be shutting down, just bail. + return; + } + + int32_t gczeal = GetWorkerPref<int32_t>(nsLiteralCString(PREF_GCZEAL), -1); + if (gczeal < 0) { + gczeal = 0; + } + + int32_t frequency = GetWorkerPref<int32_t>("gcZeal.frequency"_ns, -1); + if (frequency < 0) { + frequency = JS_DEFAULT_ZEAL_FREQ; + } + + RuntimeService::SetDefaultGCZeal(uint8_t(gczeal), uint32_t(frequency)); + + if (rts) { + rts->UpdateAllWorkerGCZeal(); + } +} +#endif + +void UpdateCommonJSGCMemoryOption(RuntimeService* aRuntimeService, + const nsACString& aPrefName, + JSGCParamKey aKey) { + AssertIsOnMainThread(); + NS_ASSERTION(!aPrefName.IsEmpty(), "Empty pref name!"); + + int32_t prefValue = GetWorkerPref(aPrefName, -1); + Maybe<uint32_t> value = (prefValue < 0 || prefValue >= 10000) + ? Nothing() + : Some(uint32_t(prefValue)); + + RuntimeService::SetDefaultJSGCSettings(aKey, value); + + if (aRuntimeService) { + aRuntimeService->UpdateAllWorkerMemoryParameter(aKey, value); + } +} + +void UpdateOtherJSGCMemoryOption(RuntimeService* aRuntimeService, + JSGCParamKey aKey, Maybe<uint32_t> aValue) { + AssertIsOnMainThread(); + + RuntimeService::SetDefaultJSGCSettings(aKey, aValue); + + if (aRuntimeService) { + aRuntimeService->UpdateAllWorkerMemoryParameter(aKey, aValue); + } +} + +void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + + if (!rts) { + // May be shutting down, just bail. + return; + } + + constexpr auto jsPrefix = nsLiteralCString{PREF_JS_OPTIONS_PREFIX}; + constexpr auto workersPrefix = nsLiteralCString{PREF_WORKERS_OPTIONS_PREFIX}; + + const nsDependentCString fullPrefName(aPrefName); + + // Pull out the string that actually distinguishes the parameter we need to + // change. + nsDependentCSubstring memPrefName; + if (StringBeginsWith(fullPrefName, jsPrefix)) { + memPrefName.Rebind(fullPrefName, jsPrefix.Length()); + } else if (StringBeginsWith(fullPrefName, workersPrefix)) { + memPrefName.Rebind(fullPrefName, workersPrefix.Length()); + } else { + NS_ERROR("Unknown pref name!"); + return; + } + + struct WorkerGCPref { + nsLiteralCString name; + JSGCParamKey key; + }; + +#define PREF(suffix_, key_) \ + { nsLiteralCString(PREF_MEM_OPTIONS_PREFIX suffix_), key_ } + constexpr WorkerGCPref kWorkerPrefs[] = { + PREF("max", JSGC_MAX_BYTES), + PREF("gc_high_frequency_time_limit_ms", JSGC_HIGH_FREQUENCY_TIME_LIMIT), + PREF("gc_low_frequency_heap_growth", JSGC_LOW_FREQUENCY_HEAP_GROWTH), + PREF("gc_high_frequency_large_heap_growth", + JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH), + PREF("gc_high_frequency_small_heap_growth", + JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH), + PREF("gc_small_heap_size_max_mb", JSGC_SMALL_HEAP_SIZE_MAX), + PREF("gc_large_heap_size_min_mb", JSGC_LARGE_HEAP_SIZE_MIN), + PREF("gc_allocation_threshold_mb", JSGC_ALLOCATION_THRESHOLD), + PREF("gc_incremental_slice_ms", JSGC_SLICE_TIME_BUDGET_MS), + PREF("gc_min_empty_chunk_count", JSGC_MIN_EMPTY_CHUNK_COUNT), + PREF("gc_max_empty_chunk_count", JSGC_MAX_EMPTY_CHUNK_COUNT), + PREF("gc_compacting", JSGC_COMPACTING_ENABLED), + }; +#undef PREF + + auto pref = kWorkerPrefs; + auto end = kWorkerPrefs + ArrayLength(kWorkerPrefs); + + if (gRuntimeServiceDuringInit) { + // During init, we want to update every pref in kWorkerPrefs. + MOZ_ASSERT(memPrefName.EqualsLiteral(PREF_MEM_OPTIONS_PREFIX), + "Pref branch prefix only expected during init"); + } else { + // Otherwise, find the single pref that changed. + while (pref != end) { + if (pref->name == memPrefName) { + end = pref + 1; + break; + } + ++pref; + } +#ifdef DEBUG + if (pref == end) { + nsAutoCString message("Workers don't support the '"); + message.Append(memPrefName); + message.AppendLiteral("' preference!"); + NS_WARNING(message.get()); + } +#endif + } + + while (pref != end) { + switch (pref->key) { + case JSGC_MAX_BYTES: { + int32_t prefValue = GetWorkerPref(pref->name, -1); + Maybe<uint32_t> value = (prefValue <= 0 || prefValue >= 0x1000) + ? Nothing() + : Some(uint32_t(prefValue) * 1024 * 1024); + UpdateOtherJSGCMemoryOption(rts, pref->key, value); + break; + } + case JSGC_SLICE_TIME_BUDGET_MS: { + int32_t prefValue = GetWorkerPref(pref->name, -1); + Maybe<uint32_t> value = (prefValue <= 0 || prefValue >= 100000) + ? Nothing() + : Some(uint32_t(prefValue)); + UpdateOtherJSGCMemoryOption(rts, pref->key, value); + break; + } + case JSGC_COMPACTING_ENABLED: { + bool present; + bool prefValue = GetWorkerPref(pref->name, false, &present); + Maybe<uint32_t> value = present ? Some(prefValue ? 1 : 0) : Nothing(); + UpdateOtherJSGCMemoryOption(rts, pref->key, value); + break; + } + case JSGC_HIGH_FREQUENCY_TIME_LIMIT: + case JSGC_LOW_FREQUENCY_HEAP_GROWTH: + case JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH: + case JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH: + case JSGC_SMALL_HEAP_SIZE_MAX: + case JSGC_LARGE_HEAP_SIZE_MIN: + case JSGC_ALLOCATION_THRESHOLD: + case JSGC_MIN_EMPTY_CHUNK_COUNT: + case JSGC_MAX_EMPTY_CHUNK_COUNT: + UpdateCommonJSGCMemoryOption(rts, pref->name, pref->key); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown JSGCParamKey value"); + break; + } + ++pref; + } +} + +bool InterruptCallback(JSContext* aCx) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(worker); + + // Now is a good time to turn on profiling if it's pending. + PROFILER_JS_INTERRUPT_CALLBACK(); + + return worker->InterruptCallback(aCx); +} + +class LogViolationDetailsRunnable final : public WorkerMainThreadRunnable { + nsString mFileName; + uint32_t mLineNum; + uint32_t mColumnNum; + nsString mScriptSample; + + public: + LogViolationDetailsRunnable(WorkerPrivate* aWorker, const nsString& aFileName, + uint32_t aLineNum, uint32_t aColumnNum, + const nsAString& aScriptSample) + : WorkerMainThreadRunnable(aWorker, + "RuntimeService :: LogViolationDetails"_ns), + mFileName(aFileName), + mLineNum(aLineNum), + mColumnNum(aColumnNum), + mScriptSample(aScriptSample) { + MOZ_ASSERT(aWorker); + } + + virtual bool MainThreadRun() override; + + private: + ~LogViolationDetailsRunnable() = default; +}; + +bool ContentSecurityPolicyAllows(JSContext* aCx, JS::HandleString aCode) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + nsAutoJSString scriptSample; + if (NS_WARN_IF(!scriptSample.init(aCx, aCode))) { + JS_ClearPendingException(aCx); + return false; + } + + if (!nsContentSecurityUtils::IsEvalAllowed(aCx, worker->UsesSystemPrincipal(), + scriptSample)) { + return false; + } + + if (worker->GetReportCSPViolations()) { + nsString fileName; + uint32_t lineNum = 0; + uint32_t columnNum = 0; + + JS::AutoFilename file; + if (JS::DescribeScriptedCaller(aCx, &file, &lineNum, &columnNum) && + file.get()) { + CopyUTF8toUTF16(MakeStringSpan(file.get()), fileName); + } else { + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + } + + RefPtr<LogViolationDetailsRunnable> runnable = + new LogViolationDetailsRunnable(worker, fileName, lineNum, columnNum, + scriptSample); + + ErrorResult rv; + runnable->Dispatch(Killing, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + } + + return worker->IsEvalAllowed(); +} + +void CTypesActivityCallback(JSContext* aCx, JS::CTypesActivityType aType) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + switch (aType) { + case JS::CTypesActivityType::BeginCall: + worker->BeginCTypesCall(); + break; + + case JS::CTypesActivityType::EndCall: + worker->EndCTypesCall(); + break; + + case JS::CTypesActivityType::BeginCallback: + worker->BeginCTypesCallback(); + break; + + case JS::CTypesActivityType::EndCallback: + worker->EndCTypesCallback(); + break; + + default: + MOZ_CRASH("Unknown type flag!"); + } +} + +// JSDispatchableRunnables are WorkerRunnables used to dispatch JS::Dispatchable +// back to their worker thread. A WorkerRunnable is used for two reasons: +// +// 1. The JS::Dispatchable::run() callback may run JS so we cannot use a control +// runnable since they use async interrupts and break JS run-to-completion. +// +// 2. The DispatchToEventLoopCallback interface is *required* to fail during +// shutdown (see jsapi.h) which is exactly what WorkerRunnable::Dispatch() will +// do. Moreover, JS_DestroyContext() does *not* block on JS::Dispatchable::run +// being called, DispatchToEventLoopCallback failure is expected to happen +// during shutdown. +class JSDispatchableRunnable final : public WorkerRunnable { + JS::Dispatchable* mDispatchable; + + ~JSDispatchableRunnable() { MOZ_ASSERT(!mDispatchable); } + + // Disable the usual pre/post-dispatch thread assertions since we are + // dispatching from some random JS engine internal thread: + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // For the benefit of the destructor assert. + if (!aDispatchResult) { + mDispatchable = nullptr; + } + } + + public: + JSDispatchableRunnable(WorkerPrivate* aWorkerPrivate, + JS::Dispatchable* aDispatchable) + : WorkerRunnable(aWorkerPrivate, + WorkerRunnable::WorkerThreadUnchangedBusyCount), + mDispatchable(aDispatchable) { + MOZ_ASSERT(mDispatchable); + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); + MOZ_ASSERT(aCx == mWorkerPrivate->GetJSContext()); + MOZ_ASSERT(mDispatchable); + + AutoJSAPI jsapi; + jsapi.Init(); + + mDispatchable->run(mWorkerPrivate->GetJSContext(), + JS::Dispatchable::NotShuttingDown); + mDispatchable = nullptr; // mDispatchable may delete itself + + return true; + } + + nsresult Cancel() override { + MOZ_ASSERT(mDispatchable); + + AutoJSAPI jsapi; + jsapi.Init(); + + mDispatchable->run(mWorkerPrivate->GetJSContext(), + JS::Dispatchable::ShuttingDown); + mDispatchable = nullptr; // mDispatchable may delete itself + + return WorkerRunnable::Cancel(); + } +}; + +static bool DispatchToEventLoop(void* aClosure, + JS::Dispatchable* aDispatchable) { + // This callback may execute either on the worker thread or a random + // JS-internal helper thread. + + // See comment at JS::InitDispatchToEventLoop() below for how we know the + // WorkerPrivate is alive. + WorkerPrivate* workerPrivate = reinterpret_cast<WorkerPrivate*>(aClosure); + + // Dispatch is expected to fail during shutdown for the reasons outlined in + // the JSDispatchableRunnable comment above. + RefPtr<JSDispatchableRunnable> r = + new JSDispatchableRunnable(workerPrivate, aDispatchable); + return r->Dispatch(); +} + +static bool ConsumeStream(JSContext* aCx, JS::HandleObject aObj, + JS::MimeType aMimeType, + JS::StreamConsumer* aConsumer) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + if (!worker) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + return false; + } + + return FetchUtil::StreamResponseToJS(aCx, aObj, aMimeType, aConsumer, worker); +} + +bool InitJSContextForWorker(WorkerPrivate* aWorkerPrivate, + JSContext* aWorkerCx) { + aWorkerPrivate->AssertIsOnWorkerThread(); + NS_ASSERTION(!aWorkerPrivate->GetJSContext(), "Already has a context!"); + + JSSettings settings; + aWorkerPrivate->CopyJSSettings(settings); + + JS::ContextOptionsRef(aWorkerCx) = settings.contextOptions; + + // This is the real place where we set the max memory for the runtime. + for (const auto& setting : settings.gcSettings) { + if (setting.value) { + JS_SetGCParameter(aWorkerCx, setting.key, *setting.value); + } else { + JS_ResetGCParameter(aWorkerCx, setting.key); + } + } + + JS_SetNativeStackQuota(aWorkerCx, WORKER_CONTEXT_NATIVE_STACK_LIMIT); + + // Security policy: + static const JSSecurityCallbacks securityCallbacks = { + ContentSecurityPolicyAllows}; + JS_SetSecurityCallbacks(aWorkerCx, &securityCallbacks); + + // A WorkerPrivate lives strictly longer than its JSRuntime so we can safely + // store a raw pointer as the callback's closure argument on the JSRuntime. + JS::InitDispatchToEventLoop(aWorkerCx, DispatchToEventLoop, + (void*)aWorkerPrivate); + + JS::InitConsumeStreamCallback(aWorkerCx, ConsumeStream, + FetchUtil::ReportJSStreamError); + + if (!JS::InitSelfHostedCode(aWorkerCx)) { + NS_WARNING("Could not init self-hosted code!"); + return false; + } + + JS_AddInterruptCallback(aWorkerCx, InterruptCallback); + + JS::SetCTypesActivityCallback(aWorkerCx, CTypesActivityCallback); + +#ifdef JS_GC_ZEAL + JS_SetGCZeal(aWorkerCx, settings.gcZeal, settings.gcZealFrequency); +#endif + + return true; +} + +static bool PreserveWrapper(JSContext* cx, JS::HandleObject obj) { + MOZ_ASSERT(cx); + MOZ_ASSERT(obj); + MOZ_ASSERT(mozilla::dom::IsDOMObject(obj)); + + return mozilla::dom::TryPreserveWrapper(obj); +} + +static bool IsWorkerDebuggerGlobalOrSandbox(JS::HandleObject aGlobal) { + return IsWorkerDebuggerGlobal(aGlobal) || IsWorkerDebuggerSandbox(aGlobal); +} + +JSObject* Wrap(JSContext* cx, JS::HandleObject existing, JS::HandleObject obj) { + JS::RootedObject targetGlobal(cx, JS::CurrentGlobalOrNull(cx)); + + // Note: the JS engine unwraps CCWs before calling this callback. + JS::RootedObject originGlobal(cx, JS::GetNonCCWObjectGlobal(obj)); + + const js::Wrapper* wrapper = nullptr; + if (IsWorkerDebuggerGlobalOrSandbox(targetGlobal) && + IsWorkerDebuggerGlobalOrSandbox(originGlobal)) { + wrapper = &js::CrossCompartmentWrapper::singleton; + } else { + wrapper = &js::OpaqueCrossCompartmentWrapper::singleton; + } + + if (existing) { + js::Wrapper::Renew(existing, obj, wrapper); + } + return js::Wrapper::New(cx, obj, wrapper); +} + +static const JSWrapObjectCallbacks WrapObjectCallbacks = { + Wrap, + nullptr, +}; + +class WorkerJSRuntime final : public mozilla::CycleCollectedJSRuntime { + public: + // The heap size passed here doesn't matter, we will change it later in the + // call to JS_SetGCParameter inside InitJSContextForWorker. + explicit WorkerJSRuntime(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + : CycleCollectedJSRuntime(aCx), mWorkerPrivate(aWorkerPrivate) { + MOZ_COUNT_CTOR_INHERITED(WorkerJSRuntime, CycleCollectedJSRuntime); + MOZ_ASSERT(aWorkerPrivate); + + { + JS::UniqueChars defaultLocale = aWorkerPrivate->AdoptDefaultLocale(); + MOZ_ASSERT(defaultLocale, + "failure of a WorkerPrivate to have a default locale should " + "have made the worker fail to spawn"); + + if (!JS_SetDefaultLocale(Runtime(), defaultLocale.get())) { + NS_WARNING("failed to set workerCx's default locale"); + } + } + } + + void Shutdown(JSContext* cx) override { + // The CC is shut down, and the superclass destructor will GC, so make sure + // we don't try to CC again. + mWorkerPrivate = nullptr; + + CycleCollectedJSRuntime::Shutdown(cx); + } + + ~WorkerJSRuntime() { + MOZ_COUNT_DTOR_INHERITED(WorkerJSRuntime, CycleCollectedJSRuntime); + } + + virtual void PrepareForForgetSkippable() override {} + + virtual void BeginCycleCollectionCallback() override {} + + virtual void EndCycleCollectionCallback( + CycleCollectorResults& aResults) override {} + + void DispatchDeferredDeletion(bool aContinuation, bool aPurge) override { + MOZ_ASSERT(!aContinuation); + + // Do it immediately, no need for asynchronous behavior here. + nsCycleCollector_doDeferredDeletion(); + } + + virtual void CustomGCCallback(JSGCStatus aStatus) override { + if (!mWorkerPrivate) { + // We're shutting down, no need to do anything. + return; + } + + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus == JSGC_END) { + bool collectedAnything = nsCycleCollector_collect(nullptr); + mWorkerPrivate->SetCCCollectedAnything(collectedAnything); + } + } + + private: + WorkerPrivate* mWorkerPrivate; +}; + +} // anonymous namespace + +} // namespace workerinternals + +class WorkerJSContext final : public mozilla::CycleCollectedJSContext { + public: + // The heap size passed here doesn't matter, we will change it later in the + // call to JS_SetGCParameter inside InitJSContextForWorker. + explicit WorkerJSContext(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) { + MOZ_COUNT_CTOR_INHERITED(WorkerJSContext, CycleCollectedJSContext); + MOZ_ASSERT(aWorkerPrivate); + // Magical number 2. Workers have the base recursion depth 1, and normal + // runnables run at level 2, and we don't want to process microtasks + // at any other level. + SetTargetedMicroTaskRecursionDepth(2); + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the + // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a + // bit of a pain. + MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkerJSContext() { + MOZ_COUNT_DTOR_INHERITED(WorkerJSContext, CycleCollectedJSContext); + JSContext* cx = MaybeContext(); + if (!cx) { + return; // Initialize() must have failed + } + + // The worker global should be unrooted and the shutdown cycle collection + // should break all remaining cycles. The superclass destructor will run + // the GC one final time and finalize any JSObjects that were participating + // in cycles that were broken during CC shutdown. + nsCycleCollector_shutdown(); + + // The CC is shut down, and the superclass destructor will GC, so make sure + // we don't try to CC again. + mWorkerPrivate = nullptr; + } + + WorkerJSContext* GetAsWorkerJSContext() override { return this; } + + CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) override { + return new WorkerJSRuntime(aCx, mWorkerPrivate); + } + + nsresult Initialize(JSRuntime* aParentRuntime) { + nsresult rv = CycleCollectedJSContext::Initialize( + aParentRuntime, WORKER_DEFAULT_RUNTIME_HEAPSIZE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = Context(); + + js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper); + JS_InitDestroyPrincipalsCallback(cx, WorkerPrincipal::Destroy); + JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); + if (mWorkerPrivate->IsDedicatedWorker()) { + JS_SetFutexCanWait(cx); + } + + return NS_OK; + } + + virtual void DispatchToMicroTask( + already_AddRefed<MicroTaskRunnable> aRunnable) override { + RefPtr<MicroTaskRunnable> runnable(aRunnable); + + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(runnable); + + std::queue<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr; + + JSContext* cx = Context(); + NS_ASSERTION(cx, "This should never be null!"); + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + NS_ASSERTION(global, "This should never be null!"); + + // On worker threads, if the current global is the worker global, we use the + // main micro task queue. Otherwise, the current global must be + // either the debugger global or a debugger sandbox, and we use the debugger + // micro task queue instead. + if (IsWorkerGlobal(global)) { + microTaskQueue = &GetMicroTaskQueue(); + } else { + MOZ_ASSERT(IsWorkerDebuggerGlobal(global) || + IsWorkerDebuggerSandbox(global)); + + microTaskQueue = &GetDebuggerMicroTaskQueue(); + } + + JS::JobQueueMayNotBeEmpty(cx); + microTaskQueue->push(std::move(runnable)); + } + + bool IsSystemCaller() const override { + return mWorkerPrivate->UsesSystemPrincipal(); + } + + void ReportError(JSErrorReport* aReport, + JS::ConstUTF8CharsZ aToStringResult) override { + mWorkerPrivate->ReportError(Context(), aToStringResult, aReport); + } + + WorkerPrivate* GetWorkerPrivate() const { return mWorkerPrivate; } + + private: + WorkerPrivate* mWorkerPrivate; +}; + +namespace workerinternals { + +namespace { + +class WorkerThreadPrimaryRunnable final : public Runnable { + WorkerPrivate* mWorkerPrivate; + SafeRefPtr<WorkerThread> mThread; + JSRuntime* mParentRuntime; + + class FinishedRunnable final : public Runnable { + SafeRefPtr<WorkerThread> mThread; + + public: + explicit FinishedRunnable(SafeRefPtr<WorkerThread> aThread) + : Runnable("WorkerThreadPrimaryRunnable::FinishedRunnable"), + mThread(std::move(aThread)) { + MOZ_ASSERT(mThread); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishedRunnable, Runnable) + + private: + ~FinishedRunnable() = default; + + NS_DECL_NSIRUNNABLE + }; + + public: + WorkerThreadPrimaryRunnable(WorkerPrivate* aWorkerPrivate, + SafeRefPtr<WorkerThread> aThread, + JSRuntime* aParentRuntime) + : mozilla::Runnable("WorkerThreadPrimaryRunnable"), + mWorkerPrivate(aWorkerPrivate), + mThread(std::move(aThread)), + mParentRuntime(aParentRuntime) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(mThread); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(WorkerThreadPrimaryRunnable, Runnable) + + private: + ~WorkerThreadPrimaryRunnable() = default; + + NS_DECL_NSIRUNNABLE +}; + +void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) { + AssertIsOnMainThread(); + + nsTArray<nsString> languages; + Navigator::GetAcceptLanguages(languages); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAllWorkerLanguages(languages); + } +} + +void AppNameOverrideChanged(const char* /* aPrefName */, void* /* aClosure */) { + AssertIsOnMainThread(); + + nsAutoString override; + Preferences::GetString("general.appname.override", override); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAppNameOverridePreference(override); + } +} + +void AppVersionOverrideChanged(const char* /* aPrefName */, + void* /* aClosure */) { + AssertIsOnMainThread(); + + nsAutoString override; + Preferences::GetString("general.appversion.override", override); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAppVersionOverridePreference(override); + } +} + +void PlatformOverrideChanged(const char* /* aPrefName */, + void* /* aClosure */) { + AssertIsOnMainThread(); + + nsAutoString override; + Preferences::GetString("general.platform.override", override); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdatePlatformOverridePreference(override); + } +} + +} /* anonymous namespace */ + +// This is only touched on the main thread. Initialized in Init() below. +UniquePtr<JSSettings> RuntimeService::sDefaultJSSettings; + +RuntimeService::RuntimeService() + : mMutex("RuntimeService::mMutex"), + mObserved(false), + mShuttingDown(false), + mNavigatorPropertiesLoaded(false) { + AssertIsOnMainThread(); + NS_ASSERTION(!gRuntimeService, "More than one service!"); +} + +RuntimeService::~RuntimeService() { + AssertIsOnMainThread(); + + // gRuntimeService can be null if Init() fails. + NS_ASSERTION(!gRuntimeService || gRuntimeService == this, + "More than one service!"); + + gRuntimeService = nullptr; +} + +// static +RuntimeService* RuntimeService::GetOrCreateService() { + AssertIsOnMainThread(); + + if (!gRuntimeService) { + // The observer service now owns us until shutdown. + gRuntimeService = new RuntimeService(); + if (NS_FAILED(gRuntimeService->Init())) { + NS_WARNING("Failed to initialize!"); + gRuntimeService->Cleanup(); + gRuntimeService = nullptr; + return nullptr; + } + } + + return gRuntimeService; +} + +// static +RuntimeService* RuntimeService::GetService() { return gRuntimeService; } + +bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) { + aWorkerPrivate.AssertIsOnParentThread(); + + WorkerPrivate* parent = aWorkerPrivate.GetParent(); + if (!parent) { + AssertIsOnMainThread(); + + if (mShuttingDown) { + return false; + } + } + + const bool isServiceWorker = aWorkerPrivate.IsServiceWorker(); + const bool isSharedWorker = aWorkerPrivate.IsSharedWorker(); + const bool isDedicatedWorker = aWorkerPrivate.IsDedicatedWorker(); + if (isServiceWorker) { + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_SPAWN_ATTEMPTS, 1); + } + + nsCString sharedWorkerScriptSpec; + if (isSharedWorker) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> scriptURI = aWorkerPrivate.GetResolvedScriptURI(); + NS_ASSERTION(scriptURI, "Null script URI!"); + + nsresult rv = scriptURI->GetSpec(sharedWorkerScriptSpec); + if (NS_FAILED(rv)) { + NS_WARNING("GetSpec failed?!"); + return false; + } + + NS_ASSERTION(!sharedWorkerScriptSpec.IsEmpty(), "Empty spec!"); + } + + bool exemptFromPerDomainMax = false; + if (isServiceWorker) { + AssertIsOnMainThread(); + exemptFromPerDomainMax = Preferences::GetBool( + "dom.serviceWorkers.exemptFromPerDomainMax", false); + } + + const nsCString& domain = aWorkerPrivate.Domain(); + + bool queued = false; + { + MutexAutoLock lock(mMutex); + + const auto& domainInfo = + mDomainMap.LookupForAdd(domain).OrInsert([&domain, parent]() { + NS_ASSERTION(!parent, "Shouldn't have a parent here!"); + Unused + << parent; // silence clang -Wunused-lambda-capture in opt builds + WorkerDomainInfo* wdi = new WorkerDomainInfo(); + wdi->mDomain = domain; + return wdi; + }); + + queued = gMaxWorkersPerDomain && + domainInfo->ActiveWorkerCount() >= gMaxWorkersPerDomain && + !domain.IsEmpty() && !exemptFromPerDomainMax; + + if (queued) { + domainInfo->mQueuedWorkers.AppendElement(&aWorkerPrivate); + + // Worker spawn gets queued due to hitting max workers per domain + // limit so let's log a warning. + WorkerPrivate::ReportErrorToConsole("HittingMaxWorkersPerDomain2"); + + if (isServiceWorker) { + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_SPAWN_GETS_QUEUED, 1); + } else if (isSharedWorker) { + Telemetry::Accumulate(Telemetry::SHARED_WORKER_SPAWN_GETS_QUEUED, 1); + } else if (isDedicatedWorker) { + Telemetry::Accumulate(Telemetry::DEDICATED_WORKER_SPAWN_GETS_QUEUED, 1); + } + } else if (parent) { + domainInfo->mChildWorkerCount++; + } else if (isServiceWorker) { + domainInfo->mActiveServiceWorkers.AppendElement(&aWorkerPrivate); + } else { + domainInfo->mActiveWorkers.AppendElement(&aWorkerPrivate); + } + } + + // From here on out we must call UnregisterWorker if something fails! + if (parent) { + if (!parent->AddChildWorker(&aWorkerPrivate)) { + UnregisterWorker(aWorkerPrivate); + return false; + } + } else { + if (!mNavigatorPropertiesLoaded) { + Navigator::AppName(mNavigatorProperties.mAppName, + aWorkerPrivate.GetPrincipal(), + false /* aUsePrefOverriddenValue */); + if (NS_FAILED(Navigator::GetAppVersion( + mNavigatorProperties.mAppVersion, aWorkerPrivate.GetPrincipal(), + false /* aUsePrefOverriddenValue */)) || + NS_FAILED(Navigator::GetPlatform( + mNavigatorProperties.mPlatform, aWorkerPrivate.GetPrincipal(), + false /* aUsePrefOverriddenValue */))) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + // The navigator overridden properties should have already been read. + + Navigator::GetAcceptLanguages(mNavigatorProperties.mLanguages); + mNavigatorPropertiesLoaded = true; + } + + nsPIDOMWindowInner* window = aWorkerPrivate.GetWindow(); + + if (!isServiceWorker) { + // Service workers are excluded since their lifetime is separate from + // that of dom windows. + const auto& windowArray = mWindowMap.LookupForAdd(window).OrInsert( + []() { return new nsTArray<WorkerPrivate*>(1); }); + if (!windowArray->Contains(&aWorkerPrivate)) { + windowArray->AppendElement(&aWorkerPrivate); + } else { + MOZ_ASSERT(aWorkerPrivate.IsSharedWorker()); + } + } + } + + if (!queued && !ScheduleWorker(aWorkerPrivate)) { + return false; + } + + if (isServiceWorker) { + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_WAS_SPAWNED, 1); + } + return true; +} + +void RuntimeService::UnregisterWorker(WorkerPrivate& aWorkerPrivate) { + aWorkerPrivate.AssertIsOnParentThread(); + + WorkerPrivate* parent = aWorkerPrivate.GetParent(); + if (!parent) { + AssertIsOnMainThread(); + } + + const nsCString& domain = aWorkerPrivate.Domain(); + + WorkerPrivate* queuedWorker = nullptr; + { + MutexAutoLock lock(mMutex); + + WorkerDomainInfo* domainInfo; + if (!mDomainMap.Get(domain, &domainInfo)) { + NS_ERROR("Don't have an entry for this domain!"); + } + + // Remove old worker from everywhere. + uint32_t index = domainInfo->mQueuedWorkers.IndexOf(&aWorkerPrivate); + if (index != kNoIndex) { + // Was queued, remove from the list. + domainInfo->mQueuedWorkers.RemoveElementAt(index); + } else if (parent) { + MOZ_ASSERT(domainInfo->mChildWorkerCount, "Must be non-zero!"); + domainInfo->mChildWorkerCount--; + } else if (aWorkerPrivate.IsServiceWorker()) { + MOZ_ASSERT(domainInfo->mActiveServiceWorkers.Contains(&aWorkerPrivate), + "Don't know about this worker!"); + domainInfo->mActiveServiceWorkers.RemoveElement(&aWorkerPrivate); + } else { + MOZ_ASSERT(domainInfo->mActiveWorkers.Contains(&aWorkerPrivate), + "Don't know about this worker!"); + domainInfo->mActiveWorkers.RemoveElement(&aWorkerPrivate); + } + + // See if there's a queued worker we can schedule. + if (domainInfo->ActiveWorkerCount() < gMaxWorkersPerDomain && + !domainInfo->mQueuedWorkers.IsEmpty()) { + queuedWorker = domainInfo->mQueuedWorkers[0]; + domainInfo->mQueuedWorkers.RemoveElementAt(0); + + if (queuedWorker->GetParent()) { + domainInfo->mChildWorkerCount++; + } else if (queuedWorker->IsServiceWorker()) { + domainInfo->mActiveServiceWorkers.AppendElement(queuedWorker); + } else { + domainInfo->mActiveWorkers.AppendElement(queuedWorker); + } + } + + if (domainInfo->HasNoWorkers()) { + MOZ_ASSERT(domainInfo->mQueuedWorkers.IsEmpty()); + mDomainMap.Remove(domain); + } + } + + if (aWorkerPrivate.IsServiceWorker()) { + AssertIsOnMainThread(); + Telemetry::AccumulateTimeDelta(Telemetry::SERVICE_WORKER_LIFE_TIME, + aWorkerPrivate.CreationTimeStamp()); + } + + // NB: For Shared Workers we used to call ShutdownOnMainThread on the + // RemoteWorkerController; however, that was redundant because + // RemoteWorkerChild uses a WeakWorkerRef which notifies at about the + // same time as us calling into the code here and would race with us. + + if (parent) { + parent->RemoveChildWorker(&aWorkerPrivate); + } else if (aWorkerPrivate.IsSharedWorker()) { + AssertIsOnMainThread(); + + mWindowMap.RemoveIf([&aWorkerPrivate](const auto& iter) { + const auto& workers = iter.Data(); + MOZ_ASSERT(workers); + + if (workers->RemoveElement(&aWorkerPrivate)) { + MOZ_ASSERT(!workers->Contains(&aWorkerPrivate), + "Added worker more than once!"); + + return workers->IsEmpty(); + } + + return false; + }); + } else if (aWorkerPrivate.IsDedicatedWorker()) { + // May be null. + nsPIDOMWindowInner* window = aWorkerPrivate.GetWindow(); + if (auto entry = mWindowMap.Lookup(window)) { + MOZ_ALWAYS_TRUE(entry.Data()->RemoveElement(&aWorkerPrivate)); + if (entry.Data()->IsEmpty()) { + entry.Remove(); + } + } else { + MOZ_ASSERT_UNREACHABLE("window is not in mWindowMap"); + } + } + + if (queuedWorker && !ScheduleWorker(*queuedWorker)) { + UnregisterWorker(*queuedWorker); + } +} + +bool RuntimeService::ScheduleWorker(WorkerPrivate& aWorkerPrivate) { + if (!aWorkerPrivate.Start()) { + // This is ok, means that we didn't need to make a thread for this worker. + return true; + } + + SafeRefPtr<WorkerThread> thread; + { + MutexAutoLock lock(mMutex); + if (!mIdleThreadArray.IsEmpty()) { + thread = std::move(mIdleThreadArray.PopLastElement().mThread); + } + } + + const WorkerThreadFriendKey friendKey; + + if (!thread) { + thread = WorkerThread::Create(friendKey); + if (!thread) { + UnregisterWorker(aWorkerPrivate); + return false; + } + } + + if (NS_FAILED(thread->SetPriority(nsISupportsPriority::PRIORITY_NORMAL))) { + NS_WARNING("Could not set the thread's priority!"); + } + + aWorkerPrivate.SetThread(thread.unsafeGetRawPtr()); + JSContext* cx = CycleCollectedJSContext::Get()->Context(); + nsCOMPtr<nsIRunnable> runnable = new WorkerThreadPrimaryRunnable( + &aWorkerPrivate, thread.clonePtr(), JS_GetParentRuntime(cx)); + if (NS_FAILED( + thread->DispatchPrimaryRunnable(friendKey, runnable.forget()))) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + return true; +} + +// static +void RuntimeService::ShutdownIdleThreads(nsITimer* aTimer, + void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* runtime = RuntimeService::GetService(); + NS_ASSERTION(runtime, "This should never be null!"); + + NS_ASSERTION(aTimer == runtime->mIdleThreadTimer, "Wrong timer!"); + + // Cheat a little and grab all threads that expire within one second of now. + const TimeStamp now = TimeStamp::NowLoRes() + TimeDuration::FromSeconds(1); + + TimeStamp nextExpiration; + + AutoTArray<SafeRefPtr<WorkerThread>, 20> expiredThreads; + { + MutexAutoLock lock(runtime->mMutex); + + for (auto& info : runtime->mIdleThreadArray) { + if (info.mExpirationTime > now) { + nextExpiration = info.mExpirationTime; + break; + } + + expiredThreads.AppendElement(std::move(info.mThread)); + } + + runtime->mIdleThreadArray.RemoveElementsAt(0, expiredThreads.Length()); + } + + if (!nextExpiration.IsNull()) { + const TimeDuration delta = nextExpiration - TimeStamp::NowLoRes(); + const uint32_t delay = delta > TimeDuration{} ? delta.ToMilliseconds() : 0; + + // Reschedule the timer. + MOZ_ALWAYS_SUCCEEDS(aTimer->InitWithNamedFuncCallback( + ShutdownIdleThreads, nullptr, delay, nsITimer::TYPE_ONE_SHOT, + "RuntimeService::ShutdownIdleThreads")); + } + + for (const auto& expiredThread : expiredThreads) { + if (NS_FAILED(expiredThread->Shutdown())) { + NS_WARNING("Failed to shutdown thread!"); + } + } +} + +nsresult RuntimeService::Init() { + AssertIsOnMainThread(); + + nsLayoutStatics::AddRef(); + + // Initialize JSSettings. + sDefaultJSSettings = MakeUnique<JSSettings>(); + SetDefaultJSGCSettings(JSGC_MAX_BYTES, Some(WORKER_DEFAULT_RUNTIME_HEAPSIZE)); + SetDefaultJSGCSettings(JSGC_ALLOCATION_THRESHOLD, + Some(WORKER_DEFAULT_ALLOCATION_THRESHOLD)); + + // nsIStreamTransportService is thread-safe but it must be initialized on the + // main-thread. FileReader needs it, so, let's initialize it now. + nsresult rv; + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + NS_ENSURE_TRUE(sts, NS_ERROR_FAILURE); + + mIdleThreadTimer = NS_NewTimer(); + NS_ENSURE_STATE(mIdleThreadTimer); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + mObserved = true; + + if (NS_FAILED(obs->AddObserver(this, GC_REQUEST_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for GC request notifications!"); + } + + if (NS_FAILED(obs->AddObserver(this, CC_REQUEST_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for CC request notifications!"); + } + + if (NS_FAILED( + obs->AddObserver(this, MEMORY_PRESSURE_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for memory pressure notifications!"); + } + + if (NS_FAILED( + obs->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false))) { + NS_WARNING("Failed to register for offline notification event!"); + } + + MOZ_ASSERT(!gRuntimeServiceDuringInit, "This should be false!"); + gRuntimeServiceDuringInit = true; + +#define WORKER_PREF(name, callback) \ + NS_FAILED(Preferences::RegisterCallbackAndCall(callback, name)) + + if (NS_FAILED(Preferences::RegisterPrefixCallback( + LoadJSGCMemoryOptions, + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX)) || + NS_FAILED(Preferences::RegisterPrefixCallbackAndCall( + LoadJSGCMemoryOptions, + PREF_WORKERS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX)) || +#ifdef JS_GC_ZEAL + NS_FAILED(Preferences::RegisterCallback( + LoadGCZealOptions, PREF_JS_OPTIONS_PREFIX PREF_GCZEAL)) || +#endif + WORKER_PREF("intl.accept_languages", PrefLanguagesChanged) || + WORKER_PREF("general.appname.override", AppNameOverrideChanged) || + WORKER_PREF("general.appversion.override", AppVersionOverrideChanged) || + WORKER_PREF("general.platform.override", PlatformOverrideChanged) || +#ifdef JS_GC_ZEAL + WORKER_PREF("dom.workers.options.gcZeal", LoadGCZealOptions) || +#endif + NS_FAILED(Preferences::RegisterPrefixCallbackAndCall( + LoadContextOptions, PREF_WORKERS_OPTIONS_PREFIX)) || + NS_FAILED(Preferences::RegisterPrefixCallback(LoadContextOptions, + PREF_JS_OPTIONS_PREFIX))) { + NS_WARNING("Failed to register pref callbacks!"); + } + +#undef WORKER_PREF + + MOZ_ASSERT(gRuntimeServiceDuringInit, "Should be true!"); + gRuntimeServiceDuringInit = false; + + int32_t maxPerDomain = + Preferences::GetInt(PREF_WORKERS_MAX_PER_DOMAIN, MAX_WORKERS_PER_DOMAIN); + gMaxWorkersPerDomain = std::max(0, maxPerDomain); + + int32_t maxHardwareConcurrency = Preferences::GetInt( + PREF_WORKERS_MAX_HARDWARE_CONCURRENCY, MAX_HARDWARE_CONCURRENCY); + gMaxHardwareConcurrency = std::max(0, maxHardwareConcurrency); + + RefPtr<OSFileConstantsService> osFileConstantsService = + OSFileConstantsService::GetOrCreate(); + if (NS_WARN_IF(!osFileConstantsService)) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate())) { + return NS_ERROR_UNEXPECTED; + } + + // PerformanceService must be initialized on the main-thread. + PerformanceService::GetOrCreate(); + + return NS_OK; +} + +void RuntimeService::Shutdown() { + AssertIsOnMainThread(); + + MOZ_ASSERT(!mShuttingDown); + // That's it, no more workers. + mShuttingDown = true; + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Failed to get observer service?!"); + + // Tell anyone that cares that they're about to lose worker support. + if (obs && NS_FAILED(obs->NotifyObservers(nullptr, WORKERS_SHUTDOWN_TOPIC, + nullptr))) { + NS_WARNING("NotifyObservers failed!"); + } + + { + AutoTArray<WorkerPrivate*, 100> workers; + + { + MutexAutoLock lock(mMutex); + + AddAllTopLevelWorkersToArray(workers); + } + + // Cancel all top-level workers. + for (const auto& worker : workers) { + if (!worker->Cancel()) { + NS_WARNING("Failed to cancel worker!"); + } + } + } + + sDefaultJSSettings = nullptr; +} + +namespace { + +class CrashIfHangingRunnable : public WorkerControlRunnable { + public: + explicit CrashIfHangingRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mMonitor("CrashIfHangingRunnable::mMonitor") {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MonitorAutoLock lock(mMonitor); + if (!mHasMsg) { + aWorkerPrivate->DumpCrashInformation(mMsg); + mHasMsg.Flip(); + } + lock.Notify(); + return true; + } + + nsresult Cancel() override { + MonitorAutoLock lock(mMonitor); + if (!mHasMsg) { + mMsg.Assign("Canceled"); + mHasMsg.Flip(); + } + lock.Notify(); + + return NS_OK; + } + + bool DispatchAndWait() { + MonitorAutoLock lock(mMonitor); + + if (!Dispatch()) { + // The worker is already dead but the main thread still didn't remove it + // from RuntimeService's registry. + return false; + } + + // To avoid any possibility of process hangs we never receive reports on + // we give the worker 1sec to react. + lock.Wait(TimeDuration::FromMilliseconds(1000)); + if (!mHasMsg) { + mMsg.Append("NoResponse"); + mHasMsg.Flip(); + } + return true; + } + + const nsCString& MsgData() const { return mMsg; } + + private: + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override {} + + Monitor mMonitor; + nsCString mMsg; + FlippedOnce<false> mHasMsg; +}; + +struct ActiveWorkerStats { + template <uint32_t ActiveWorkerStats::*Category> + void Update(const nsTArray<WorkerPrivate*>& aWorkers) { + for (const auto worker : aWorkers) { + RefPtr<CrashIfHangingRunnable> runnable = + new CrashIfHangingRunnable(worker); + if (runnable->DispatchAndWait()) { + ++(this->*Category); + + // BC: Busy Count + mMessage.AppendPrintf("-BC:%d", worker->BusyCount()); + mMessage.Append(runnable->MsgData()); + } else { + mMessage.AppendPrintf("-BC:%d DispatchFailed", worker->BusyCount()); + } + } + } + + uint32_t mWorkers = 0; + uint32_t mServiceWorkers = 0; + nsCString mMessage; +}; + +} // namespace + +void RuntimeService::CrashIfHanging() { + MutexAutoLock lock(mMutex); + + ActiveWorkerStats activeStats; + uint32_t inactiveWorkers = 0; + + for (const auto& entry : mDomainMap) { + const WorkerDomainInfo* const aData = entry.GetData().get(); + + activeStats.Update<&ActiveWorkerStats::mWorkers>(aData->mActiveWorkers); + activeStats.Update<&ActiveWorkerStats::mServiceWorkers>( + aData->mActiveServiceWorkers); + + // These might not be top-level workers... + inactiveWorkers += std::count_if( + aData->mQueuedWorkers.begin(), aData->mQueuedWorkers.end(), + [](const auto* const worker) { return !worker->GetParent(); }); + } + + if (activeStats.mWorkers + activeStats.mServiceWorkers + inactiveWorkers == + 0) { + return; + } + + nsCString msg; + + // A: active Workers | S: active ServiceWorkers | Q: queued Workers + msg.AppendPrintf("Workers Hanging - %d|A:%d|S:%d|Q:%d", mShuttingDown ? 1 : 0, + activeStats.mWorkers, activeStats.mServiceWorkers, + inactiveWorkers); + msg.Append(activeStats.mMessage); + + // This string will be leaked. + MOZ_CRASH_UNSAFE(strdup(msg.BeginReading())); +} + +// This spins the event loop until all workers are finished and their threads +// have been joined. +void RuntimeService::Cleanup() { + AssertIsOnMainThread(); + + if (!mShuttingDown) { + Shutdown(); + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Failed to get observer service?!"); + + if (mIdleThreadTimer) { + if (NS_FAILED(mIdleThreadTimer->Cancel())) { + NS_WARNING("Failed to cancel idle timer!"); + } + mIdleThreadTimer = nullptr; + } + + { + MutexAutoLock lock(mMutex); + + AutoTArray<WorkerPrivate*, 100> workers; + AddAllTopLevelWorkersToArray(workers); + + if (!workers.IsEmpty()) { + nsIThread* currentThread = NS_GetCurrentThread(); + NS_ASSERTION(currentThread, "This should never be null!"); + + // Shut down any idle threads. + if (!mIdleThreadArray.IsEmpty()) { + AutoTArray<SafeRefPtr<WorkerThread>, 20> idleThreads; + idleThreads.SetCapacity(mIdleThreadArray.Length()); + +#ifdef DEBUG + const bool anyNullThread = std::any_of( + mIdleThreadArray.begin(), mIdleThreadArray.end(), + [](const auto& entry) { return entry.mThread == nullptr; }); + MOZ_ASSERT(!anyNullThread); +#endif + + std::transform(mIdleThreadArray.begin(), mIdleThreadArray.end(), + MakeBackInserter(idleThreads), + [](auto& entry) { return std::move(entry.mThread); }); + + mIdleThreadArray.Clear(); + + MutexAutoUnlock unlock(mMutex); + + for (const auto& idleThread : idleThreads) { + if (NS_FAILED(idleThread->Shutdown())) { + NS_WARNING("Failed to shutdown thread!"); + } + } + } + + // And make sure all their final messages have run and all their threads + // have joined. + while (mDomainMap.Count()) { + MutexAutoUnlock unlock(mMutex); + + if (!NS_ProcessNextEvent(currentThread)) { + NS_WARNING("Something bad happened!"); + break; + } + } + } + } + + NS_ASSERTION(!mWindowMap.Count(), "All windows should have been released!"); + +#define WORKER_PREF(name, callback) \ + NS_FAILED(Preferences::UnregisterCallback(callback, name)) + + if (mObserved) { + if (NS_FAILED(Preferences::UnregisterPrefixCallback( + LoadContextOptions, PREF_JS_OPTIONS_PREFIX)) || + NS_FAILED(Preferences::UnregisterPrefixCallback( + LoadContextOptions, PREF_WORKERS_OPTIONS_PREFIX)) || + WORKER_PREF("intl.accept_languages", PrefLanguagesChanged) || + WORKER_PREF("general.appname.override", AppNameOverrideChanged) || + WORKER_PREF("general.appversion.override", AppVersionOverrideChanged) || + WORKER_PREF("general.platform.override", PlatformOverrideChanged) || +#ifdef JS_GC_ZEAL + WORKER_PREF("dom.workers.options.gcZeal", LoadGCZealOptions) || + NS_FAILED(Preferences::UnregisterCallback( + LoadGCZealOptions, PREF_JS_OPTIONS_PREFIX PREF_GCZEAL)) || +#endif + NS_FAILED(Preferences::UnregisterPrefixCallback( + LoadJSGCMemoryOptions, + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX)) || + NS_FAILED(Preferences::UnregisterPrefixCallback( + LoadJSGCMemoryOptions, + PREF_WORKERS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX))) { + NS_WARNING("Failed to unregister pref callbacks!"); + } + +#undef WORKER_PREF + + if (obs) { + if (NS_FAILED(obs->RemoveObserver(this, GC_REQUEST_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for GC request notifications!"); + } + + if (NS_FAILED(obs->RemoveObserver(this, CC_REQUEST_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for CC request notifications!"); + } + + if (NS_FAILED( + obs->RemoveObserver(this, MEMORY_PRESSURE_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for memory pressure notifications!"); + } + + if (NS_FAILED( + obs->RemoveObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC))) { + NS_WARNING("Failed to unregister for offline notification event!"); + } + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + mObserved = false; + } + } + + nsLayoutStatics::Release(); +} + +void RuntimeService::AddAllTopLevelWorkersToArray( + nsTArray<WorkerPrivate*>& aWorkers) { + for (const auto& entry : mDomainMap) { + WorkerDomainInfo* const aData = entry.GetData().get(); + +#ifdef DEBUG + for (const auto& activeWorker : aData->mActiveWorkers) { + MOZ_ASSERT(!activeWorker->GetParent(), + "Shouldn't have a parent in this list!"); + } + for (const auto& activeServiceWorker : aData->mActiveServiceWorkers) { + MOZ_ASSERT(!activeServiceWorker->GetParent(), + "Shouldn't have a parent in this list!"); + } +#endif + + aWorkers.AppendElements(aData->mActiveWorkers); + aWorkers.AppendElements(aData->mActiveServiceWorkers); + + // These might not be top-level workers... + std::copy_if(aData->mQueuedWorkers.begin(), aData->mQueuedWorkers.end(), + MakeBackInserter(aWorkers), + [](const auto& worker) { return !worker->GetParent(); }); + } +} + +nsTArray<WorkerPrivate*> RuntimeService::GetWorkersForWindow( + const nsPIDOMWindowInner& aWindow) const { + AssertIsOnMainThread(); + + nsTArray<WorkerPrivate*> result; + if (nsTArray<WorkerPrivate*>* const workers = mWindowMap.Get(&aWindow)) { + NS_ASSERTION(!workers->IsEmpty(), "Should have been removed!"); + result.AppendElements(*workers); + } + return result; +} + +void RuntimeService::CancelWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->Cancel(); + } +} + +void RuntimeService::FreezeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->Freeze(&aWindow); + } +} + +void RuntimeService::ThawWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->Thaw(&aWindow); + } +} + +void RuntimeService::SuspendWorkersForWindow( + const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->ParentWindowPaused(); + } +} + +void RuntimeService::ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->ParentWindowResumed(); + } +} + +void RuntimeService::PropagateStorageAccessPermissionGranted( + const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + MOZ_ASSERT_IF(aWindow.GetExtantDoc(), aWindow.GetExtantDoc() + ->CookieJarSettings() + ->GetRejectThirdPartyContexts()); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + worker->PropagateStorageAccessPermissionGranted(); + } +} + +void RuntimeService::NoteIdleThread(SafeRefPtr<WorkerThread> aThread) { + AssertIsOnMainThread(); + MOZ_ASSERT(aThread); + + bool shutdownThread = mShuttingDown; + bool scheduleTimer = false; + + if (!shutdownThread) { + static TimeDuration timeout = + TimeDuration::FromSeconds(IDLE_THREAD_TIMEOUT_SEC); + + const TimeStamp expirationTime = TimeStamp::NowLoRes() + timeout; + + MutexAutoLock lock(mMutex); + + const uint32_t previousIdleCount = mIdleThreadArray.Length(); + + if (previousIdleCount < MAX_IDLE_THREADS) { + IdleThreadInfo* const info = mIdleThreadArray.AppendElement(); + info->mThread = std::move(aThread); + info->mExpirationTime = expirationTime; + + scheduleTimer = previousIdleCount == 0; + } else { + shutdownThread = true; + } + } + + MOZ_ASSERT_IF(shutdownThread, !scheduleTimer); + MOZ_ASSERT_IF(scheduleTimer, !shutdownThread); + + // Too many idle threads, just shut this one down. + if (shutdownThread) { + MOZ_ALWAYS_SUCCEEDS(aThread->Shutdown()); + } else if (scheduleTimer) { + MOZ_ALWAYS_SUCCEEDS(mIdleThreadTimer->InitWithNamedFuncCallback( + ShutdownIdleThreads, nullptr, IDLE_THREAD_TIMEOUT_SEC * 1000, + nsITimer::TYPE_ONE_SHOT, "RuntimeService::ShutdownIdleThreads")); + } +} + +template <typename Func> +void RuntimeService::BroadcastAllWorkers(const Func& aFunc) { + AssertIsOnMainThread(); + + AutoTArray<WorkerPrivate*, 100> workers; + { + MutexAutoLock lock(mMutex); + + AddAllTopLevelWorkersToArray(workers); + } + + for (const auto& worker : workers) { + aFunc(*worker); + } +} + +void RuntimeService::UpdateAllWorkerContextOptions() { + BroadcastAllWorkers([](auto& worker) { + worker.UpdateContextOptions(sDefaultJSSettings->contextOptions); + }); +} + +void RuntimeService::UpdateAppNameOverridePreference(const nsAString& aValue) { + AssertIsOnMainThread(); + mNavigatorProperties.mAppNameOverridden = aValue; +} + +void RuntimeService::UpdateAppVersionOverridePreference( + const nsAString& aValue) { + AssertIsOnMainThread(); + mNavigatorProperties.mAppVersionOverridden = aValue; +} + +void RuntimeService::UpdatePlatformOverridePreference(const nsAString& aValue) { + AssertIsOnMainThread(); + mNavigatorProperties.mPlatformOverridden = aValue; +} + +void RuntimeService::UpdateAllWorkerLanguages( + const nsTArray<nsString>& aLanguages) { + MOZ_ASSERT(NS_IsMainThread()); + + mNavigatorProperties.mLanguages = aLanguages.Clone(); + BroadcastAllWorkers( + [&aLanguages](auto& worker) { worker.UpdateLanguages(aLanguages); }); +} + +void RuntimeService::UpdateAllWorkerMemoryParameter(JSGCParamKey aKey, + Maybe<uint32_t> aValue) { + BroadcastAllWorkers([aKey, aValue](auto& worker) { + worker.UpdateJSWorkerMemoryParameter(aKey, aValue); + }); +} + +#ifdef JS_GC_ZEAL +void RuntimeService::UpdateAllWorkerGCZeal() { + BroadcastAllWorkers([](auto& worker) { + worker.UpdateGCZeal(sDefaultJSSettings->gcZeal, + sDefaultJSSettings->gcZealFrequency); + }); +} +#endif + +void RuntimeService::SetLowMemoryStateAllWorkers(bool aState) { + BroadcastAllWorkers( + [aState](auto& worker) { worker.SetLowMemoryState(aState); }); +} + +void RuntimeService::GarbageCollectAllWorkers(bool aShrinking) { + BroadcastAllWorkers( + [aShrinking](auto& worker) { worker.GarbageCollect(aShrinking); }); +} + +void RuntimeService::CycleCollectAllWorkers() { + BroadcastAllWorkers([](auto& worker) { worker.CycleCollect(); }); +} + +void RuntimeService::SendOfflineStatusChangeEventToAllWorkers(bool aIsOffline) { + BroadcastAllWorkers([aIsOffline](auto& worker) { + worker.OfflineStatusChangeEvent(aIsOffline); + }); +} + +void RuntimeService::MemoryPressureAllWorkers() { + BroadcastAllWorkers([](auto& worker) { worker.MemoryPressure(); }); +} + +uint32_t RuntimeService::ClampedHardwareConcurrency() const { + // The Firefox Hardware Report says 70% of Firefox users have exactly 2 cores. + // When the resistFingerprinting pref is set, we want to blend into the crowd + // so spoof navigator.hardwareConcurrency = 2 to reduce user uniqueness. + if (MOZ_UNLIKELY(nsContentUtils::ShouldResistFingerprinting())) { + return 2; + } + + // This needs to be atomic, because multiple workers, and even mainthread, + // could race to initialize it at once. + static Atomic<uint32_t> clampedHardwareConcurrency; + + // No need to loop here: if compareExchange fails, that just means that some + // other worker has initialized numberOfProcessors, so we're good to go. + if (!clampedHardwareConcurrency) { + int32_t numberOfProcessors = 0; +#if defined(XP_MACOSX) + if (nsMacUtilsImpl::IsTCSMAvailable()) { + // On failure, zero is returned from GetPhysicalCPUCount() + // and we fallback to PR_GetNumberOfProcessors below. + numberOfProcessors = nsMacUtilsImpl::GetPhysicalCPUCount(); + } +#endif + if (numberOfProcessors == 0) { + numberOfProcessors = PR_GetNumberOfProcessors(); + } + if (numberOfProcessors <= 0) { + numberOfProcessors = 1; // Must be one there somewhere + } + uint32_t clampedValue = + std::min(uint32_t(numberOfProcessors), gMaxHardwareConcurrency); + Unused << clampedHardwareConcurrency.compareExchange(0, clampedValue); + } + + return clampedHardwareConcurrency; +} + +// nsISupports +NS_IMPL_ISUPPORTS(RuntimeService, nsIObserver) + +// nsIObserver +NS_IMETHODIMP +RuntimeService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + AssertIsOnMainThread(); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + return NS_OK; + } + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)) { + Cleanup(); + return NS_OK; + } + if (!strcmp(aTopic, GC_REQUEST_OBSERVER_TOPIC)) { + GarbageCollectAllWorkers(/* shrinking = */ false); + return NS_OK; + } + if (!strcmp(aTopic, CC_REQUEST_OBSERVER_TOPIC)) { + CycleCollectAllWorkers(); + return NS_OK; + } + if (!strcmp(aTopic, MEMORY_PRESSURE_OBSERVER_TOPIC)) { + nsDependentString data(aData); + // Don't continue to GC/CC if we are in an ongoing low-memory state since + // its very slow and it likely won't help us anyway. + if (data.EqualsLiteral(LOW_MEMORY_ONGOING_DATA)) { + return NS_OK; + } + if (data.EqualsLiteral(LOW_MEMORY_DATA)) { + SetLowMemoryStateAllWorkers(true); + } + GarbageCollectAllWorkers(/* shrinking = */ true); + CycleCollectAllWorkers(); + MemoryPressureAllWorkers(); + return NS_OK; + } + if (!strcmp(aTopic, MEMORY_PRESSURE_STOP_OBSERVER_TOPIC)) { + SetLowMemoryStateAllWorkers(false); + return NS_OK; + } + if (!strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC)) { + SendOfflineStatusChangeEventToAllWorkers(NS_IsOffline()); + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Unknown observer topic!"); + return NS_OK; +} + +bool LogViolationDetailsRunnable::MainThreadRun() { + AssertIsOnMainThread(); + + nsIContentSecurityPolicy* csp = mWorkerPrivate->GetCSP(); + if (csp) { + if (mWorkerPrivate->GetReportCSPViolations()) { + csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, + nullptr, // triggering element + mWorkerPrivate->CSPEventListener(), mFileName, + mScriptSample, mLineNum, mColumnNum, u""_ns, + u""_ns); + } + } + + return true; +} + +// MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See +// bug 1535398. +MOZ_CAN_RUN_SCRIPT_BOUNDARY +NS_IMETHODIMP +WorkerThreadPrimaryRunnable::Run() { + AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING( + "WorkerThreadPrimaryRunnable::Run", OTHER, mWorkerPrivate->ScriptURL()); + + using mozilla::ipc::BackgroundChild; + + { + auto failureCleanup = MakeScopeExit([&]() { + // The creation of threadHelper above is the point at which a worker is + // considered to have run, because the `mPreStartRunnables` are all + // re-dispatched after `mThread` is set. We need to let the WorkerPrivate + // know so it can clean up the various event loops and delete the worker. + mWorkerPrivate->RunLoopNeverRan(); + }); + + mWorkerPrivate->SetWorkerPrivateInWorkerThread(mThread.unsafeGetRawPtr()); + + const auto threadCleanup = MakeScopeExit([&] { + // This must be called before ScheduleDeletion, which is either called + // from failureCleanup leaving scope, or from the outer scope. + mWorkerPrivate->ResetWorkerPrivateInWorkerThread(); + }); + + mWorkerPrivate->AssertIsOnWorkerThread(); + + // This needs to be initialized on the worker thread before being used on + // the main thread. + mWorkerPrivate->EnsurePerformanceStorage(); + + if (NS_WARN_IF(!BackgroundChild::GetOrCreateForCurrentThread())) { + return NS_ERROR_FAILURE; + } + + { + nsCycleCollector_startup(); + + auto context = MakeUnique<WorkerJSContext>(mWorkerPrivate); + nsresult rv = context->Initialize(mParentRuntime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = context->Context(); + + if (!InitJSContextForWorker(mWorkerPrivate, cx)) { + return NS_ERROR_FAILURE; + } + + failureCleanup.release(); + + { + PROFILER_SET_JS_CONTEXT(cx); + + { + // We're on the worker thread here, and WorkerPrivate's refcounting is + // non-threadsafe: you can only do it on the parent thread. What that + // means in practice is that we're relying on it being kept alive + // while we run. Hopefully. + MOZ_KnownLive(mWorkerPrivate)->DoRunLoop(cx); + // The AutoJSAPI in DoRunLoop should have reported any exceptions left + // on cx. + MOZ_ASSERT(!JS_IsExceptionPending(cx)); + } + + BackgroundChild::CloseForCurrentThread(); + + PROFILER_CLEAR_JS_CONTEXT(); + } + + // There may still be runnables on the debugger event queue that hold a + // strong reference to the debugger global scope. These runnables are not + // visible to the cycle collector, so we need to make sure to clear the + // debugger event queue before we try to destroy the context. If we don't, + // the garbage collector will crash. + mWorkerPrivate->ClearDebuggerEventQueue(); + + // Perform a full GC. This will collect the main worker global and CC, + // which should break all cycles that touch JS. + JS_GC(cx, JS::GCReason::WORKER_SHUTDOWN); + + // Before shutting down the cycle collector we need to do one more pass + // through the event loop to clean up any C++ objects that need deferred + // cleanup. + mWorkerPrivate->ClearMainEventQueue(WorkerPrivate::WorkerRan); + + // Now WorkerJSContext goes out of scope and its destructor will shut + // down the cycle collector. This breaks any remaining cycles and collects + // any remaining C++ objects. + } + } + + mWorkerPrivate->ScheduleDeletion(WorkerPrivate::WorkerRan); + + // It is no longer safe to touch mWorkerPrivate. + mWorkerPrivate = nullptr; + + // Now recycle this thread. + nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadEventTarget(); + MOZ_ASSERT(mainTarget); + + RefPtr<FinishedRunnable> finishedRunnable = + new FinishedRunnable(std::move(mThread)); + MOZ_ALWAYS_SUCCEEDS( + mainTarget->Dispatch(finishedRunnable, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +NS_IMETHODIMP +WorkerThreadPrimaryRunnable::FinishedRunnable::Run() { + AssertIsOnMainThread(); + + SafeRefPtr<WorkerThread> thread = std::move(mThread); + + RuntimeService* rts = RuntimeService::GetService(); + if (rts) { + rts->NoteIdleThread(std::move(thread)); + } else if (thread->ShutdownRequired()) { + MOZ_ALWAYS_SUCCEEDS(thread->Shutdown()); + } + + return NS_OK; +} + +} // namespace workerinternals + +void CancelWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->CancelWorkersForWindow(aWindow); + } +} + +void FreezeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->FreezeWorkersForWindow(aWindow); + } +} + +void ThawWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->ThawWorkersForWindow(aWindow); + } +} + +void SuspendWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->SuspendWorkersForWindow(aWindow); + } +} + +void ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->ResumeWorkersForWindow(aWindow); + } +} + +void PropagateStorageAccessPermissionGrantedToWorkers( + const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + MOZ_ASSERT_IF(aWindow.GetExtantDoc(), aWindow.GetExtantDoc() + ->CookieJarSettings() + ->GetRejectThirdPartyContexts()); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->PropagateStorageAccessPermissionGranted(aWindow); + } +} + +WorkerPrivate* GetWorkerPrivateFromContext(JSContext* aCx) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCx); + + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::GetFor(aCx); + if (!ccjscx) { + return nullptr; + } + + WorkerJSContext* workerjscx = ccjscx->GetAsWorkerJSContext(); + // GetWorkerPrivateFromContext is called only for worker contexts. The + // context private is cleared early in ~CycleCollectedJSContext() and so + // GetFor() returns null above if called after ccjscx is no longer a + // WorkerJSContext. + MOZ_ASSERT(workerjscx); + return workerjscx->GetWorkerPrivate(); +} + +WorkerPrivate* GetCurrentThreadWorkerPrivate() { + if (NS_IsMainThread()) { + return nullptr; + } + + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + if (!ccjscx) { + return nullptr; + } + + WorkerJSContext* workerjscx = ccjscx->GetAsWorkerJSContext(); + // Even when GetCurrentThreadWorkerPrivate() is called on worker + // threads, the ccjscx will no longer be a WorkerJSContext if called from + // stable state events during ~CycleCollectedJSContext(). + if (!workerjscx) { + return nullptr; + } + + return workerjscx->GetWorkerPrivate(); +} + +bool IsCurrentThreadRunningWorker() { + return !NS_IsMainThread() && !!GetCurrentThreadWorkerPrivate(); +} + +bool IsCurrentThreadRunningChromeWorker() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + return wp && wp->UsesSystemPrincipal(); +} + +JSContext* GetCurrentWorkerThreadJSContext() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + return wp->GetJSContext(); +} + +JSObject* GetCurrentThreadWorkerGlobal() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + WorkerGlobalScope* scope = wp->GlobalScope(); + if (!scope) { + return nullptr; + } + return scope->GetGlobalJSObject(); +} + +JSObject* GetCurrentThreadWorkerDebuggerGlobal() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + WorkerDebuggerGlobalScope* scope = wp->DebuggerGlobalScope(); + if (!scope) { + return nullptr; + } + return scope->GetGlobalJSObject(); +} + +} // namespace dom +} // namespace mozilla |