/* -*- 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/AppShutdown.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/UniquePtr.h"
#include "xpcprivate.h"
#include "xpcpublic.h"
#include "XPCMaps.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 "nsPIDOMWindow.h"
#include "nsPrintfCString.h"
#include "nsScriptSecurityManager.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 "mozilla/glean/JsXpconnectMetrics.h"
#include "mozilla/glean/XpcomMetrics.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 "js/Utility.h" // JS::UniqueTwoByteChars
#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 "nsGlobalWindowInner.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 <windows.h>
#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
"locks",
// IDX_LOCKS
#ifdef ENABLE_EXPLICIT_RESOURCE_MANAGEMENT
"suppressed",
// IDX_SUPPRESSED
"error",
// IDX_ERROR
#endif
};
/***************************************************************************/
// *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);
auto timerId = glean::cycle_collector::async_snow_white_freeing.Start();
// 2 ms budget, given that kICCSliceBudget is only 3 ms
SliceBudget budget = SliceBudget(TimeBudget(2));
bool hadSnowWhiteObjects =
nsCycleCollector_doDeferredDeletionWithBudget(budget);
glean::cycle_collector::async_snow_white_freeing.StopAndAccumulate(
std::move(timerId));
if (hadSnowWhiteObjects && !mContinuation) {
mContinuation =
true;
if (NS_FAILED(Dispatch())) {
mActive =
false;
}
}
else {
mActive =
false;
}
return NS_OK;
}
nsresult Dispatch() {
nsCOMPtr<nsIRunnable> self(
this);
return NS_DispatchToCurrentThreadQueue(self.forget(), 1000,
EventQueuePriority::Idle);
}
void Start(
bool aContinuation =
false,
bool aPurge =
false) {
if (mContinuation) {
mContinuation = aContinuation;
}
mPurge = aPurge;
if (!mActive && NS_SUCCEEDED(Dispatch())) {
mActive =
true;
}
}
AsyncFreeSnowWhite()
: Runnable(
"AsyncFreeSnowWhite"),
mContinuation(
false),
mActive(
false),
mPurge(
false) {}
public:
bool mContinuation;
bool mActive;
bool mPurge;
};
namespace xpc {
CompartmentPrivate::CompartmentPrivate(
JS::Compartment* c, mozilla::UniquePtr<XPCWrappedNativeScope> scope,
mozilla::BasePrincipal* origin,
const SiteIdentifier& site)
: originInfo(origin, site),
wantXrays(
false),
allowWaivers(
true),
isWebExtensionContentScript(
false),
isUAWidgetCompartment(
false),
hasExclusiveExpandos(
false),
wasShutdown(
false),
mWrappedJSMap(mozilla::MakeUnique<JSObject2WrappedJSMap>()),
mScope(std::move(scope)) {
MOZ_COUNT_CTOR(xpc::CompartmentPrivate);
}
CompartmentPrivate::~CompartmentPrivate() {
MOZ_COUNT_DTOR(xpc::CompartmentPrivate);
}
void CompartmentPrivate::SystemIsBeingShutDown() {
// We may call this multiple times when the compartment contains more than one
// realm.
if (!wasShutdown) {
mWrappedJSMap->ShutdownMarker();
wasShutdown =
true;
}
}
RealmPrivate::RealmPrivate(JS::Realm* realm) : scriptability(realm) {
mozilla::PodArrayZero(wrapperDenialWarnings);
}
/* static */
void RealmPrivate::Init(HandleObject aGlobal,
const SiteIdentifier& aSite) {
MOZ_ASSERT(aGlobal);
DebugOnly<
const JSClass*> clasp = JS::GetClass(aGlobal);
MOZ_ASSERT(clasp->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<XPCWrappedNativeScope>(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<nsIObserverService> 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<nsIURI> uri;
if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), uristr))) {
return false;
}
nsAutoCString scheme;
if (NS_FAILED(uri->GetScheme(scheme))) {
return false;
}
// Cannot really map data: and blob:.
// Also, data: URIs are pretty memory hungry, which is kinda bad
// for memory reporter use.
if (scheme.EqualsLiteral(
"data") || scheme.EqualsLiteral(
"blob")) {
return false;
}
uri.forget(aURI);
return true;
}
bool RealmPrivate::TryParseLocationURI(RealmPrivate::LocationHint aLocationHint,
nsIURI** aURI) {
if (!aURI) {
return false;
}
// Need to parse the URI.
if (location.IsEmpty()) {
return false;
}
// Handle Sandbox location strings.
// A sandbox string looks like this, for anonymous sandboxes, and builds
// where Sandbox location tagging is enabled:
//
// <sandboxName> (from: <js-stack-frame-filename>:<lineno>)
//
// where <sandboxName> is user-provided via Cu.Sandbox()
// and <js-stack-frame-filename> and <lineno> is the stack frame location
// from where Cu.Sandbox was called.
//
// Otherwise, it is simply the caller-provided name, which is usually a URI.
//
// <js-stack-frame-filename> furthermore is "free form", often using a
// "uri -> uri -> ..." chain. The following code will and must handle this
// common case.
//
// It should be noted that other parts of the code may already rely on the
// "format" of these strings.
static const nsDependentCString from(
"(from: ");
static const nsDependentCString arrow(
" -> ");
static const size_t fromLength = from.Length();
static const size_t arrowLength = arrow.Length();
// See: XPCComponents.cpp#AssembleSandboxMemoryReporterName
int32_t idx = location.Find(from);
if (idx < 0) {
return TryParseLocationURICandidate(location, aLocationHint, aURI);
}
// When parsing we're looking for the right-most URI. This URI may be in
// <sandboxName>, so we try this first.
if (TryParseLocationURICandidate(Substring(location, 0, idx), aLocationHint,
aURI)) {
return true;
}
// Not in <sandboxName> so we need to inspect <js-stack-frame-filename> and
// the chain that is potentially contained within and grab the rightmost
// item that is actually a URI.
// First, hack off the :<lineno>) part as well
int32_t ridx = location.RFind(
":"_ns);
nsAutoCString chain(
Substring(location, idx + fromLength, ridx - idx - fromLength));
// Loop over the "->" chain. This loop also works for non-chains, or more
// correctly chains with only one item.
for (;;) {
idx = chain.RFind(arrow);
if (idx < 0) {
// This is the last chain item. Try to parse what is left.
return TryParseLocationURICandidate(chain, aLocationHint, aURI);
}
// Try to parse current chain item
if (TryParseLocationURICandidate(Substring(chain, idx + arrowLength),
aLocationHint, aURI)) {
return true;
}
// Current chain item couldn't be parsed.
// Strip current item and continue.
chain = Substring(chain, 0, idx);
}
MOZ_CRASH(
"Chain parser loop does not terminate");
}
static bool PrincipalImmuneToScriptPolicy(nsIPrincipal* aPrincipal) {
// System principal gets a free pass.
if (aPrincipal->IsSystemPrincipal()) {
return true;
}
auto* principal = BasePrincipal::Cast(aPrincipal);
// ExpandedPrincipal gets a free pass.
if (principal->Is<ExpandedPrincipal>()) {
return true;
}
// WebExtension principals get a free pass.
if (principal->AddonPolicy()) {
return true;
}
// pdf.js is a special-case too.
if (nsContentUtils::IsPDFJS(principal)) {
return true;
}
// Check whether our URI is an "about:" URI that allows scripts. If it is,
// we need to allow JS to run.
if (aPrincipal->SchemeIs(
"about")) {
uint32_t flags;
nsresult rv = aPrincipal->GetAboutModuleFlags(&flags);
if (NS_SUCCEEDED(rv) && (flags & nsIAboutModule::ALLOW_SCRIPT)) {
return true;
}
}
return false;
}
void RealmPrivate::RegisterStackFrame(JSStackFrameBase* aFrame) {
mJSStackFrames.PutEntry(aFrame);
}
void RealmPrivate::UnregisterStackFrame(JSStackFrameBase* aFrame) {
mJSStackFrames.RemoveEntry(aFrame);
}
void RealmPrivate::NukeJSStackFrames() {
for (
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 mozJSModuleLoader::Get()->GetSharedGlobal();
}
JSObject* CompilationScope() {
return mozJSModuleLoader::Get()->GetSharedGlobal();
}
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);
}
JSObject* SandboxPrototypeOrNull(JSContext* aCx, JSObject* aObj) {
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;
}
return js::CheckedUnwrapDynamic(proto, aCx,
/* stopAtWindowProxy = */ false);
}
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<CompartmentPrivate> priv(
CompartmentPrivate::Get(compartment));
JS_SetCompartmentPrivate(compartment, nullptr);
}
static size_t CompartmentSizeOfIncludingThisCallback(
MallocSizeOf mallocSizeOf, JS::Compartment* compartment) {
CompartmentPrivate* priv = CompartmentPrivate::Get(compartment);
return priv ? priv->SizeOfIncludingThis(mallocSizeOf) : 0;
}
/*
* Return true if there exists a non-system inner window which is a current
* inner window and whose reflector is gray. We don't merge system
* compartments, so we don't use them to trigger merging CCs.
*/
bool XPCJSRuntime::UsefulToMergeZones()
const {
MOZ_ASSERT(NS_IsMainThread());
// Turns out, actually making this return true often enough makes Windows
// mochitest-gl OOM a lot. Need to figure out what's going on there; see
// bug 1277036.
return false;
}
void XPCJSRuntime::TraceNativeBlackRoots(JSTracer* trc) {
if (CycleCollectedJSContext* ccx = GetContext()) {
const auto* cx =
static_cast<
const XPCJSContext*>(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<nsIObserverService> obs = xpc::GetObserverService();
if (obs) {
obs->NotifyObservers(nullptr,
"cycle-collector-forget-skippable", nullptr);
}
}
void XPCJSRuntime::BeginCycleCollectionCallback(CCReason aReason) {
nsJSContext::BeginCycleCollectionCallback(aReason);
nsCOMPtr<nsIObserverService> obs = xpc::GetObserverService();
if (obs) {
obs->NotifyObservers(nullptr,
"cycle-collector-begin", nullptr);
}
}
void XPCJSRuntime::EndCycleCollectionCallback(CycleCollectorResults& aResults)
{
nsJSContext::EndCycleCollectionCallback(aResults);
nsCOMPtr<nsIObserverService> 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<nsIObserverService> 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<xpcGCCallback> 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<const XPCJSContext*>(ccx);
if (AutoMarkingPtr* roots = cx->mAutoRoots) {
roots->MarkAfterJSFinalizeAll();
}
// Now we are going to recycle any unused WrappedNativeTearoffs.
// We do this by iterating all the live callcontexts
// and marking the tearoffs in use. And then we
// iterate over all the WrappedNative wrappers and sweep their
// tearoffs.
//
// This allows us to perhaps minimize the growth of the
// tearoffs. And also makes us not hold references to interfaces
// on our wrapped natives that we are not actually using.
//
// XXX We may decide to not do this on *every* gc cycle.
XPCCallContext* ccxp = cx->GetCallContext();
while (ccxp) {
// Deal with the strictness of callcontext that
// complains if you ask for a tearoff when
// it is in a state where the tearoff could not
// possibly be valid.
if (ccxp->CanGetTearOff()) {
XPCWrappedNativeTearOff* to = ccxp->GetTearOff();
if (to) {
to->Mark();
}
}
ccxp = ccxp->GetPrevCallContext();
}
}
XPCWrappedNativeScope::SweepAllWrappedNativeTearOffs();
// Now we need to kill the 'Dying' XPCWrappedNativeProtos.
//
// We 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<XPCJSRuntime*>(data);
// This callback is always called from within the GC so set the mGCIsRunning
// flag to prevent AssertInvalidWrappedJSNotInTable from trying to call back
// into the JS API. This has often already been set by FinalizeCallback by the
// time we get here, but it may not be if we are doing a shutdown GC or if we
// are called for compacting GC.
AutoRestore<bool> restoreState(self->mGCIsRunning);
self->mGCIsRunning = true;
self->mWrappedJSMap->UpdateWeakPointersAfterGC(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<nsIMemoryInfoDumper> dumper =
do_GetService("@mozilla.org/memory-info-dumper;1");
if (!dumper) {
return;
}
// If this fails, it fails silently.
dumper->DumpMemoryInfoToTempDir(u"due-to-JS-OOM"_ns,
/* anonymize = */ false,
/* minimizeMemoryUsage = */ false);
}
void XPCJSRuntime::OnLargeAllocationFailure() {
CycleCollectedJSRuntime::SetLargeAllocationFailure(OOMState::Reporting);
nsCOMPtr<nsIObserverService> os = 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<LargeAllocationFailureRunnable> 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<char>::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 + "bigints/gc-heap"_ns, zStats.bigIntsGCHeap,
"BigInt values.");
ZRREPORT_BYTES(pathPrefix + "bigints/malloc-heap"_ns,
zStats.bigIntsMallocHeap, "BigInt values.");
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.");
// zStats.smallBuffersGCHeap is not reported as a separate item here as it's
// reported as part of the owning cell. We must still count it as part of the
// total heap size.
gcTotal += zStats.smallBuffersGCHeap;
ZRREPORT_BYTES(pathPrefix + "zone-object"_ns, zStats.zoneObject,
"The JS::Zone object itself.");
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 + "cacheir-stubs"_ns, zStats.cacheIRStubs,
"The JIT's 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/gc-buffers/slots"_ns, KIND_NONHEAP,
classInfo.objectsMallocHeapSlots, "Non-fixed object slots.");
}
if (classInfo.objectsMallocHeapElementsNormal > 0) {
REPORT_BYTES(path + "objects/gc-buffers/elements/normal"_ns, KIND_NONHEAP,
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.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,
--> --------------------
--> maximum size reached
--> --------------------