Bug 1091459 - Only interrupt JS execution after running long enough that the slow script dialog might need to be shown. r=bholley, a=lmandel
authorBrian Hackett <bhackett1024@gmail.com>
Mon, 03 Nov 2014 10:51:22 -0700
changeset 225962 de49643707ae
parent 225961 967cb2edcd52
child 225963 f6b893ef9186
push id4088
push userryanvm@gmail.com
push date2014-11-06 15:24 +0000
treeherdermozilla-beta@dfe08b30f41f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley, lmandel
bugs1091459
milestone34.0
Bug 1091459 - Only interrupt JS execution after running long enough that the slow script dialog might need to be shown. r=bholley, a=lmandel
js/xpconnect/src/XPCJSRuntime.cpp
js/xpconnect/tests/unit/head_watchdog.js
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -977,16 +977,17 @@ class Watchdog
     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; }
@@ -1050,16 +1051,24 @@ class Watchdog
         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;
@@ -1072,31 +1081,40 @@ class Watchdog
     }
     void Finished()
     {
         MOZ_ASSERT(!NS_IsMainThread());
         mShuttingDown = false;
         PR_NotifyCondVar(mWakeup);
     }
 
+    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;
 };
 
 #ifdef MOZ_NUWA_PROCESS
 #include "ipc/Nuwa.h"
 #endif
 
+#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"
+
 class WatchdogManager : public nsIObserver
 {
   public:
 
     NS_DECL_ISUPPORTS
     explicit WatchdogManager(XPCJSRuntime *aRuntime) : mRuntime(aRuntime)
                                                      , mRuntimeState(RUNTIME_INACTIVE)
     {
@@ -1104,27 +1122,31 @@ class WatchdogManager : public nsIObserv
         PodArrayZero(mTimestamps);
         mTimestamps[TimestampRuntimeStateChange] = PR_Now();
 
         // Enable the watchdog, if appropriate.
         RefreshWatchdog();
 
         // Register ourselves as an observer to get updates on the pref.
         mozilla::Preferences::AddStrongObserver(this, "dom.use_watchdog");
+        mozilla::Preferences::AddStrongObserver(this, PREF_MAX_SCRIPT_RUN_TIME_CONTENT);
+        mozilla::Preferences::AddStrongObserver(this, PREF_MAX_SCRIPT_RUN_TIME_CHROME);
     }
 
   protected:
 
     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);
         mozilla::Preferences::RemoveObserver(this, "dom.use_watchdog");
+        mozilla::Preferences::RemoveObserver(this, PREF_MAX_SCRIPT_RUN_TIME_CONTENT);
+        mozilla::Preferences::RemoveObserver(this, PREF_MAX_SCRIPT_RUN_TIME_CHROME);
     }
 
   public:
 
     NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
                        const char16_t* aData)
     {
         RefreshWatchdog();
@@ -1178,22 +1200,32 @@ class WatchdogManager : public nsIObserv
     }
 
     XPCJSRuntime* Runtime() { return mRuntime; }
     Watchdog* GetWatchdog() { return mWatchdog; }
 
     void RefreshWatchdog()
     {
         bool wantWatchdog = Preferences::GetBool("dom.use_watchdog", true);
-        if (wantWatchdog == !!mWatchdog)
-            return;
-        if (wantWatchdog)
-            StartWatchdog();
-        else
-            StopWatchdog();
+        if (wantWatchdog != !!mWatchdog) {
+            if (wantWatchdog)
+                StartWatchdog();
+            else
+                StopWatchdog();
+        }
+
+        if (mWatchdog) {
+            int32_t contentTime = Preferences::GetInt(PREF_MAX_SCRIPT_RUN_TIME_CONTENT, 10);
+            if (contentTime <= 0)
+                contentTime = INT32_MAX;
+            int32_t chromeTime = Preferences::GetInt(PREF_MAX_SCRIPT_RUN_TIME_CHROME, 20);
+            if (chromeTime <= 0)
+                chromeTime = INT32_MAX;
+            mWatchdog->SetMinScriptRunTimeSeconds(std::min(contentTime, chromeTime));
+        }
     }
 
     void StartWatchdog()
     {
         MOZ_ASSERT(!mWatchdog);
         mWatchdog = new Watchdog(this);
         mWatchdog->Init();
     }
@@ -1257,21 +1289,22 @@ WatchdogMain(void *arg)
             manager->RecordTimestamp(TimestampWatchdogHibernateStart);
             self->Hibernate();
             manager->RecordTimestamp(TimestampWatchdogHibernateStop);
         }
 
         // Rise and shine.
         manager->RecordTimestamp(TimestampWatchdogWakeup);
 
-        // Don't request an interrupt callback if activity started less than one second ago.
-        // The callback is only used for detecting long running scripts, and triggering the
-        // callback from off the main thread can be expensive.
+        // 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.
+        PRTime usecs = self->MinScriptRunTimeSeconds() * PR_USEC_PER_SEC;
         if (manager->IsRuntimeActive() &&
-            manager->TimeSinceLastRuntimeStateChange() >= PRTime(PR_USEC_PER_SEC))
+            manager->TimeSinceLastRuntimeStateChange() >= usecs)
         {
             bool debuggerAttached = false;
             nsCOMPtr<nsIDebug2> dbg = do_GetService("@mozilla.org/xpcom/debug;1");
             if (dbg)
                 dbg->GetIsDebuggerAttached(&debuggerAttached);
             if (!debuggerAttached)
                 JS_RequestInterruptCallback(manager->Runtime()->Runtime());
         }
@@ -1343,18 +1376,18 @@ XPCJSRuntime::InterruptCallback(JSContex
     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.
     TimeDuration duration = TimeStamp::NowLoRes() - self->mSlowScriptCheckpoint;
     bool chrome = nsContentUtils::IsCallerChrome();
-    const char *prefName = chrome ? "dom.max_chrome_script_run_time"
-                                  : "dom.max_script_run_time";
+    const char *prefName = chrome ? PREF_MAX_SCRIPT_RUN_TIME_CHROME
+                                  : PREF_MAX_SCRIPT_RUN_TIME_CONTENT;
     int32_t limit = Preferences::GetInt(prefName, chrome ? 20 : 10);
 
     // If there's no limit, or we're within the limit, let it go.
     if (limit == 0 || duration.ToSeconds() < limit)
         return true;
 
     //
     // This has gone on long enough! Time to take action. ;-)
--- a/js/xpconnect/tests/unit/head_watchdog.js
+++ b/js/xpconnect/tests/unit/head_watchdog.js
@@ -15,16 +15,21 @@ var gPrefs = Cc["@mozilla.org/preference
 function setWatchdogEnabled(enabled) {
   gPrefs.setBoolPref("dom.use_watchdog", enabled);
 }
 
 function isWatchdogEnabled() {
   return gPrefs.getBoolPref("dom.use_watchdog");
 }
 
+function setScriptTimeout(seconds) {
+  var oldTimeout = gPrefs.getIntPref("dom.max_script_run_time");
+  gPrefs.setIntPref("dom.max_script_run_time", seconds);
+}
+
 //
 // Utilities.
 //
 
 function busyWait(ms) {
   var start = new Date();
   while ((new Date()) - start < ms) {}
 }
@@ -41,40 +46,44 @@ function executeSoon(fn) {
   var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
   tm.mainThread.dispatch({run: fn}, Ci.nsIThread.DISPATCH_NORMAL);
 }
 
 //
 // Asynchronous watchdog diagnostics.
 //
 // When running, the watchdog wakes up every second, and fires the operation
-// callback if the script has been running for >= one second. As such, a script
-// should never be able to run for two seconds or longer without servicing the
-// operation callback. We wait 3 seconds, just to be safe.
+// callback if the script has been running for >= the minimum script timeout.
+// As such, if the script timeout is 1 second, a script should never be able to
+// run for two seconds or longer without servicing the operation callback.
+// We wait 3 seconds, just to be safe.
 //
 
 function checkWatchdog(expectInterrupt, continuation) {
+  var oldTimeout = setScriptTimeout(1);
   var lastWatchdogWakeup = Cu.getWatchdogTimestamp("WatchdogWakeup");
   setInterruptCallback(function() {
     // If the watchdog didn't actually trigger the operation callback, ignore
     // this call. This allows us to test the actual watchdog behavior without
     // interference from other sites where we trigger the operation callback.
     if (lastWatchdogWakeup == Cu.getWatchdogTimestamp("WatchdogWakeup")) {
       return true;
     }
     do_check_true(expectInterrupt);
     setInterruptCallback(undefined);
+    setScriptTimeout(oldTimeout);
     // Schedule our continuation before we kill this script.
     executeSoon(continuation);
     return false;
   });
   executeSoon(function() {
     busyWait(3000);
     do_check_true(!expectInterrupt);
     setInterruptCallback(undefined);
+    setScriptTimeout(oldTimeout);
     continuation();
   });
 }
 
 var gGenerator;
 function continueTest() {
   gGenerator.next();
 }