diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/xpconnect/src/XPCJSRuntime.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/xpconnect/src/XPCJSRuntime.cpp')
-rw-r--r-- | js/xpconnect/src/XPCJSRuntime.cpp | 3322 |
1 files changed, 3322 insertions, 0 deletions
diff --git a/js/xpconnect/src/XPCJSRuntime.cpp b/js/xpconnect/src/XPCJSRuntime.cpp new file mode 100644 index 0000000000..faad20d233 --- /dev/null +++ b/js/xpconnect/src/XPCJSRuntime.cpp @@ -0,0 +1,3322 @@ +/* -*- 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/. */ + +/* Per JSRuntime object */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +#include "xpcprivate.h" +#include "xpcpublic.h" +#include "XPCWrapper.h" +#include "XPCJSMemoryReporter.h" +#include "XPCJSThreadPool.h" +#include "XrayWrapper.h" +#include "WrapperFactory.h" +#include "mozJSComponentLoader.h" +#include "nsNetUtil.h" +#include "nsContentSecurityUtils.h" + +#include "nsExceptionHandler.h" +#include "nsIMemoryInfoDumper.h" +#include "nsIMemoryReporter.h" +#include "nsIObserverService.h" +#include "mozilla/dom/Document.h" +#include "nsIRunnable.h" +#include "nsIPlatformInfo.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsScriptSecurityManager.h" +#include "nsThreadPool.h" +#include "nsWindowSizes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Services.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/ScriptSettings.h" + +#include "nsContentUtils.h" +#include "nsCCUncollectableMarker.h" +#include "nsCycleCollectionNoteRootCallback.h" +#include "nsCycleCollector.h" +#include "jsapi.h" +#include "js/BuildId.h" // JS::BuildIdCharVector, JS::SetProcessBuildIdOp +#include "js/experimental/SourceHook.h" // js::{,Set}SourceHook +#include "js/GCAPI.h" +#include "js/MemoryFunctions.h" +#include "js/MemoryMetrics.h" +#include "js/Object.h" // JS::GetClass +#include "js/Stream.h" // JS::AbortSignalIsAborted, JS::InitPipeToHandling +#include "js/SliceBudget.h" +#include "js/UbiNode.h" +#include "js/UbiNodeUtils.h" +#include "js/friend/UsageStatistics.h" // JS_TELEMETRY_*, JS_SetAccumulateTelemetryCallback +#include "js/friend/WindowProxy.h" // js::SetWindowProxyClass +#include "js/friend/XrayJitInfo.h" // JS::SetXrayJitInfo +#include "mozilla/dom/AbortSignalBinding.h" +#include "mozilla/dom/GeneratedAtomList.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/WindowBinding.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/ProcessHangMonitor.h" +#include "mozilla/Sprintf.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/Unused.h" +#include "AccessCheck.h" +#include "nsGlobalWindow.h" +#include "nsAboutProtocolUtils.h" + +#include "GeckoProfiler.h" +#include "NodeUbiReporting.h" +#include "nsIInputStream.h" +#include "nsJSPrincipals.h" + +#ifdef XP_WIN +# include <windows.h> +#endif + +using namespace mozilla; +using namespace xpc; +using namespace JS; +using mozilla::dom::AutoEntryScript; +using mozilla::dom::PerThreadAtomCache; + +/***************************************************************************/ + +const char* const XPCJSRuntime::mStrings[] = { + "constructor", // IDX_CONSTRUCTOR + "toString", // IDX_TO_STRING + "toSource", // IDX_TO_SOURCE + "lastResult", // IDX_LAST_RESULT + "returnCode", // IDX_RETURN_CODE + "value", // IDX_VALUE + "QueryInterface", // IDX_QUERY_INTERFACE + "Components", // IDX_COMPONENTS + "Cc", // IDX_CC + "Ci", // IDX_CI + "Cr", // IDX_CR + "Cu", // IDX_CU + "wrappedJSObject", // IDX_WRAPPED_JSOBJECT + "Object", // IDX_OBJECT + "Function", // IDX_FUNCTION + "prototype", // IDX_PROTOTYPE + "createInstance", // IDX_CREATE_INSTANCE + "item", // IDX_ITEM + "__proto__", // IDX_PROTO + "eval", // IDX_EVAL + "controllers", // IDX_CONTROLLERS + "Controllers", // IDX_CONTROLLERS_CLASS + "realFrameElement", // IDX_REALFRAMEELEMENT + "length", // IDX_LENGTH + "name", // IDX_NAME + "undefined", // IDX_UNDEFINED + "", // IDX_EMPTYSTRING + "fileName", // IDX_FILENAME + "lineNumber", // IDX_LINENUMBER + "columnNumber", // IDX_COLUMNNUMBER + "stack", // IDX_STACK + "message", // IDX_MESSAGE + "errors", // IDX_ERRORS + "lastIndex", // IDX_LASTINDEX + "then", // IDX_THEN + "isInstance", // IDX_ISINSTANCE + "Infinity", // IDX_INFINITY + "NaN", // IDX_NAN + "classId", // IDX_CLASS_ID + "interfaceId", // IDX_INTERFACE_ID + "initializer", // IDX_INITIALIZER + "print", // IDX_PRINT +}; + +/***************************************************************************/ + +// *Some* NativeSets are referenced from mClassInfo2NativeSetMap. +// *All* NativeSets are referenced from mNativeSetMap. +// So, in mClassInfo2NativeSetMap we just clear references to the unmarked. +// In mNativeSetMap we clear the references to the unmarked *and* delete them. + +class AsyncFreeSnowWhite : public Runnable { + public: + NS_IMETHOD Run() override { + AUTO_PROFILER_LABEL("AsyncFreeSnowWhite::Run", GCCC_FreeSnowWhite); + + TimeStamp start = TimeStamp::Now(); + // 2 ms budget, given that kICCSliceBudget is only 3 ms + js::SliceBudget budget = js::SliceBudget(js::TimeBudget(2)); + bool hadSnowWhiteObjects = + nsCycleCollector_doDeferredDeletionWithBudget(budget); + Telemetry::Accumulate( + Telemetry::CYCLE_COLLECTOR_ASYNC_SNOW_WHITE_FREEING, + uint32_t((TimeStamp::Now() - start).ToMilliseconds())); + if (hadSnowWhiteObjects && !mContinuation) { + mContinuation = true; + if (NS_FAILED(Dispatch())) { + mActive = false; + } + } else { + mActive = false; + } + return NS_OK; + } + + nsresult Dispatch() { + nsCOMPtr<nsIRunnable> self(this); + return NS_DispatchToCurrentThreadQueue(self.forget(), 500, + EventQueuePriority::Idle); + } + + void Start(bool aContinuation = false, bool aPurge = false) { + if (mContinuation) { + mContinuation = aContinuation; + } + mPurge = aPurge; + if (!mActive && NS_SUCCEEDED(Dispatch())) { + mActive = true; + } + } + + AsyncFreeSnowWhite() + : Runnable("AsyncFreeSnowWhite"), + mContinuation(false), + mActive(false), + mPurge(false) {} + + public: + bool mContinuation; + bool mActive; + bool mPurge; +}; + +namespace xpc { + +CompartmentPrivate::CompartmentPrivate( + JS::Compartment* c, mozilla::UniquePtr<XPCWrappedNativeScope> scope, + mozilla::BasePrincipal* origin, const SiteIdentifier& site) + : originInfo(origin, site), + wantXrays(false), + allowWaivers(true), + isWebExtensionContentScript(false), + isUAWidgetCompartment(false), + hasExclusiveExpandos(false), + wasShutdown(false), + mWrappedJSMap(mozilla::MakeUnique<JSObject2WrappedJSMap>()), + mScope(std::move(scope)) { + MOZ_COUNT_CTOR(xpc::CompartmentPrivate); +} + +CompartmentPrivate::~CompartmentPrivate() { + MOZ_COUNT_DTOR(xpc::CompartmentPrivate); +} + +void CompartmentPrivate::SystemIsBeingShutDown() { + // We may call this multiple times when the compartment contains more than one + // realm. + if (!wasShutdown) { + mWrappedJSMap->ShutdownMarker(); + wasShutdown = true; + } +} + +RealmPrivate::RealmPrivate(JS::Realm* realm) : scriptability(realm) { + mozilla::PodArrayZero(wrapperDenialWarnings); +} + +/* static */ +void RealmPrivate::Init(HandleObject aGlobal, const SiteIdentifier& aSite) { + MOZ_ASSERT(aGlobal); + DebugOnly<const JSClass*> clasp = JS::GetClass(aGlobal); + MOZ_ASSERT(clasp->flags & + (JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE) || + dom::IsDOMClass(clasp)); + + Realm* realm = GetObjectRealmOrNull(aGlobal); + + // Create the realm private. + RealmPrivate* realmPriv = new RealmPrivate(realm); + MOZ_ASSERT(!GetRealmPrivate(realm)); + SetRealmPrivate(realm, realmPriv); + + nsIPrincipal* principal = GetRealmPrincipal(realm); + Compartment* c = JS::GetCompartment(aGlobal); + + // Create the compartment private if needed. + if (CompartmentPrivate* priv = CompartmentPrivate::Get(c)) { + MOZ_ASSERT(priv->originInfo.IsSameOrigin(principal)); + } else { + auto scope = mozilla::MakeUnique<XPCWrappedNativeScope>(c, aGlobal); + priv = new CompartmentPrivate(c, std::move(scope), + BasePrincipal::Cast(principal), aSite); + JS_SetCompartmentPrivate(c, priv); + } +} + +static bool TryParseLocationURICandidate( + const nsACString& uristr, RealmPrivate::LocationHint aLocationHint, + nsIURI** aURI) { + static constexpr auto kGRE = "resource://gre/"_ns; + static constexpr auto kToolkit = "chrome://global/"_ns; + static constexpr auto kBrowser = "chrome://browser/"_ns; + + if (aLocationHint == RealmPrivate::LocationHintAddon) { + // Blacklist some known locations which are clearly not add-on related. + if (StringBeginsWith(uristr, kGRE) || StringBeginsWith(uristr, kToolkit) || + StringBeginsWith(uristr, kBrowser)) + return false; + + // -- GROSS HACK ALERT -- + // The Yandex Elements 8.10.2 extension implements its own "xb://" URL + // scheme. If we call NS_NewURI() on an "xb://..." URL, we'll end up + // calling into the extension's own JS-implemented nsIProtocolHandler + // object, which we can't allow while we're iterating over the JS heap. + // So just skip any such URL. + // -- GROSS HACK ALERT -- + if (StringBeginsWith(uristr, "xb"_ns)) { + return false; + } + } + + nsCOMPtr<nsIURI> uri; + if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), uristr))) { + return false; + } + + nsAutoCString scheme; + if (NS_FAILED(uri->GetScheme(scheme))) { + return false; + } + + // Cannot really map data: and blob:. + // Also, data: URIs are pretty memory hungry, which is kinda bad + // for memory reporter use. + if (scheme.EqualsLiteral("data") || scheme.EqualsLiteral("blob")) { + return false; + } + + uri.forget(aURI); + return true; +} + +bool RealmPrivate::TryParseLocationURI(RealmPrivate::LocationHint aLocationHint, + nsIURI** aURI) { + if (!aURI) { + return false; + } + + // Need to parse the URI. + if (location.IsEmpty()) { + return false; + } + + // Handle Sandbox location strings. + // A sandbox string looks like this, for anonymous sandboxes, and builds + // where Sandbox location tagging is enabled: + // + // <sandboxName> (from: <js-stack-frame-filename>:<lineno>) + // + // where <sandboxName> is user-provided via Cu.Sandbox() + // and <js-stack-frame-filename> and <lineno> is the stack frame location + // from where Cu.Sandbox was called. + // + // Otherwise, it is simply the caller-provided name, which is usually a URI. + // + // <js-stack-frame-filename> furthermore is "free form", often using a + // "uri -> uri -> ..." chain. The following code will and must handle this + // common case. + // + // It should be noted that other parts of the code may already rely on the + // "format" of these strings. + + static const nsDependentCString from("(from: "); + static const nsDependentCString arrow(" -> "); + static const size_t fromLength = from.Length(); + static const size_t arrowLength = arrow.Length(); + + // See: XPCComponents.cpp#AssembleSandboxMemoryReporterName + int32_t idx = location.Find(from); + if (idx < 0) { + return TryParseLocationURICandidate(location, aLocationHint, aURI); + } + + // When parsing we're looking for the right-most URI. This URI may be in + // <sandboxName>, so we try this first. + if (TryParseLocationURICandidate(Substring(location, 0, idx), aLocationHint, + aURI)) + return true; + + // Not in <sandboxName> so we need to inspect <js-stack-frame-filename> and + // the chain that is potentially contained within and grab the rightmost + // item that is actually a URI. + + // First, hack off the :<lineno>) part as well + int32_t ridx = location.RFind(":"_ns); + nsAutoCString chain( + Substring(location, idx + fromLength, ridx - idx - fromLength)); + + // Loop over the "->" chain. This loop also works for non-chains, or more + // correctly chains with only one item. + for (;;) { + idx = chain.RFind(arrow); + if (idx < 0) { + // This is the last chain item. Try to parse what is left. + return TryParseLocationURICandidate(chain, aLocationHint, aURI); + } + + // Try to parse current chain item + if (TryParseLocationURICandidate(Substring(chain, idx + arrowLength), + aLocationHint, aURI)) + return true; + + // Current chain item couldn't be parsed. + // Strip current item and continue. + chain = Substring(chain, 0, idx); + } + + MOZ_CRASH("Chain parser loop does not terminate"); +} + +static bool PrincipalImmuneToScriptPolicy(nsIPrincipal* aPrincipal) { + // System principal gets a free pass. + if (aPrincipal->IsSystemPrincipal()) { + return true; + } + + auto principal = BasePrincipal::Cast(aPrincipal); + + // ExpandedPrincipal gets a free pass. + if (principal->Is<ExpandedPrincipal>()) { + return true; + } + + // WebExtension principals get a free pass. + if (principal->AddonPolicy()) { + return true; + } + + // pdf.js is a special-case too. + if (nsContentUtils::IsPDFJS(principal)) { + return true; + } + + // Check whether our URI is an "about:" URI that allows scripts. If it is, + // we need to allow JS to run. + if (aPrincipal->SchemeIs("about")) { + uint32_t flags; + nsresult rv = aPrincipal->GetAboutModuleFlags(&flags); + if (NS_SUCCEEDED(rv) && (flags & nsIAboutModule::ALLOW_SCRIPT)) { + return true; + } + } + + return false; +} + +void RealmPrivate::RegisterStackFrame(JSStackFrameBase* aFrame) { + mJSStackFrames.PutEntry(aFrame); +} + +void RealmPrivate::UnregisterStackFrame(JSStackFrameBase* aFrame) { + mJSStackFrames.RemoveEntry(aFrame); +} + +void RealmPrivate::NukeJSStackFrames() { + for (auto iter = mJSStackFrames.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetKey()->Clear(); + } + + mJSStackFrames.Clear(); +} + +void RegisterJSStackFrame(JS::Realm* aRealm, JSStackFrameBase* aStackFrame) { + RealmPrivate* realmPrivate = RealmPrivate::Get(aRealm); + if (!realmPrivate) { + return; + } + + realmPrivate->RegisterStackFrame(aStackFrame); +} + +void UnregisterJSStackFrame(JS::Realm* aRealm, JSStackFrameBase* aStackFrame) { + RealmPrivate* realmPrivate = RealmPrivate::Get(aRealm); + if (!realmPrivate) { + return; + } + + realmPrivate->UnregisterStackFrame(aStackFrame); +} + +void NukeJSStackFrames(JS::Realm* aRealm) { + RealmPrivate* realmPrivate = RealmPrivate::Get(aRealm); + if (!realmPrivate) { + return; + } + + realmPrivate->NukeJSStackFrames(); +} + +Scriptability::Scriptability(JS::Realm* realm) + : mScriptBlocks(0), + mDocShellAllowsScript(true), + mScriptBlockedByPolicy(false) { + nsIPrincipal* prin = nsJSPrincipals::get(JS::GetRealmPrincipals(realm)); + + mImmuneToScriptPolicy = PrincipalImmuneToScriptPolicy(prin); + if (mImmuneToScriptPolicy) { + return; + } + // If we're not immune, we should have a real principal with a URI. + // Check the principal against the new-style domain policy. + bool policyAllows; + nsresult rv = prin->GetIsScriptAllowedByPolicy(&policyAllows); + if (NS_SUCCEEDED(rv)) { + mScriptBlockedByPolicy = !policyAllows; + return; + } + // Something went wrong - be safe and block script. + mScriptBlockedByPolicy = true; +} + +bool Scriptability::Allowed() { + return mDocShellAllowsScript && !mScriptBlockedByPolicy && mScriptBlocks == 0; +} + +bool Scriptability::IsImmuneToScriptPolicy() { return mImmuneToScriptPolicy; } + +void Scriptability::Block() { ++mScriptBlocks; } + +void Scriptability::Unblock() { + MOZ_ASSERT(mScriptBlocks > 0); + --mScriptBlocks; +} + +void Scriptability::SetDocShellAllowsScript(bool aAllowed) { + mDocShellAllowsScript = aAllowed || mImmuneToScriptPolicy; +} + +/* static */ +Scriptability& Scriptability::Get(JSObject* aScope) { + return RealmPrivate::Get(aScope)->scriptability; +} + +bool IsUAWidgetCompartment(JS::Compartment* compartment) { + // We always eagerly create compartment privates for UA Widget compartments. + CompartmentPrivate* priv = CompartmentPrivate::Get(compartment); + return priv && priv->isUAWidgetCompartment; +} + +bool IsUAWidgetScope(JS::Realm* realm) { + return IsUAWidgetCompartment(JS::GetCompartmentForRealm(realm)); +} + +bool IsInUAWidgetScope(JSObject* obj) { + return IsUAWidgetCompartment(JS::GetCompartment(obj)); +} + +bool CompartmentOriginInfo::MightBeWebContent() const { + // Compartments with principals that are either the system principal or an + // expanded principal are definitely not web content. + return !nsContentUtils::IsSystemOrExpandedPrincipal(mOrigin); +} + +bool MightBeWebContentCompartment(JS::Compartment* compartment) { + if (CompartmentPrivate* priv = CompartmentPrivate::Get(compartment)) { + return priv->originInfo.MightBeWebContent(); + } + + // No CompartmentPrivate; try IsSystemCompartment. + return !js::IsSystemCompartment(compartment); +} + +bool CompartmentOriginInfo::IsSameOrigin(nsIPrincipal* aOther) const { + return mOrigin->FastEquals(aOther); +} + +/* static */ +bool CompartmentOriginInfo::Subsumes(JS::Compartment* aCompA, + JS::Compartment* aCompB) { + CompartmentPrivate* apriv = CompartmentPrivate::Get(aCompA); + CompartmentPrivate* bpriv = CompartmentPrivate::Get(aCompB); + MOZ_ASSERT(apriv); + MOZ_ASSERT(bpriv); + return apriv->originInfo.mOrigin->FastSubsumes(bpriv->originInfo.mOrigin); +} + +/* static */ +bool CompartmentOriginInfo::SubsumesIgnoringFPD(JS::Compartment* aCompA, + JS::Compartment* aCompB) { + CompartmentPrivate* apriv = CompartmentPrivate::Get(aCompA); + CompartmentPrivate* bpriv = CompartmentPrivate::Get(aCompB); + MOZ_ASSERT(apriv); + MOZ_ASSERT(bpriv); + return apriv->originInfo.mOrigin->FastSubsumesIgnoringFPD( + bpriv->originInfo.mOrigin); +} + +void SetCompartmentChangedDocumentDomain(JS::Compartment* compartment) { + // Note: we call this for all compartments that contain realms with a + // particular principal. Not all of these compartments have a + // CompartmentPrivate (for instance the temporary compartment/realm + // created by the JS engine for off-thread parsing). + if (CompartmentPrivate* priv = CompartmentPrivate::Get(compartment)) { + priv->originInfo.SetChangedDocumentDomain(); + } +} + +JSObject* UnprivilegedJunkScope() { + return XPCJSRuntime::Get()->UnprivilegedJunkScope(); +} + +JSObject* UnprivilegedJunkScope(const fallible_t&) { + return XPCJSRuntime::Get()->UnprivilegedJunkScope(fallible); +} + +bool IsUnprivilegedJunkScope(JSObject* obj) { + return XPCJSRuntime::Get()->IsUnprivilegedJunkScope(obj); +} + +JSObject* NACScope(JSObject* global) { + // If we're a chrome global, just use ourselves. + if (AccessCheck::isChrome(global)) { + return global; + } + + JSObject* scope = UnprivilegedJunkScope(); + JS::ExposeObjectToActiveJS(scope); + return scope; +} + +JSObject* PrivilegedJunkScope() { return XPCJSRuntime::Get()->LoaderGlobal(); } + +JSObject* CompilationScope() { return XPCJSRuntime::Get()->LoaderGlobal(); } + +nsGlobalWindowInner* WindowOrNull(JSObject* aObj) { + MOZ_ASSERT(aObj); + MOZ_ASSERT(!js::IsWrapper(aObj)); + + nsGlobalWindowInner* win = nullptr; + UNWRAP_NON_WRAPPER_OBJECT(Window, aObj, win); + return win; +} + +nsGlobalWindowInner* WindowGlobalOrNull(JSObject* aObj) { + MOZ_ASSERT(aObj); + JSObject* glob = JS::GetNonCCWObjectGlobal(aObj); + + return WindowOrNull(glob); +} + +nsGlobalWindowInner* CurrentWindowOrNull(JSContext* cx) { + JSObject* glob = JS::CurrentGlobalOrNull(cx); + return glob ? WindowOrNull(glob) : nullptr; +} + +// Nukes all wrappers into or out of the given realm, and prevents new +// wrappers from being created. Additionally marks the realm as +// unscriptable after wrappers have been nuked. +// +// Note: This should *only* be called for browser or extension realms. +// Wrappers between web compartments must never be cut in web-observable +// ways. +void NukeAllWrappersForRealm( + JSContext* cx, JS::Realm* realm, + js::NukeReferencesToWindow nukeReferencesToWindow) { + // We do the following: + // * Nuke all wrappers into the realm. + // * Nuke all wrappers out of the realm's compartment, once we have nuked all + // realms in it. + js::NukeCrossCompartmentWrappers(cx, js::AllCompartments(), realm, + nukeReferencesToWindow, + js::NukeAllReferences); + + // Mark the realm as unscriptable. + xpc::RealmPrivate::Get(realm)->scriptability.Block(); +} + +} // namespace xpc + +static void CompartmentDestroyedCallback(JSFreeOp* fop, + JS::Compartment* compartment) { + // NB - This callback may be called in JS_DestroyContext, which happens + // after the XPCJSRuntime has been torn down. + + // Get the current compartment private into a UniquePtr (which will do the + // cleanup for us), and null out the private (which may already be null). + mozilla::UniquePtr<CompartmentPrivate> priv( + CompartmentPrivate::Get(compartment)); + JS_SetCompartmentPrivate(compartment, nullptr); +} + +static size_t CompartmentSizeOfIncludingThisCallback( + MallocSizeOf mallocSizeOf, JS::Compartment* compartment) { + CompartmentPrivate* priv = CompartmentPrivate::Get(compartment); + return priv ? priv->SizeOfIncludingThis(mallocSizeOf) : 0; +} + +/* + * Return true if there exists a non-system inner window which is a current + * inner window and whose reflector is gray. We don't merge system + * compartments, so we don't use them to trigger merging CCs. + */ +bool XPCJSRuntime::UsefulToMergeZones() const { + MOZ_ASSERT(NS_IsMainThread()); + + // Turns out, actually making this return true often enough makes Windows + // mochitest-gl OOM a lot. Need to figure out what's going on there; see + // bug 1277036. + + return false; +} + +void XPCJSRuntime::TraceNativeBlackRoots(JSTracer* trc) { + if (CycleCollectedJSContext* ccx = GetContext()) { + auto* cx = static_cast<const XPCJSContext*>(ccx); + if (AutoMarkingPtr* roots = cx->mAutoRoots) { + roots->TraceJSAll(trc); + } + } + + dom::TraceBlackJS(trc, nsIXPConnect::XPConnect()->GetIsShuttingDown()); +} + +void XPCJSRuntime::TraceAdditionalNativeGrayRoots(JSTracer* trc) { + XPCWrappedNativeScope::TraceWrappedNativesInAllScopes(this, trc); + + for (XPCRootSetElem* e = mVariantRoots; e; e = e->GetNextRoot()) { + static_cast<XPCTraceableVariant*>(e)->TraceJS(trc); + } + + for (XPCRootSetElem* e = mWrappedJSRoots; e; e = e->GetNextRoot()) { + static_cast<nsXPCWrappedJS*>(e)->TraceJS(trc); + } +} + +void XPCJSRuntime::TraverseAdditionalNativeRoots( + nsCycleCollectionNoteRootCallback& cb) { + XPCWrappedNativeScope::SuspectAllWrappers(cb); + + for (XPCRootSetElem* e = mVariantRoots; e; e = e->GetNextRoot()) { + XPCTraceableVariant* v = static_cast<XPCTraceableVariant*>(e); + cb.NoteXPCOMRoot( + v, + XPCTraceableVariant::NS_CYCLE_COLLECTION_INNERCLASS::GetParticipant()); + } + + for (XPCRootSetElem* e = mWrappedJSRoots; e; e = e->GetNextRoot()) { + cb.NoteXPCOMRoot( + ToSupports(static_cast<nsXPCWrappedJS*>(e)), + nsXPCWrappedJS::NS_CYCLE_COLLECTION_INNERCLASS::GetParticipant()); + } +} + +void XPCJSRuntime::UnmarkSkippableJSHolders() { + CycleCollectedJSRuntime::UnmarkSkippableJSHolders(); +} + +void XPCJSRuntime::PrepareForForgetSkippable() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "cycle-collector-forget-skippable", nullptr); + } +} + +void XPCJSRuntime::BeginCycleCollectionCallback() { + nsJSContext::BeginCycleCollectionCallback(); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "cycle-collector-begin", nullptr); + } +} + +void XPCJSRuntime::EndCycleCollectionCallback(CycleCollectorResults& aResults) { + nsJSContext::EndCycleCollectionCallback(aResults); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "cycle-collector-end", nullptr); + } +} + +void XPCJSRuntime::DispatchDeferredDeletion(bool aContinuation, bool aPurge) { + mAsyncSnowWhiteFreer->Start(aContinuation, aPurge); +} + +void xpc_UnmarkSkippableJSHolders() { + if (nsXPConnect::GetRuntimeInstance()) { + nsXPConnect::GetRuntimeInstance()->UnmarkSkippableJSHolders(); + } +} + +/* static */ +void XPCJSRuntime::GCSliceCallback(JSContext* cx, JS::GCProgress progress, + const JS::GCDescription& desc) { + XPCJSRuntime* self = nsXPConnect::GetRuntimeInstance(); + if (!self) { + return; + } + + CrashReporter::SetGarbageCollecting(progress == JS::GC_CYCLE_BEGIN); + + if (self->mPrevGCSliceCallback) { + (*self->mPrevGCSliceCallback)(cx, progress, desc); + } +} + +/* static */ +void XPCJSRuntime::DoCycleCollectionCallback(JSContext* cx) { + // The GC has detected that a CC at this point would collect a tremendous + // amount of garbage that is being revivified unnecessarily. + NS_DispatchToCurrentThread( + NS_NewRunnableFunction("XPCJSRuntime::DoCycleCollectionCallback", + []() { nsJSContext::CycleCollectNow(nullptr); })); + + XPCJSRuntime* self = nsXPConnect::GetRuntimeInstance(); + if (!self) { + return; + } + + if (self->mPrevDoCycleCollectionCallback) { + (*self->mPrevDoCycleCollectionCallback)(cx); + } +} + +void XPCJSRuntime::CustomGCCallback(JSGCStatus status) { + nsTArray<xpcGCCallback> callbacks(extraGCCallbacks.Clone()); + for (uint32_t i = 0; i < callbacks.Length(); ++i) { + callbacks[i](status); + } +} + +/* static */ +void XPCJSRuntime::FinalizeCallback(JSFreeOp* fop, JSFinalizeStatus status, + void* data) { + XPCJSRuntime* self = nsXPConnect::GetRuntimeInstance(); + if (!self) { + return; + } + + switch (status) { + case JSFINALIZE_GROUP_PREPARE: { + MOZ_ASSERT(!self->mDoingFinalization, "bad state"); + + MOZ_ASSERT(!self->mGCIsRunning, "bad state"); + self->mGCIsRunning = true; + + self->mDoingFinalization = true; + + break; + } + case JSFINALIZE_GROUP_START: { + MOZ_ASSERT(self->mDoingFinalization, "bad state"); + + MOZ_ASSERT(self->mGCIsRunning, "bad state"); + self->mGCIsRunning = false; + + break; + } + case JSFINALIZE_GROUP_END: { + MOZ_ASSERT(self->mDoingFinalization, "bad state"); + self->mDoingFinalization = false; + + break; + } + case JSFINALIZE_COLLECTION_END: { + MOZ_ASSERT(!self->mGCIsRunning, "bad state"); + self->mGCIsRunning = true; + + if (CycleCollectedJSContext* ccx = self->GetContext()) { + auto* cx = static_cast<const XPCJSContext*>(ccx); + if (AutoMarkingPtr* roots = cx->mAutoRoots) { + roots->MarkAfterJSFinalizeAll(); + } + + // Now we are going to recycle any unused WrappedNativeTearoffs. + // We do this by iterating all the live callcontexts + // and marking the tearoffs in use. And then we + // iterate over all the WrappedNative wrappers and sweep their + // tearoffs. + // + // This allows us to perhaps minimize the growth of the + // tearoffs. And also makes us not hold references to interfaces + // on our wrapped natives that we are not actually using. + // + // XXX We may decide to not do this on *every* gc cycle. + + XPCCallContext* ccxp = cx->GetCallContext(); + while (ccxp) { + // Deal with the strictness of callcontext that + // complains if you ask for a tearoff when + // it is in a state where the tearoff could not + // possibly be valid. + if (ccxp->CanGetTearOff()) { + XPCWrappedNativeTearOff* to = ccxp->GetTearOff(); + if (to) { + to->Mark(); + } + } + ccxp = ccxp->GetPrevCallContext(); + } + } + + XPCWrappedNativeScope::SweepAllWrappedNativeTearOffs(); + + // Now we need to kill the 'Dying' XPCWrappedNativeProtos. + // We transfered these native objects to this table when their + // JSObject's were finalized. We did not destroy them immediately + // at that point because the ordering of JS finalization is not + // deterministic and we did not yet know if any wrappers that + // might still be referencing the protos where still yet to be + // finalized and destroyed. We *do* know that the protos' + // JSObjects would not have been finalized if there were any + // wrappers that referenced the proto but where not themselves + // slated for finalization in this gc cycle. So... at this point + // we know that any and all wrappers that might have been + // referencing the protos in the dying list are themselves dead. + // So, we can safely delete all the protos in the list. + + for (auto i = self->mDyingWrappedNativeProtoMap->Iter(); !i.Done(); + i.Next()) { + auto entry = static_cast<XPCWrappedNativeProtoMap::Entry*>(i.Get()); + delete static_cast<const XPCWrappedNativeProto*>(entry->key); + i.Remove(); + } + + MOZ_ASSERT(self->mGCIsRunning, "bad state"); + self->mGCIsRunning = false; + + break; + } + } +} + +/* static */ +void XPCJSRuntime::WeakPointerZonesCallback(JSContext* cx, void* data) { + // Called before each sweeping slice -- after processing any final marking + // triggered by barriers -- to clear out any references to things that are + // about to be finalized and update any pointers to moved GC things. + XPCJSRuntime* self = static_cast<XPCJSRuntime*>(data); + + // This callback is always called from within the GC so set the mGCIsRunning + // flag to prevent AssertInvalidWrappedJSNotInTable from trying to call back + // into the JS API. This has often already been set by FinalizeCallback by the + // time we get here, but it may not be if we are doing a shutdown GC or if we + // are called for compacting GC. + AutoRestore<bool> restoreState(self->mGCIsRunning); + self->mGCIsRunning = true; + + self->mWrappedJSMap->UpdateWeakPointersAfterGC(); + self->mUAWidgetScopeMap.sweep(); +} + +/* static */ +void XPCJSRuntime::WeakPointerCompartmentCallback(JSContext* cx, + JS::Compartment* comp, + void* data) { + // Called immediately after the ZoneGroup weak pointer callback, but only + // once for each compartment that is being swept. + CompartmentPrivate* xpcComp = CompartmentPrivate::Get(comp); + if (xpcComp) { + xpcComp->UpdateWeakPointersAfterGC(); + } +} + +void CompartmentPrivate::UpdateWeakPointersAfterGC() { + mRemoteProxies.sweep(); + mWrappedJSMap->UpdateWeakPointersAfterGC(); + mScope->UpdateWeakPointersAfterGC(); +} + +void XPCJSRuntime::CustomOutOfMemoryCallback() { + if (!Preferences::GetBool("memory.dump_reports_on_oom")) { + return; + } + + nsCOMPtr<nsIMemoryInfoDumper> dumper = + do_GetService("@mozilla.org/memory-info-dumper;1"); + if (!dumper) { + return; + } + + // If this fails, it fails silently. + dumper->DumpMemoryInfoToTempDir(u"due-to-JS-OOM"_ns, + /* anonymize = */ false, + /* minimizeMemoryUsage = */ false); +} + +void XPCJSRuntime::OnLargeAllocationFailure() { + CycleCollectedJSRuntime::SetLargeAllocationFailure(OOMState::Reporting); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "memory-pressure", u"heap-minimize"); + } + + CycleCollectedJSRuntime::SetLargeAllocationFailure(OOMState::Reported); +} + +class LargeAllocationFailureRunnable final : public Runnable { + Mutex mMutex; + CondVar mCondVar; + bool mWaiting; + + virtual ~LargeAllocationFailureRunnable() { MOZ_ASSERT(!mWaiting); } + + protected: + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + XPCJSRuntime::Get()->OnLargeAllocationFailure(); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + mCondVar.Notify(); + return NS_OK; + } + + public: + LargeAllocationFailureRunnable() + : mozilla::Runnable("LargeAllocationFailureRunnable"), + mMutex("LargeAllocationFailureRunnable::mMutex"), + mCondVar(mMutex, "LargeAllocationFailureRunnable::mCondVar"), + mWaiting(true) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + void BlockUntilDone() { + MOZ_ASSERT(!NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + while (mWaiting) { + mCondVar.Wait(); + } + } +}; + +static void OnLargeAllocationFailureCallback() { + // This callback can be called from any thread, including internal JS helper + // and DOM worker threads. We need to send the low-memory event via the + // observer service which can only be called on the main thread, so proxy to + // the main thread if we're not there already. The purpose of this callback + // is to synchronously free some memory so the caller can retry a failed + // allocation, so block on the completion. + + if (NS_IsMainThread()) { + XPCJSRuntime::Get()->OnLargeAllocationFailure(); + return; + } + + RefPtr<LargeAllocationFailureRunnable> r = new LargeAllocationFailureRunnable; + if (NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread(r)))) { + return; + } + + r->BlockUntilDone(); +} + +bool mozilla::GetBuildId(JS::BuildIdCharVector* aBuildID) { + nsCOMPtr<nsIPlatformInfo> info = do_GetService("@mozilla.org/xre/app-info;1"); + if (!info) { + return false; + } + + nsCString buildID; + nsresult rv = info->GetPlatformBuildID(buildID); + NS_ENSURE_SUCCESS(rv, false); + + if (!aBuildID->resize(buildID.Length())) { + return false; + } + + for (size_t i = 0; i < buildID.Length(); i++) { + (*aBuildID)[i] = buildID[i]; + } + + return true; +} + +size_t XPCJSRuntime::SizeOfIncludingThis(MallocSizeOf mallocSizeOf) { + size_t n = 0; + n += mallocSizeOf(this); + n += mWrappedJSMap->SizeOfIncludingThis(mallocSizeOf); + n += mIID2NativeInterfaceMap->SizeOfIncludingThis(mallocSizeOf); + n += mClassInfo2NativeSetMap->ShallowSizeOfIncludingThis(mallocSizeOf); + n += mNativeSetMap->SizeOfIncludingThis(mallocSizeOf); + + n += CycleCollectedJSRuntime::SizeOfExcludingThis(mallocSizeOf); + + // There are other XPCJSRuntime members that could be measured; the above + // ones have been seen by DMD to be worth measuring. More stuff may be + // added later. + + return n; +} + +size_t CompartmentPrivate::SizeOfIncludingThis(MallocSizeOf mallocSizeOf) { + size_t n = mallocSizeOf(this); + n += mWrappedJSMap->SizeOfIncludingThis(mallocSizeOf); + n += mWrappedJSMap->SizeOfWrappedJS(mallocSizeOf); + return n; +} + +/***************************************************************************/ + +void XPCJSRuntime::SystemIsBeingShutDown() { + // We don't want to track wrapped JS roots after this point since we're + // making them !IsValid anyway through SystemIsBeingShutDown. + while (mWrappedJSRoots) { + mWrappedJSRoots->RemoveFromRootSet(); + } +} + +StaticAutoPtr<HelperThreadPool> gHelperThreads; + +void InitializeHelperThreadPool() { gHelperThreads = new HelperThreadPool(); } + +bool DispatchOffThreadTask(js::UniquePtr<RunnableTask> task) { + return NS_SUCCEEDED(gHelperThreads->Dispatch( + MakeAndAddRef<HelperThreadTaskHandler>(std::move(task)))); +} + +void XPCJSRuntime::Shutdown(JSContext* cx) { + // This destructor runs before ~CycleCollectedJSContext, which does the actual + // JS_DestroyContext() call. But destroying the context triggers one final GC, + // which can call back into the context with various callbacks if we aren't + // careful. Remove the relevant callbacks, but leave the weak pointer + // callbacks to clear out any remaining table entries. + JS_RemoveFinalizeCallback(cx, FinalizeCallback); + xpc_DelocalizeRuntime(JS_GetRuntime(cx)); + + JS::SetGCSliceCallback(cx, mPrevGCSliceCallback); + + nsScriptSecurityManager::ClearJSCallbacks(cx); + + // Clean up and destroy maps. Any remaining entries in mWrappedJSMap will be + // cleaned up by the weak pointer callbacks. + mIID2NativeInterfaceMap = nullptr; + + mClassInfo2NativeSetMap = nullptr; + + mNativeSetMap = nullptr; + + mDyingWrappedNativeProtoMap = nullptr; + + // Prevent ~LinkedList assertion failures if we leaked things. + mWrappedNativeScopes.clear(); + + CycleCollectedJSRuntime::Shutdown(cx); +} + +XPCJSRuntime::~XPCJSRuntime() { + MOZ_COUNT_DTOR_INHERITED(XPCJSRuntime, CycleCollectedJSRuntime); +} + +// If |*anonymizeID| is non-zero and this is a user realm, the name will +// be anonymized. +static void GetRealmName(JS::Realm* realm, nsCString& name, int* anonymizeID, + bool replaceSlashes) { + if (*anonymizeID && !js::IsSystemRealm(realm)) { + name.AppendPrintf("<anonymized-%d>", *anonymizeID); + *anonymizeID += 1; + } else if (JSPrincipals* principals = JS::GetRealmPrincipals(realm)) { + nsresult rv = nsJSPrincipals::get(principals)->GetScriptLocation(name); + if (NS_FAILED(rv)) { + name.AssignLiteral("(unknown)"); + } + + // If the realm's location (name) differs from the principal's script + // location, append the realm's location to allow differentiation of + // multiple realms owned by the same principal (e.g. components owned + // by the system or null principal). + RealmPrivate* realmPrivate = RealmPrivate::Get(realm); + if (realmPrivate) { + const nsACString& location = realmPrivate->GetLocation(); + if (!location.IsEmpty() && !location.Equals(name)) { + name.AppendLiteral(", "); + name.Append(location); + } + } + + if (*anonymizeID) { + // We might have a file:// URL that includes a path from the local + // filesystem, which should be omitted if we're anonymizing. + static const char* filePrefix = "file://"; + int filePos = name.Find(filePrefix); + if (filePos >= 0) { + int pathPos = filePos + strlen(filePrefix); + int lastSlashPos = -1; + for (int i = pathPos; i < int(name.Length()); i++) { + if (name[i] == '/' || name[i] == '\\') { + lastSlashPos = i; + } + } + if (lastSlashPos != -1) { + name.ReplaceLiteral(pathPos, lastSlashPos - pathPos, "<anonymized>"); + } else { + // Something went wrong. Anonymize the entire path to be + // safe. + name.Truncate(pathPos); + name += "<anonymized?!>"; + } + } + + // We might have a location like this: + // inProcessBrowserChildGlobal?ownedBy=http://www.example.com/ + // The owner should be omitted if it's not a chrome: URI and we're + // anonymizing. + static const char* ownedByPrefix = "inProcessBrowserChildGlobal?ownedBy="; + int ownedByPos = name.Find(ownedByPrefix); + if (ownedByPos >= 0) { + const char* chrome = "chrome:"; + int ownerPos = ownedByPos + strlen(ownedByPrefix); + const nsDependentCSubstring& ownerFirstPart = + Substring(name, ownerPos, strlen(chrome)); + if (!ownerFirstPart.EqualsASCII(chrome)) { + name.Truncate(ownerPos); + name += "<anonymized>"; + } + } + } + + // A hack: replace forward slashes with '\\' so they aren't + // treated as path separators. Users of the reporters + // (such as about:memory) have to undo this change. + if (replaceSlashes) { + name.ReplaceChar('/', '\\'); + } + } else { + name.AssignLiteral("null-principal"); + } +} + +extern void xpc::GetCurrentRealmName(JSContext* cx, nsCString& name) { + RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + if (!global) { + name.AssignLiteral("no global"); + return; + } + + JS::Realm* realm = GetNonCCWObjectRealm(global); + int anonymizeID = 0; + GetRealmName(realm, name, &anonymizeID, false); +} + +void xpc::AddGCCallback(xpcGCCallback cb) { + XPCJSRuntime::Get()->AddGCCallback(cb); +} + +void xpc::RemoveGCCallback(xpcGCCallback cb) { + XPCJSRuntime::Get()->RemoveGCCallback(cb); +} + +static int64_t JSMainRuntimeGCHeapDistinguishedAmount() { + JSContext* cx = danger::GetJSContext(); + return int64_t(JS_GetGCParameter(cx, JSGC_TOTAL_CHUNKS)) * js::gc::ChunkSize; +} + +static int64_t JSMainRuntimeTemporaryPeakDistinguishedAmount() { + JSContext* cx = danger::GetJSContext(); + return JS::PeakSizeOfTemporary(cx); +} + +static int64_t JSMainRuntimeCompartmentsSystemDistinguishedAmount() { + JSContext* cx = danger::GetJSContext(); + return JS::SystemCompartmentCount(cx); +} + +static int64_t JSMainRuntimeCompartmentsUserDistinguishedAmount() { + JSContext* cx = XPCJSContext::Get()->Context(); + return JS::UserCompartmentCount(cx); +} + +static int64_t JSMainRuntimeRealmsSystemDistinguishedAmount() { + JSContext* cx = danger::GetJSContext(); + return JS::SystemRealmCount(cx); +} + +static int64_t JSMainRuntimeRealmsUserDistinguishedAmount() { + JSContext* cx = XPCJSContext::Get()->Context(); + return JS::UserRealmCount(cx); +} + +class JSMainRuntimeTemporaryPeakReporter final : public nsIMemoryReporter { + ~JSMainRuntimeTemporaryPeakReporter() = default; + + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override { + MOZ_COLLECT_REPORT( + "js-main-runtime-temporary-peak", KIND_OTHER, UNITS_BYTES, + JSMainRuntimeTemporaryPeakDistinguishedAmount(), + "Peak transient data size in the main JSRuntime (the current size " + "of which is reported as " + "'explicit/js-non-window/runtime/temporary')."); + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(JSMainRuntimeTemporaryPeakReporter, nsIMemoryReporter) + +// The REPORT* macros do an unconditional report. The ZRREPORT* macros are for +// realms and zones; they aggregate any entries smaller than +// SUNDRIES_THRESHOLD into the "sundries/gc-heap" and "sundries/malloc-heap" +// entries for the realm. + +#define SUNDRIES_THRESHOLD js::MemoryReportingSundriesThreshold() + +#define REPORT(_path, _kind, _units, _amount, _desc) \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::_kind, \ + nsIMemoryReporter::_units, _amount, \ + nsLiteralCString(_desc), data); + +#define REPORT_BYTES(_path, _kind, _amount, _desc) \ + REPORT(_path, _kind, UNITS_BYTES, _amount, _desc); + +#define REPORT_GC_BYTES(_path, _amount, _desc) \ + do { \ + size_t amount = _amount; /* evaluate _amount only once */ \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::KIND_NONHEAP, \ + nsIMemoryReporter::UNITS_BYTES, amount, \ + nsLiteralCString(_desc), data); \ + gcTotal += amount; \ + } while (0) + +// Report realm/zone non-GC (KIND_HEAP) bytes. +#define ZRREPORT_BYTES(_path, _amount, _desc) \ + do { \ + /* Assign _descLiteral plus "" into a char* to prove that it's */ \ + /* actually a literal. */ \ + size_t amount = _amount; /* evaluate _amount only once */ \ + if (amount >= SUNDRIES_THRESHOLD) { \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::KIND_HEAP, \ + nsIMemoryReporter::UNITS_BYTES, amount, \ + nsLiteralCString(_desc), data); \ + } else { \ + sundriesMallocHeap += amount; \ + } \ + } while (0) + +// Report realm/zone GC bytes. +#define ZRREPORT_GC_BYTES(_path, _amount, _desc) \ + do { \ + size_t amount = _amount; /* evaluate _amount only once */ \ + if (amount >= SUNDRIES_THRESHOLD) { \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::KIND_NONHEAP, \ + nsIMemoryReporter::UNITS_BYTES, amount, \ + nsLiteralCString(_desc), data); \ + gcTotal += amount; \ + } else { \ + sundriesGCHeap += amount; \ + } \ + } while (0) + +// Report realm/zone non-heap bytes. +#define ZRREPORT_NONHEAP_BYTES(_path, _amount, _desc) \ + do { \ + size_t amount = _amount; /* evaluate _amount only once */ \ + if (amount >= SUNDRIES_THRESHOLD) { \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::KIND_NONHEAP, \ + nsIMemoryReporter::UNITS_BYTES, amount, \ + nsLiteralCString(_desc), data); \ + } else { \ + sundriesNonHeap += amount; \ + } \ + } while (0) + +// Report runtime bytes. +#define RREPORT_BYTES(_path, _kind, _amount, _desc) \ + do { \ + size_t amount = _amount; /* evaluate _amount only once */ \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::_kind, \ + nsIMemoryReporter::UNITS_BYTES, amount, \ + nsLiteralCString(_desc), data); \ + rtTotal += amount; \ + } while (0) + +// Report GC thing bytes. +#define MREPORT_BYTES(_path, _kind, _amount, _desc) \ + do { \ + size_t amount = _amount; /* evaluate _amount only once */ \ + handleReport->Callback(""_ns, _path, nsIMemoryReporter::_kind, \ + nsIMemoryReporter::UNITS_BYTES, amount, \ + nsLiteralCString(_desc), data); \ + gcThingTotal += amount; \ + } while (0) + +MOZ_DEFINE_MALLOC_SIZE_OF(JSMallocSizeOf) + +namespace xpc { + +static void ReportZoneStats(const JS::ZoneStats& zStats, + const xpc::ZoneStatsExtras& extras, + nsIHandleReportCallback* handleReport, + nsISupports* data, bool anonymize, + size_t* gcTotalOut = nullptr) { + const nsCString& pathPrefix = extras.pathPrefix; + size_t gcTotal = 0; + size_t sundriesGCHeap = 0; + size_t sundriesMallocHeap = 0; + size_t sundriesNonHeap = 0; + + MOZ_ASSERT(!gcTotalOut == zStats.isTotals); + + ZRREPORT_GC_BYTES(pathPrefix + "symbols/gc-heap"_ns, zStats.symbolsGCHeap, + "Symbols."); + + ZRREPORT_GC_BYTES( + pathPrefix + "gc-heap-arena-admin"_ns, zStats.gcHeapArenaAdmin, + "Bookkeeping information and alignment padding within GC arenas."); + + ZRREPORT_GC_BYTES(pathPrefix + "unused-gc-things"_ns, + zStats.unusedGCThings.totalSize(), + "Unused GC thing cells within non-empty arenas."); + + ZRREPORT_BYTES(pathPrefix + "unique-id-map"_ns, zStats.uniqueIdMap, + "Address-independent cell identities."); + + ZRREPORT_BYTES(pathPrefix + "shape-tables"_ns, zStats.shapeTables, + "Tables storing shape information."); + + ZRREPORT_BYTES(pathPrefix + "compartments/compartment-objects"_ns, + zStats.compartmentObjects, + "The JS::Compartment objects in this zone."); + + ZRREPORT_BYTES( + pathPrefix + "compartments/cross-compartment-wrapper-tables"_ns, + zStats.crossCompartmentWrappersTables, + "The cross-compartment wrapper tables."); + + ZRREPORT_BYTES( + pathPrefix + "compartments/private-data"_ns, + zStats.compartmentsPrivateData, + "Extra data attached to each compartment by XPConnect, including " + "its wrapped-js."); + + ZRREPORT_GC_BYTES(pathPrefix + "jit-codes-gc-heap"_ns, zStats.jitCodesGCHeap, + "References to executable code pools used by the JITs."); + + ZRREPORT_GC_BYTES( + pathPrefix + "object-groups/gc-heap"_ns, zStats.objectGroupsGCHeap, + "Classification and type inference information about objects."); + + ZRREPORT_GC_BYTES(pathPrefix + "scopes/gc-heap"_ns, zStats.scopesGCHeap, + "Scope information for scripts."); + + ZRREPORT_BYTES(pathPrefix + "scopes/malloc-heap"_ns, zStats.scopesMallocHeap, + "Arrays of binding names and other binding-related data."); + + ZRREPORT_GC_BYTES(pathPrefix + "regexp-shareds/gc-heap"_ns, + zStats.regExpSharedsGCHeap, "Shared compiled regexp data."); + + ZRREPORT_BYTES(pathPrefix + "regexp-shareds/malloc-heap"_ns, + zStats.regExpSharedsMallocHeap, + "Shared compiled regexp data."); + + ZRREPORT_BYTES(pathPrefix + "regexp-zone"_ns, zStats.regexpZone, + "The regexp zone and regexp data."); + + ZRREPORT_BYTES(pathPrefix + "jit-zone"_ns, zStats.jitZone, "The JIT zone."); + + ZRREPORT_BYTES(pathPrefix + "baseline/optimized-stubs"_ns, + zStats.baselineStubsOptimized, + "The Baseline JIT's optimized IC stubs (excluding code)."); + + ZRREPORT_BYTES(pathPrefix + "script-counts-map"_ns, zStats.scriptCountsMap, + "Profiling-related information for scripts."); + + ZRREPORT_NONHEAP_BYTES(pathPrefix + "code/ion"_ns, zStats.code.ion, + "Code generated by the IonMonkey JIT."); + + ZRREPORT_NONHEAP_BYTES(pathPrefix + "code/baseline"_ns, zStats.code.baseline, + "Code generated by the Baseline JIT."); + + ZRREPORT_NONHEAP_BYTES(pathPrefix + "code/regexp"_ns, zStats.code.regexp, + "Code generated by the regexp JIT."); + + ZRREPORT_NONHEAP_BYTES( + pathPrefix + "code/other"_ns, zStats.code.other, + "Code generated by the JITs for wrappers and trampolines."); + + ZRREPORT_NONHEAP_BYTES(pathPrefix + "code/unused"_ns, zStats.code.unused, + "Memory allocated by one of the JITs to hold code, " + "but which is currently unused."); + + size_t stringsNotableAboutMemoryGCHeap = 0; + size_t stringsNotableAboutMemoryMallocHeap = 0; + +#define MAYBE_INLINE "The characters may be inline or on the malloc heap." +#define MAYBE_OVERALLOCATED \ + "Sometimes over-allocated to simplify string concatenation." + + for (size_t i = 0; i < zStats.notableStrings.length(); i++) { + const JS::NotableStringInfo& info = zStats.notableStrings[i]; + + MOZ_ASSERT(!zStats.isTotals); + + // We don't do notable string detection when anonymizing, because + // there's a good chance its for crash submission, and the memory + // required for notable string detection is high. + MOZ_ASSERT(!anonymize); + + nsDependentCString notableString(info.buffer.get()); + + // Viewing about:memory generates many notable strings which contain + // "string(length=". If we report these as notable, then we'll create + // even more notable strings the next time we open about:memory (unless + // there's a GC in the meantime), and so on ad infinitum. + // + // To avoid cluttering up about:memory like this, we stick notable + // strings which contain "string(length=" into their own bucket. +#define STRING_LENGTH "string(length=" + if (FindInReadable(nsLiteralCString(STRING_LENGTH), notableString)) { + stringsNotableAboutMemoryGCHeap += info.gcHeapLatin1; + stringsNotableAboutMemoryGCHeap += info.gcHeapTwoByte; + stringsNotableAboutMemoryMallocHeap += info.mallocHeapLatin1; + stringsNotableAboutMemoryMallocHeap += info.mallocHeapTwoByte; + continue; + } + + // Escape / to \ before we put notableString into the memory reporter + // path, because we don't want any forward slashes in the string to + // count as path separators. + nsCString escapedString(notableString); + escapedString.ReplaceSubstring("/", "\\"); + + bool truncated = notableString.Length() < info.length; + + nsCString path = + pathPrefix + + nsPrintfCString("strings/" STRING_LENGTH "%zu, copies=%d, \"%s\"%s)/", + info.length, info.numCopies, escapedString.get(), + truncated ? " (truncated)" : ""); + + if (info.gcHeapLatin1 > 0) { + REPORT_GC_BYTES(path + "gc-heap/latin1"_ns, info.gcHeapLatin1, + "Latin1 strings. " MAYBE_INLINE); + } + + if (info.gcHeapTwoByte > 0) { + REPORT_GC_BYTES(path + "gc-heap/two-byte"_ns, info.gcHeapTwoByte, + "TwoByte strings. " MAYBE_INLINE); + } + + if (info.mallocHeapLatin1 > 0) { + REPORT_BYTES(path + "malloc-heap/latin1"_ns, KIND_HEAP, + info.mallocHeapLatin1, + "Non-inline Latin1 string characters. " MAYBE_OVERALLOCATED); + } + + if (info.mallocHeapTwoByte > 0) { + REPORT_BYTES( + path + "malloc-heap/two-byte"_ns, KIND_HEAP, info.mallocHeapTwoByte, + "Non-inline TwoByte string characters. " MAYBE_OVERALLOCATED); + } + } + + nsCString nonNotablePath = pathPrefix; + nonNotablePath += (zStats.isTotals || anonymize) + ? "strings/"_ns + : "strings/string(<non-notable strings>)/"_ns; + + if (zStats.stringInfo.gcHeapLatin1 > 0) { + REPORT_GC_BYTES(nonNotablePath + "gc-heap/latin1"_ns, + zStats.stringInfo.gcHeapLatin1, + "Latin1 strings. " MAYBE_INLINE); + } + + if (zStats.stringInfo.gcHeapTwoByte > 0) { + REPORT_GC_BYTES(nonNotablePath + "gc-heap/two-byte"_ns, + zStats.stringInfo.gcHeapTwoByte, + "TwoByte strings. " MAYBE_INLINE); + } + + if (zStats.stringInfo.mallocHeapLatin1 > 0) { + REPORT_BYTES(nonNotablePath + "malloc-heap/latin1"_ns, KIND_HEAP, + zStats.stringInfo.mallocHeapLatin1, + "Non-inline Latin1 string characters. " MAYBE_OVERALLOCATED); + } + + if (zStats.stringInfo.mallocHeapTwoByte > 0) { + REPORT_BYTES(nonNotablePath + "malloc-heap/two-byte"_ns, KIND_HEAP, + zStats.stringInfo.mallocHeapTwoByte, + "Non-inline TwoByte string characters. " MAYBE_OVERALLOCATED); + } + + if (stringsNotableAboutMemoryGCHeap > 0) { + MOZ_ASSERT(!zStats.isTotals); + REPORT_GC_BYTES( + pathPrefix + "strings/string(<about-memory>)/gc-heap"_ns, + stringsNotableAboutMemoryGCHeap, + "Strings that contain the characters '" STRING_LENGTH + "', which " + "are probably from about:memory itself." MAYBE_INLINE + " We filter them out rather than display them, because displaying " + "them would create even more such strings every time about:memory " + "is refreshed."); + } + + if (stringsNotableAboutMemoryMallocHeap > 0) { + MOZ_ASSERT(!zStats.isTotals); + REPORT_BYTES( + pathPrefix + "strings/string(<about-memory>)/malloc-heap"_ns, KIND_HEAP, + stringsNotableAboutMemoryMallocHeap, + "Non-inline string characters of strings that contain the " + "characters '" STRING_LENGTH + "', which are probably from " + "about:memory itself. " MAYBE_OVERALLOCATED + " We filter them out rather than display them, because displaying " + "them would create even more such strings every time about:memory " + "is refreshed."); + } + + const JS::ShapeInfo& shapeInfo = zStats.shapeInfo; + if (shapeInfo.shapesGCHeapTree > 0) { + REPORT_GC_BYTES(pathPrefix + "shapes/gc-heap/tree"_ns, + shapeInfo.shapesGCHeapTree, "Shapes in a property tree."); + } + + if (shapeInfo.shapesGCHeapDict > 0) { + REPORT_GC_BYTES(pathPrefix + "shapes/gc-heap/dict"_ns, + shapeInfo.shapesGCHeapDict, "Shapes in dictionary mode."); + } + + if (shapeInfo.shapesGCHeapBase > 0) { + REPORT_GC_BYTES(pathPrefix + "shapes/gc-heap/base"_ns, + shapeInfo.shapesGCHeapBase, + "Base shapes, which collate data common to many shapes."); + } + + if (shapeInfo.shapesMallocHeapTreeTables > 0) { + REPORT_BYTES(pathPrefix + "shapes/malloc-heap/tree-tables"_ns, KIND_HEAP, + shapeInfo.shapesMallocHeapTreeTables, + "Property tables of shapes in a property tree."); + } + + if (shapeInfo.shapesMallocHeapDictTables > 0) { + REPORT_BYTES(pathPrefix + "shapes/malloc-heap/dict-tables"_ns, KIND_HEAP, + shapeInfo.shapesMallocHeapDictTables, + "Property tables of shapes in dictionary mode."); + } + + if (shapeInfo.shapesMallocHeapTreeChildren > 0) { + REPORT_BYTES(pathPrefix + "shapes/malloc-heap/tree-children"_ns, KIND_HEAP, + shapeInfo.shapesMallocHeapTreeChildren, + "Sets of shape children in a property tree."); + } + + if (sundriesGCHeap > 0) { + // We deliberately don't use ZRREPORT_GC_BYTES here. + REPORT_GC_BYTES( + pathPrefix + "sundries/gc-heap"_ns, sundriesGCHeap, + "The sum of all 'gc-heap' measurements that are too small to be " + "worth showing individually."); + } + + if (sundriesMallocHeap > 0) { + // We deliberately don't use ZRREPORT_BYTES here. + REPORT_BYTES( + pathPrefix + "sundries/malloc-heap"_ns, KIND_HEAP, sundriesMallocHeap, + "The sum of all 'malloc-heap' measurements that are too small to " + "be worth showing individually."); + } + + if (sundriesNonHeap > 0) { + // We deliberately don't use ZRREPORT_NONHEAP_BYTES here. + REPORT_BYTES(pathPrefix + "sundries/other-heap"_ns, KIND_NONHEAP, + sundriesNonHeap, + "The sum of non-malloc/gc measurements that are too small to " + "be worth showing individually."); + } + + if (gcTotalOut) { + *gcTotalOut += gcTotal; + } + +#undef STRING_LENGTH +} + +static void ReportClassStats(const ClassInfo& classInfo, const nsACString& path, + nsIHandleReportCallback* handleReport, + nsISupports* data, size_t& gcTotal) { + // We deliberately don't use ZRREPORT_BYTES, so that these per-class values + // don't go into sundries. + + if (classInfo.objectsGCHeap > 0) { + REPORT_GC_BYTES(path + "objects/gc-heap"_ns, classInfo.objectsGCHeap, + "Objects, including fixed slots."); + } + + if (classInfo.objectsMallocHeapSlots > 0) { + REPORT_BYTES(path + "objects/malloc-heap/slots"_ns, KIND_HEAP, + classInfo.objectsMallocHeapSlots, "Non-fixed object slots."); + } + + if (classInfo.objectsMallocHeapElementsNormal > 0) { + REPORT_BYTES(path + "objects/malloc-heap/elements/normal"_ns, KIND_HEAP, + classInfo.objectsMallocHeapElementsNormal, + "Normal (non-wasm) indexed elements."); + } + + if (classInfo.objectsMallocHeapElementsAsmJS > 0) { + REPORT_BYTES(path + "objects/malloc-heap/elements/asm.js"_ns, KIND_HEAP, + classInfo.objectsMallocHeapElementsAsmJS, + "asm.js array buffer elements allocated in the malloc heap."); + } + + if (classInfo.objectsMallocHeapMisc > 0) { + REPORT_BYTES(path + "objects/malloc-heap/misc"_ns, KIND_HEAP, + classInfo.objectsMallocHeapMisc, "Miscellaneous object data."); + } + + if (classInfo.objectsNonHeapElementsNormal > 0) { + REPORT_BYTES(path + "objects/non-heap/elements/normal"_ns, KIND_NONHEAP, + classInfo.objectsNonHeapElementsNormal, + "Memory-mapped non-shared array buffer elements."); + } + + if (classInfo.objectsNonHeapElementsShared > 0) { + REPORT_BYTES( + path + "objects/non-heap/elements/shared"_ns, KIND_NONHEAP, + classInfo.objectsNonHeapElementsShared, + "Memory-mapped shared array buffer elements. These elements are " + "shared between one or more runtimes; the reported size is divided " + "by the buffer's refcount."); + } + + // WebAssembly memories are always non-heap-allocated (mmap). We never put + // these under sundries, because (a) in practice they're almost always + // larger than the sundries threshold, and (b) we'd need a third category of + // sundries ("non-heap"), which would be a pain. + if (classInfo.objectsNonHeapElementsWasm > 0) { + REPORT_BYTES(path + "objects/non-heap/elements/wasm"_ns, KIND_NONHEAP, + classInfo.objectsNonHeapElementsWasm, + "wasm/asm.js array buffer elements allocated outside both the " + "malloc heap and the GC heap."); + } + + if (classInfo.objectsNonHeapCodeWasm > 0) { + REPORT_BYTES(path + "objects/non-heap/code/wasm"_ns, KIND_NONHEAP, + classInfo.objectsNonHeapCodeWasm, + "AOT-compiled wasm/asm.js code."); + } + + // Although wasm guard pages aren't committed in memory they can be very + // large and contribute greatly to vsize and so are worth reporting. + if (classInfo.wasmGuardPages > 0) { + REPORT_BYTES( + "wasm-guard-pages"_ns, KIND_OTHER, classInfo.wasmGuardPages, + "Guard pages mapped after the end of wasm memories, reserved for " + "optimization tricks, but not committed and thus never contributing" + " to RSS, only vsize."); + } +} + +static void ReportRealmStats(const JS::RealmStats& realmStats, + const xpc::RealmStatsExtras& extras, + nsIHandleReportCallback* handleReport, + nsISupports* data, size_t* gcTotalOut = nullptr) { + static const nsDependentCString addonPrefix("explicit/add-ons/"); + + size_t gcTotal = 0, sundriesGCHeap = 0, sundriesMallocHeap = 0; + nsAutoCString realmJSPathPrefix(extras.jsPathPrefix); + nsAutoCString realmDOMPathPrefix(extras.domPathPrefix); + + MOZ_ASSERT(!gcTotalOut == realmStats.isTotals); + + nsCString nonNotablePath = realmJSPathPrefix; + nonNotablePath += realmStats.isTotals + ? "classes/"_ns + : "classes/class(<non-notable classes>)/"_ns; + + ReportClassStats(realmStats.classInfo, nonNotablePath, handleReport, data, + gcTotal); + + for (size_t i = 0; i < realmStats.notableClasses.length(); i++) { + MOZ_ASSERT(!realmStats.isTotals); + const JS::NotableClassInfo& classInfo = realmStats.notableClasses[i]; + + nsCString classPath = + realmJSPathPrefix + + nsPrintfCString("classes/class(%s)/", classInfo.className_.get()); + + ReportClassStats(classInfo, classPath, handleReport, data, gcTotal); + } + + // Note that we use realmDOMPathPrefix here. This is because we measure + // orphan DOM nodes in the JS reporter, but we want to report them in a "dom" + // sub-tree rather than a "js" sub-tree. + ZRREPORT_BYTES( + realmDOMPathPrefix + "orphan-nodes"_ns, realmStats.objectsPrivate, + "Orphan DOM nodes, i.e. those that are only reachable from JavaScript " + "objects."); + + ZRREPORT_GC_BYTES( + realmJSPathPrefix + "scripts/gc-heap"_ns, realmStats.scriptsGCHeap, + "JSScript instances. There is one per user-defined function in a " + "script, and one for the top-level code in a script."); + + ZRREPORT_BYTES(realmJSPathPrefix + "scripts/malloc-heap/data"_ns, + realmStats.scriptsMallocHeapData, + "Various variable-length tables in JSScripts."); + + ZRREPORT_BYTES(realmJSPathPrefix + "baseline/data"_ns, + realmStats.baselineData, + "The Baseline JIT's compilation data (BaselineScripts)."); + + ZRREPORT_BYTES(realmJSPathPrefix + "baseline/fallback-stubs"_ns, + realmStats.baselineStubsFallback, + "The Baseline JIT's fallback IC stubs (excluding code)."); + + ZRREPORT_BYTES(realmJSPathPrefix + "ion-data"_ns, realmStats.ionData, + "The IonMonkey JIT's compilation data (IonScripts)."); + + ZRREPORT_BYTES(realmJSPathPrefix + "jit-scripts"_ns, realmStats.jitScripts, + "JIT data associated with scripts."); + + ZRREPORT_BYTES(realmJSPathPrefix + "realm-object"_ns, realmStats.realmObject, + "The JS::Realm object itself."); + + ZRREPORT_BYTES( + realmJSPathPrefix + "realm-tables"_ns, realmStats.realmTables, + "Realm-wide tables storing object group information and wasm instances."); + + ZRREPORT_BYTES(realmJSPathPrefix + "inner-views"_ns, + realmStats.innerViewsTable, + "The table for array buffer inner views."); + + ZRREPORT_BYTES( + realmJSPathPrefix + "object-metadata"_ns, realmStats.objectMetadataTable, + "The table used by debugging tools for tracking object metadata"); + + ZRREPORT_BYTES(realmJSPathPrefix + "saved-stacks-set"_ns, + realmStats.savedStacksSet, "The saved stacks set."); + + ZRREPORT_BYTES(realmJSPathPrefix + "non-syntactic-lexical-scopes-table"_ns, + realmStats.nonSyntacticLexicalScopesTable, + "The non-syntactic lexical scopes table."); + + ZRREPORT_BYTES(realmJSPathPrefix + "jit-realm"_ns, realmStats.jitRealm, + "The JIT realm."); + + if (sundriesGCHeap > 0) { + // We deliberately don't use ZRREPORT_GC_BYTES here. + REPORT_GC_BYTES( + realmJSPathPrefix + "sundries/gc-heap"_ns, sundriesGCHeap, + "The sum of all 'gc-heap' measurements that are too small to be " + "worth showing individually."); + } + + if (sundriesMallocHeap > 0) { + // We deliberately don't use ZRREPORT_BYTES here. + REPORT_BYTES( + realmJSPathPrefix + "sundries/malloc-heap"_ns, KIND_HEAP, + sundriesMallocHeap, + "The sum of all 'malloc-heap' measurements that are too small to " + "be worth showing individually."); + } + + if (gcTotalOut) { + *gcTotalOut += gcTotal; + } +} + +static void ReportScriptSourceStats(const ScriptSourceInfo& scriptSourceInfo, + const nsACString& path, + nsIHandleReportCallback* handleReport, + nsISupports* data, size_t& rtTotal) { + if (scriptSourceInfo.misc > 0) { + RREPORT_BYTES(path + "misc"_ns, KIND_HEAP, scriptSourceInfo.misc, + "Miscellaneous data relating to JavaScript source code."); + } +} + +void ReportJSRuntimeExplicitTreeStats(const JS::RuntimeStats& rtStats, + const nsACString& rtPath, + nsIHandleReportCallback* handleReport, + nsISupports* data, bool anonymize, + size_t* rtTotalOut) { + size_t gcTotal = 0; + + for (size_t i = 0; i < rtStats.zoneStatsVector.length(); i++) { + const JS::ZoneStats& zStats = rtStats.zoneStatsVector[i]; + const xpc::ZoneStatsExtras* extras = + static_cast<const xpc::ZoneStatsExtras*>(zStats.extra); + ReportZoneStats(zStats, *extras, handleReport, data, anonymize, &gcTotal); + } + + for (size_t i = 0; i < rtStats.realmStatsVector.length(); i++) { + const JS::RealmStats& realmStats = rtStats.realmStatsVector[i]; + const xpc::RealmStatsExtras* extras = + static_cast<const xpc::RealmStatsExtras*>(realmStats.extra); + + ReportRealmStats(realmStats, *extras, handleReport, data, &gcTotal); + } + + // Report the rtStats.runtime numbers under "runtime/", and compute their + // total for later. + + size_t rtTotal = 0; + + RREPORT_BYTES(rtPath + "runtime/runtime-object"_ns, KIND_HEAP, + rtStats.runtime.object, "The JSRuntime object."); + + RREPORT_BYTES(rtPath + "runtime/atoms-table"_ns, KIND_HEAP, + rtStats.runtime.atomsTable, "The atoms table."); + + RREPORT_BYTES(rtPath + "runtime/atoms-mark-bitmaps"_ns, KIND_HEAP, + rtStats.runtime.atomsMarkBitmaps, + "Mark bitmaps for atoms held by each zone."); + + RREPORT_BYTES(rtPath + "runtime/contexts"_ns, KIND_HEAP, + rtStats.runtime.contexts, + "JSContext objects and structures that belong to them."); + + RREPORT_BYTES( + rtPath + "runtime/temporary"_ns, KIND_HEAP, rtStats.runtime.temporary, + "Transient data (mostly parse nodes) held by the JSRuntime during " + "compilation."); + + RREPORT_BYTES(rtPath + "runtime/interpreter-stack"_ns, KIND_HEAP, + rtStats.runtime.interpreterStack, "JS interpreter frames."); + + RREPORT_BYTES( + rtPath + "runtime/shared-immutable-strings-cache"_ns, KIND_HEAP, + rtStats.runtime.sharedImmutableStringsCache, + "Immutable strings (such as JS scripts' source text) shared across all " + "JSRuntimes."); + + RREPORT_BYTES(rtPath + "runtime/shared-intl-data"_ns, KIND_HEAP, + rtStats.runtime.sharedIntlData, + "Shared internationalization data."); + + RREPORT_BYTES(rtPath + "runtime/uncompressed-source-cache"_ns, KIND_HEAP, + rtStats.runtime.uncompressedSourceCache, + "The uncompressed source code cache."); + + RREPORT_BYTES(rtPath + "runtime/script-data"_ns, KIND_HEAP, + rtStats.runtime.scriptData, + "The table holding script data shared in the runtime."); + + RREPORT_BYTES(rtPath + "runtime/tracelogger"_ns, KIND_HEAP, + rtStats.runtime.tracelogger, + "The memory used for the tracelogger (per-runtime)."); + + nsCString nonNotablePath = + rtPath + + nsPrintfCString( + "runtime/script-sources/source(scripts=%d, <non-notable files>)/", + rtStats.runtime.scriptSourceInfo.numScripts); + + ReportScriptSourceStats(rtStats.runtime.scriptSourceInfo, nonNotablePath, + handleReport, data, rtTotal); + + for (size_t i = 0; i < rtStats.runtime.notableScriptSources.length(); i++) { + const JS::NotableScriptSourceInfo& scriptSourceInfo = + rtStats.runtime.notableScriptSources[i]; + + // Escape / to \ before we put the filename into the memory reporter + // path, because we don't want any forward slashes in the string to + // count as path separators. Consumers of memory reporters (e.g. + // about:memory) will convert them back to / after doing path + // splitting. + nsCString escapedFilename; + if (anonymize) { + escapedFilename.AppendPrintf("<anonymized-source-%d>", int(i)); + } else { + nsDependentCString filename(scriptSourceInfo.filename_.get()); + escapedFilename.Append(filename); + escapedFilename.ReplaceSubstring("/", "\\"); + } + + nsCString notablePath = + rtPath + + nsPrintfCString("runtime/script-sources/source(scripts=%d, %s)/", + scriptSourceInfo.numScripts, escapedFilename.get()); + + ReportScriptSourceStats(scriptSourceInfo, notablePath, handleReport, data, + rtTotal); + } + + RREPORT_BYTES(rtPath + "runtime/gc/marker"_ns, KIND_HEAP, + rtStats.runtime.gc.marker, "The GC mark stack and gray roots."); + + RREPORT_BYTES(rtPath + "runtime/gc/nursery-committed"_ns, KIND_NONHEAP, + rtStats.runtime.gc.nurseryCommitted, + "Memory being used by the GC's nursery."); + + RREPORT_BYTES( + rtPath + "runtime/gc/nursery-malloced-buffers"_ns, KIND_HEAP, + rtStats.runtime.gc.nurseryMallocedBuffers, + "Out-of-line slots and elements belonging to objects in the nursery."); + + RREPORT_BYTES(rtPath + "runtime/gc/store-buffer/vals"_ns, KIND_HEAP, + rtStats.runtime.gc.storeBufferVals, + "Values in the store buffer."); + + RREPORT_BYTES(rtPath + "runtime/gc/store-buffer/cells"_ns, KIND_HEAP, + rtStats.runtime.gc.storeBufferCells, + "Cells in the store buffer."); + + RREPORT_BYTES(rtPath + "runtime/gc/store-buffer/slots"_ns, KIND_HEAP, + rtStats.runtime.gc.storeBufferSlots, + "Slots in the store buffer."); + + RREPORT_BYTES(rtPath + "runtime/gc/store-buffer/whole-cells"_ns, KIND_HEAP, + rtStats.runtime.gc.storeBufferWholeCells, + "Whole cells in the store buffer."); + + RREPORT_BYTES(rtPath + "runtime/gc/store-buffer/generics"_ns, KIND_HEAP, + rtStats.runtime.gc.storeBufferGenerics, + "Generic things in the store buffer."); + + RREPORT_BYTES(rtPath + "runtime/jit-lazylink"_ns, KIND_HEAP, + rtStats.runtime.jitLazyLink, + "IonMonkey compilations waiting for lazy linking."); + + if (rtTotalOut) { + *rtTotalOut = rtTotal; + } + + // Report GC numbers that don't belong to a realm. + + // We don't want to report decommitted memory in "explicit", so we just + // change the leading "explicit/" to "decommitted/". + nsCString rtPath2(rtPath); + rtPath2.ReplaceLiteral(0, strlen("explicit"), "decommitted"); + + REPORT_GC_BYTES( + rtPath2 + "gc-heap/decommitted-arenas"_ns, + rtStats.gcHeapDecommittedArenas, + "GC arenas in non-empty chunks that is decommitted, i.e. it takes up " + "address space but no physical memory or swap space."); + + REPORT_GC_BYTES( + rtPath + "gc-heap/unused-chunks"_ns, rtStats.gcHeapUnusedChunks, + "Empty GC chunks which will soon be released unless claimed for new " + "allocations."); + + REPORT_GC_BYTES(rtPath + "gc-heap/unused-arenas"_ns, + rtStats.gcHeapUnusedArenas, + "Empty GC arenas within non-empty chunks."); + + REPORT_GC_BYTES(rtPath + "gc-heap/chunk-admin"_ns, rtStats.gcHeapChunkAdmin, + "Bookkeeping information within GC chunks."); + + // gcTotal is the sum of everything we've reported for the GC heap. It + // should equal rtStats.gcHeapChunkTotal. + MOZ_ASSERT(gcTotal == rtStats.gcHeapChunkTotal); +} + +} // namespace xpc + +class JSMainRuntimeRealmsReporter final : public nsIMemoryReporter { + ~JSMainRuntimeRealmsReporter() = default; + + public: + NS_DECL_ISUPPORTS + + struct Data { + int anonymizeID; + js::Vector<nsCString, 0, js::SystemAllocPolicy> paths; + }; + + static void RealmCallback(JSContext* cx, void* vdata, Realm* realm, + const JS::AutoRequireNoGC& nogc) { + // silently ignore OOM errors + Data* data = static_cast<Data*>(vdata); + nsCString path; + GetRealmName(realm, path, &data->anonymizeID, /* replaceSlashes = */ true); + path.Insert(js::IsSystemRealm(realm) ? "js-main-runtime-realms/system/"_ns + : "js-main-runtime-realms/user/"_ns, + 0); + mozilla::Unused << data->paths.append(path); + } + + NS_IMETHOD CollectReports(nsIHandleReportCallback* handleReport, + nsISupports* data, bool anonymize) override { + // First we collect the realm paths. Then we report them. Doing + // the two steps interleaved is a bad idea, because calling + // |handleReport| from within RealmCallback() leads to all manner + // of assertions. + + Data d; + d.anonymizeID = anonymize ? 1 : 0; + JS::IterateRealms(XPCJSContext::Get()->Context(), &d, RealmCallback); + + for (size_t i = 0; i < d.paths.length(); i++) { + REPORT(nsCString(d.paths[i]), KIND_OTHER, UNITS_COUNT, 1, + "A live realm in the main JSRuntime."); + } + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(JSMainRuntimeRealmsReporter, nsIMemoryReporter) + +MOZ_DEFINE_MALLOC_SIZE_OF(OrphanMallocSizeOf) + +namespace xpc { + +class OrphanReporter : public JS::ObjectPrivateVisitor { + public: + explicit OrphanReporter(GetISupportsFun aGetISupports) + : JS::ObjectPrivateVisitor(aGetISupports), mState(OrphanMallocSizeOf) {} + + virtual size_t sizeOfIncludingThis(nsISupports* aSupports) override { + nsCOMPtr<nsINode> node = do_QueryInterface(aSupports); + if (!node || node->IsInComposedDoc()) { + return 0; + } + + // This is an orphan node. If we haven't already handled the sub-tree that + // this node belongs to, measure the sub-tree's size and then record its + // root so we don't measure it again. + nsCOMPtr<nsINode> orphanTree = node->SubtreeRoot(); + if (!orphanTree || mState.HaveSeenPtr(orphanTree.get())) { + return 0; + } + + nsWindowSizes sizes(mState); + mozilla::dom::Document::AddSizeOfNodeTree(*orphanTree, sizes); + + // We combine the node size with nsStyleSizes here. It's not ideal, but it's + // hard to get the style structs measurements out to nsWindowMemoryReporter. + // Also, we drop mServoData in UnbindFromTree(), so in theory any + // non-in-tree element won't have any style data to measure. + // + // FIXME(emilio): We should ideally not do this, since ShadowRoots keep + // their StyleSheets alive even when detached from a document, and those + // could be significant in theory. + return sizes.getTotalSize(); + } + + private: + SizeOfState mState; +}; + +#ifdef DEBUG +static bool StartsWithExplicit(nsACString& s) { + return StringBeginsWith(s, "explicit/"_ns); +} +#endif + +class XPCJSRuntimeStats : public JS::RuntimeStats { + WindowPaths* mWindowPaths; + WindowPaths* mTopWindowPaths; + int mAnonymizeID; + + public: + XPCJSRuntimeStats(WindowPaths* windowPaths, WindowPaths* topWindowPaths, + bool anonymize) + : JS::RuntimeStats(JSMallocSizeOf), + mWindowPaths(windowPaths), + mTopWindowPaths(topWindowPaths), + mAnonymizeID(anonymize ? 1 : 0) {} + + ~XPCJSRuntimeStats() { + for (size_t i = 0; i != realmStatsVector.length(); ++i) { + delete static_cast<xpc::RealmStatsExtras*>(realmStatsVector[i].extra); + } + + for (size_t i = 0; i != zoneStatsVector.length(); ++i) { + delete static_cast<xpc::ZoneStatsExtras*>(zoneStatsVector[i].extra); + } + } + + virtual void initExtraZoneStats(JS::Zone* zone, JS::ZoneStats* zStats, + const JS::AutoRequireNoGC& nogc) override { + xpc::ZoneStatsExtras* extras = new xpc::ZoneStatsExtras; + extras->pathPrefix.AssignLiteral("explicit/js-non-window/zones/"); + + // Get some global in this zone. + Rooted<Realm*> realm(dom::RootingCx(), js::GetAnyRealmInZone(zone)); + if (realm) { + RootedObject global(dom::RootingCx(), JS::GetRealmGlobalOrNull(realm)); + if (global) { + RefPtr<nsGlobalWindowInner> window; + if (NS_SUCCEEDED(UNWRAP_NON_WRAPPER_OBJECT(Window, global, window))) { + // The global is a |window| object. Use the path prefix that + // we should have already created for it. + if (mTopWindowPaths->Get(window->WindowID(), &extras->pathPrefix)) + extras->pathPrefix.AppendLiteral("/js-"); + } + } + } + + extras->pathPrefix += nsPrintfCString("zone(0x%p)/", (void*)zone); + + MOZ_ASSERT(StartsWithExplicit(extras->pathPrefix)); + + zStats->extra = extras; + } + + virtual void initExtraRealmStats(Realm* realm, JS::RealmStats* realmStats, + const JS::AutoRequireNoGC& nogc) override { + xpc::RealmStatsExtras* extras = new xpc::RealmStatsExtras; + nsCString rName; + GetRealmName(realm, rName, &mAnonymizeID, /* replaceSlashes = */ true); + + // Get the realm's global. + bool needZone = true; + RootedObject global(dom::RootingCx(), JS::GetRealmGlobalOrNull(realm)); + if (global) { + RefPtr<nsGlobalWindowInner> window; + if (NS_SUCCEEDED(UNWRAP_NON_WRAPPER_OBJECT(Window, global, window))) { + // The global is a |window| object. Use the path prefix that + // we should have already created for it. + if (mWindowPaths->Get(window->WindowID(), &extras->jsPathPrefix)) { + extras->domPathPrefix.Assign(extras->jsPathPrefix); + extras->domPathPrefix.AppendLiteral("/dom/"); + extras->jsPathPrefix.AppendLiteral("/js-"); + needZone = false; + } else { + extras->jsPathPrefix.AssignLiteral("explicit/js-non-window/zones/"); + extras->domPathPrefix.AssignLiteral( + "explicit/dom/unknown-window-global?!/"); + } + } else { + extras->jsPathPrefix.AssignLiteral("explicit/js-non-window/zones/"); + extras->domPathPrefix.AssignLiteral( + "explicit/dom/non-window-global?!/"); + } + } else { + extras->jsPathPrefix.AssignLiteral("explicit/js-non-window/zones/"); + extras->domPathPrefix.AssignLiteral("explicit/dom/no-global?!/"); + } + + if (needZone) { + extras->jsPathPrefix += + nsPrintfCString("zone(0x%p)/", (void*)js::GetRealmZone(realm)); + } + + extras->jsPathPrefix += "realm("_ns + rName + ")/"_ns; + + // extras->jsPathPrefix is used for almost all the realm-specific + // reports. At this point it has the form + // "<something>realm(<rname>)/". + // + // extras->domPathPrefix is used for DOM orphan nodes, which are + // counted by the JS reporter but reported as part of the DOM + // measurements. At this point it has the form "<something>/dom/" if + // this realm belongs to an nsGlobalWindow, and + // "explicit/dom/<something>?!/" otherwise (in which case it shouldn't + // be used, because non-nsGlobalWindow realms shouldn't have + // orphan DOM nodes). + + MOZ_ASSERT(StartsWithExplicit(extras->jsPathPrefix)); + MOZ_ASSERT(StartsWithExplicit(extras->domPathPrefix)); + + realmStats->extra = extras; + } +}; + +void JSReporter::CollectReports(WindowPaths* windowPaths, + WindowPaths* topWindowPaths, + nsIHandleReportCallback* handleReport, + nsISupports* data, bool anonymize) { + XPCJSRuntime* xpcrt = nsXPConnect::GetRuntimeInstance(); + + // In the first step we get all the stats and stash them in a local + // data structure. In the second step we pass all the stashed stats to + // the callback. Separating these steps is important because the + // callback may be a JS function, and executing JS while getting these + // stats seems like a bad idea. + + XPCJSRuntimeStats rtStats(windowPaths, topWindowPaths, anonymize); + OrphanReporter orphanReporter(XPCConvert::GetISupportsFromJSObject); + JSContext* cx = XPCJSContext::Get()->Context(); + if (!JS::CollectRuntimeStats(cx, &rtStats, &orphanReporter, anonymize)) { + return; + } + + // Collect JS stats not associated with a Runtime such as helper threads or + // global tracelogger data. We do this here in JSReporter::CollectReports + // as this is used for the main Runtime in process. + JS::GlobalStats gStats(JSMallocSizeOf); + if (!JS::CollectGlobalStats(&gStats)) { + return; + } + + size_t xpcJSRuntimeSize = xpcrt->SizeOfIncludingThis(JSMallocSizeOf); + + size_t wrappedJSSize = + xpcrt->GetMultiCompartmentWrappedJSMap()->SizeOfWrappedJS(JSMallocSizeOf); + + XPCWrappedNativeScope::ScopeSizeInfo sizeInfo(JSMallocSizeOf); + XPCWrappedNativeScope::AddSizeOfAllScopesIncludingThis(cx, &sizeInfo); + + mozJSComponentLoader* loader = mozJSComponentLoader::Get(); + size_t jsComponentLoaderSize = + loader ? loader->SizeOfIncludingThis(JSMallocSizeOf) : 0; + + // This is the second step (see above). First we report stuff in the + // "explicit" tree, then we report other stuff. + + size_t rtTotal = 0; + xpc::ReportJSRuntimeExplicitTreeStats(rtStats, "explicit/js-non-window/"_ns, + handleReport, data, anonymize, + &rtTotal); + + // Report the sums of the realm numbers. + xpc::RealmStatsExtras realmExtrasTotal; + realmExtrasTotal.jsPathPrefix.AssignLiteral("js-main-runtime/realms/"); + realmExtrasTotal.domPathPrefix.AssignLiteral("window-objects/dom/"); + ReportRealmStats(rtStats.realmTotals, realmExtrasTotal, handleReport, data); + + xpc::ZoneStatsExtras zExtrasTotal; + zExtrasTotal.pathPrefix.AssignLiteral("js-main-runtime/zones/"); + ReportZoneStats(rtStats.zTotals, zExtrasTotal, handleReport, data, anonymize); + + // Report the sum of the runtime/ numbers. + REPORT_BYTES( + "js-main-runtime/runtime"_ns, KIND_OTHER, rtTotal, + "The sum of all measurements under 'explicit/js-non-window/runtime/'."); + + // Report the number of HelperThread + + REPORT("js-helper-threads/idle"_ns, KIND_OTHER, UNITS_COUNT, + gStats.helperThread.idleThreadCount, + "The current number of idle JS HelperThreads."); + + REPORT( + "js-helper-threads/active"_ns, KIND_OTHER, UNITS_COUNT, + gStats.helperThread.activeThreadCount, + "The current number of active JS HelperThreads. Memory held by these is" + " not reported."); + + // Report the numbers for memory used by wasm Runtime state. + REPORT_BYTES("wasm-runtime"_ns, KIND_OTHER, rtStats.runtime.wasmRuntime, + "The memory used for wasm runtime bookkeeping."); + + // Report the numbers for memory outside of realms. + + REPORT_BYTES("js-main-runtime/gc-heap/unused-chunks"_ns, KIND_OTHER, + rtStats.gcHeapUnusedChunks, + "The same as 'explicit/js-non-window/gc-heap/unused-chunks'."); + + REPORT_BYTES("js-main-runtime/gc-heap/unused-arenas"_ns, KIND_OTHER, + rtStats.gcHeapUnusedArenas, + "The same as 'explicit/js-non-window/gc-heap/unused-arenas'."); + + REPORT_BYTES("js-main-runtime/gc-heap/chunk-admin"_ns, KIND_OTHER, + rtStats.gcHeapChunkAdmin, + "The same as 'explicit/js-non-window/gc-heap/chunk-admin'."); + + // Report a breakdown of the committed GC space. + + REPORT_BYTES("js-main-runtime-gc-heap-committed/unused/chunks"_ns, KIND_OTHER, + rtStats.gcHeapUnusedChunks, + "The same as 'explicit/js-non-window/gc-heap/unused-chunks'."); + + REPORT_BYTES("js-main-runtime-gc-heap-committed/unused/arenas"_ns, KIND_OTHER, + rtStats.gcHeapUnusedArenas, + "The same as 'explicit/js-non-window/gc-heap/unused-arenas'."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/objects"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.object, + "Unused object cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/strings"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.string, + "Unused string cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/symbols"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.symbol, + "Unused symbol cells within non-empty arenas."); + + REPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/shapes"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.shape, + "Unused shape cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/base-shapes"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.baseShape, + "Unused base shape cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/object-groups"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.objectGroup, + "Unused object group cells within non-empty arenas."); + + REPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/scopes"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.scope, + "Unused scope cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/scripts"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.script, + "Unused script cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/jitcode"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.jitcode, + "Unused jitcode cells within non-empty arenas."); + + REPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/unused/gc-things/regexp-shareds"), + KIND_OTHER, rtStats.zTotals.unusedGCThings.regExpShared, + "Unused regexpshared cells within non-empty arenas."); + + REPORT_BYTES("js-main-runtime-gc-heap-committed/used/chunk-admin"_ns, + KIND_OTHER, rtStats.gcHeapChunkAdmin, + "The same as 'explicit/js-non-window/gc-heap/chunk-admin'."); + + REPORT_BYTES("js-main-runtime-gc-heap-committed/used/arena-admin"_ns, + KIND_OTHER, rtStats.zTotals.gcHeapArenaAdmin, + "The same as 'js-main-runtime/zones/gc-heap-arena-admin'."); + + size_t gcThingTotal = 0; + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/objects"), + KIND_OTHER, rtStats.realmTotals.classInfo.objectsGCHeap, + "Used object cells."); + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/strings"), + KIND_OTHER, rtStats.zTotals.stringInfo.sizeOfLiveGCThings(), + "Used string cells."); + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/symbols"), + KIND_OTHER, rtStats.zTotals.symbolsGCHeap, + "Used symbol cells."); + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/shapes"), + KIND_OTHER, + rtStats.zTotals.shapeInfo.shapesGCHeapTree + + rtStats.zTotals.shapeInfo.shapesGCHeapDict, + "Used shape cells."); + + MREPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/base-shapes"), + KIND_OTHER, rtStats.zTotals.shapeInfo.shapesGCHeapBase, + "Used base shape cells."); + + MREPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/object-groups"), + KIND_OTHER, rtStats.zTotals.objectGroupsGCHeap, + "Used object group cells."); + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/scopes"), + KIND_OTHER, rtStats.zTotals.scopesGCHeap, "Used scope cells."); + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/scripts"), + KIND_OTHER, rtStats.realmTotals.scriptsGCHeap, + "Used script cells."); + + MREPORT_BYTES(nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/jitcode"), + KIND_OTHER, rtStats.zTotals.jitCodesGCHeap, + "Used jitcode cells."); + + MREPORT_BYTES( + nsLiteralCString( + "js-main-runtime-gc-heap-committed/used/gc-things/regexp-shareds"), + KIND_OTHER, rtStats.zTotals.regExpSharedsGCHeap, + "Used regexpshared cells."); + + MOZ_ASSERT(gcThingTotal == rtStats.gcHeapGCThings); + + // Report xpconnect. + + REPORT_BYTES("explicit/xpconnect/runtime"_ns, KIND_HEAP, xpcJSRuntimeSize, + "The XPConnect runtime."); + + REPORT_BYTES("explicit/xpconnect/wrappedjs"_ns, KIND_HEAP, wrappedJSSize, + "Wrappers used to implement XPIDL interfaces with JS."); + + REPORT_BYTES("explicit/xpconnect/scopes"_ns, KIND_HEAP, + sizeInfo.mScopeAndMapSize, "XPConnect scopes."); + + REPORT_BYTES("explicit/xpconnect/proto-iface-cache"_ns, KIND_HEAP, + sizeInfo.mProtoAndIfaceCacheSize, + "Prototype and interface binding caches."); + + REPORT_BYTES("explicit/xpconnect/js-component-loader"_ns, KIND_HEAP, + jsComponentLoaderSize, "XPConnect's JS component loader."); + + // Report tracelogger (global). + + REPORT_BYTES( + "explicit/js-non-window/tracelogger"_ns, KIND_HEAP, gStats.tracelogger, + "The memory used for the tracelogger, including the graph and events."); + + // Report HelperThreadState. + + REPORT_BYTES("explicit/js-non-window/helper-thread/heap-other"_ns, KIND_HEAP, + gStats.helperThread.stateData, + "Memory used by HelperThreadState."); + + REPORT_BYTES("explicit/js-non-window/helper-thread/parse-task"_ns, KIND_HEAP, + gStats.helperThread.parseTask, + "The memory used by ParseTasks waiting in HelperThreadState."); + + REPORT_BYTES( + "explicit/js-non-window/helper-thread/ion-compile-task"_ns, KIND_HEAP, + gStats.helperThread.ionCompileTask, + "The memory used by IonCompileTasks waiting in HelperThreadState."); + + REPORT_BYTES( + "explicit/js-non-window/helper-thread/wasm-compile"_ns, KIND_HEAP, + gStats.helperThread.wasmCompile, + "The memory used by Wasm compilations waiting in HelperThreadState."); + + REPORT_BYTES("explicit/js-non-window/helper-thread/contexts"_ns, KIND_HEAP, + gStats.helperThread.contexts, + "The memory used by the JSContexts in HelperThreadState."); +} + +static nsresult JSSizeOfTab(JSObject* objArg, size_t* jsObjectsSize, + size_t* jsStringsSize, size_t* jsPrivateSize, + size_t* jsOtherSize) { + JSContext* cx = XPCJSContext::Get()->Context(); + JS::RootedObject obj(cx, objArg); + + TabSizes sizes; + OrphanReporter orphanReporter(XPCConvert::GetISupportsFromJSObject); + NS_ENSURE_TRUE( + JS::AddSizeOfTab(cx, obj, moz_malloc_size_of, &orphanReporter, &sizes), + NS_ERROR_OUT_OF_MEMORY); + + *jsObjectsSize = sizes.objects_; + *jsStringsSize = sizes.strings_; + *jsPrivateSize = sizes.private_; + *jsOtherSize = sizes.other_; + return NS_OK; +} + +} // namespace xpc + +static void AccumulateTelemetryCallback(int id, uint32_t sample, + const char* key) { + switch (id) { + case JS_TELEMETRY_GC_REASON: + Telemetry::Accumulate(Telemetry::GC_REASON_2, sample); + break; + case JS_TELEMETRY_GC_IS_ZONE_GC: + Telemetry::Accumulate(Telemetry::GC_IS_COMPARTMENTAL, sample); + break; + case JS_TELEMETRY_GC_MS: + Telemetry::Accumulate(Telemetry::GC_MS, sample); + break; + case JS_TELEMETRY_GC_BUDGET_MS_2: + Telemetry::Accumulate(Telemetry::GC_BUDGET_MS_2, sample); + break; + case JS_TELEMETRY_GC_BUDGET_OVERRUN: + Telemetry::Accumulate(Telemetry::GC_BUDGET_OVERRUN, sample); + break; + case JS_TELEMETRY_GC_ANIMATION_MS: + Telemetry::Accumulate(Telemetry::GC_ANIMATION_MS, sample); + break; + case JS_TELEMETRY_GC_MAX_PAUSE_MS_2: + Telemetry::Accumulate(Telemetry::GC_MAX_PAUSE_MS_2, sample); + break; + case JS_TELEMETRY_GC_PREPARE_MS: + Telemetry::Accumulate(Telemetry::GC_PREPARE_MS, sample); + break; + case JS_TELEMETRY_GC_MARK_MS: + Telemetry::Accumulate(Telemetry::GC_MARK_MS, sample); + break; + case JS_TELEMETRY_GC_SWEEP_MS: + Telemetry::Accumulate(Telemetry::GC_SWEEP_MS, sample); + break; + case JS_TELEMETRY_GC_COMPACT_MS: + Telemetry::Accumulate(Telemetry::GC_COMPACT_MS, sample); + break; + case JS_TELEMETRY_GC_MARK_ROOTS_US: + Telemetry::Accumulate(Telemetry::GC_MARK_ROOTS_US, sample); + break; + case JS_TELEMETRY_GC_MARK_GRAY_MS_2: + Telemetry::Accumulate(Telemetry::GC_MARK_GRAY_MS_2, sample); + break; + case JS_TELEMETRY_GC_MARK_WEAK_MS: + Telemetry::Accumulate(Telemetry::GC_MARK_WEAK_MS, sample); + break; + case JS_TELEMETRY_GC_SLICE_MS: + Telemetry::Accumulate(Telemetry::GC_SLICE_MS, sample); + break; + case JS_TELEMETRY_GC_SLOW_PHASE: + Telemetry::Accumulate(Telemetry::GC_SLOW_PHASE, sample); + break; + case JS_TELEMETRY_GC_SLOW_TASK: + Telemetry::Accumulate(Telemetry::GC_SLOW_TASK, sample); + break; + case JS_TELEMETRY_GC_MMU_50: + Telemetry::Accumulate(Telemetry::GC_MMU_50, sample); + break; + case JS_TELEMETRY_GC_RESET: + Telemetry::Accumulate(Telemetry::GC_RESET, sample); + break; + case JS_TELEMETRY_GC_RESET_REASON: + Telemetry::Accumulate(Telemetry::GC_RESET_REASON, sample); + break; + case JS_TELEMETRY_GC_NON_INCREMENTAL: + Telemetry::Accumulate(Telemetry::GC_NON_INCREMENTAL, sample); + break; + case JS_TELEMETRY_GC_NON_INCREMENTAL_REASON: + Telemetry::Accumulate(Telemetry::GC_NON_INCREMENTAL_REASON, sample); + break; + case JS_TELEMETRY_GC_MINOR_REASON: + Telemetry::Accumulate(Telemetry::GC_MINOR_REASON, sample); + break; + case JS_TELEMETRY_GC_MINOR_REASON_LONG: + Telemetry::Accumulate(Telemetry::GC_MINOR_REASON_LONG, sample); + break; + case JS_TELEMETRY_GC_MINOR_US: + Telemetry::Accumulate(Telemetry::GC_MINOR_US, sample); + break; + case JS_TELEMETRY_GC_NURSERY_BYTES: + Telemetry::Accumulate(Telemetry::GC_NURSERY_BYTES_2, sample); + break; + case JS_TELEMETRY_GC_PRETENURE_COUNT_2: + Telemetry::Accumulate(Telemetry::GC_PRETENURE_COUNT_2, sample); + break; + case JS_TELEMETRY_GC_NURSERY_PROMOTION_RATE: + Telemetry::Accumulate(Telemetry::GC_NURSERY_PROMOTION_RATE, sample); + break; + case JS_TELEMETRY_GC_TENURED_SURVIVAL_RATE: + Telemetry::Accumulate(Telemetry::GC_TENURED_SURVIVAL_RATE, sample); + break; + case JS_TELEMETRY_GC_MARK_RATE_2: + Telemetry::Accumulate(Telemetry::GC_MARK_RATE_2, sample); + break; + case JS_TELEMETRY_GC_TIME_BETWEEN_S: + Telemetry::Accumulate(Telemetry::GC_TIME_BETWEEN_S, sample); + break; + case JS_TELEMETRY_GC_TIME_BETWEEN_SLICES_MS: + Telemetry::Accumulate(Telemetry::GC_TIME_BETWEEN_SLICES_MS, sample); + break; + case JS_TELEMETRY_GC_SLICE_COUNT: + Telemetry::Accumulate(Telemetry::GC_SLICE_COUNT, sample); + break; + case JS_TELEMETRY_GC_EFFECTIVENESS: + Telemetry::Accumulate(Telemetry::GC_EFFECTIVENESS, sample); + break; + case JS_TELEMETRY_RUN_TIME_US: + Telemetry::ScalarAdd(Telemetry::ScalarID::JS_RUN_TIME_US, sample); + break; + case JS_TELEMETRY_WASM_COMPILE_TIME_BASELINE_US: + Telemetry::ScalarAdd(Telemetry::ScalarID::WASM_COMPILE_TIME_BASELINE_US, + sample); + break; + case JS_TELEMETRY_WASM_COMPILE_TIME_ION_US: + Telemetry::ScalarAdd(Telemetry::ScalarID::WASM_COMPILE_TIME_ION_US, + sample); + break; + case JS_TELEMETRY_WASM_COMPILE_TIME_CRANELIFT_US: + Telemetry::ScalarAdd(Telemetry::ScalarID::WASM_COMPILE_TIME_CRANELIFT_US, + sample); + break; + default: + // Some telemetry only exists in the JS Shell, and are not reported here. + break; + } +} + +static void SetUseCounterCallback(JSObject* obj, JSUseCounter counter) { + switch (counter) { + case JSUseCounter::ASMJS: + SetUseCounter(obj, eUseCounter_custom_JS_asmjs); + break; + case JSUseCounter::WASM: + SetUseCounter(obj, eUseCounter_custom_JS_wasm); + break; + case JSUseCounter::WASM_DUPLICATE_IMPORTS: + SetUseCounter(obj, eUseCounter_custom_JS_wasm_duplicate_imports); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected JSUseCounter id"); + } +} + +static void GetRealmNameCallback(JSContext* cx, Realm* realm, char* buf, + size_t bufsize, + const JS::AutoRequireNoGC& nogc) { + nsCString name; + // This is called via the JSAPI and isn't involved in memory reporting, so + // we don't need to anonymize realm names. + int anonymizeID = 0; + GetRealmName(realm, name, &anonymizeID, /* replaceSlashes = */ false); + if (name.Length() >= bufsize) { + name.Truncate(bufsize - 1); + } + memcpy(buf, name.get(), name.Length() + 1); +} + +static void DestroyRealm(JSFreeOp* fop, JS::Realm* realm) { + // Get the current compartment private into an AutoPtr (which will do the + // cleanup for us), and null out the private field. + mozilla::UniquePtr<RealmPrivate> priv(RealmPrivate::Get(realm)); + JS::SetRealmPrivate(realm, nullptr); +} + +static bool PreserveWrapper(JSContext* cx, JS::Handle<JSObject*> obj) { + MOZ_ASSERT(cx); + MOZ_ASSERT(obj); + MOZ_ASSERT(mozilla::dom::IsDOMObject(obj)); + + if (!mozilla::dom::TryPreserveWrapper(obj)) { + return false; + } + + MOZ_ASSERT(!mozilla::dom::HasReleasedWrapper(obj), + "There should be no released wrapper since we just preserved it"); + + return true; +} + +static nsresult ReadSourceFromFilename(JSContext* cx, const char* filename, + char16_t** twoByteSource, + char** utf8Source, size_t* len) { + MOZ_ASSERT(*len == 0); + MOZ_ASSERT((twoByteSource != nullptr) != (utf8Source != nullptr), + "must be called requesting only one of UTF-8 or UTF-16 source"); + MOZ_ASSERT_IF(twoByteSource, !*twoByteSource); + MOZ_ASSERT_IF(utf8Source, !*utf8Source); + + nsresult rv; + + // mozJSSubScriptLoader prefixes the filenames of the scripts it loads with + // the filename of its caller. Axe that if present. + const char* arrow; + while ((arrow = strstr(filename, " -> "))) { + filename = arrow + strlen(" -> "); + } + + // Get the URI. + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), filename); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> scriptChannel; + rv = NS_NewChannel(getter_AddRefs(scriptChannel), uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + NS_ENSURE_SUCCESS(rv, rv); + + // Only allow local reading. + nsCOMPtr<nsIURI> actualUri; + rv = scriptChannel->GetURI(getter_AddRefs(actualUri)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString scheme; + rv = actualUri->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + if (!scheme.EqualsLiteral("file") && !scheme.EqualsLiteral("jar")) { + return NS_OK; + } + + // Explicitly set the content type so that we don't load the + // exthandler to guess it. + scriptChannel->SetContentType("text/plain"_ns); + + nsCOMPtr<nsIInputStream> scriptStream; + rv = scriptChannel->Open(getter_AddRefs(scriptStream)); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t rawLen; + rv = scriptStream->Available(&rawLen); + NS_ENSURE_SUCCESS(rv, rv); + if (!rawLen) { + return NS_ERROR_FAILURE; + } + + // Technically, this should be SIZE_MAX, but we don't run on machines + // where that would be less than UINT32_MAX, and the latter is already + // well beyond a reasonable limit. + if (rawLen > UINT32_MAX) { + return NS_ERROR_FILE_TOO_BIG; + } + + // Allocate a buffer the size of the file to initially fill with the UTF-8 + // contents of the file. Use the JS allocator so that if UTF-8 source was + // requested, we can return this memory directly. + JS::UniqueChars buf(js_pod_malloc<char>(rawLen)); + if (!buf) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* ptr = buf.get(); + char* end = ptr + rawLen; + while (ptr < end) { + uint32_t bytesRead; + rv = scriptStream->Read(ptr, PointerRangeSize(ptr, end), &bytesRead); + if (NS_FAILED(rv)) { + return rv; + } + MOZ_ASSERT(bytesRead > 0, "stream promised more bytes before EOF"); + ptr += bytesRead; + } + + if (utf8Source) { + // |buf| is already UTF-8, so we can directly return it. + *len = rawLen; + *utf8Source = buf.release(); + } else { + MOZ_ASSERT(twoByteSource != nullptr); + + // |buf| can't be directly returned -- convert it to UTF-16. + + // On success this overwrites |*twoByteSource| and |*len|. + rv = ScriptLoader::ConvertToUTF16( + scriptChannel, reinterpret_cast<const unsigned char*>(buf.get()), + rawLen, u"UTF-8"_ns, nullptr, *twoByteSource, *len); + NS_ENSURE_SUCCESS(rv, rv); + + if (!*twoByteSource) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +// The JS engine calls this object's 'load' member function when it needs +// the source for a chrome JS function. See the comment in the XPCJSRuntime +// constructor. +class XPCJSSourceHook : public js::SourceHook { + bool load(JSContext* cx, const char* filename, char16_t** twoByteSource, + char** utf8Source, size_t* length) override { + MOZ_ASSERT((twoByteSource != nullptr) != (utf8Source != nullptr), + "must be called requesting only one of UTF-8 or UTF-16 source"); + + *length = 0; + if (twoByteSource) { + *twoByteSource = nullptr; + } else { + *utf8Source = nullptr; + } + + if (!nsContentUtils::IsSystemCaller(cx)) { + return true; + } + + if (!filename) { + return true; + } + + nsresult rv = + ReadSourceFromFilename(cx, filename, twoByteSource, utf8Source, length); + if (NS_FAILED(rv)) { + xpc::Throw(cx, rv); + return false; + } + + return true; + } +}; + +static const JSWrapObjectCallbacks WrapObjectCallbacks = { + xpc::WrapperFactory::Rewrap, xpc::WrapperFactory::PrepareForWrapping}; + +XPCJSRuntime::XPCJSRuntime(JSContext* aCx) + : CycleCollectedJSRuntime(aCx), + mWrappedJSMap(mozilla::MakeUnique<JSObject2WrappedJSMap>()), + mIID2NativeInterfaceMap(mozilla::MakeUnique<IID2NativeInterfaceMap>()), + mClassInfo2NativeSetMap(mozilla::MakeUnique<ClassInfo2NativeSetMap>()), + mNativeSetMap(mozilla::MakeUnique<NativeSetMap>()), + mWrappedNativeScopes(), + mDyingWrappedNativeProtoMap( + mozilla::MakeUnique<XPCWrappedNativeProtoMap>()), + mGCIsRunning(false), + mNativesToReleaseArray(), + mDoingFinalization(false), + mVariantRoots(nullptr), + mWrappedJSRoots(nullptr), + mAsyncSnowWhiteFreer(new AsyncFreeSnowWhite()) { + MOZ_COUNT_CTOR_INHERITED(XPCJSRuntime, CycleCollectedJSRuntime); +} + +/* static */ +XPCJSRuntime* XPCJSRuntime::Get() { return nsXPConnect::GetRuntimeInstance(); } + +// Subclass of JS::ubi::Base for DOM reflector objects for the JS::ubi::Node +// memory analysis framework; see js/public/UbiNode.h. In +// XPCJSRuntime::Initialize, we register the ConstructUbiNode function as a hook +// with the SpiderMonkey runtime for it to use to construct ubi::Nodes of this +// class for JSObjects whose class has the JSCLASS_IS_DOMJSCLASS flag set. +// ReflectorNode specializes Concrete<JSObject> for DOM reflector nodes, +// reporting the edge from the JSObject to the nsINode it represents, in +// addition to the usual edges departing any normal JSObject. +namespace JS { +namespace ubi { +class ReflectorNode : public Concrete<JSObject> { + protected: + explicit ReflectorNode(JSObject* ptr) : Concrete<JSObject>(ptr) {} + + public: + static void construct(void* storage, JSObject* ptr) { + new (storage) ReflectorNode(ptr); + } + js::UniquePtr<JS::ubi::EdgeRange> edges(JSContext* cx, + bool wantNames) const override; +}; + +js::UniquePtr<EdgeRange> ReflectorNode::edges(JSContext* cx, + bool wantNames) const { + js::UniquePtr<SimpleEdgeRange> range(static_cast<SimpleEdgeRange*>( + Concrete<JSObject>::edges(cx, wantNames).release())); + if (!range) { + return nullptr; + } + // UNWRAP_NON_WRAPPER_OBJECT assumes the object is completely initialized, + // but ours may not be. Luckily, UnwrapDOMObjectToISupports checks for the + // uninitialized case (and returns null if uninitialized), so we can use that + // to guard against uninitialized objects. + nsISupports* supp = UnwrapDOMObjectToISupports(&get()); + if (supp) { + JS::AutoSuppressGCAnalysis nogc; // bug 1582326 + + nsINode* node; + // UnwrapDOMObjectToISupports can only return non-null if its argument is + // an actual DOM object, not a cross-compartment wrapper. + if (NS_SUCCEEDED(UNWRAP_NON_WRAPPER_OBJECT(Node, &get(), node))) { + char16_t* edgeName = nullptr; + if (wantNames) { + edgeName = NS_xstrdup(u"Reflected Node"); + } + if (!range->addEdge(Edge(edgeName, node))) { + return nullptr; + } + } + } + return js::UniquePtr<EdgeRange>(range.release()); +} + +} // Namespace ubi +} // Namespace JS + +void ConstructUbiNode(void* storage, JSObject* ptr) { + JS::ubi::ReflectorNode::construct(storage, ptr); +} + +class HelperThreadPoolShutdownObserver : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + protected: + virtual ~HelperThreadPoolShutdownObserver() = default; +}; + +NS_IMPL_ISUPPORTS(HelperThreadPoolShutdownObserver, nsIObserver, nsISupports) + +NS_IMETHODIMP +HelperThreadPoolShutdownObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_RELEASE_ASSERT(!strcmp(aTopic, "xpcom-shutdown-threads")); + + // Shut down the helper threads + gHelperThreads->Shutdown(); + gHelperThreads = nullptr; + + return NS_OK; +} + +void XPCJSRuntime::Initialize(JSContext* cx) { + mLoaderGlobal.init(cx, nullptr); + + // these jsids filled in later when we have a JSContext to work with. + mStrIDs[0] = JSID_VOID; + + nsScriptSecurityManager::GetScriptSecurityManager()->InitJSCallbacks(cx); + + // Unconstrain the runtime's threshold on nominal heap size, to avoid + // triggering GC too often if operating continuously near an arbitrary + // finite threshold (0xffffffff is infinity for uint32_t parameters). + // This leaves the maximum-JS_malloc-bytes threshold still in effect + // to cause period, and we hope hygienic, last-ditch GCs from within + // the GC's allocator. + JS_SetGCParameter(cx, JSGC_MAX_BYTES, 0xffffffff); + + JS_SetDestroyCompartmentCallback(cx, CompartmentDestroyedCallback); + JS_SetSizeOfIncludingThisCompartmentCallback( + cx, CompartmentSizeOfIncludingThisCallback); + JS::SetDestroyRealmCallback(cx, DestroyRealm); + JS::SetRealmNameCallback(cx, GetRealmNameCallback); + mPrevGCSliceCallback = JS::SetGCSliceCallback(cx, GCSliceCallback); + mPrevDoCycleCollectionCallback = + JS::SetDoCycleCollectionCallback(cx, DoCycleCollectionCallback); + JS_AddFinalizeCallback(cx, FinalizeCallback, nullptr); + JS_AddWeakPointerZonesCallback(cx, WeakPointerZonesCallback, this); + JS_AddWeakPointerCompartmentCallback(cx, WeakPointerCompartmentCallback, + this); + JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); + if (XRE_IsE10sParentProcess()) { + JS::SetFilenameValidationCallback( + nsContentSecurityUtils::ValidateScriptFilename); + } + js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper); + JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals); + JS_SetAccumulateTelemetryCallback(cx, AccumulateTelemetryCallback); + JS_SetSetUseCounterCallback(cx, SetUseCounterCallback); + + js::SetWindowProxyClass(cx, &OuterWindowProxyClass); + + { + JS::AbortSignalIsAborted isAborted = [](JSObject* obj) { + dom::AbortSignal* domObj = dom::UnwrapDOMObject<dom::AbortSignal>(obj); + MOZ_ASSERT(domObj); + return domObj->Aborted(); + }; + + JS::InitPipeToHandling(dom::AbortSignal_Binding::GetJSClass(), isAborted, + cx); + } + + JS::SetXrayJitInfo(&gXrayJitInfo); + JS::SetProcessLargeAllocationFailureCallback( + OnLargeAllocationFailureCallback); + JS::SetProcessBuildIdOp(GetBuildId); + + // Initialize a helper thread pool for JS offthread tasks. Set the + // task callback to divert tasks to the helperthreads. + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + nsCOMPtr<nsIObserver> obs = new HelperThreadPoolShutdownObserver(); + obsService->AddObserver(obs, "xpcom-shutdown-threads", false); + InitializeHelperThreadPool(); + SetHelperThreadTaskCallback(&DispatchOffThreadTask); + + // The JS engine needs to keep the source code around in order to implement + // Function.prototype.toSource(). It'd be nice to not have to do this for + // chrome code and simply stub out requests for source on it. Life is not so + // easy, unfortunately. Nobody relies on chrome toSource() working in core + // browser code, but chrome tests use it. The worst offenders are addons, + // which like to monkeypatch chrome functions by calling toSource() on them + // and using regular expressions to modify them. We avoid keeping most browser + // JS source code in memory by setting LAZY_SOURCE on JS::CompileOptions when + // compiling some chrome code. This causes the JS engine not save the source + // code in memory. When the JS engine is asked to provide the source for a + // function compiled with LAZY_SOURCE, it calls SourceHook to load it. + /// + // Note we do have to retain the source code in memory for scripts compiled in + // isRunOnce mode and compiled function bodies (from + // JS::CompileFunction). In practice, this means content scripts and event + // handlers. + mozilla::UniquePtr<XPCJSSourceHook> hook(new XPCJSSourceHook); + js::SetSourceHook(cx, std::move(hook)); + + // Register memory reporters and distinguished amount functions. + RegisterStrongMemoryReporter(new JSMainRuntimeRealmsReporter()); + RegisterStrongMemoryReporter(new JSMainRuntimeTemporaryPeakReporter()); + RegisterJSMainRuntimeGCHeapDistinguishedAmount( + JSMainRuntimeGCHeapDistinguishedAmount); + RegisterJSMainRuntimeTemporaryPeakDistinguishedAmount( + JSMainRuntimeTemporaryPeakDistinguishedAmount); + RegisterJSMainRuntimeCompartmentsSystemDistinguishedAmount( + JSMainRuntimeCompartmentsSystemDistinguishedAmount); + RegisterJSMainRuntimeCompartmentsUserDistinguishedAmount( + JSMainRuntimeCompartmentsUserDistinguishedAmount); + RegisterJSMainRuntimeRealmsSystemDistinguishedAmount( + JSMainRuntimeRealmsSystemDistinguishedAmount); + RegisterJSMainRuntimeRealmsUserDistinguishedAmount( + JSMainRuntimeRealmsUserDistinguishedAmount); + mozilla::RegisterJSSizeOfTab(JSSizeOfTab); + + // Set the callback for reporting memory to ubi::Node. + JS::ubi::SetConstructUbiNodeForDOMObjectCallback(cx, &ConstructUbiNode); + + xpc_LocalizeRuntime(JS_GetRuntime(cx)); +} + +bool XPCJSRuntime::InitializeStrings(JSContext* cx) { + // if it is our first context then we need to generate our string ids + if (JSID_IS_VOID(mStrIDs[0])) { + RootedString str(cx); + for (unsigned i = 0; i < XPCJSContext::IDX_TOTAL_COUNT; i++) { + str = JS_AtomizeAndPinString(cx, mStrings[i]); + if (!str) { + mStrIDs[0] = JSID_VOID; + return false; + } + mStrIDs[i] = PropertyKey::fromPinnedString(str); + mStrJSVals[i].setString(str); + } + + if (!mozilla::dom::DefineStaticJSVals(cx)) { + return false; + } + } + + return true; +} + +bool XPCJSRuntime::DescribeCustomObjects(JSObject* obj, const JSClass* clasp, + char (&name)[72]) const { + if (clasp != &XPC_WN_Proto_JSClass) { + return false; + } + + XPCWrappedNativeProto* p = + static_cast<XPCWrappedNativeProto*>(xpc_GetJSPrivate(obj)); + nsCOMPtr<nsIXPCScriptable> scr = p->GetScriptable(); + if (!scr) { + return false; + } + + SprintfLiteral(name, "JS Object (%s - %s)", clasp->name, + scr->GetJSClass()->name); + return true; +} + +bool XPCJSRuntime::NoteCustomGCThingXPCOMChildren( + const JSClass* clasp, JSObject* obj, + nsCycleCollectionTraversalCallback& cb) const { + if (clasp != &XPC_WN_Tearoff_JSClass) { + return false; + } + + // A tearoff holds a strong reference to its native object + // (see XPCWrappedNative::FlatJSObjectFinalized). Its XPCWrappedNative + // will be held alive through tearoff's XPC_WN_TEAROFF_FLAT_OBJECT_SLOT, + // which points to the XPCWrappedNative's mFlatJSObject. + XPCWrappedNativeTearOff* to = + static_cast<XPCWrappedNativeTearOff*>(xpc_GetJSPrivate(obj)); + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "xpc_GetJSPrivate(obj)->mNative"); + cb.NoteXPCOMChild(to->GetNative()); + return true; +} + +/***************************************************************************/ + +void XPCJSRuntime::DebugDump(int16_t depth) { +#ifdef DEBUG + depth--; + XPC_LOG_ALWAYS(("XPCJSRuntime @ %p", this)); + XPC_LOG_INDENT(); + + // iterate wrappers... + XPC_LOG_ALWAYS(("mWrappedJSMap @ %p with %d wrappers(s)", mWrappedJSMap.get(), + mWrappedJSMap->Count())); + if (depth && mWrappedJSMap->Count()) { + XPC_LOG_INDENT(); + mWrappedJSMap->Dump(depth); + XPC_LOG_OUTDENT(); + } + + XPC_LOG_ALWAYS(("mIID2NativeInterfaceMap @ %p with %d interface(s)", + mIID2NativeInterfaceMap.get(), + mIID2NativeInterfaceMap->Count())); + + XPC_LOG_ALWAYS(("mClassInfo2NativeSetMap @ %p with %d sets(s)", + mClassInfo2NativeSetMap.get(), + mClassInfo2NativeSetMap->Count())); + + XPC_LOG_ALWAYS(("mNativeSetMap @ %p with %d sets(s)", mNativeSetMap.get(), + mNativeSetMap->Count())); + + // iterate sets... + if (depth && mNativeSetMap->Count()) { + XPC_LOG_INDENT(); + for (auto i = mNativeSetMap->Iter(); !i.Done(); i.Next()) { + auto entry = static_cast<NativeSetMap::Entry*>(i.Get()); + entry->key_value->DebugDump(depth); + } + XPC_LOG_OUTDENT(); + } + + XPC_LOG_OUTDENT(); +#endif +} + +/***************************************************************************/ + +void XPCRootSetElem::AddToRootSet(XPCRootSetElem** listHead) { + MOZ_ASSERT(!mSelfp, "Must be not linked"); + + mSelfp = listHead; + mNext = *listHead; + if (mNext) { + MOZ_ASSERT(mNext->mSelfp == listHead, "Must be list start"); + mNext->mSelfp = &mNext; + } + *listHead = this; +} + +void XPCRootSetElem::RemoveFromRootSet() { + JS::NotifyGCRootsRemoved(XPCJSContext::Get()->Context()); + + MOZ_ASSERT(mSelfp, "Must be linked"); + + MOZ_ASSERT(*mSelfp == this, "Link invariant"); + *mSelfp = mNext; + if (mNext) { + mNext->mSelfp = mSelfp; + } +#ifdef DEBUG + mSelfp = nullptr; + mNext = nullptr; +#endif +} + +void XPCJSRuntime::AddGCCallback(xpcGCCallback cb) { + MOZ_ASSERT(cb, "null callback"); + extraGCCallbacks.AppendElement(cb); +} + +void XPCJSRuntime::RemoveGCCallback(xpcGCCallback cb) { + MOZ_ASSERT(cb, "null callback"); + bool found = extraGCCallbacks.RemoveElement(cb); + if (!found) { + NS_ERROR("Removing a callback which was never added."); + } +} + +JSObject* XPCJSRuntime::GetUAWidgetScope(JSContext* cx, + nsIPrincipal* principal) { + MOZ_ASSERT(!principal->IsSystemPrincipal(), "Running UA Widget in chrome"); + + RootedObject scope(cx); + do { + RefPtr<BasePrincipal> key = BasePrincipal::Cast(principal); + if (Principal2JSObjectMap::Ptr p = mUAWidgetScopeMap.lookup(key)) { + scope = p->value(); + break; // Need ~RefPtr to run, and potentially GC, before returning. + } + + SandboxOptions options; + options.sandboxName.AssignLiteral("UA Widget Scope"); + options.wantXrays = false; + options.wantComponents = false; + options.isUAWidgetScope = true; + + // Use an ExpandedPrincipal to create asymmetric security. + MOZ_ASSERT(!nsContentUtils::IsExpandedPrincipal(principal)); + nsTArray<nsCOMPtr<nsIPrincipal>> principalAsArray{principal}; + RefPtr<ExpandedPrincipal> ep = ExpandedPrincipal::Create( + principalAsArray, principal->OriginAttributesRef()); + + // Create the sandbox. + RootedValue v(cx); + nsresult rv = CreateSandboxObject( + cx, &v, static_cast<nsIExpandedPrincipal*>(ep), options); + NS_ENSURE_SUCCESS(rv, nullptr); + scope = &v.toObject(); + + JSObject* unwrapped = js::UncheckedUnwrap(scope); + MOZ_ASSERT(xpc::IsInUAWidgetScope(unwrapped)); + + MOZ_ALWAYS_TRUE(mUAWidgetScopeMap.putNew(key, unwrapped)); + } while (false); + + return scope; +} + +JSObject* XPCJSRuntime::UnprivilegedJunkScope(const mozilla::fallible_t&) { + if (!mUnprivilegedJunkScope) { + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + SandboxOptions options; + options.sandboxName.AssignLiteral("XPConnect Junk Compartment"); + options.invisibleToDebugger = true; + + RootedValue sandbox(cx); + nsresult rv = CreateSandboxObject(cx, &sandbox, nullptr, options); + NS_ENSURE_SUCCESS(rv, nullptr); + + mUnprivilegedJunkScope = + SandboxPrivate::GetPrivate(sandbox.toObjectOrNull()); + } + MOZ_ASSERT(mUnprivilegedJunkScope->GetWrapper(), + "Wrapper should have same lifetime as weak reference"); + return mUnprivilegedJunkScope->GetWrapper(); +} + +JSObject* XPCJSRuntime::UnprivilegedJunkScope() { + JSObject* scope = UnprivilegedJunkScope(fallible); + MOZ_RELEASE_ASSERT(scope); + return scope; +} + +bool XPCJSRuntime::IsUnprivilegedJunkScope(JSObject* obj) { + return mUnprivilegedJunkScope && obj == mUnprivilegedJunkScope->GetWrapper(); +} + +void XPCJSRuntime::DeleteSingletonScopes() { + // We're pretty late in shutdown, so we call ReleaseWrapper on the scopes. + // This way the GC can collect them immediately, and we don't rely on the CC + // to clean up. + if (RefPtr<SandboxPrivate> sandbox = mUnprivilegedJunkScope.get()) { + sandbox->ReleaseWrapper(sandbox); + mUnprivilegedJunkScope = nullptr; + } + mLoaderGlobal = nullptr; +} + +JSObject* XPCJSRuntime::LoaderGlobal() { + if (!mLoaderGlobal) { + RefPtr<mozJSComponentLoader> loader = mozJSComponentLoader::Get(); + + dom::AutoJSAPI jsapi; + jsapi.Init(); + + mLoaderGlobal = loader->GetSharedGlobal(jsapi.cx()); + MOZ_RELEASE_ASSERT(!JS_IsExceptionPending(jsapi.cx())); + } + return mLoaderGlobal; +} + +uint32_t GetAndClampCPUCount() { + // See HelperThreads.cpp for why we want between 2-8 threads + int32_t proc = GetNumberOfProcessors(); + if (proc < 2) { + return 2; + } + return std::min(proc, 8); +} +nsresult HelperThreadPool::Dispatch( + already_AddRefed<HelperThreadTaskHandler> aRunnable) { + return mPool->Dispatch(std::move(aRunnable), NS_DISPATCH_NORMAL); +} + +HelperThreadPool::HelperThreadPool() { + mPool = new nsThreadPool(); + mPool->SetName("JSHelperThreads"_ns); + mPool->SetThreadLimit(GetAndClampCPUCount()); + // Helper threads need a larger stack size than the default nsThreadPool stack + // size. These values are described in detail in HelperThreads.cpp. + const uint32_t kDefaultHelperStackSize = 2048 * 1024 - 2 * 4096; + +#if defined(MOZ_TSAN) + const uint32_t HELPER_STACK_SIZE = 2 * kDefaultHelperStackSize; +#else + const uint32_t HELPER_STACK_SIZE = kDefaultHelperStackSize; +#endif + + mPool->SetThreadStackSize(HELPER_STACK_SIZE); +} + +void HelperThreadPool::Shutdown() { mPool->Shutdown(); } |