/* -*- 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 "XPCMaps.h" #include "XPCWrapper.h" #include "XPCJSMemoryReporter.h" #include "XrayWrapper.h" #include "WrapperFactory.h" #include "mozJSModuleLoader.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/RealmIterators.h" #include "js/SliceBudget.h" #include "js/UbiNode.h" #include "js/UbiNodeUtils.h" #include "js/friend/UsageStatistics.h" // JSMetric, 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/FetchUtil.h" #include "mozilla/dom/WindowBinding.h" #include "mozilla/Atomics.h" #include "mozilla/Attributes.h" #include "mozilla/ProcessHangMonitor.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/Sprintf.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/Unused.h" #include "AccessCheck.h" #include "nsGlobalWindow.h" #include "nsAboutProtocolUtils.h" #include "NodeUbiReporting.h" #include "ExpandedPrincipal.h" #include "nsIInputStream.h" #include "nsJSPrincipals.h" #include "nsJSEnvironment.h" #include "XPCInlines.h" #ifdef XP_WIN # include #endif using namespace mozilla; using namespace mozilla::dom; using namespace xpc; using namespace JS; using namespace js; using mozilla::dom::PerThreadAtomCache; /***************************************************************************/ const char* const XPCJSRuntime::mStrings[] = { "constructor", // IDX_CONSTRUCTOR "toString", // IDX_TO_STRING "toSource", // IDX_TO_SOURCE "value", // IDX_VALUE "QueryInterface", // IDX_QUERY_INTERFACE "Components", // IDX_COMPONENTS "Cc", // IDX_CC "Ci", // IDX_CI "Cr", // IDX_CR "Cu", // IDX_CU "Services", // IDX_SERVICES "wrappedJSObject", // IDX_WRAPPED_JSOBJECT "prototype", // IDX_PROTOTYPE "eval", // IDX_EVAL "controllers", // IDX_CONTROLLERS "Controllers", // IDX_CONTROLLERS_CLASS "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 "cause", // IDX_CAUSE "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 "fetch", // IDX_FETCH "crypto", // IDX_CRYPTO "indexedDB", // IDX_INDEXEDDB "structuredClone", // IDX_STRUCTUREDCLONE }; /***************************************************************************/ // *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_RELEVANT_FOR_JS("Incremental CC", GCCC); 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 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 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()), 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 clasp = JS::GetClass(aGlobal); MOZ_ASSERT(clasp->slot0IsISupports() || 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(c, aGlobal); priv = new CompartmentPrivate(c, std::move(scope), BasePrincipal::Cast(principal), aSite); JS_SetCompartmentPrivate(c, priv); } } // As XPCJSRuntime can live longer than when we shutdown the observer service, // we have our own getter to account for this. static nsCOMPtr GetObserverService() { if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownFinal)) { return nullptr; } return mozilla::services::GetObserverService(); } 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 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: // // (from: :) // // where is user-provided via Cu.Sandbox() // and and is the stack frame location // from where Cu.Sandbox was called. // // Otherwise, it is simply the caller-provided name, which is usually a URI. // // 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 // , so we try this first. if (TryParseLocationURICandidate(Substring(location, 0, idx), aLocationHint, aURI)) { return true; } // Not in so we need to inspect and // the chain that is potentially contained within and grab the rightmost // item that is actually a URI. // First, hack off the :) 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()) { 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 (const auto& key : mJSStackFrames.Keys()) { key->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), mWindowAllowsScript(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 mWindowAllowsScript && !mScriptBlockedByPolicy && mScriptBlocks == 0; } bool Scriptability::IsImmuneToScriptPolicy() { return mImmuneToScriptPolicy; } void Scriptability::Block() { ++mScriptBlocks; } void Scriptability::Unblock() { MOZ_ASSERT(mScriptBlocks > 0); --mScriptBlocks; } void Scriptability::SetWindowAllowsScript(bool aAllowed) { mWindowAllowsScript = aAllowed || mImmuneToScriptPolicy; } /* static */ bool Scriptability::AllowedIfExists(JSObject* aScope) { RealmPrivate* realmPrivate = RealmPrivate::Get(aScope); return realmPrivate ? realmPrivate->scriptability.Allowed() : true; } /* 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* SandboxWindowOrNull(JSObject* aObj, JSContext* aCx) { MOZ_ASSERT(aObj); if (!IsSandbox(aObj)) { return nullptr; } // Sandbox can't be a Proxy so it must have a static prototype. JSObject* proto = GetStaticPrototype(aObj); if (!proto || !IsSandboxPrototypeProxy(proto)) { return nullptr; } proto = js::CheckedUnwrapDynamic(proto, aCx, /* stopAtWindowProxy = */ false); if (!proto) { return nullptr; } return WindowOrNull(proto); } 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(JS::GCContext* gcx, 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 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()) { const auto* cx = static_cast(ccx); if (AutoMarkingPtr* roots = cx->mAutoRoots) { roots->TraceJSAll(trc); } } if (mIID2NativeInterfaceMap) { mIID2NativeInterfaceMap->Trace(trc); } dom::TraceBlackJS(trc); } void XPCJSRuntime::TraceAdditionalNativeGrayRoots(JSTracer* trc) { XPCWrappedNativeScope::TraceWrappedNativesInAllScopes(this, trc); } void XPCJSRuntime::TraverseAdditionalNativeRoots( nsCycleCollectionNoteRootCallback& cb) { XPCWrappedNativeScope::SuspectAllWrappers(cb); auto* parti = NS_CYCLE_COLLECTION_PARTICIPANT(nsXPCWrappedJS); for (auto* wjs : mSubjectToFinalizationWJS) { MOZ_DIAGNOSTIC_ASSERT(wjs->IsSubjectToFinalization()); cb.NoteXPCOMRoot(ToSupports(wjs), parti); } } void XPCJSRuntime::UnmarkSkippableJSHolders() { CycleCollectedJSRuntime::UnmarkSkippableJSHolders(); } void XPCJSRuntime::PrepareForForgetSkippable() { nsCOMPtr obs = xpc::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "cycle-collector-forget-skippable", nullptr); } } void XPCJSRuntime::BeginCycleCollectionCallback(CCReason aReason) { nsJSContext::BeginCycleCollectionCallback(aReason); nsCOMPtr obs = xpc::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "cycle-collector-begin", nullptr); } } void XPCJSRuntime::EndCycleCollectionCallback(CycleCollectorResults& aResults) { nsJSContext::EndCycleCollectionCallback(aResults); nsCOMPtr obs = xpc::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; } nsCOMPtr obs = xpc::GetObserverService(); if (obs) { switch (progress) { case JS::GC_CYCLE_BEGIN: obs->NotifyObservers(nullptr, "garbage-collector-begin", nullptr); break; case JS::GC_CYCLE_END: obs->NotifyObservers(nullptr, "garbage-collector-end", nullptr); break; default: break; } } 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. // // The GC_WAITING reason is a little overloaded here, but we want to do // a CC to allow Realms to be collected when they are referenced by a cycle. NS_DispatchToCurrentThread(NS_NewRunnableFunction( "XPCJSRuntime::DoCycleCollectionCallback", []() { nsJSContext::CycleCollectNow(CCReason::GC_WAITING, nullptr); })); XPCJSRuntime* self = nsXPConnect::GetRuntimeInstance(); if (!self) { return; } if (self->mPrevDoCycleCollectionCallback) { (*self->mPrevDoCycleCollectionCallback)(cx); } } void XPCJSRuntime::CustomGCCallback(JSGCStatus status) { nsTArray callbacks(extraGCCallbacks.Clone()); for (uint32_t i = 0; i < callbacks.Length(); ++i) { callbacks[i](status); } } /* static */ void XPCJSRuntime::FinalizeCallback(JS::GCContext* gcx, 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()) { const auto* cx = static_cast(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 transferred these native objects to this list when their JSObjects // 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 // were 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 were not themselves slated for // finalization in this gc cycle. // // 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. self->mDyingWrappedNativeProtos.clear(); MOZ_ASSERT(self->mGCIsRunning, "bad state"); self->mGCIsRunning = false; break; } } } /* static */ void XPCJSRuntime::WeakPointerZonesCallback(JSTracer* trc, 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(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 restoreState(self->mGCIsRunning); self->mGCIsRunning = true; self->mWrappedJSMap->UpdateWeakPointersAfterGC(trc); self->mUAWidgetScopeMap.traceWeak(trc); } /* static */ void XPCJSRuntime::WeakPointerCompartmentCallback(JSTracer* trc, 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(trc); } } void CompartmentPrivate::UpdateWeakPointersAfterGC(JSTracer* trc) { mRemoteProxies.traceWeak(trc); mWrappedJSMap->UpdateWeakPointersAfterGC(trc); mScope->UpdateWeakPointersAfterGC(trc); } void XPCJSRuntime::CustomOutOfMemoryCallback() { if (!Preferences::GetBool("memory.dump_reports_on_oom")) { return; } nsCOMPtr 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 os = xpc::GetObserverService(); if (os) { os->NotifyObservers(nullptr, "memory-pressure", u"heap-minimize"); } CycleCollectedJSRuntime::SetLargeAllocationFailure(OOMState::Reported); } class LargeAllocationFailureRunnable final : public Runnable { Mutex mMutex MOZ_UNANNOTATED; 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 r = new LargeAllocationFailureRunnable; if (NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread(r)))) { return; } r->BlockUntilDone(); } // Usually this is used through nsIPlatformInfo. However, being able to query // this interface on all threads risk triggering some main-thread assertions // which is not guaranteed by the callers of GetBuildId. extern const char gToolkitBuildID[]; bool mozilla::GetBuildId(JS::BuildIdCharVector* aBuildID) { size_t length = std::char_traits::length(gToolkitBuildID); return aBuildID->append(gToolkitBuildID, length); } 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::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; // Prevent ~LinkedList assertion failures if we leaked things. mWrappedNativeScopes.clear(); mSubjectToFinalizationWJS.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("", *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, ""); } else { // Something went wrong. Anonymize the entire path to be // safe. name.Truncate(pathPos); name += ""; } } // 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 += ""; } } } // 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 + "propmap-tables"_ns, zStats.initialPropMapTable, "Tables storing property map information."); 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 + "getter-setters-gc-heap"_ns, zStats.getterSettersGCHeap, "Information for getter/setter properties."); ZRREPORT_GC_BYTES(pathPrefix + "property-maps/gc-heap/compact"_ns, zStats.compactPropMapsGCHeap, "Information about object properties."); ZRREPORT_GC_BYTES(pathPrefix + "property-maps/gc-heap/normal"_ns, zStats.normalPropMapsGCHeap, "Information about object properties."); ZRREPORT_GC_BYTES(pathPrefix + "property-maps/gc-heap/dict"_ns, zStats.dictPropMapsGCHeap, "Information about dictionary mode object properties."); ZRREPORT_BYTES(pathPrefix + "property-maps/malloc-heap/children"_ns, zStats.propMapChildren, "Tables for PropMap children."); ZRREPORT_BYTES(pathPrefix + "property-maps/malloc-heap/tables"_ns, zStats.propMapTables, "HashTables for PropMaps."); 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()/"_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()/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()/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.shapesGCHeapShared > 0) { REPORT_GC_BYTES(pathPrefix + "shapes/gc-heap/shared"_ns, shapeInfo.shapesGCHeapShared, "Shared shapes."); } 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.shapesMallocHeapCache > 0) { REPORT_BYTES(pathPrefix + "shapes/malloc-heap/shape-cache"_ns, KIND_HEAP, shapeInfo.shapesMallocHeapCache, "Shape cache hash set for adding properties."); } 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.objectsMallocHeapGlobalData > 0) { REPORT_BYTES(path + "objects/malloc-heap/global-data"_ns, KIND_HEAP, classInfo.objectsMallocHeapGlobalData, "Data for global objects."); } if (classInfo.objectsMallocHeapGlobalVarNamesSet > 0) { REPORT_BYTES(path + "objects/malloc-heap/global-varnames-set"_ns, KIND_HEAP, classInfo.objectsMallocHeapGlobalVarNamesSet, "Set of global names."); } 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.objectsNonHeapElementsWasmShared > 0) { REPORT_BYTES( path + "objects/non-heap/elements/wasm-shared"_ns, KIND_NONHEAP, classInfo.objectsNonHeapElementsWasmShared, "wasm/asm.js array buffer elements allocated outside both the " "malloc heap and the GC heap. These elements are shared between " "one or more runtimes; the reported size is divided by the " "buffer's refcount."); } if (classInfo.objectsNonHeapCodeWasm > 0) { REPORT_BYTES(path + "objects/non-heap/code/wasm"_ns, KIND_NONHEAP, classInfo.objectsNonHeapCodeWasm, "AOT-compiled wasm/asm.js code."); } } 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()/"_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 (const auto& zStats : rtStats.zoneStatsVector) { const xpc::ZoneStatsExtras* extras = static_cast(zStats.extra); ReportZoneStats(zStats, *extras, handleReport, data, anonymize, &gcTotal); } for (const auto& realmStats : rtStats.realmStatsVector) { const xpc::RealmStatsExtras* extras = static_cast(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/self-host-stencil"_ns, KIND_HEAP, rtStats.runtime.selfHostStencil, "The self-hosting CompilationStencil."); 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."); nsCString nonNotablePath = rtPath + nsPrintfCString( "runtime/script-sources/source(scripts=%d, )/", 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("", 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-pages"_ns, rtStats.gcHeapDecommittedPages, "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 paths; }; static void RealmCallback(JSContext* cx, void* vdata, Realm* realm, const JS::AutoRequireNoGC& nogc) { // silently ignore OOM errors Data* data = static_cast(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 (auto& path : d.paths) { REPORT(nsCString(path), 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 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 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(realmStatsVector[i].extra); } for (size_t i = 0; i != zoneStatsVector.length(); ++i) { delete static_cast(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(dom::RootingCx(), js::GetAnyRealmInZone(zone)); if (realm) { RootedObject global(dom::RootingCx(), JS::GetRealmGlobalOrNull(realm)); if (global) { RefPtr 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 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 // "realm()/". // // 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 "/dom/" if // this realm belongs to an nsGlobalWindow, and // "explicit/dom/?!/" 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); mozJSModuleLoader* loader = mozJSModuleLoader::Get(); size_t jsModuleLoaderSize = loader ? loader->SizeOfIncludingThis(JSMallocSizeOf) : 0; mozJSModuleLoader* devToolsLoader = mozJSModuleLoader::GetDevToolsLoader(); size_t jsDevToolsModuleLoaderSize = devToolsLoader ? devToolsLoader->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."); // 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 (rtStats.runtime.wasmGuardPages > 0) { REPORT_BYTES( "wasm-guard-pages"_ns, KIND_OTHER, rtStats.runtime.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."); } // 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/getter-setters"), KIND_OTHER, rtStats.zTotals.unusedGCThings.getterSetter, "Unused getter-setter cells within non-empty arenas."); REPORT_BYTES( nsLiteralCString( "js-main-runtime-gc-heap-committed/unused/gc-things/property-maps"), KIND_OTHER, rtStats.zTotals.unusedGCThings.propMap, "Unused property map 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.shapesGCHeapShared + 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/getter-setters"), KIND_OTHER, rtStats.zTotals.getterSettersGCHeap, "Used getter/setter cells."); MREPORT_BYTES( nsLiteralCString( "js-main-runtime-gc-heap-committed/used/gc-things/property-maps"), KIND_OTHER, rtStats.zTotals.dictPropMapsGCHeap + rtStats.zTotals.compactPropMapsGCHeap + rtStats.zTotals.normalPropMapsGCHeap, "Used property map 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); (void)gcThingTotal; // 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-module-loader"_ns, KIND_HEAP, jsModuleLoaderSize, "XPConnect's JS module loader."); REPORT_BYTES("explicit/xpconnect/js-devtools-module-loader"_ns, KIND_HEAP, jsDevToolsModuleLoaderSize, "DevTools's JS module loader."); // 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(JSMetric id, uint32_t sample) { // clang-format off switch (id) { #define CASE_ACCUMULATE(NAME, _) \ case JSMetric::NAME: \ Telemetry::Accumulate(Telemetry::NAME, sample); \ break; FOR_EACH_JS_METRIC(CASE_ACCUMULATE) #undef CASE_ACCUMULATE default: MOZ_CRASH("Bad metric id"); } // clang-format on } 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; 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(JS::GCContext* gcx, 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 priv(RealmPrivate::Get(realm)); JS::SetRealmPrivate(realm, nullptr); } static bool PreserveWrapper(JSContext* cx, JS::Handle 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 uri; rv = NS_NewURI(getter_AddRefs(uri), filename); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 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 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(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(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()), mIID2NativeInterfaceMap(mozilla::MakeUnique()), mClassInfo2NativeSetMap(mozilla::MakeUnique()), mNativeSetMap(mozilla::MakeUnique()), mWrappedNativeScopes(), mGCIsRunning(false), mNativesToReleaseArray(), mDoingFinalization(false), 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 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 { protected: explicit ReflectorNode(JSObject* ptr) : Concrete(ptr) {} public: static void construct(void* storage, JSObject* ptr) { new (storage) ReflectorNode(ptr); } js::UniquePtr edges(JSContext* cx, bool wantNames) const override; }; js::UniquePtr ReflectorNode::edges(JSContext* cx, bool wantNames) const { js::UniquePtr range(static_cast( Concrete::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(range.release()); } } // Namespace ubi } // Namespace JS void ConstructUbiNode(void* storage, JSObject* ptr) { JS::ubi::ReflectorNode::construct(storage, ptr); } void XPCJSRuntime::Initialize(JSContext* cx) { mLoaderGlobal.init(cx, nullptr); // these jsids filled in later when we have a JSContext to work with. mStrIDs[0] = JS::PropertyKey::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::SetXrayJitInfo(&gXrayJitInfo); JS::SetProcessLargeAllocationFailureCallback( OnLargeAllocationFailureCallback); // The WasmAltDataType is build by the JS engine from the build id. JS::SetProcessBuildIdOp(GetBuildId); FetchUtil::InitWasmAltDataType(); // 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 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 (mStrIDs[0].isVoid()) { RootedString str(cx); for (unsigned i = 0; i < XPCJSContext::IDX_TOTAL_COUNT; i++) { str = JS_AtomizeAndPinString(cx, mStrings[i]); if (!str) { mStrIDs[0] = JS::PropertyKey::Void(); return false; } mStrIDs[i] = PropertyKey::fromPinnedString(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 = XPCWrappedNativeProto::Get(obj); // Nothing here can GC. The analysis would otherwise think that ~nsCOMPtr // could GC, but that's only possible if nsIXPCScriptable::GetJSClass() // somehow released a reference to the nsIXPCScriptable, which isn't going to // happen. JS::AutoSuppressGCAnalysis nogc; nsCOMPtr 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 = XPCWrappedNativeTearOff::Get(obj); NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( cb, "XPCWrappedNativeTearOff::Get(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()) { i.get()->DebugDump(depth); } XPC_LOG_OUTDENT(); } XPC_LOG_OUTDENT(); #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 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> principalAsArray{principal}; RefPtr ep = ExpandedPrincipal::Create( principalAsArray, principal->OriginAttributesRef()); // Create the sandbox. RootedValue v(cx); nsresult rv = CreateSandboxObject( cx, &v, static_cast(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 sandbox = mUnprivilegedJunkScope.get()) { sandbox->ReleaseWrapper(sandbox); mUnprivilegedJunkScope = nullptr; } mLoaderGlobal = nullptr; } JSObject* XPCJSRuntime::LoaderGlobal() { if (!mLoaderGlobal) { RefPtr loader = mozJSModuleLoader::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); }