Bug 1361900: Part 4 - Use a separate script cache for scripts loaded in the child process. r?erahm,gabor draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 09 May 2017 19:52:17 -0700
changeset 577041 e3bd7cc4843a0a7758d5fe37321c89b90a465ed0
parent 575178 05b18b96f0e2083d14aaa8ef570e80dcbb1c8b02
child 577042 32ed7211fce8c6916ef25ff6dfbc18cb190c371c
push id58588
push usermaglione.k@gmail.com
push dateFri, 12 May 2017 19:00:00 +0000
reviewerserahm, gabor
bugs1361900
milestone55.0a1
Bug 1361900: Part 4 - Use a separate script cache for scripts loaded in the child process. r?erahm,gabor MozReview-Commit-ID: EIdwmuTOl90
dom/base/nsFrameMessageManager.cpp
js/xpconnect/loader/ScriptPreloader.cpp
js/xpconnect/loader/ScriptPreloader.h
xpcom/build/XPCOMInit.cpp
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -1604,17 +1604,17 @@ nsMessageManagerScriptExecutor::TryCache
   AutoJSAPI jsapi;
   if (!jsapi.Init(xpc::CompilationScope())) {
     return;
   }
   JSContext* cx = jsapi.cx();
   JS::Rooted<JSScript*> script(cx);
 
   if (XRE_IsParentProcess()) {
-    script = ScriptPreloader::GetSingleton().GetCachedScript(cx, url);
+    script = ScriptPreloader::GetChildSingleton().GetCachedScript(cx, url);
   }
 
   if (!script) {
     nsCOMPtr<nsIChannel> channel;
     NS_NewChannel(getter_AddRefs(channel),
                   uri,
                   nsContentUtils::GetSystemPrincipal(),
                   nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
@@ -1669,17 +1669,17 @@ nsMessageManagerScriptExecutor::TryCache
   MOZ_ASSERT(script);
   aScriptp.set(script);
 
   nsAutoCString scheme;
   uri->GetScheme(scheme);
   // We don't cache data: scripts!
   if (aShouldCache && !scheme.EqualsLiteral("data")) {
     if (XRE_IsParentProcess()) {
-      ScriptPreloader::GetSingleton().NoteScript(url, url, script);
+      ScriptPreloader::GetChildSingleton().NoteScript(url, url, script);
     }
     // Root the object also for caching.
     auto* holder = new nsMessageManagerScriptHolder(cx, script, aRunInGlobalScope);
     sCachedScripts->Put(aURL, holder);
   }
 }
 
 void
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -77,17 +77,63 @@ ScriptPreloader::CollectReports(nsIHandl
 
 
 ScriptPreloader&
 ScriptPreloader::GetSingleton()
 {
     static RefPtr<ScriptPreloader> singleton;
 
     if (!singleton) {
+        if (XRE_IsParentProcess()) {
+            singleton = new ScriptPreloader();
+            singleton->mChildCache = &GetChildSingleton();
+            Unused << singleton->InitCache();
+        } else {
+            singleton = &GetChildSingleton();
+        }
+
+        ClearOnShutdown(&singleton);
+    }
+
+    return *singleton;
+}
+
+// The child singleton is available in all processes, including the parent, and
+// is used for scripts which are expected to be loaded into child processes
+// (such as process and frame scripts), or scripts that have already been loaded
+// into a child. The child caches are managed as follows:
+//
+// - Every startup, we open the cache file from the last session, move it to a
+//  new location, and begin pre-loading the scripts that are stored in it. There
+//  is a separate cache file for parent and content processes, but the parent
+//  process opens both the parent and content cache files.
+//
+// - Once startup is complete, we write a new cache file for the next session,
+//   containing only the scripts that were used during early startup, so we don't
+//   waste pre-loading scripts that may not be needed.
+//
+// - For content processes, opening and writing the cache file is handled in the
+//  parent process. The first content process of each type sends back the data
+//  for scripts that were loaded in early startup, and the parent merges them and
+//  writes them to a cache file.
+//
+// - Currently, content processes only benefit from the cache data written
+//  during the *previous* session. Ideally, new content processes should probably
+//  use the cache data written during this session if there was no previous cache
+//  file, but I'd rather do that as a follow-up.
+ScriptPreloader&
+ScriptPreloader::GetChildSingleton()
+{
+    static RefPtr<ScriptPreloader> singleton;
+
+    if (!singleton) {
         singleton = new ScriptPreloader();
+        if (XRE_IsParentProcess()) {
+            Unused << singleton->InitCache(NS_LITERAL_STRING("scriptCache-child"));
+        }
         ClearOnShutdown(&singleton);
     }
 
     return *singleton;
 }
 
 
 ProcessType
@@ -209,96 +255,98 @@ ScriptPreloader::FlushCache()
     FlushScripts(mSavedScripts);
     FlushScripts(mRestoredScripts);
 
     // If we've already finished saving the cache at this point, start a new
     // delayed save operation. This will write out an empty cache file in place
     // of any cache file we've already written out this session, which will
     // prevent us from falling back to the current session's cache file on the
     // next startup.
-    if (mSaveComplete) {
+    if (mSaveComplete && mChildCache) {
         mSaveComplete = false;
 
         Unused << NS_NewNamedThread("SaveScripts",
                                     getter_AddRefs(mSaveThread), this);
     }
 }
 
 nsresult
 ScriptPreloader::Observe(nsISupports* subject, const char* topic, const char16_t* data)
 {
     if (!strcmp(topic, DELAYED_STARTUP_TOPIC)) {
         nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
         obs->RemoveObserver(this, DELAYED_STARTUP_TOPIC);
 
         mStartupFinished = true;
 
-        if (XRE_IsParentProcess()) {
+
+        if (XRE_IsParentProcess() && mChildCache) {
             Unused << NS_NewNamedThread("SaveScripts",
                                         getter_AddRefs(mSaveThread), this);
         }
     } else if (!strcmp(topic, SHUTDOWN_TOPIC)) {
         ForceWriteCacheFile();
     } else if (!strcmp(topic, CLEANUP_TOPIC)) {
         Cleanup();
     } else if (!strcmp(topic, CACHE_FLUSH_TOPIC)) {
         FlushCache();
     }
 
     return NS_OK;
 }
 
 
 Result<nsCOMPtr<nsIFile>, nsresult>
-ScriptPreloader::GetCacheFile(const char* leafName)
+ScriptPreloader::GetCacheFile(const nsAString& suffix)
 {
     nsCOMPtr<nsIFile> cacheFile;
     NS_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));
 
     NS_TRY(cacheFile->AppendNative(NS_LITERAL_CSTRING("startupCache")));
     Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);
 
-    NS_TRY(cacheFile->AppendNative(nsDependentCString(leafName)));
+    NS_TRY(cacheFile->Append(mBaseName + suffix));
 
     return Move(cacheFile);
 }
 
 static const uint8_t MAGIC[] = "mozXDRcachev001";
 
 Result<Ok, nsresult>
 ScriptPreloader::OpenCache()
 {
     NS_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
 
     nsCOMPtr<nsIFile> cacheFile;
-    MOZ_TRY_VAR(cacheFile, GetCacheFile("scriptCache.bin"));
+    MOZ_TRY_VAR(cacheFile, GetCacheFile(NS_LITERAL_STRING(".bin")));
 
     bool exists;
     NS_TRY(cacheFile->Exists(&exists));
     if (exists) {
-        NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("scriptCache-current.bin")));
+        NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING("-current.bin")));
     } else {
-        NS_TRY(cacheFile->SetLeafName(NS_LITERAL_STRING("scriptCache-current.bin")));
+        NS_TRY(cacheFile->SetLeafName(mBaseName + NS_LITERAL_STRING("-current.bin")));
         NS_TRY(cacheFile->Exists(&exists));
         if (!exists) {
             return Err(NS_ERROR_FILE_NOT_FOUND);
         }
     }
 
     MOZ_TRY(mCacheData.init(cacheFile));
 
     return Ok();
 }
 
 // Opens the script cache file for this session, and initializes the script
 // cache based on its contents. See WriteCache for details of the cache file.
 Result<Ok, nsresult>
-ScriptPreloader::InitCache()
+ScriptPreloader::InitCache(const nsAString& basePath)
 {
     mCacheInitialized = true;
+    mBaseName = basePath;
 
     RegisterWeakMemoryReporter(this);
 
     if (!XRE_IsParentProcess()) {
         return Ok();
     }
 
     MOZ_TRY(OpenCache());
@@ -330,17 +378,17 @@ ScriptPreloader::InitCache()
 
         Range<uint8_t> header(data, data + headerSize);
         data += headerSize;
 
         InputBuffer buf(header);
 
         size_t offset = 0;
         while (!buf.finished()) {
-            auto script = MakeUnique<CachedScript>(buf);
+            auto script = MakeUnique<CachedScript>(*this, buf);
 
             auto scriptData = data + script->mOffset;
             if (scriptData + script->mSize > end) {
                 return Err(NS_ERROR_UNEXPECTED);
             }
 
             // Make sure offsets match what we'd expect based on script ordering and
             // size, as a basic sanity check.
@@ -368,16 +416,17 @@ ScriptPreloader::InitCache()
     MOZ_RELEASE_ASSERT(jsapi.Init(xpc::CompilationScope()));
     JSContext* cx = jsapi.cx();
 
     auto start = TimeStamp::Now();
     LOG(Info, "Off-thread decoding scripts...\n");
 
     JS::CompileOptions options(cx, JSVERSION_LATEST);
     for (auto script : mRestoredScripts) {
+        // Only async decode scripts which have been used in this process type.
         if (script->mProcessTypes.contains(CurrentProcessType()) &&
             script->mSize > MIN_OFFTHREAD_SIZE &&
             JS::CanCompileOffThread(cx, options, script->mSize)) {
             DecodeScriptOffThread(cx, script);
         } else {
             script->mReadyToExecute = true;
         }
     }
@@ -397,16 +446,22 @@ Write(PRFileDesc* fd, const void* data, 
     return Ok();
 }
 
 void
 ScriptPreloader::PrepareCacheWrite()
 {
     MOZ_ASSERT(NS_IsMainThread());
 
+    auto cleanup = MakeScopeExit([&] () {
+        if (mChildCache) {
+            mChildCache->PrepareCacheWrite();
+        }
+    });
+
     if (mDataPrepared) {
         return;
     }
 
     if (mRestoredScripts.isEmpty()) {
         // Check for any new scripts that we need to save. If there aren't
         // any, and there aren't any saved scripts that we need to remove,
         // don't bother writing out a new cache file.
@@ -426,17 +481,30 @@ ScriptPreloader::PrepareCacheWrite()
     AutoSafeJSAPI jsapi;
 
     LinkedList<CachedScript> asyncScripts;
 
     for (CachedScript* next = mSavedScripts.getFirst(); next; ) {
         CachedScript* script = next;
         next = script->getNext();
 
-        if (!script->mSize && !script->XDREncode(jsapi.cx())) {
+        // Don't write any scripts that are also in the child cache. They'll be
+        // loaded from the child cache in that case, so there's no need to write
+        // them twice.
+        CachedScript* childScript = mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr;
+        if (childScript) {
+            if (FindScript(mChildCache->mSavedScripts, script->mCachePath)) {
+                childScript->UpdateLoadTime(script->mLoadTime);
+                childScript->mProcessTypes += script->mProcessTypes;
+            } else {
+                childScript = nullptr;
+            }
+        }
+
+        if (childScript || (!script->mSize && !script->XDREncode(jsapi.cx()))) {
             script->remove();
             delete script;
         } else {
             script->mSize = script->Range().length();
 
             if (script->mSize > MIN_OFFTHREAD_SIZE) {
                 script->remove();
                 asyncScripts.insertBack(script);
@@ -482,17 +550,17 @@ ScriptPreloader::WriteCache()
     }
 
     if (mSaveComplete) {
         // If we don't have anything we need to save, we're done.
         return Ok();
     }
 
     nsCOMPtr<nsIFile> cacheFile;
-    MOZ_TRY_VAR(cacheFile, GetCacheFile("scriptCache-new.bin"));
+    MOZ_TRY_VAR(cacheFile, GetCacheFile(NS_LITERAL_STRING("-new.bin")));
 
     bool exists;
     NS_TRY(cacheFile->Exists(&exists));
     if (exists) {
         NS_TRY(cacheFile->Remove(false));
     }
 
     AutoFDClose fd;
@@ -514,33 +582,37 @@ ScriptPreloader::WriteCache()
     MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
     MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
 
     for (auto script : mSavedScripts) {
         MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
         script->mXDRData.reset();
     }
 
-    NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("scriptCache.bin")));
+    NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING(".bin")));
 
     return Ok();
 }
 
 // Runs in the mSaveThread thread, and writes out the cache file for the next
 // session after a reasonable delay.
 nsresult
 ScriptPreloader::Run()
 {
     MonitorAutoLock mal(mSaveMonitor);
 
     // Ideally wait about 10 seconds before saving, to avoid unnecessary IO
     // during early startup.
     mal.Wait(10000);
 
-    Unused << WriteCache();
+    auto result = WriteCache();
+    Unused << NS_WARN_IF(result.isErr());
+
+    result = mChildCache->WriteCache();
+    Unused << NS_WARN_IF(result.isErr());
 
     mSaveComplete = true;
     NS_ReleaseOnMainThread(mSaveThread.forget());
 
     mal.NotifyAll();
     return NS_OK;
 }
 
@@ -585,27 +657,36 @@ ScriptPreloader::NoteScript(const nsCStr
         restored->remove();
         mSavedScripts.insertBack(restored);
 
         MOZ_ASSERT(script);
         restored->mProcesses += CurrentProcessType();
         restored->mScript = script;
         restored->mReadyToExecute = true;
     } else if (!exists) {
-        auto cachedScript = new CachedScript(url, cachePath, script);
+        auto cachedScript = new CachedScript(*this, url, cachePath, script);
         cachedScript->mProcesses += CurrentProcessType();
 
         mSavedScripts.insertBack(cachedScript);
         mScripts.Put(cachePath, cachedScript);
     }
 }
 
 JSScript*
 ScriptPreloader::GetCachedScript(JSContext* cx, const nsCString& path)
 {
+    // If a script is used by both the parent and the child, it's stored only
+    // in the child cache.
+    if (mChildCache) {
+        auto script = mChildCache->GetCachedScript(cx, path);
+        if (script) {
+            return script;
+        }
+    }
+
     auto script = mScripts.Get(path);
     if (script) {
         return WaitForCachedScript(cx, script);
     }
 
     return nullptr;
 }
 
@@ -657,38 +738,38 @@ ScriptPreloader::CancelOffThreadParse(vo
     JS::CancelOffThreadScriptDecoder(jsapi.cx(), token);
 }
 
 /* static */ void
 ScriptPreloader::OffThreadDecodeCallback(void* token, void* context)
 {
     auto script = static_cast<CachedScript*>(context);
 
-    MonitorAutoLock mal(GetSingleton().mMonitor);
+    MonitorAutoLock mal(script->mCache.mMonitor);
 
     if (script->mReadyToExecute) {
         // We've already executed this script on the main thread, and opted to
         // main thread decode it rather waiting for off-thread decoding to
         // finish. So just cancel the off-thread parse rather than completing
         // it.
         NS_DispatchToMainThread(
-            NewRunnableMethod<void*>(&GetSingleton(),
+            NewRunnableMethod<void*>(&script->mCache,
                                      &ScriptPreloader::CancelOffThreadParse,
                                      token));
         return;
     }
 
     script->mToken = token;
     script->mReadyToExecute = true;
 
     mal.NotifyAll();
 }
 
-inline
-ScriptPreloader::CachedScript::CachedScript(InputBuffer& buf)
+ScriptPreloader::CachedScript::CachedScript(ScriptPreloader& cache, InputBuffer& buf)
+    : mCache(cache)
 {
     Code(buf);
 }
 
 bool
 ScriptPreloader::CachedScript::XDREncode(JSContext* cx)
 {
     JSAutoCompartment ac(cx, mScript);
@@ -704,17 +785,17 @@ ScriptPreloader::CachedScript::XDREncode
     JS_ClearPendingException(cx);
     return false;
 }
 
 void
 ScriptPreloader::CachedScript::Cancel()
 {
     if (mToken) {
-        GetSingleton().mMonitor.AssertCurrentThreadOwns();
+        mCache.mMonitor.AssertCurrentThreadOwns();
 
         AutoSafeJSAPI jsapi;
         JS::CancelOffThreadScriptDecoder(jsapi.cx(), mToken);
 
         mReadyToExecute = true;
         mToken = nullptr;
     }
 }
--- a/js/xpconnect/loader/ScriptPreloader.h
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -47,30 +47,31 @@ class ScriptPreloader : public nsIObserv
 
 public:
     NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSIOBSERVER
     NS_DECL_NSIMEMORYREPORTER
     NS_DECL_NSIRUNNABLE
 
     static ScriptPreloader& GetSingleton();
+    static ScriptPreloader& GetChildSingleton();
 
     static ProcessType GetChildProcessType(const nsAString& remoteType);
 
     // 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);
 
     // Initializes the script cache from the startup script cache file.
-    Result<Ok, nsresult> InitCache();
+    Result<Ok, nsresult> InitCache(const nsAString& = NS_LITERAL_STRING("scriptCache"));
 
     void Trace(JSTracer* trc);
 
     static ProcessType CurrentProcessType()
     {
         return sProcessType;
     }
 
@@ -102,33 +103,33 @@ 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&&) = default;
 
-        CachedScript(const nsCString& url, const nsCString& cachePath, JSScript* script)
-            : mURL(url)
+        CachedScript(ScriptPreloader& cache, const nsCString& url, const nsCString& cachePath, JSScript* script)
+            : mCache(cache)
+            , mURL(url)
             , mCachePath(cachePath)
             , mScript(script)
             , mReadyToExecute(true)
         {}
 
-        explicit inline CachedScript(InputBuffer& buf);
+        inline CachedScript(ScriptPreloader& cache, InputBuffer& buf);
 
         ~CachedScript()
         {
-            auto& cache = GetSingleton();
 #ifdef DEBUG
-            auto hashValue = cache.mScripts.Get(mCachePath);
+            auto hashValue = mCache->mScripts.Get(mCachePath);
             MOZ_ASSERT_IF(hashValue, hashValue == this);
 #endif
-            cache.mScripts.Remove(mCachePath);
+            mCache->mScripts.Remove(mCachePath);
         }
 
         void Cancel();
 
         // Encodes this script into XDR data, and stores the result in mXDRData.
         // Returns true on success, false on failure.
         bool XDREncode(JSContext* cx);
 
@@ -167,16 +168,18 @@ private:
             if (mXDRData.isSome()) {
                 size += (mXDRData->sizeOfExcludingThis(mallocSizeOf) +
                          mURL.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
                          mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf));
             }
             return size;
         }
 
+        ScriptPreloader& mCache;
+
         // The URL from which this script was initially read and compiled.
         nsCString mURL;
         // A unique identifier for this script's filesystem location, used as a
         // primary cache lookup value.
         nsCString mCachePath;
 
         // The offset of this script in the cache file, from the start of the XDR
         // data block.
@@ -242,17 +245,17 @@ private:
 
     // Prepares scripts for writing to the cache, serializing new scripts to
     // XDR, and calculating their size-based offsets.
     void PrepareCacheWrite();
 
     // Returns a file pointer for the cache file with the given name in the
     // current profile.
     Result<nsCOMPtr<nsIFile>, nsresult>
-    GetCacheFile(const char* leafName);
+    GetCacheFile(const nsAString& suffix);
 
     static CachedScript* FindScript(LinkedList<CachedScript>& scripts, const nsCString& cachePath);
 
     // 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);
 
     // Begins decoding the given script in a background thread.
@@ -294,16 +297,20 @@ private:
 
     bool mCacheInitialized = false;
     bool mSaveComplete = false;
     bool mDataPrepared = false;
 
     // The process type of the current process.
     static ProcessType sProcessType;
 
+    RefPtr<ScriptPreloader> mChildCache;
+
+    nsString mBaseName;
+
     nsCOMPtr<nsIFile> mProfD;
     nsCOMPtr<nsIThread> mSaveThread;
 
     // The mmapped cache data from this session's cache file.
     AutoMemMap mCacheData;
 
     Monitor mMonitor;
     Monitor mSaveMonitor;
--- a/xpcom/build/XPCOMInit.cpp
+++ b/xpcom/build/XPCOMInit.cpp
@@ -705,17 +705,17 @@ NS_InitXPCOM2(nsIServiceManager** aResul
 
   // Force layout to spin up so that nsContentUtils is available for cx stack
   // munging.  Note that layout registers a number of static atoms, and also
   // seals the static atom table, so NS_RegisterStaticAtom may not be called
   // beyond this point.
   nsCOMPtr<nsISupports> componentLoader =
     do_GetService("@mozilla.org/moz/jsloader;1");
 
-  Unused << mozilla::ScriptPreloader::GetSingleton().InitCache();
+  mozilla::ScriptPreloader::GetSingleton();
   mozilla::scache::StartupCache::GetSingleton();
   mozilla::AvailableMemoryTracker::Activate();
 
   // Notify observers of xpcom autoregistration start
   NS_CreateServicesFromCategory(NS_XPCOM_STARTUP_CATEGORY,
                                 nullptr,
                                 NS_XPCOM_STARTUP_OBSERVER_ID);
 #ifdef XP_WIN