Bug 1777198 - Cancel content JS execution on quit-application-granted or on normal content process shutdown. r=smaug

We want to signal content processes to cancel content JS unconditionally on shutdown.
In the case of parent shutdown this has to happen as early as "quit-application-granted", given that both extensions and session storage shutdown rely on the possibility to interact with content processes (which is not possible when they are inside long running JS).
In addition in the case of a normal child shutdown we cancel content JS execution, too.
For now we put this behind the pref "dom.abort_script_on_child_shutdown" which remains default off.

Depends on D150539

Differential Revision: https://phabricator.services.mozilla.com/D150598
This commit is contained in:
Jens Stutte 2022-08-02 14:02:40 +00:00
Родитель 4c88b66171
Коммит 3ca1fb43c7
7 изменённых файлов: 128 добавлений и 8 удалений

Просмотреть файл

@ -1652,6 +1652,7 @@ const nsACString& ContentParent::GetRemoteType() const { return mRemoteType; }
static StaticRefPtr<nsIAsyncShutdownClient> sXPCOMShutdownClient;
static StaticRefPtr<nsIAsyncShutdownClient> sProfileBeforeChangeClient;
static StaticRefPtr<nsIAsyncShutdownClient> sQuitApplicationGrantedClient;
void ContentParent::Init() {
MOZ_ASSERT(sXPCOMShutdownClient);
@ -1722,6 +1723,7 @@ void ContentParent::MaybeBeginShutDown(uint32_t aExpectedBrowserCount,
// We're dying now, prevent anything from re-using this process.
MarkAsDead();
SignalImpendingShutdownToContentJS();
StartForceKillTimer();
if (aSendShutDown) {
@ -1799,7 +1801,12 @@ void ContentParent::ShutDownProcess(ShutDownMethod aMethod) {
__LINE__);
mShutdownPending = true;
// Start the force-kill timer if we haven't already.
StartForceKillTimer();
// This can happen if we shutdown a process while launching or
// because it is removed from the cached processes pool.
if (!mForceKillTimer) {
SignalImpendingShutdownToContentJS();
StartForceKillTimer();
}
} else {
MaybeLogBlockShutdownDiagnostics(
this, "ShutDownProcess: !!! Send shutdown message failed! !!!",
@ -2367,8 +2374,6 @@ void ContentParent::StartForceKillTimer() {
return;
}
NotifyImpendingShutdown();
int32_t timeoutSecs = StaticPrefs::dom_ipc_tabs_shutdownTimeoutSecs();
if (timeoutSecs > 0) {
NS_NewTimerWithFuncCallback(getter_AddRefs(mForceKillTimer),
@ -3578,14 +3583,71 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ContentParent)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMProcessParent)
NS_INTERFACE_MAP_END
class RequestContentJSInterruptRunnable final : public Runnable {
public:
explicit RequestContentJSInterruptRunnable(PProcessHangMonitorParent* aActor)
: Runnable("dom::RequestContentJSInterruptRunnable"),
mHangMonitorActor(aActor) {}
NS_IMETHOD Run() override {
MOZ_ASSERT(mHangMonitorActor);
Unused << mHangMonitorActor->SendRequestContentJSInterrupt();
return NS_OK;
}
private:
// The end-of-life of ContentParent::mHangMonitorActor is bound to
// ContentParent::ActorDestroy and then HangMonitorParent::Shutdown
// dispatches a shutdown runnable to this queue and waits for it to be
// executed. So the runnable needs not to care about keeping it alive,
// as it is surely dispatched earlier than the
// HangMonitorParent::ShutdownOnThread.
PProcessHangMonitorParent* mHangMonitorActor;
};
void ContentParent::SignalImpendingShutdownToContentJS() {
if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown)) {
MaybeLogBlockShutdownDiagnostics(
this, "BlockShutdown: NotifyImpendingShutdown.", __FILE__, __LINE__);
NotifyImpendingShutdown();
if (mHangMonitorActor &&
StaticPrefs::dom_abort_script_on_child_shutdown()) {
MaybeLogBlockShutdownDiagnostics(
this, "BlockShutdown: RequestContentJSInterrupt.", __FILE__,
__LINE__);
RefPtr<RequestContentJSInterruptRunnable> r =
new RequestContentJSInterruptRunnable(mHangMonitorActor);
ProcessHangMonitor::Get()->Dispatch(r.forget());
}
}
}
// Async shutdown blocker
NS_IMETHODIMP
ContentParent::BlockShutdown(nsIAsyncShutdownClient* aClient) {
if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown)) {
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
// We register two shutdown blockers and both would call us, but
// if things go well we will unregister both as (delayed) reaction
// to the first call we get and thus never receive a second call.
// Thus we believe that we will get called only once.
mBlockShutdownCalled = true;
#endif
// Our real shutdown has not yet started. Just notify the
// impending shutdown and eventually cancel content JS.
SignalImpendingShutdownToContentJS();
if (sQuitApplicationGrantedClient) {
Unused << sQuitApplicationGrantedClient->RemoveBlocker(this);
}
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
mBlockShutdownCalled = false;
#endif
return NS_OK;
}
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
// We register two final shutdown blockers and both would call us, but if
// things go well we will unregister both as (delayed) reaction to the first
// call we get and thus never receive a second call. Thus we believe that we
// will get called only once except for quit-application-granted, which is
// handled above.
MOZ_ASSERT(!mBlockShutdownCalled);
mBlockShutdownCalled = true;
#endif
@ -3671,6 +3733,15 @@ static void InitShutdownClients() {
ClearOnShutdown(&sProfileBeforeChangeClient);
}
}
// TODO: ShutdownPhase::AppShutdownConfirmed is not mapping to
// QuitApplicationGranted, see bug 1762840 comment 4.
if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
rv = svc->GetQuitApplicationGranted(getter_AddRefs(client));
if (NS_SUCCEEDED(rv)) {
sQuitApplicationGrantedClient = client.forget();
ClearOnShutdown(&sQuitApplicationGrantedClient);
}
}
}
}
@ -3687,6 +3758,10 @@ void ContentParent::AddShutdownBlockers() {
sProfileBeforeChangeClient->AddBlocker(
this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
}
if (sQuitApplicationGrantedClient) {
sQuitApplicationGrantedClient->AddBlocker(
this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
}
}
void ContentParent::RemoveShutdownBlockers() {
@ -3705,6 +3780,9 @@ void ContentParent::RemoveShutdownBlockers() {
if (sProfileBeforeChangeClient) {
Unused << sProfileBeforeChangeClient->RemoveBlocker(this);
}
if (sQuitApplicationGrantedClient) {
Unused << sQuitApplicationGrantedClient->RemoveBlocker(this);
}
}
NS_IMETHODIMP

Просмотреть файл

@ -817,6 +817,13 @@ class ContentParent final : public PContentParent,
*/
void MarkAsDead();
/**
* Let the process know we are about to send a shutdown through a
* non-mainthread side channel in order to bypass mainthread congestion.
* This potentially cancels mainthread content JS execution.
*/
void SignalImpendingShutdownToContentJS();
/**
* Check if this process is ready to be shut down, and if it is, begin the
* shutdown process. Should be called whenever a change occurs which could

Просмотреть файл

@ -32,6 +32,7 @@ parent:
child:
async TerminateScript();
async RequestContentJSInterrupt();
async BeginStartingDebugger();
async EndStartingDebugger();

Просмотреть файл

@ -22,6 +22,7 @@
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/ipc/Endpoint.h"
#include "mozilla/ipc/ProcessChild.h"
#include "mozilla/ipc/TaskFactory.h"
#include "mozilla/Monitor.h"
#include "mozilla/Preferences.h"
@ -110,6 +111,7 @@ class HangMonitorChild : public PProcessHangMonitorChild,
void MaybeStartPaintWhileInterruptingJS();
mozilla::ipc::IPCResult RecvTerminateScript() override;
mozilla::ipc::IPCResult RecvRequestContentJSInterrupt() override;
mozilla::ipc::IPCResult RecvBeginStartingDebugger() override;
mozilla::ipc::IPCResult RecvEndStartingDebugger() override;
@ -344,6 +346,18 @@ HangMonitorChild::~HangMonitorChild() {
bool HangMonitorChild::InterruptCallback() {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
if (StaticPrefs::dom_abort_script_on_child_shutdown() &&
mozilla::ipc::ProcessChild::ExpectingShutdown()) {
// We preserve chrome JS from cancel, but not extension content JS.
if (!nsContentUtils::IsCallerChrome()) {
NS_WARNING(
"HangMonitorChild::InterruptCallback: ExpectingShutdown, "
"canceling content JS execution.\n");
return false;
}
return true;
}
// Don't start painting if we're not in a good place to run script. We run
// chrome script during layout and such, and it wouldn't be good to interrupt
// painting code from there.
@ -495,6 +509,18 @@ mozilla::ipc::IPCResult HangMonitorChild::RecvTerminateScript() {
return IPC_OK();
}
mozilla::ipc::IPCResult HangMonitorChild::RecvRequestContentJSInterrupt() {
MOZ_RELEASE_ASSERT(IsOnThread());
CrashReporter::AnnotateCrashReport(
CrashReporter::Annotation::IPCShutdownState,
"HangMonitorChild::RecvRequestContentJSInterrupt"_ns);
// In order to cancel JS execution on shutdown, we expect that
// ProcessChild::NotifiedImpendingShutdown has been called before.
JS_RequestInterruptCallback(mContext);
return IPC_OK();
}
mozilla::ipc::IPCResult HangMonitorChild::RecvBeginStartingDebugger() {
MOZ_RELEASE_ASSERT(IsOnThread());

Просмотреть файл

@ -70,6 +70,8 @@ add_task(async () => {
return;
}
await pushPref("dom.abort_script_on_child_shutdown", true);
// Ensure the process cache cannot interfere.
pushPref("dom.ipc.processPreload.enabled", false);
// Ensure we have no cached processes from previous tests.

Просмотреть файл

@ -74,7 +74,7 @@ void ProcessChild::NotifiedImpendingShutdown() {
sExpectingShutdown = true;
CrashReporter::AnnotateCrashReport(
CrashReporter::Annotation::IPCShutdownState,
"NotifyImpendingShutdown received."_ns);
"NotifiedImpendingShutdown"_ns);
}
/* static */

Просмотреть файл

@ -4514,6 +4514,12 @@
#endif
mirror: always
# Controls if a content script will be aborted on child process shutdown.
- name: dom.abort_script_on_child_shutdown
type: bool
value: false
mirror: always
- name: dom.max_chrome_script_run_time
type: int32_t
value: 0