1464 lines
51 KiB
C++
1464 lines
51 KiB
C++
/* -*- 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 JSContext object */
|
|
|
|
#include "mozilla/MemoryReporting.h"
|
|
#include "mozilla/UniquePtr.h"
|
|
|
|
#include "xpcprivate.h"
|
|
#include "xpcpublic.h"
|
|
#include "XPCWrapper.h"
|
|
#include "XPCJSMemoryReporter.h"
|
|
#include "XPCSelfHostedShmem.h"
|
|
#include "WrapperFactory.h"
|
|
#include "mozJSModuleLoader.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsThreadUtils.h"
|
|
|
|
#include "nsIObserverService.h"
|
|
#include "nsIDebug2.h"
|
|
#include "nsPIDOMWindow.h"
|
|
#include "nsPrintfCString.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/Telemetry.h"
|
|
#include "mozilla/MemoryTelemetry.h"
|
|
#include "mozilla/Services.h"
|
|
#ifdef FUZZING
|
|
# include "mozilla/StaticPrefs_fuzzing.h"
|
|
#endif
|
|
#include "mozilla/StaticPrefs_dom.h"
|
|
#include "mozilla/StaticPrefs_browser.h"
|
|
#include "mozilla/StaticPrefs_javascript.h"
|
|
#include "mozilla/dom/ScriptSettings.h"
|
|
|
|
#include "nsContentUtils.h"
|
|
#include "nsCCUncollectableMarker.h"
|
|
#include "nsCycleCollectionNoteRootCallback.h"
|
|
#include "nsCycleCollector.h"
|
|
#include "nsJSEnvironment.h"
|
|
#include "jsapi.h"
|
|
#include "js/ArrayBuffer.h"
|
|
#include "js/ContextOptions.h"
|
|
#include "js/HelperThreadAPI.h"
|
|
#include "js/Initialization.h"
|
|
#include "js/MemoryMetrics.h"
|
|
#include "js/Prefs.h"
|
|
#include "js/WasmFeatures.h"
|
|
#include "mozilla/dom/BindingUtils.h"
|
|
#include "mozilla/dom/ContentChild.h"
|
|
#include "mozilla/dom/Document.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/ScriptLoader.h"
|
|
#include "mozilla/dom/WindowBinding.h"
|
|
#include "mozilla/dom/WakeLockBinding.h"
|
|
#include "mozilla/extensions/WebExtensionPolicy.h"
|
|
#include "mozilla/Atomics.h"
|
|
#include "mozilla/Attributes.h"
|
|
#include "mozilla/ProcessHangMonitor.h"
|
|
#include "mozilla/Sprintf.h"
|
|
#include "mozilla/SystemPrincipal.h"
|
|
#include "mozilla/TaskController.h"
|
|
#include "mozilla/ThreadLocal.h"
|
|
#include "mozilla/UniquePtrExtensions.h"
|
|
#include "mozilla/Unused.h"
|
|
#include "AccessCheck.h"
|
|
#include "nsGlobalWindowInner.h"
|
|
#include "nsAboutProtocolUtils.h"
|
|
|
|
#include "GeckoProfiler.h"
|
|
#include "nsIXULRuntime.h"
|
|
#include "nsJSPrincipals.h"
|
|
#include "ExpandedPrincipal.h"
|
|
|
|
#if defined(XP_LINUX) && !defined(ANDROID)
|
|
// For getrlimit and min/max.
|
|
# include <algorithm>
|
|
# include <sys/resource.h>
|
|
#endif
|
|
|
|
#ifdef XP_WIN
|
|
// For min.
|
|
# include <algorithm>
|
|
# include <windows.h>
|
|
#endif
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::dom;
|
|
using namespace xpc;
|
|
using namespace JS;
|
|
|
|
// We will clamp to reasonable values if this isn't set.
|
|
#if !defined(PTHREAD_STACK_MIN)
|
|
# define PTHREAD_STACK_MIN 0
|
|
#endif
|
|
|
|
static void WatchdogMain(void* arg);
|
|
class Watchdog;
|
|
class WatchdogManager;
|
|
class MOZ_RAII AutoLockWatchdog final {
|
|
Watchdog* const mWatchdog;
|
|
|
|
public:
|
|
explicit AutoLockWatchdog(Watchdog* aWatchdog);
|
|
~AutoLockWatchdog();
|
|
};
|
|
|
|
class Watchdog {
|
|
public:
|
|
explicit Watchdog(WatchdogManager* aManager)
|
|
: mManager(aManager),
|
|
mLock(nullptr),
|
|
mWakeup(nullptr),
|
|
mThread(nullptr),
|
|
mHibernating(false),
|
|
mInitialized(false),
|
|
mShuttingDown(false),
|
|
mMinScriptRunTimeSeconds(1) {}
|
|
~Watchdog() { MOZ_ASSERT(!Initialized()); }
|
|
|
|
WatchdogManager* Manager() { return mManager; }
|
|
bool Initialized() { return mInitialized; }
|
|
bool ShuttingDown() { return mShuttingDown; }
|
|
PRLock* GetLock() { return mLock; }
|
|
bool Hibernating() { return mHibernating; }
|
|
void WakeUp() {
|
|
MOZ_ASSERT(Initialized());
|
|
MOZ_ASSERT(Hibernating());
|
|
mHibernating = false;
|
|
PR_NotifyCondVar(mWakeup);
|
|
}
|
|
|
|
//
|
|
// Invoked by the main thread only.
|
|
//
|
|
|
|
void Init() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
mLock = PR_NewLock();
|
|
if (!mLock) {
|
|
MOZ_CRASH("PR_NewLock failed.");
|
|
}
|
|
|
|
mWakeup = PR_NewCondVar(mLock);
|
|
if (!mWakeup) {
|
|
MOZ_CRASH("PR_NewCondVar failed.");
|
|
}
|
|
|
|
{
|
|
// Make sure the debug service is instantiated before we create the
|
|
// watchdog thread, since we intentionally try to keep the thread's stack
|
|
// segment as small as possible. It isn't always large enough to
|
|
// instantiate a new service, and even when it is, we don't want fault in
|
|
// extra pages if we can avoid it.
|
|
nsCOMPtr<nsIDebug2> dbg = do_GetService("@mozilla.org/xpcom/debug;1");
|
|
Unused << dbg;
|
|
}
|
|
|
|
{
|
|
AutoLockWatchdog lock(this);
|
|
|
|
// The watchdog thread loop is pretty trivial, and should not
|
|
// require much stack space to do its job. So only give it 32KiB
|
|
// or the platform minimum. On modern Linux libc this might resolve to
|
|
// a runtime call.
|
|
size_t watchdogStackSize = PTHREAD_STACK_MIN;
|
|
watchdogStackSize = std::max<size_t>(32 * 1024, watchdogStackSize);
|
|
|
|
// Gecko uses thread private for accounting and has to clean up at thread
|
|
// exit. Therefore, even though we don't have a return value from the
|
|
// watchdog, we need to join it on shutdown.
|
|
mThread = PR_CreateThread(PR_USER_THREAD, WatchdogMain, this,
|
|
PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD,
|
|
PR_JOINABLE_THREAD, watchdogStackSize);
|
|
if (!mThread) {
|
|
MOZ_CRASH("PR_CreateThread failed!");
|
|
}
|
|
|
|
// WatchdogMain acquires the lock and then asserts mInitialized. So
|
|
// make sure to set mInitialized before releasing the lock here so
|
|
// that it's atomic with the creation of the thread.
|
|
mInitialized = true;
|
|
}
|
|
}
|
|
|
|
void Shutdown() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(Initialized());
|
|
{ // Scoped lock.
|
|
AutoLockWatchdog lock(this);
|
|
|
|
// Signal to the watchdog thread that it's time to shut down.
|
|
mShuttingDown = true;
|
|
|
|
// Wake up the watchdog, and wait for it to call us back.
|
|
PR_NotifyCondVar(mWakeup);
|
|
}
|
|
|
|
PR_JoinThread(mThread);
|
|
|
|
// The thread sets mShuttingDown to false as it exits.
|
|
MOZ_ASSERT(!mShuttingDown);
|
|
|
|
// Destroy state.
|
|
mThread = nullptr;
|
|
PR_DestroyCondVar(mWakeup);
|
|
mWakeup = nullptr;
|
|
PR_DestroyLock(mLock);
|
|
mLock = nullptr;
|
|
|
|
// All done.
|
|
mInitialized = false;
|
|
}
|
|
|
|
void SetMinScriptRunTimeSeconds(int32_t seconds) {
|
|
// This variable is atomic, and is set from the main thread without
|
|
// locking.
|
|
MOZ_ASSERT(seconds > 0);
|
|
mMinScriptRunTimeSeconds = seconds;
|
|
}
|
|
|
|
//
|
|
// Invoked by the watchdog thread only.
|
|
//
|
|
|
|
void Hibernate() {
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
mHibernating = true;
|
|
Sleep(PR_INTERVAL_NO_TIMEOUT);
|
|
}
|
|
void Sleep(PRIntervalTime timeout) {
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
AUTO_PROFILER_THREAD_SLEEP;
|
|
MOZ_ALWAYS_TRUE(PR_WaitCondVar(mWakeup, timeout) == PR_SUCCESS);
|
|
}
|
|
void Finished() {
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
mShuttingDown = false;
|
|
}
|
|
|
|
int32_t MinScriptRunTimeSeconds() { return mMinScriptRunTimeSeconds; }
|
|
|
|
private:
|
|
WatchdogManager* mManager;
|
|
|
|
PRLock* mLock;
|
|
PRCondVar* mWakeup;
|
|
PRThread* mThread;
|
|
bool mHibernating;
|
|
bool mInitialized;
|
|
bool mShuttingDown;
|
|
mozilla::Atomic<int32_t> mMinScriptRunTimeSeconds;
|
|
};
|
|
|
|
#define PREF_MAX_SCRIPT_RUN_TIME_CONTENT "dom.max_script_run_time"
|
|
#define PREF_MAX_SCRIPT_RUN_TIME_CHROME "dom.max_chrome_script_run_time"
|
|
#define PREF_MAX_SCRIPT_RUN_TIME_EXT_CONTENT \
|
|
"dom.max_ext_content_script_run_time"
|
|
|
|
static const char* gCallbackPrefs[] = {
|
|
"dom.use_watchdog",
|
|
PREF_MAX_SCRIPT_RUN_TIME_CONTENT,
|
|
PREF_MAX_SCRIPT_RUN_TIME_CHROME,
|
|
PREF_MAX_SCRIPT_RUN_TIME_EXT_CONTENT,
|
|
nullptr,
|
|
};
|
|
|
|
class WatchdogManager {
|
|
public:
|
|
explicit WatchdogManager() {
|
|
// All the timestamps start at zero.
|
|
PodArrayZero(mTimestamps);
|
|
|
|
// Register ourselves as an observer to get updates on the pref.
|
|
Preferences::RegisterCallbacks(PrefsChanged, gCallbackPrefs, this);
|
|
}
|
|
|
|
virtual ~WatchdogManager() {
|
|
// Shutting down the watchdog requires context-switching to the watchdog
|
|
// thread, which isn't great to do in a destructor. So we require
|
|
// consumers to shut it down manually before releasing it.
|
|
MOZ_ASSERT(!mWatchdog);
|
|
}
|
|
|
|
private:
|
|
static void PrefsChanged(const char* aPref, void* aSelf) {
|
|
static_cast<WatchdogManager*>(aSelf)->RefreshWatchdog();
|
|
}
|
|
|
|
public:
|
|
void Shutdown() {
|
|
Preferences::UnregisterCallbacks(PrefsChanged, gCallbackPrefs, this);
|
|
}
|
|
|
|
void RegisterContext(XPCJSContext* aContext) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
AutoLockWatchdog lock(mWatchdog.get());
|
|
|
|
if (aContext->mActive == XPCJSContext::CONTEXT_ACTIVE) {
|
|
mActiveContexts.insertBack(aContext);
|
|
} else {
|
|
mInactiveContexts.insertBack(aContext);
|
|
}
|
|
|
|
// Enable the watchdog, if appropriate.
|
|
RefreshWatchdog();
|
|
}
|
|
|
|
void UnregisterContext(XPCJSContext* aContext) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
AutoLockWatchdog lock(mWatchdog.get());
|
|
|
|
// aContext must be in one of our two lists, simply remove it.
|
|
aContext->LinkedListElement<XPCJSContext>::remove();
|
|
|
|
#ifdef DEBUG
|
|
// If this was the last context, we should have already shut down
|
|
// the watchdog.
|
|
if (mActiveContexts.isEmpty() && mInactiveContexts.isEmpty()) {
|
|
MOZ_ASSERT(!mWatchdog);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Context statistics. These live on the watchdog manager, are written
|
|
// from the main thread, and are read from the watchdog thread (holding
|
|
// the lock in each case).
|
|
void RecordContextActivity(XPCJSContext* aContext, bool active) {
|
|
// The watchdog reads this state, so acquire the lock before writing it.
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
AutoLockWatchdog lock(mWatchdog.get());
|
|
|
|
// Write state.
|
|
aContext->mLastStateChange = PR_Now();
|
|
aContext->mActive =
|
|
active ? XPCJSContext::CONTEXT_ACTIVE : XPCJSContext::CONTEXT_INACTIVE;
|
|
UpdateContextLists(aContext);
|
|
|
|
// The watchdog may be hibernating, waiting for the context to go
|
|
// active. Wake it up if necessary.
|
|
if (active && mWatchdog && mWatchdog->Hibernating()) {
|
|
mWatchdog->WakeUp();
|
|
}
|
|
}
|
|
|
|
bool IsAnyContextActive() { return !mActiveContexts.isEmpty(); }
|
|
PRTime TimeSinceLastActiveContext() {
|
|
// Must be called on the watchdog thread with the lock held.
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
PR_ASSERT_CURRENT_THREAD_OWNS_LOCK(mWatchdog->GetLock());
|
|
MOZ_ASSERT(mActiveContexts.isEmpty());
|
|
MOZ_ASSERT(!mInactiveContexts.isEmpty());
|
|
|
|
// We store inactive contexts with the most recently added inactive
|
|
// context at the end of the list.
|
|
return PR_Now() - mInactiveContexts.getLast()->mLastStateChange;
|
|
}
|
|
|
|
void RecordTimestamp(WatchdogTimestampCategory aCategory) {
|
|
// Must be called on the watchdog thread with the lock held.
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
PR_ASSERT_CURRENT_THREAD_OWNS_LOCK(mWatchdog->GetLock());
|
|
MOZ_ASSERT(aCategory != TimestampContextStateChange,
|
|
"Use RecordContextActivity to update this");
|
|
|
|
mTimestamps[aCategory] = PR_Now();
|
|
}
|
|
|
|
PRTime GetContextTimestamp(XPCJSContext* aContext,
|
|
const AutoLockWatchdog& aProofOfLock) {
|
|
return aContext->mLastStateChange;
|
|
}
|
|
|
|
PRTime GetTimestamp(WatchdogTimestampCategory aCategory,
|
|
const AutoLockWatchdog& aProofOfLock) {
|
|
MOZ_ASSERT(aCategory != TimestampContextStateChange,
|
|
"Use GetContextTimestamp to retrieve this");
|
|
return mTimestamps[aCategory];
|
|
}
|
|
|
|
Watchdog* GetWatchdog() { return mWatchdog.get(); }
|
|
|
|
void RefreshWatchdog() {
|
|
bool wantWatchdog = Preferences::GetBool("dom.use_watchdog", true);
|
|
if (wantWatchdog != !!mWatchdog) {
|
|
if (wantWatchdog) {
|
|
StartWatchdog();
|
|
} else {
|
|
StopWatchdog();
|
|
}
|
|
}
|
|
|
|
if (mWatchdog) {
|
|
int32_t contentTime = StaticPrefs::dom_max_script_run_time();
|
|
if (contentTime <= 0) {
|
|
contentTime = INT32_MAX;
|
|
}
|
|
int32_t chromeTime = StaticPrefs::dom_max_chrome_script_run_time();
|
|
if (chromeTime <= 0) {
|
|
chromeTime = INT32_MAX;
|
|
}
|
|
int32_t extTime = StaticPrefs::dom_max_ext_content_script_run_time();
|
|
if (extTime <= 0) {
|
|
extTime = INT32_MAX;
|
|
}
|
|
mWatchdog->SetMinScriptRunTimeSeconds(
|
|
std::min({contentTime, chromeTime, extTime}));
|
|
}
|
|
}
|
|
|
|
void StartWatchdog() {
|
|
MOZ_ASSERT(!mWatchdog);
|
|
mWatchdog = mozilla::MakeUnique<Watchdog>(this);
|
|
mWatchdog->Init();
|
|
}
|
|
|
|
void StopWatchdog() {
|
|
MOZ_ASSERT(mWatchdog);
|
|
mWatchdog->Shutdown();
|
|
mWatchdog = nullptr;
|
|
}
|
|
|
|
template <class Callback>
|
|
void ForAllActiveContexts(Callback&& aCallback) {
|
|
// This function must be called on the watchdog thread with the lock held.
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
PR_ASSERT_CURRENT_THREAD_OWNS_LOCK(mWatchdog->GetLock());
|
|
|
|
for (auto* context = mActiveContexts.getFirst(); context;
|
|
context = context->LinkedListElement<XPCJSContext>::getNext()) {
|
|
if (!aCallback(context)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
void UpdateContextLists(XPCJSContext* aContext) {
|
|
// Given aContext whose activity state or timestamp has just changed,
|
|
// put it back in the proper position in the proper list.
|
|
aContext->LinkedListElement<XPCJSContext>::remove();
|
|
auto& list = aContext->mActive == XPCJSContext::CONTEXT_ACTIVE
|
|
? mActiveContexts
|
|
: mInactiveContexts;
|
|
|
|
// Either the new list is empty or aContext must be more recent than
|
|
// the existing last element.
|
|
MOZ_ASSERT_IF(!list.isEmpty(), list.getLast()->mLastStateChange <
|
|
aContext->mLastStateChange);
|
|
list.insertBack(aContext);
|
|
}
|
|
|
|
LinkedList<XPCJSContext> mActiveContexts;
|
|
LinkedList<XPCJSContext> mInactiveContexts;
|
|
mozilla::UniquePtr<Watchdog> mWatchdog;
|
|
|
|
// We store ContextStateChange on the contexts themselves.
|
|
PRTime mTimestamps[kWatchdogTimestampCategoryCount - 1];
|
|
};
|
|
|
|
AutoLockWatchdog::AutoLockWatchdog(Watchdog* aWatchdog) : mWatchdog(aWatchdog) {
|
|
if (mWatchdog) {
|
|
PR_Lock(mWatchdog->GetLock());
|
|
}
|
|
}
|
|
|
|
AutoLockWatchdog::~AutoLockWatchdog() {
|
|
if (mWatchdog) {
|
|
PR_Unlock(mWatchdog->GetLock());
|
|
}
|
|
}
|
|
|
|
static void WatchdogMain(void* arg) {
|
|
AUTO_PROFILER_REGISTER_THREAD("JS Watchdog");
|
|
// Create an nsThread wrapper for the thread and register it with the thread
|
|
// manager.
|
|
Unused << NS_GetCurrentThread();
|
|
NS_SetCurrentThreadName("JS Watchdog");
|
|
|
|
Watchdog* self = static_cast<Watchdog*>(arg);
|
|
WatchdogManager* manager = self->Manager();
|
|
|
|
// Lock lasts until we return
|
|
AutoLockWatchdog lock(self);
|
|
|
|
MOZ_ASSERT(self->Initialized());
|
|
while (!self->ShuttingDown()) {
|
|
// Sleep only 1 second if recently (or currently) active; otherwise,
|
|
// hibernate
|
|
if (manager->IsAnyContextActive() ||
|
|
manager->TimeSinceLastActiveContext() <= PRTime(2 * PR_USEC_PER_SEC)) {
|
|
self->Sleep(PR_TicksPerSecond());
|
|
} else {
|
|
manager->RecordTimestamp(TimestampWatchdogHibernateStart);
|
|
self->Hibernate();
|
|
manager->RecordTimestamp(TimestampWatchdogHibernateStop);
|
|
}
|
|
|
|
// Rise and shine.
|
|
manager->RecordTimestamp(TimestampWatchdogWakeup);
|
|
|
|
// Don't request an interrupt callback unless the current script has
|
|
// been running long enough that we might show the slow script dialog.
|
|
// Triggering the callback from off the main thread can be expensive.
|
|
|
|
// We want to avoid showing the slow script dialog if the user's laptop
|
|
// goes to sleep in the middle of running a script. To ensure this, we
|
|
// invoke the interrupt callback after only half the timeout has
|
|
// elapsed. The callback simply records the fact that it was called in
|
|
// the mSlowScriptSecondHalf flag. Then we wait another (timeout/2)
|
|
// seconds and invoke the callback again. This time around it sees
|
|
// mSlowScriptSecondHalf is set and so it shows the slow script
|
|
// dialog. If the computer is put to sleep during one of the (timeout/2)
|
|
// periods, the script still has the other (timeout/2) seconds to
|
|
// finish.
|
|
if (!self->ShuttingDown() && manager->IsAnyContextActive()) {
|
|
bool debuggerAttached = false;
|
|
nsCOMPtr<nsIDebug2> dbg = do_GetService("@mozilla.org/xpcom/debug;1");
|
|
if (dbg) {
|
|
dbg->GetIsDebuggerAttached(&debuggerAttached);
|
|
}
|
|
if (debuggerAttached) {
|
|
// We won't be interrupting these scripts anyway.
|
|
continue;
|
|
}
|
|
|
|
PRTime usecs = self->MinScriptRunTimeSeconds() * PR_USEC_PER_SEC / 2;
|
|
manager->ForAllActiveContexts([usecs, manager,
|
|
&lock](XPCJSContext* aContext) -> bool {
|
|
auto timediff = PR_Now() - manager->GetContextTimestamp(aContext, lock);
|
|
if (timediff > usecs) {
|
|
JS_RequestInterruptCallback(aContext->Context());
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Tell the manager that we've shut down.
|
|
self->Finished();
|
|
}
|
|
|
|
PRTime XPCJSContext::GetWatchdogTimestamp(WatchdogTimestampCategory aCategory) {
|
|
AutoLockWatchdog lock(mWatchdogManager->GetWatchdog());
|
|
return aCategory == TimestampContextStateChange
|
|
? mWatchdogManager->GetContextTimestamp(this, lock)
|
|
: mWatchdogManager->GetTimestamp(aCategory, lock);
|
|
}
|
|
|
|
// static
|
|
bool XPCJSContext::RecordScriptActivity(bool aActive) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
XPCJSContext* xpccx = XPCJSContext::Get();
|
|
if (!xpccx) {
|
|
// mozilla::SpinEventLoopUntil may use AutoScriptActivity(false) after
|
|
// we destroyed the XPCJSContext.
|
|
MOZ_ASSERT(!aActive);
|
|
return false;
|
|
}
|
|
|
|
bool oldValue = xpccx->SetHasScriptActivity(aActive);
|
|
if (aActive == oldValue) {
|
|
// Nothing to do.
|
|
return oldValue;
|
|
}
|
|
|
|
if (!aActive) {
|
|
ProcessHangMonitor::ClearHang();
|
|
}
|
|
xpccx->mWatchdogManager->RecordContextActivity(xpccx, aActive);
|
|
|
|
return oldValue;
|
|
}
|
|
|
|
AutoScriptActivity::AutoScriptActivity(bool aActive)
|
|
: mActive(aActive),
|
|
mOldValue(XPCJSContext::RecordScriptActivity(aActive)) {}
|
|
|
|
AutoScriptActivity::~AutoScriptActivity() {
|
|
MOZ_ALWAYS_TRUE(mActive == XPCJSContext::RecordScriptActivity(mOldValue));
|
|
}
|
|
|
|
static const double sChromeSlowScriptTelemetryCutoff(10.0);
|
|
static bool sTelemetryEventEnabled(false);
|
|
|
|
// static
|
|
bool XPCJSContext::InterruptCallback(JSContext* cx) {
|
|
XPCJSContext* self = XPCJSContext::Get();
|
|
|
|
// Now is a good time to turn on profiling if it's pending.
|
|
PROFILER_JS_INTERRUPT_CALLBACK();
|
|
|
|
if (profiler_thread_is_being_profiled_for_markers()) {
|
|
nsDependentCString filename("unknown file");
|
|
JS::AutoFilename scriptFilename;
|
|
// Computing the line number can be very expensive (see bug 1330231 for
|
|
// example), so don't request it here.
|
|
if (JS::DescribeScriptedCaller(cx, &scriptFilename)) {
|
|
if (const char* file = scriptFilename.get()) {
|
|
filename.Assign(file, strlen(file));
|
|
}
|
|
PROFILER_MARKER_TEXT("JS::InterruptCallback", JS, {}, filename);
|
|
}
|
|
}
|
|
|
|
// Normally we record mSlowScriptCheckpoint when we start to process an
|
|
// event. However, we can run JS outside of event handlers. This code takes
|
|
// care of that case.
|
|
if (self->mSlowScriptCheckpoint.IsNull()) {
|
|
self->mSlowScriptCheckpoint = TimeStamp::NowLoRes();
|
|
self->mSlowScriptSecondHalf = false;
|
|
self->mSlowScriptActualWait = mozilla::TimeDuration();
|
|
self->mTimeoutAccumulated = false;
|
|
self->mExecutedChromeScript = false;
|
|
return true;
|
|
}
|
|
|
|
// Sometimes we get called back during XPConnect initialization, before Gecko
|
|
// has finished bootstrapping. Avoid crashing in nsContentUtils below.
|
|
if (!nsContentUtils::IsInitialized()) {
|
|
return true;
|
|
}
|
|
|
|
// This is at least the second interrupt callback we've received since
|
|
// returning to the event loop. See how long it's been, and what the limit
|
|
// is.
|
|
TimeStamp now = TimeStamp::NowLoRes();
|
|
TimeDuration duration = now - self->mSlowScriptCheckpoint;
|
|
int32_t limit;
|
|
|
|
nsString addonId;
|
|
const char* prefName;
|
|
auto principal = BasePrincipal::Cast(nsContentUtils::SubjectPrincipal(cx));
|
|
bool chrome = principal->Is<SystemPrincipal>();
|
|
if (chrome) {
|
|
prefName = PREF_MAX_SCRIPT_RUN_TIME_CHROME;
|
|
limit = StaticPrefs::dom_max_chrome_script_run_time();
|
|
self->mExecutedChromeScript = true;
|
|
} else if (auto policy = principal->ContentScriptAddonPolicy()) {
|
|
policy->GetId(addonId);
|
|
prefName = PREF_MAX_SCRIPT_RUN_TIME_EXT_CONTENT;
|
|
limit = StaticPrefs::dom_max_ext_content_script_run_time();
|
|
} else {
|
|
prefName = PREF_MAX_SCRIPT_RUN_TIME_CONTENT;
|
|
limit = StaticPrefs::dom_max_script_run_time();
|
|
}
|
|
|
|
// When the parent process slow script dialog is disabled, we still want
|
|
// to be able to track things for telemetry, so set `mSlowScriptSecondHalf`
|
|
// to true in that case:
|
|
if (limit == 0 && chrome &&
|
|
duration.ToSeconds() > sChromeSlowScriptTelemetryCutoff / 2.0) {
|
|
self->mSlowScriptSecondHalf = true;
|
|
return true;
|
|
}
|
|
// If there's no limit, or we're within the limit, let it go.
|
|
if (limit == 0 || duration.ToSeconds() < limit / 2.0) {
|
|
return true;
|
|
}
|
|
|
|
self->mSlowScriptCheckpoint = now;
|
|
self->mSlowScriptActualWait += duration;
|
|
|
|
// In order to guard against time changes or laptops going to sleep, we
|
|
// don't trigger the slow script warning until (limit/2) seconds have
|
|
// elapsed twice.
|
|
if (!self->mSlowScriptSecondHalf) {
|
|
self->mSlowScriptSecondHalf = true;
|
|
return true;
|
|
}
|
|
|
|
// For scripts in content processes, we only want to show the slow script
|
|
// dialogue if the user is actually trying to perform an important
|
|
// interaction. In theory this could be a chrome script running in the
|
|
// content process, which we probably don't want to give the user the ability
|
|
// to terminate. However, if this is the case we won't be able to map the
|
|
// script global to a window and we'll bail out below.
|
|
if (XRE_IsContentProcess() &&
|
|
StaticPrefs::dom_max_script_run_time_require_critical_input()) {
|
|
// Call possibly slow PeekMessages after the other common early returns in
|
|
// this method.
|
|
ContentChild* contentChild = ContentChild::GetSingleton();
|
|
mozilla::ipc::MessageChannel* channel =
|
|
contentChild ? contentChild->GetIPCChannel() : nullptr;
|
|
if (channel) {
|
|
bool foundInputEvent = false;
|
|
channel->PeekMessages(
|
|
[&foundInputEvent](const IPC::Message& aMsg) -> bool {
|
|
if (nsContentUtils::IsMessageCriticalInputEvent(aMsg)) {
|
|
foundInputEvent = true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (!foundInputEvent) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We use a fixed value of 2 from browser_parent_process_hang_telemetry.js
|
|
// to check if the telemetry events work. Do not interrupt it with a dialog.
|
|
if (chrome && limit == 2 && xpc::IsInAutomation()) {
|
|
return true;
|
|
}
|
|
|
|
//
|
|
// This has gone on long enough! Time to take action. ;-)
|
|
//
|
|
|
|
// Get the DOM window associated with the running script. If the script is
|
|
// running in a non-DOM scope, we have to just let it keep running.
|
|
RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
|
|
RefPtr<nsGlobalWindowInner> win = WindowOrNull(global);
|
|
if (!win) {
|
|
// If this is a sandbox associated with a DOMWindow via a
|
|
// sandboxPrototype, use that DOMWindow. This supports WebExtension
|
|
// content scripts.
|
|
win = SandboxWindowOrNull(global, cx);
|
|
}
|
|
|
|
if (!win) {
|
|
NS_WARNING("No active window");
|
|
return true;
|
|
}
|
|
|
|
if (win->IsDying()) {
|
|
// The window is being torn down. When that happens we try to prevent
|
|
// the dispatch of new runnables, so it also makes sense to kill any
|
|
// long-running script. The user is primarily interested in this page
|
|
// going away.
|
|
return false;
|
|
}
|
|
|
|
// Accumulate slow script invokation delay.
|
|
if (!chrome && !self->mTimeoutAccumulated) {
|
|
uint32_t delay = uint32_t(self->mSlowScriptActualWait.ToMilliseconds() -
|
|
(limit * 1000.0));
|
|
Telemetry::Accumulate(Telemetry::SLOW_SCRIPT_NOTIFY_DELAY, delay);
|
|
self->mTimeoutAccumulated = true;
|
|
}
|
|
|
|
// Show the prompt to the user, and kill if requested.
|
|
nsGlobalWindowInner::SlowScriptResponse response = win->ShowSlowScriptDialog(
|
|
cx, addonId, self->mSlowScriptActualWait.ToMilliseconds());
|
|
if (response == nsGlobalWindowInner::KillSlowScript) {
|
|
if (Preferences::GetBool("dom.global_stop_script", true)) {
|
|
xpc::Scriptability::Get(global).Block();
|
|
}
|
|
if (nsCOMPtr<Document> doc = win->GetExtantDoc()) {
|
|
doc->UnlockAllWakeLocks(WakeLockType::Screen);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The user chose to continue the script. Reset the timer, and disable this
|
|
// machinery with a pref if the user opted out of future slow-script dialogs.
|
|
if (response != nsGlobalWindowInner::ContinueSlowScriptAndKeepNotifying) {
|
|
self->mSlowScriptCheckpoint = TimeStamp::NowLoRes();
|
|
}
|
|
|
|
if (response == nsGlobalWindowInner::AlwaysContinueSlowScript) {
|
|
Preferences::SetInt(prefName, 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#define JS_OPTIONS_DOT_STR "javascript.options."
|
|
|
|
static mozilla::Atomic<bool> sDiscardSystemSource(false);
|
|
|
|
bool xpc::ShouldDiscardSystemSource() { return sDiscardSystemSource; }
|
|
|
|
static mozilla::Atomic<bool> sSharedMemoryEnabled(false);
|
|
static mozilla::Atomic<bool> sStreamsEnabled(false);
|
|
|
|
void xpc::SetPrefableRealmOptions(JS::RealmOptions& options) {
|
|
options.creationOptions()
|
|
.setSharedMemoryAndAtomicsEnabled(sSharedMemoryEnabled)
|
|
.setCoopAndCoepEnabled(
|
|
StaticPrefs::browser_tabs_remote_useCrossOriginOpenerPolicy() &&
|
|
StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy());
|
|
}
|
|
|
|
void xpc::SetPrefableCompileOptions(JS::PrefableCompileOptions& options) {
|
|
options
|
|
.setSourcePragmas(StaticPrefs::javascript_options_source_pragmas())
|
|
#ifdef NIGHTLY_BUILD
|
|
.setImportAttributes(
|
|
StaticPrefs::javascript_options_experimental_import_attributes())
|
|
.setImportAttributesAssertSyntax(
|
|
StaticPrefs::
|
|
javascript_options_experimental_import_attributes_assert_syntax())
|
|
#endif
|
|
.setAsmJS(StaticPrefs::javascript_options_asmjs())
|
|
.setThrowOnAsmJSValidationFailure(
|
|
StaticPrefs::javascript_options_throw_on_asmjs_validation_failure());
|
|
}
|
|
|
|
void xpc::SetPrefableContextOptions(JS::ContextOptions& options) {
|
|
options
|
|
#ifdef FUZZING
|
|
.setFuzzing(Preferences::GetBool(JS_OPTIONS_DOT_STR "fuzzing.enabled"))
|
|
#endif
|
|
.setWasm(Preferences::GetBool(JS_OPTIONS_DOT_STR "wasm"))
|
|
.setWasmForTrustedPrinciples(
|
|
Preferences::GetBool(JS_OPTIONS_DOT_STR "wasm_trustedprincipals"))
|
|
.setWasmIon(Preferences::GetBool(JS_OPTIONS_DOT_STR "wasm_optimizingjit"))
|
|
.setWasmBaseline(
|
|
Preferences::GetBool(JS_OPTIONS_DOT_STR "wasm_baselinejit"))
|
|
.setWasmVerbose(Preferences::GetBool(JS_OPTIONS_DOT_STR "wasm_verbose"))
|
|
.setAsyncStack(Preferences::GetBool(JS_OPTIONS_DOT_STR "asyncstack"))
|
|
.setAsyncStackCaptureDebuggeeOnly(Preferences::GetBool(
|
|
JS_OPTIONS_DOT_STR "asyncstack_capture_debuggee_only"));
|
|
|
|
SetPrefableCompileOptions(options.compileOptions());
|
|
}
|
|
|
|
// Mirrored value of javascript.options.self_hosted.use_shared_memory.
|
|
static bool sSelfHostedUseSharedMemory = false;
|
|
|
|
static void LoadStartupJSPrefs(XPCJSContext* xpccx) {
|
|
// Prefs that require a restart are handled here. This includes the
|
|
// process-wide JIT options because toggling these at runtime can easily cause
|
|
// races or get us into an inconsistent state.
|
|
//
|
|
// 'Live' prefs are handled by ReloadPrefsCallback below.
|
|
|
|
// Note: JS::Prefs are set earlier in startup, in InitializeJS in
|
|
// XPCOMInit.cpp.
|
|
|
|
JSContext* cx = xpccx->Context();
|
|
|
|
// Some prefs are unlisted in all.js / StaticPrefs (and thus are invisible in
|
|
// about:config). Make sure we use explicit defaults here.
|
|
bool useJitForTrustedPrincipals =
|
|
Preferences::GetBool(JS_OPTIONS_DOT_STR "jit_trustedprincipals", false);
|
|
bool disableWasmHugeMemory = Preferences::GetBool(
|
|
JS_OPTIONS_DOT_STR "wasm_disable_huge_memory", false);
|
|
|
|
bool safeMode = false;
|
|
nsCOMPtr<nsIXULRuntime> xr = do_GetService("@mozilla.org/xre/runtime;1");
|
|
if (xr) {
|
|
xr->GetInSafeMode(&safeMode);
|
|
}
|
|
|
|
// NOTE: Baseline Interpreter is still used in safe-mode. This gives a big
|
|
// perf gain and is our simplest JIT so we make a tradeoff.
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE,
|
|
StaticPrefs::javascript_options_blinterp_DoNotUseDirectly());
|
|
|
|
// Disable most JITs in Safe-Mode.
|
|
if (safeMode) {
|
|
JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_BASELINE_ENABLE, false);
|
|
JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_ION_ENABLE, false);
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_JIT_TRUSTEDPRINCIPALS_ENABLE, false);
|
|
JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_NATIVE_REGEXP_ENABLE,
|
|
false);
|
|
JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_JIT_HINTS_ENABLE, false);
|
|
sSelfHostedUseSharedMemory = false;
|
|
} else {
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_BASELINE_ENABLE,
|
|
StaticPrefs::javascript_options_baselinejit_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_ION_ENABLE,
|
|
StaticPrefs::javascript_options_ion_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(cx,
|
|
JSJITCOMPILER_JIT_TRUSTEDPRINCIPALS_ENABLE,
|
|
useJitForTrustedPrincipals);
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_NATIVE_REGEXP_ENABLE,
|
|
StaticPrefs::javascript_options_native_regexp_DoNotUseDirectly());
|
|
// Only enable the jit hints cache for the content process to avoid
|
|
// any possible jank or delays on the parent process.
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_JIT_HINTS_ENABLE,
|
|
XRE_IsContentProcess()
|
|
? StaticPrefs::javascript_options_jithints_DoNotUseDirectly()
|
|
: false);
|
|
sSelfHostedUseSharedMemory = StaticPrefs::
|
|
javascript_options_self_hosted_use_shared_memory_DoNotUseDirectly();
|
|
}
|
|
|
|
JS_SetOffthreadIonCompilationEnabled(
|
|
cx, StaticPrefs::
|
|
javascript_options_ion_offthread_compilation_DoNotUseDirectly());
|
|
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_BASELINE_INTERPRETER_WARMUP_TRIGGER,
|
|
StaticPrefs::javascript_options_blinterp_threshold_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_BASELINE_WARMUP_TRIGGER,
|
|
StaticPrefs::javascript_options_baselinejit_threshold_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_ION_NORMAL_WARMUP_TRIGGER,
|
|
StaticPrefs::javascript_options_ion_threshold_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_ION_FREQUENT_BAILOUT_THRESHOLD,
|
|
StaticPrefs::
|
|
javascript_options_ion_frequent_bailout_threshold_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_INLINING_BYTECODE_MAX_LENGTH,
|
|
StaticPrefs::
|
|
javascript_options_inlining_bytecode_max_length_DoNotUseDirectly());
|
|
|
|
#ifdef DEBUG
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_FULL_DEBUG_CHECKS,
|
|
StaticPrefs::javascript_options_jit_full_debug_checks_DoNotUseDirectly());
|
|
#endif
|
|
|
|
#if !defined(JS_CODEGEN_MIPS32) && !defined(JS_CODEGEN_MIPS64) && \
|
|
!defined(JS_CODEGEN_RISCV64) && !defined(JS_CODEGEN_LOONG64)
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_SPECTRE_INDEX_MASKING,
|
|
StaticPrefs::javascript_options_spectre_index_masking_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_SPECTRE_OBJECT_MITIGATIONS,
|
|
StaticPrefs::
|
|
javascript_options_spectre_object_mitigations_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_SPECTRE_STRING_MITIGATIONS,
|
|
StaticPrefs::
|
|
javascript_options_spectre_string_mitigations_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_SPECTRE_VALUE_MASKING,
|
|
StaticPrefs::javascript_options_spectre_value_masking_DoNotUseDirectly());
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_SPECTRE_JIT_TO_CXX_CALLS,
|
|
StaticPrefs::
|
|
javascript_options_spectre_jit_to_cxx_calls_DoNotUseDirectly());
|
|
#endif
|
|
|
|
bool writeProtectCode = true;
|
|
if (XRE_IsContentProcess()) {
|
|
writeProtectCode =
|
|
StaticPrefs::javascript_options_content_process_write_protect_code();
|
|
}
|
|
JS_SetGlobalJitCompilerOption(cx, JSJITCOMPILER_WRITE_PROTECT_CODE,
|
|
writeProtectCode);
|
|
|
|
if (disableWasmHugeMemory) {
|
|
bool disabledHugeMemory = JS::DisableWasmHugeMemory();
|
|
MOZ_RELEASE_ASSERT(disabledHugeMemory);
|
|
}
|
|
}
|
|
|
|
static void ReloadPrefsCallback(const char* pref, void* aXpccx) {
|
|
// Note: Prefs that require a restart are handled in LoadStartupJSPrefs above.
|
|
|
|
// Update all non-startup JS::Prefs.
|
|
SET_NON_STARTUP_JS_PREFS_FROM_BROWSER_PREFS;
|
|
|
|
auto xpccx = static_cast<XPCJSContext*>(aXpccx);
|
|
JSContext* cx = xpccx->Context();
|
|
|
|
sDiscardSystemSource =
|
|
Preferences::GetBool(JS_OPTIONS_DOT_STR "discardSystemSource");
|
|
sSharedMemoryEnabled =
|
|
Preferences::GetBool(JS_OPTIONS_DOT_STR "shared_memory");
|
|
sStreamsEnabled = Preferences::GetBool(JS_OPTIONS_DOT_STR "streams");
|
|
|
|
#ifdef JS_GC_ZEAL
|
|
int32_t zeal = Preferences::GetInt(JS_OPTIONS_DOT_STR "mem.gc_zeal.mode", -1);
|
|
int32_t zeal_frequency =
|
|
Preferences::GetInt(JS_OPTIONS_DOT_STR "mem.gc_zeal.frequency",
|
|
JS::BrowserDefaultGCZealFrequency);
|
|
if (zeal >= 0) {
|
|
JS::SetGCZeal(cx, (uint8_t)zeal, zeal_frequency);
|
|
}
|
|
#endif // JS_GC_ZEAL
|
|
|
|
auto& contextOptions = JS::ContextOptionsRef(cx);
|
|
SetPrefableContextOptions(contextOptions);
|
|
|
|
#ifdef NIGHTLY_BUILD
|
|
JS_SetGlobalJitCompilerOption(
|
|
cx, JSJITCOMPILER_REGEXP_DUPLICATE_NAMED_GROUPS,
|
|
StaticPrefs::
|
|
javascript_options_experimental_regexp_duplicate_named_groups());
|
|
#endif
|
|
|
|
// Set options not shared with workers.
|
|
contextOptions
|
|
.setThrowOnDebuggeeWouldRun(Preferences::GetBool(
|
|
JS_OPTIONS_DOT_STR "throw_on_debuggee_would_run"))
|
|
.setDumpStackOnDebuggeeWouldRun(Preferences::GetBool(
|
|
JS_OPTIONS_DOT_STR "dump_stack_on_debuggee_would_run"));
|
|
|
|
nsCOMPtr<nsIXULRuntime> xr = do_GetService("@mozilla.org/xre/runtime;1");
|
|
if (xr) {
|
|
bool safeMode = false;
|
|
xr->GetInSafeMode(&safeMode);
|
|
if (safeMode) {
|
|
contextOptions.disableOptionsForSafeMode();
|
|
}
|
|
}
|
|
|
|
JS_SetParallelParsingEnabled(
|
|
cx, StaticPrefs::javascript_options_parallel_parsing());
|
|
}
|
|
|
|
XPCJSContext::~XPCJSContext() {
|
|
MOZ_COUNT_DTOR_INHERITED(XPCJSContext, CycleCollectedJSContext);
|
|
// Elsewhere we abort immediately if XPCJSContext initialization fails.
|
|
// Therefore the context must be non-null.
|
|
MOZ_ASSERT(MaybeContext());
|
|
|
|
Preferences::UnregisterPrefixCallback(ReloadPrefsCallback, JS_OPTIONS_DOT_STR,
|
|
this);
|
|
|
|
#ifdef FUZZING
|
|
Preferences::UnregisterCallback(ReloadPrefsCallback, "fuzzing.enabled", this);
|
|
#endif
|
|
|
|
// Clear any pending exception. It might be an XPCWrappedJS, and if we try
|
|
// to destroy it later we will crash.
|
|
SetPendingException(nullptr);
|
|
|
|
// If we're the last XPCJSContext around, clean up the watchdog manager.
|
|
if (--sInstanceCount == 0) {
|
|
if (mWatchdogManager->GetWatchdog()) {
|
|
mWatchdogManager->StopWatchdog();
|
|
}
|
|
|
|
mWatchdogManager->UnregisterContext(this);
|
|
mWatchdogManager->Shutdown();
|
|
sWatchdogInstance = nullptr;
|
|
} else {
|
|
// Otherwise, simply remove ourselves from the list.
|
|
mWatchdogManager->UnregisterContext(this);
|
|
}
|
|
|
|
if (mCallContext) {
|
|
mCallContext->SystemIsBeingShutDown();
|
|
}
|
|
|
|
PROFILER_CLEAR_JS_CONTEXT();
|
|
}
|
|
|
|
XPCJSContext::XPCJSContext()
|
|
: mCallContext(nullptr),
|
|
mAutoRoots(nullptr),
|
|
mResolveName(JS::PropertyKey::Void()),
|
|
mResolvingWrapper(nullptr),
|
|
mWatchdogManager(GetWatchdogManager()),
|
|
mSlowScriptSecondHalf(false),
|
|
mTimeoutAccumulated(false),
|
|
mExecutedChromeScript(false),
|
|
mHasScriptActivity(false),
|
|
mPendingResult(NS_OK),
|
|
mActive(CONTEXT_INACTIVE),
|
|
mLastStateChange(PR_Now()) {
|
|
MOZ_COUNT_CTOR_INHERITED(XPCJSContext, CycleCollectedJSContext);
|
|
MOZ_ASSERT(mWatchdogManager);
|
|
++sInstanceCount;
|
|
mWatchdogManager->RegisterContext(this);
|
|
}
|
|
|
|
/* static */
|
|
XPCJSContext* XPCJSContext::Get() {
|
|
// Do an explicit null check, because this can get called from a process that
|
|
// does not run JS.
|
|
nsXPConnect* xpc = static_cast<nsXPConnect*>(nsXPConnect::XPConnect());
|
|
return xpc ? xpc->GetContext() : nullptr;
|
|
}
|
|
|
|
#ifdef XP_WIN
|
|
static size_t GetWindowsStackSize() {
|
|
// First, get the stack base. Because the stack grows down, this is the top
|
|
// of the stack.
|
|
const uint8_t* stackTop;
|
|
# ifdef _WIN64
|
|
PNT_TIB64 pTib = reinterpret_cast<PNT_TIB64>(NtCurrentTeb());
|
|
stackTop = reinterpret_cast<const uint8_t*>(pTib->StackBase);
|
|
# else
|
|
PNT_TIB pTib = reinterpret_cast<PNT_TIB>(NtCurrentTeb());
|
|
stackTop = reinterpret_cast<const uint8_t*>(pTib->StackBase);
|
|
# endif
|
|
|
|
// Now determine the stack bottom. Note that we can't use tib->StackLimit,
|
|
// because that's the size of the committed area and we're also interested
|
|
// in the reserved pages below that.
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
if (!VirtualQuery(&mbi, &mbi, sizeof(mbi))) {
|
|
MOZ_CRASH("VirtualQuery failed");
|
|
}
|
|
|
|
const uint8_t* stackBottom =
|
|
reinterpret_cast<const uint8_t*>(mbi.AllocationBase);
|
|
|
|
// Do some sanity checks.
|
|
size_t stackSize = size_t(stackTop - stackBottom);
|
|
MOZ_RELEASE_ASSERT(stackSize >= 1 * 1024 * 1024);
|
|
MOZ_RELEASE_ASSERT(stackSize <= 32 * 1024 * 1024);
|
|
|
|
// Subtract 40 KB (Win32) or 80 KB (Win64) to account for things like
|
|
// the guard page and large PGO stack frames.
|
|
return stackSize - 10 * sizeof(uintptr_t) * 1024;
|
|
}
|
|
#endif
|
|
|
|
XPCJSRuntime* XPCJSContext::Runtime() const {
|
|
return static_cast<XPCJSRuntime*>(CycleCollectedJSContext::Runtime());
|
|
}
|
|
|
|
CycleCollectedJSRuntime* XPCJSContext::CreateRuntime(JSContext* aCx) {
|
|
return new XPCJSRuntime(aCx);
|
|
}
|
|
|
|
class HelperThreadTaskHandler : public Task {
|
|
JS::HelperThreadTask* mTask;
|
|
|
|
public:
|
|
explicit HelperThreadTaskHandler(JS::HelperThreadTask* aTask)
|
|
: Task(Kind::OffMainThreadOnly, EventQueuePriority::Normal),
|
|
mTask(aTask) {
|
|
// Bug 1703185: Currently all tasks are run at the same priority.
|
|
}
|
|
|
|
TaskResult Run() override {
|
|
JS::RunHelperThreadTask(mTask);
|
|
return TaskResult::Complete;
|
|
}
|
|
|
|
#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
|
|
bool GetName(nsACString& aName) override {
|
|
aName.AssignLiteral("HelperThreadTask");
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
private:
|
|
~HelperThreadTaskHandler() = default;
|
|
};
|
|
|
|
static void DispatchOffThreadTask(JS::HelperThreadTask* aTask) {
|
|
TaskController::Get()->AddTask(MakeAndAddRef<HelperThreadTaskHandler>(aTask));
|
|
}
|
|
|
|
static bool CreateSelfHostedSharedMemory(JSContext* aCx,
|
|
JS::SelfHostedCache aBuf) {
|
|
auto& shm = xpc::SelfHostedShmem::GetSingleton();
|
|
MOZ_RELEASE_ASSERT(shm.Content().IsEmpty());
|
|
// Failures within InitFromParent output warnings but do not cause
|
|
// unrecoverable failures.
|
|
shm.InitFromParent(aBuf);
|
|
return true;
|
|
}
|
|
|
|
nsresult XPCJSContext::Initialize() {
|
|
if (StaticPrefs::javascript_options_external_thread_pool_DoNotUseDirectly()) {
|
|
size_t threadCount = TaskController::GetPoolThreadCount();
|
|
size_t stackSize = TaskController::GetThreadStackSize();
|
|
SetHelperThreadTaskCallback(&DispatchOffThreadTask, threadCount, stackSize);
|
|
}
|
|
|
|
nsresult rv =
|
|
CycleCollectedJSContext::Initialize(nullptr, JS::DefaultHeapMaxBytes);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
MOZ_ASSERT(Context());
|
|
JSContext* cx = Context();
|
|
|
|
// The JS engine permits us to set different stack limits for system code,
|
|
// trusted script, and untrusted script. We have tests that ensure that
|
|
// we can always execute 10 "heavy" (eval+with) stack frames deeper in
|
|
// privileged code. Our stack sizes vary greatly in different configurations,
|
|
// so satisfying those tests requires some care. Manual measurements of the
|
|
// number of heavy stack frames achievable gives us the following rough data,
|
|
// ordered by the effective categories in which they are grouped in the
|
|
// JS_SetNativeStackQuota call (which predates this analysis).
|
|
//
|
|
// The following "Stack Frames" numbers come from `chromeLimit` in
|
|
// js/xpconnect/tests/chrome/test_bug732665.xul
|
|
//
|
|
// Platform | Build | Stack Quota | Stack Frames | Stack Frame Size
|
|
// ------------+-------+-------------+--------------+------------------
|
|
// OSX 64 | Opt | 7MB | 1331 | ~5.4k
|
|
// OSX 64 | Debug | 7MB | 1202 | ~6.0k
|
|
// ------------+-------+-------------+--------------+------------------
|
|
// Linux 32 | Opt | 7.875MB | 2513 | ~3.2k
|
|
// Linux 32 | Debug | 7.875MB | 2146 | ~3.8k
|
|
// ------------+-------+-------------+--------------+------------------
|
|
// Linux 64 | Opt | 7.875MB | 1360 | ~5.9k
|
|
// Linux 64 | Debug | 7.875MB | 1180 | ~6.8k
|
|
// Linux 64 | ASan | 7.875MB | 473 | ~17.0k
|
|
// ------------+-------+-------------+--------------+------------------
|
|
// Windows 32 | Opt | 984k | 188 | ~5.2k
|
|
// Windows 32 | Debug | 984k | 208 | ~4.7k
|
|
// ------------+-------+-------------+--------------+------------------
|
|
// Windows 64 | Opt | 1.922MB | 189 | ~10.4k
|
|
// Windows 64 | Debug | 1.922MB | 175 | ~11.2k
|
|
//
|
|
// We tune the trusted/untrusted quotas for each configuration to achieve our
|
|
// invariants while attempting to minimize overhead. In contrast, our buffer
|
|
// between system code and trusted script is a very unscientific 10k.
|
|
const size_t kSystemCodeBuffer = 10 * 1024;
|
|
|
|
// Our "default" stack is what we use in configurations where we don't have
|
|
// a compelling reason to do things differently. This is effectively 512KB
|
|
// on 32-bit platforms and 1MB on 64-bit platforms.
|
|
const size_t kDefaultStackQuota = 128 * sizeof(size_t) * 1024;
|
|
|
|
// Set maximum stack size for different configurations. This value is then
|
|
// capped below because huge stacks are not web-compatible.
|
|
|
|
#if defined(XP_MACOSX) || defined(DARWIN)
|
|
// MacOS has a gargantuan default stack size of 8MB. Go wild with 7MB,
|
|
// and give trusted script 180k extra. The stack is huge on mac anyway.
|
|
const size_t kUncappedStackQuota = 7 * 1024 * 1024;
|
|
const size_t kTrustedScriptBuffer = 180 * 1024;
|
|
#elif defined(XP_LINUX) && !defined(ANDROID)
|
|
// Most Linux distributions set default stack size to 8MB. Use it as the
|
|
// maximum value.
|
|
const size_t kStackQuotaMax = 8 * 1024 * 1024;
|
|
# if defined(MOZ_ASAN) || defined(DEBUG)
|
|
// Bug 803182: account for the 4x difference in the size of js::Interpret
|
|
// between optimized and debug builds. We use 2x since the JIT part
|
|
// doesn't increase much.
|
|
// See the standalone MOZ_ASAN branch below for the ASan case.
|
|
const size_t kStackQuotaMin = 2 * kDefaultStackQuota;
|
|
# else
|
|
const size_t kStackQuotaMin = kDefaultStackQuota;
|
|
# endif
|
|
// Allocate 128kB margin for the safe space.
|
|
const size_t kStackSafeMargin = 128 * 1024;
|
|
|
|
struct rlimit rlim;
|
|
const size_t kUncappedStackQuota =
|
|
getrlimit(RLIMIT_STACK, &rlim) == 0
|
|
? std::max(std::min(size_t(rlim.rlim_cur - kStackSafeMargin),
|
|
kStackQuotaMax - kStackSafeMargin),
|
|
kStackQuotaMin)
|
|
: kStackQuotaMin;
|
|
# if defined(MOZ_ASAN)
|
|
// See the standalone MOZ_ASAN branch below for the ASan case.
|
|
const size_t kTrustedScriptBuffer = 450 * 1024;
|
|
# else
|
|
const size_t kTrustedScriptBuffer = 180 * 1024;
|
|
# endif
|
|
#elif defined(XP_WIN)
|
|
// 1MB is the default stack size on Windows. We use the -STACK linker flag
|
|
// (see WIN32_EXE_LDFLAGS in config/config.mk) to request a larger stack, so
|
|
// we determine the stack size at runtime.
|
|
const size_t kUncappedStackQuota = GetWindowsStackSize();
|
|
# if defined(MOZ_ASAN)
|
|
// See the standalone MOZ_ASAN branch below for the ASan case.
|
|
const size_t kTrustedScriptBuffer = 450 * 1024;
|
|
# else
|
|
const size_t kTrustedScriptBuffer = (sizeof(size_t) == 8)
|
|
? 180 * 1024 // win64
|
|
: 120 * 1024; // win32
|
|
# endif
|
|
#elif defined(MOZ_ASAN)
|
|
// ASan requires more stack space due to red-zones, so give it double the
|
|
// default (1MB on 32-bit, 2MB on 64-bit). ASAN stack frame measurements
|
|
// were not taken at the time of this writing, so we hazard a guess that
|
|
// ASAN builds have roughly thrice the stack overhead as normal builds.
|
|
// On normal builds, the largest stack frame size we might encounter is
|
|
// 9.0k (see above), so let's use a buffer of 9.0 * 5 * 10 = 450k.
|
|
//
|
|
// FIXME: Does this branch make sense for Windows and Android?
|
|
// (See bug 1415195)
|
|
const size_t kUncappedStackQuota = 2 * kDefaultStackQuota;
|
|
const size_t kTrustedScriptBuffer = 450 * 1024;
|
|
#elif defined(ANDROID)
|
|
// Android appears to have 1MB stacks. Allow the use of 3/4 of that size
|
|
// (768KB on 32-bit), since otherwise we can crash with a stack overflow
|
|
// when nearing the 1MB limit.
|
|
const size_t kUncappedStackQuota =
|
|
kDefaultStackQuota + kDefaultStackQuota / 2;
|
|
const size_t kTrustedScriptBuffer = sizeof(size_t) * 12800;
|
|
#else
|
|
// Catch-all configuration for other environments.
|
|
# if defined(DEBUG)
|
|
const size_t kUncappedStackQuota = 2 * kDefaultStackQuota;
|
|
# else
|
|
const size_t kUncappedStackQuota = kDefaultStackQuota;
|
|
# endif
|
|
// Given the numbers above, we use 50k and 100k trusted buffers on 32-bit
|
|
// and 64-bit respectively.
|
|
const size_t kTrustedScriptBuffer = sizeof(size_t) * 12800;
|
|
#endif
|
|
|
|
// Avoid an unused variable warning on platforms where we don't use the
|
|
// default.
|
|
(void)kDefaultStackQuota;
|
|
|
|
// Large stacks are not web-compatible so cap to a smaller value.
|
|
// See bug 1537609 and bug 1562700.
|
|
const size_t kStackQuotaCap =
|
|
StaticPrefs::javascript_options_main_thread_stack_quota_cap();
|
|
const size_t kStackQuota = std::min(kUncappedStackQuota, kStackQuotaCap);
|
|
|
|
JS_SetNativeStackQuota(
|
|
cx, kStackQuota, kStackQuota - kSystemCodeBuffer,
|
|
kStackQuota - kSystemCodeBuffer - kTrustedScriptBuffer);
|
|
|
|
PROFILER_SET_JS_CONTEXT(cx);
|
|
|
|
JS_AddInterruptCallback(cx, InterruptCallback);
|
|
|
|
Runtime()->Initialize(cx);
|
|
|
|
LoadStartupJSPrefs(this);
|
|
|
|
// Watch for the JS boolean options.
|
|
ReloadPrefsCallback(nullptr, this);
|
|
Preferences::RegisterPrefixCallback(ReloadPrefsCallback, JS_OPTIONS_DOT_STR,
|
|
this);
|
|
|
|
#ifdef FUZZING
|
|
Preferences::RegisterCallback(ReloadPrefsCallback, "fuzzing.enabled", this);
|
|
#endif
|
|
|
|
// Initialize the MIME type used for the bytecode cache, after calling
|
|
// SetProcessBuildIdOp and loading JS prefs.
|
|
if (!nsContentUtils::InitJSBytecodeMimeType()) {
|
|
NS_ABORT_OOM(0); // Size is unknown.
|
|
}
|
|
|
|
// When available, set the self-hosted shared memory to be read, so that we
|
|
// can decode the self-hosted content instead of parsing it.
|
|
auto& shm = xpc::SelfHostedShmem::GetSingleton();
|
|
JS::SelfHostedCache selfHostedContent = shm.Content();
|
|
JS::SelfHostedWriter writer = nullptr;
|
|
if (XRE_IsParentProcess() && sSelfHostedUseSharedMemory) {
|
|
// Only the Parent process has permissions to write to the self-hosted
|
|
// shared memory.
|
|
writer = CreateSelfHostedSharedMemory;
|
|
}
|
|
|
|
if (!JS::InitSelfHostedCode(cx, selfHostedContent, writer)) {
|
|
// Note: If no exception is pending, failure is due to OOM.
|
|
if (!JS_IsExceptionPending(cx) || JS_IsThrowingOutOfMemory(cx)) {
|
|
NS_ABORT_OOM(0); // Size is unknown.
|
|
}
|
|
|
|
// Failed to execute self-hosted JavaScript! Uh oh.
|
|
MOZ_CRASH("InitSelfHostedCode failed");
|
|
}
|
|
|
|
MOZ_RELEASE_ASSERT(Runtime()->InitializeStrings(cx),
|
|
"InitializeStrings failed");
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// static
|
|
uint32_t XPCJSContext::sInstanceCount;
|
|
|
|
// static
|
|
StaticAutoPtr<WatchdogManager> XPCJSContext::sWatchdogInstance;
|
|
|
|
// static
|
|
WatchdogManager* XPCJSContext::GetWatchdogManager() {
|
|
if (sWatchdogInstance) {
|
|
return sWatchdogInstance;
|
|
}
|
|
|
|
MOZ_ASSERT(sInstanceCount == 0);
|
|
sWatchdogInstance = new WatchdogManager();
|
|
return sWatchdogInstance;
|
|
}
|
|
|
|
// static
|
|
XPCJSContext* XPCJSContext::NewXPCJSContext() {
|
|
XPCJSContext* self = new XPCJSContext();
|
|
nsresult rv = self->Initialize();
|
|
if (rv == NS_ERROR_OUT_OF_MEMORY) {
|
|
mozalloc_handle_oom(0);
|
|
} else if (NS_FAILED(rv)) {
|
|
MOZ_CRASH("new XPCJSContext failed to initialize.");
|
|
}
|
|
|
|
if (self->Context()) {
|
|
return self;
|
|
}
|
|
|
|
MOZ_CRASH("new XPCJSContext failed to initialize.");
|
|
}
|
|
|
|
void XPCJSContext::BeforeProcessTask(bool aMightBlock) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
// Start the slow script timer.
|
|
mSlowScriptCheckpoint = mozilla::TimeStamp::NowLoRes();
|
|
mSlowScriptSecondHalf = false;
|
|
mSlowScriptActualWait = mozilla::TimeDuration();
|
|
mTimeoutAccumulated = false;
|
|
mExecutedChromeScript = false;
|
|
CycleCollectedJSContext::BeforeProcessTask(aMightBlock);
|
|
}
|
|
|
|
void XPCJSContext::AfterProcessTask(uint32_t aNewRecursionDepth) {
|
|
// Record hangs in the parent process for telemetry.
|
|
if (mSlowScriptSecondHalf && XRE_IsE10sParentProcess()) {
|
|
double hangDuration = (mozilla::TimeStamp::NowLoRes() -
|
|
mSlowScriptCheckpoint + mSlowScriptActualWait)
|
|
.ToSeconds();
|
|
// We use the pref to test this code.
|
|
double limit = sChromeSlowScriptTelemetryCutoff;
|
|
if (xpc::IsInAutomation()) {
|
|
double prefLimit = StaticPrefs::dom_max_chrome_script_run_time();
|
|
if (prefLimit > 0) {
|
|
limit = std::min(prefLimit, sChromeSlowScriptTelemetryCutoff);
|
|
}
|
|
}
|
|
if (hangDuration > limit) {
|
|
if (!sTelemetryEventEnabled) {
|
|
sTelemetryEventEnabled = true;
|
|
Telemetry::SetEventRecordingEnabled("slow_script_warning"_ns, true);
|
|
}
|
|
|
|
auto uriType = mExecutedChromeScript ? "browser"_ns : "content"_ns;
|
|
// Use AppendFloat to avoid printf-type APIs using locale-specific
|
|
// decimal separators, when we definitely want a `.`.
|
|
nsCString durationStr;
|
|
durationStr.AppendFloat(hangDuration);
|
|
auto extra = Some<nsTArray<Telemetry::EventExtraEntry>>(
|
|
{Telemetry::EventExtraEntry{"hang_duration"_ns, durationStr},
|
|
Telemetry::EventExtraEntry{"uri_type"_ns, uriType}});
|
|
Telemetry::RecordEvent(
|
|
Telemetry::EventID::Slow_script_warning_Shown_Browser, Nothing(),
|
|
extra);
|
|
}
|
|
}
|
|
|
|
// Now that we're back to the event loop, reset the slow script checkpoint.
|
|
mSlowScriptCheckpoint = mozilla::TimeStamp();
|
|
mSlowScriptSecondHalf = false;
|
|
|
|
// Call cycle collector occasionally.
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
nsJSContext::MaybePokeCC();
|
|
CycleCollectedJSContext::AfterProcessTask(aNewRecursionDepth);
|
|
|
|
// Poke the memory telemetry reporter
|
|
if (AppShutdown::GetCurrentShutdownPhase() == ShutdownPhase::NotInShutdown) {
|
|
MemoryTelemetry::Get().Poke();
|
|
}
|
|
|
|
// This exception might have been set if we called an XPCWrappedJS that threw,
|
|
// but now we're returning to the event loop, so nothing is going to look at
|
|
// this value again. Clear it to prevent leaks.
|
|
SetPendingException(nullptr);
|
|
}
|
|
|
|
void XPCJSContext::MaybePokeGC() { nsJSContext::MaybePokeGC(); }
|
|
|
|
bool XPCJSContext::IsSystemCaller() const {
|
|
return nsContentUtils::IsSystemCaller(Context());
|
|
}
|