Bug 1471091: Avoid cloning and caching process scripts. r?mccr8 draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 29 Jun 2018 18:07:46 -0700
changeset 812773 aebc5812bef4413d497ac4fdb59aced00a5a4c8e
parent 812387 d6120c2bb51e2057df51f4d52510bb5f4e8b4ca5
push id114673
push usermaglione.k@gmail.com
push dateSat, 30 Jun 2018 02:00:07 +0000
reviewersmccr8
bugs1471091
milestone63.0a1
Bug 1471091: Avoid cloning and caching process scripts. r?mccr8 We only run process scripts once per process, so there's no need to compile them for the compilation scope, or to keep a separate cloned copy alive for the length of the session. This patch changes the caching behavior of message managers to compile single-use scripts directly for the target global, and avoid caching them for the rest of the session. It also changes the preloader to drop references to these scripts after they've been executed and/or encoded, as appropriate. MozReview-Commit-ID: EfKo2aYbBxl
dom/base/ProcessGlobal.h
dom/base/nsFrameMessageManager.cpp
dom/base/nsFrameMessageManager.h
js/xpconnect/loader/ScriptPreloader.cpp
js/xpconnect/loader/ScriptPreloader.h
--- a/dom/base/ProcessGlobal.h
+++ b/dom/base/ProcessGlobal.h
@@ -89,16 +89,21 @@ public:
   virtual void LoadScript(const nsAString& aURL);
 
   virtual JSObject* GetGlobalJSObject() override
   {
     return GetWrapper();
   }
   virtual nsIPrincipal* GetPrincipal() override { return mPrincipal; }
 
+  bool IsProcessScoped() const override
+  {
+    return true;
+  }
+
   void SetInitialProcessData(JS::HandleValue aInitialData);
 
 protected:
   virtual ~ProcessGlobal();
 
 private:
   bool mInitialized;
 
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -1253,17 +1253,17 @@ nsMessageManagerScriptExecutor::LoadScri
   nsMessageManagerScriptHolder* holder = sCachedScripts->Get(aURL);
   if (holder && holder->WillRunInGlobalScope() == aRunInGlobalScope) {
     script = holder->mScript;
   } else {
     // Don't put anything in the cache if we already have an entry
     // with a different WillRunInGlobalScope() value.
     bool shouldCache = !holder;
     TryCacheLoadAndCompileScript(aURL, aRunInGlobalScope,
-                                 shouldCache, &script);
+                                 shouldCache, aGlobal, &script);
   }
 
   AutoEntryScript aes(aGlobal, "message manager script load");
   JSContext* cx = aes.cx();
   if (script) {
     if (aRunInGlobalScope) {
       JS::RootedValue rval(cx);
       JS::CloneAndExecuteScript(cx, script, &rval);
@@ -1278,16 +1278,17 @@ nsMessageManagerScriptExecutor::LoadScri
   }
 }
 
 void
 nsMessageManagerScriptExecutor::TryCacheLoadAndCompileScript(
   const nsAString& aURL,
   bool aRunInGlobalScope,
   bool aShouldCache,
+  JS::Handle<JSObject*> aGlobal,
   JS::MutableHandle<JSScript*> aScriptp)
 {
   nsCString url = NS_ConvertUTF16toUTF8(aURL);
   nsCOMPtr<nsIURI> uri;
   nsresult rv = NS_NewURI(getter_AddRefs(uri), url);
   if (NS_FAILED(rv)) {
     return;
   }
@@ -1296,20 +1297,27 @@ nsMessageManagerScriptExecutor::TryCache
   rv = NS_URIChainHasFlags(uri,
                            nsIProtocolHandler::URI_IS_LOCAL_RESOURCE,
                            &hasFlags);
   if (NS_FAILED(rv) || !hasFlags) {
     NS_WARNING("Will not load a frame script!");
     return;
   }
 
-  // Compile the script in the compilation scope instead of the current global
-  // to avoid keeping the current compartment alive.
+  // If this script won't be cached, or there is only one of this type of
+  // message manager per process, treat this script as run-once. Run-once
+  // scripts can be compiled directly for the target global, and will be dropped
+  // from the preloader cache after they're executed and serialized.
+  bool isRunOnce = !aShouldCache || IsProcessScoped();
+
+  // If the script will be reused in this session, compile it in the compilation
+  // scope instead of the current global to avoid keeping the current
+  // compartment alive.
   AutoJSAPI jsapi;
-  if (!jsapi.Init(xpc::CompilationScope())) {
+  if (!jsapi.Init(isRunOnce ? aGlobal : xpc::CompilationScope())) {
     return;
   }
   JSContext* cx = jsapi.cx();
   JS::Rooted<JSScript*> script(cx);
 
   script = ScriptPreloader::GetChildSingleton().GetCachedScript(cx, url);
 
   if (!script) {
@@ -1366,30 +1374,26 @@ nsMessageManagerScriptExecutor::TryCache
 
   MOZ_ASSERT(script);
   aScriptp.set(script);
 
   nsAutoCString scheme;
   uri->GetScheme(scheme);
   // We don't cache data: scripts!
   if (aShouldCache && !scheme.EqualsLiteral("data")) {
-    ScriptPreloader::GetChildSingleton().NoteScript(url, url, script);
-    // Root the object also for caching.
-    auto* holder = new nsMessageManagerScriptHolder(cx, script, aRunInGlobalScope);
-    sCachedScripts->Put(aURL, holder);
-  }
-}
+    ScriptPreloader::GetChildSingleton().NoteScript(url, url, script, isRunOnce);
 
-void
-nsMessageManagerScriptExecutor::TryCacheLoadAndCompileScript(
-  const nsAString& aURL,
-  bool aRunInGlobalScope)
-{
-  JS::Rooted<JSScript*> script(RootingCx());
-  TryCacheLoadAndCompileScript(aURL, aRunInGlobalScope, true, &script);
+    // If this script will only run once per process, only cache it in the
+    // preloader cache, not the session cache.
+    if (!isRunOnce) {
+      // Root the object also for caching.
+      auto* holder = new nsMessageManagerScriptHolder(cx, script, aRunInGlobalScope);
+      sCachedScripts->Put(aURL, holder);
+    }
+  }
 }
 
 void
 nsMessageManagerScriptExecutor::Trace(const TraceCallbacks& aCallbacks, void* aClosure)
 {
   for (size_t i = 0, length = mAnonymousGlobalScopes.Length(); i < length; ++i) {
     aCallbacks.Trace(&mAnonymousGlobalScopes[i], "mAnonymousGlobalScopes[i]", aClosure);
   }
--- a/dom/base/nsFrameMessageManager.h
+++ b/dom/base/nsFrameMessageManager.h
@@ -426,28 +426,35 @@ protected:
   ~nsMessageManagerScriptExecutor() { MOZ_COUNT_DTOR(nsMessageManagerScriptExecutor); }
 
   void DidCreateGlobal();
   void LoadScriptInternal(JS::Handle<JSObject*> aGlobal, const nsAString& aURL,
                           bool aRunInGlobalScope);
   void TryCacheLoadAndCompileScript(const nsAString& aURL,
                                     bool aRunInGlobalScope,
                                     bool aShouldCache,
+                                    JS::Handle<JSObject*> aGlobal,
                                     JS::MutableHandle<JSScript*> aScriptp);
-  void TryCacheLoadAndCompileScript(const nsAString& aURL,
-                                    bool aRunInGlobalScope);
   bool InitChildGlobalInternal(const nsACString& aID);
   virtual bool WrapGlobalObject(JSContext* aCx,
                                 JS::RealmOptions& aOptions,
                                 JS::MutableHandle<JSObject*> aReflector) = 0;
   void Trace(const TraceCallbacks& aCallbacks, void* aClosure);
   void Unlink();
   nsCOMPtr<nsIPrincipal> mPrincipal;
   AutoTArray<JS::Heap<JSObject*>, 2> mAnonymousGlobalScopes;
 
+  // Returns true if this is a process message manager. There should only be a
+  // single process message manager per session, so instances of this type will
+  // optimize their script loading to avoid unnecessary duplication.
+  virtual bool IsProcessScoped() const
+  {
+    return false;
+  }
+
   static nsDataHashtable<nsStringHashKey, nsMessageManagerScriptHolder*>* sCachedScripts;
   static mozilla::StaticRefPtr<nsScriptCacheCleaner> sScriptCacheCleaner;
 };
 
 class nsScriptCacheCleaner final : public nsIObserver
 {
   ~nsScriptCacheCleaner() {}
 
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -402,16 +402,22 @@ ScriptPreloader::FinishContentStartup()
 
     mStartupFinished = true;
 
     if (mChildActor) {
         mChildActor->SendScriptsAndFinalize(mScripts);
     }
 }
 
+bool
+ScriptPreloader::WillWriteScripts()
+{
+    return Active() && (XRE_IsParentProcess() || mChildActor);
+}
+
 Result<nsCOMPtr<nsIFile>, nsresult>
 ScriptPreloader::GetCacheFile(const nsAString& suffix)
 {
     NS_ENSURE_TRUE(mProfD, Err(NS_ERROR_NOT_INITIALIZED));
 
     nsCOMPtr<nsIFile> cacheFile;
     MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));
 
@@ -792,33 +798,46 @@ ScriptPreloader::Run()
                                       mSaveThread.forget());
 
     mal.NotifyAll();
     return NS_OK;
 }
 
 void
 ScriptPreloader::NoteScript(const nsCString& url, const nsCString& cachePath,
-                            JS::HandleScript jsscript)
+                            JS::HandleScript jsscript, bool isRunOnce)
 {
+    if (!Active()) {
+        if (isRunOnce) {
+            if (auto script = mScripts.Get(cachePath)) {
+                script->mIsRunOnce = true;
+                script->MaybeDropScript();
+            }
+        }
+        return;
+    }
+
     // Don't bother trying to cache any URLs with cache-busting query
     // parameters.
-    if (!Active() || cachePath.FindChar('?') >= 0) {
+    if (cachePath.FindChar('?') >= 0) {
         return;
     }
 
     // Don't bother caching files that belong to the mochitest harness.
     NS_NAMED_LITERAL_CSTRING(mochikitPrefix, "chrome://mochikit/");
     if (StringHead(url, mochikitPrefix.Length()) == mochikitPrefix) {
         return;
     }
 
     auto script = mScripts.LookupOrAdd(cachePath, *this, url, cachePath, jsscript);
+    if (isRunOnce) {
+        script->mIsRunOnce = true;
+    }
 
-    if (!script->mScript) {
+    if (!script->MaybeDropScript() && !script->mScript) {
         MOZ_ASSERT(jsscript);
         script->mScript = jsscript;
         script->mReadyToExecute = true;
     }
 
     script->UpdateLoadTime(TimeStamp::Now());
     script->mProcessTypes += CurrentProcessType();
 }
@@ -1108,16 +1127,20 @@ ScriptPreloader::CachedScript::CachedScr
     // compare against last session's values later.
     mOriginalProcessTypes = mProcessTypes;
     mProcessTypes = {};
 }
 
 bool
 ScriptPreloader::CachedScript::XDREncode(JSContext* cx)
 {
+    auto cleanup = MakeScopeExit([&] () {
+        MaybeDropScript();
+    });
+
     JSAutoRealm ar(cx, mScript);
     JS::RootedScript jsscript(cx, mScript);
 
     mXDRData.construct<JS::TranscodeBuffer>();
 
     JS::TranscodeResult code = JS::EncodeScript(cx, Buffer(), jsscript);
     if (code == JS::TranscodeResult_Ok) {
         mXDRRange.emplace(Buffer().begin(), Buffer().length());
--- a/js/xpconnect/loader/ScriptPreloader.h
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -77,17 +77,22 @@ public:
 
     // Retrieves the script with the given cache key from the script cache.
     // Returns null if the script is not cached.
     JSScript* GetCachedScript(JSContext* cx, const nsCString& name);
 
     // Notes the execution of a script with the given URL and cache key.
     // Depending on the stage of startup, the script may be serialized and
     // stored to the startup script cache.
-    void NoteScript(const nsCString& url, const nsCString& cachePath, JS::HandleScript script);
+    //
+    // If isRunOnce is true, this script is expected to run only once per
+    // process per browser session. A cached instance will not be kept alive
+    // for repeated execution.
+    void NoteScript(const nsCString& url, const nsCString& cachePath, JS::HandleScript script,
+                    bool isRunOnce = false);
 
     void NoteScript(const nsCString& url, const nsCString& cachePath,
                     ProcessType processType, nsTArray<uint8_t>&& xdrData,
                     TimeStamp loadTime);
 
     // Initializes the script cache from the startup script cache file.
     Result<Ok, nsresult> InitCache(const nsAString& = NS_LITERAL_STRING("scriptCache"));
 
@@ -144,22 +149,24 @@ private:
     // the next session's cache file. If it was compiled in this session, its
     // mXDRRange will initially be empty, and its mXDRData buffer will be
     // populated just before it is written to the cache file.
     class CachedScript : public LinkedListElement<CachedScript>
     {
     public:
         CachedScript(CachedScript&&) = delete;
 
-        CachedScript(ScriptPreloader& cache, const nsCString& url, const nsCString& cachePath, JSScript* script)
+        CachedScript(ScriptPreloader& cache, const nsCString& url, const nsCString& cachePath,
+                     JSScript* script)
             : mCache(cache)
             , mURL(url)
             , mCachePath(cachePath)
             , mScript(script)
             , mReadyToExecute(true)
+            , mIsRunOnce(false)
         {}
 
         inline CachedScript(ScriptPreloader& cache, InputBuffer& buf);
 
         ~CachedScript() = default;
 
         ScriptStatus Status() const
         {
@@ -208,16 +215,33 @@ private:
 
         void UpdateLoadTime(const TimeStamp& loadTime)
         {
           if (mLoadTime.IsNull() || loadTime < mLoadTime) {
             mLoadTime = loadTime;
           }
         }
 
+        // Checks whether the cached JSScript for this entry will be needed
+        // again and, if not, drops it and returns true. This is the case for
+        // run-once scripts that do not still need to be encoded into the
+        // cache.
+        //
+        // If this method returns false, callers may set mScript to a cached
+        // JSScript instance for this entry. If it returns false, they should
+        // not.
+        bool MaybeDropScript()
+        {
+            if (mIsRunOnce && (HasRange() || !mCache.WillWriteScripts())) {
+                mScript = nullptr;
+                return true;
+            }
+            return false;
+        }
+
         // Encodes this script into XDR data, and stores the result in mXDRData.
         // Returns true on success, false on failure.
         bool XDREncode(JSContext* cx);
 
         // Encodes or decodes this script, in the storage format required by the
         // script cache file.
         template<typename Buffer>
         void Code(Buffer& buffer)
@@ -299,16 +323,21 @@ private:
         JS::Heap<JSScript*> mScript;
 
         // True if this script is ready to be executed. This means that either the
         // off-thread portion of an off-thread decode has finished, or the script
         // is too small to be decoded off-thread, and may be immediately decoded
         // whenever it is first executed.
         bool mReadyToExecute = false;
 
+        // True if this script is expected to run once per process. If so, its
+        // JSScript instance will be dropped as soon as the script has
+        // executed and been encoded into the cache.
+        bool mIsRunOnce = false;
+
         // The set of processes in which this script has been used.
         EnumSet<ProcessType> mProcessTypes{};
 
         // The set of processes which the script was loaded into during the
         // last session, as read from the cache file.
         EnumSet<ProcessType> mOriginalProcessTypes{};
 
         // The read-only XDR data for this script, which was either read from an
@@ -378,16 +407,22 @@ private:
     // Prepares scripts for writing to the cache, serializing new scripts to
     // XDR, and calculating their size-based offsets.
     void PrepareCacheWrite();
 
     void PrepareCacheWriteInternal();
 
     void FinishContentStartup();
 
+    // Returns true if scripts added to the cache now will be encoded and
+    // written to the cache. If we've passed the startup script loading
+    // window, or this is a content process which hasn't been asked to return
+    // script bytecode, this will return false.
+    bool WillWriteScripts();
+
     // Returns a file pointer for the cache file with the given name in the
     // current profile.
     Result<nsCOMPtr<nsIFile>, nsresult>
     GetCacheFile(const nsAString& suffix);
 
     // Waits for the given cached script to finish compiling off-thread, or
     // decodes it synchronously on the main thread, as appropriate.
     JSScript* WaitForCachedScript(JSContext* cx, CachedScript* script);