Bug 1361900: Part 9 - Sort scripts by initial load time before saving. r?erahm draft
authorKris Maglione <maglione.k@gmail.com>
Mon, 01 May 2017 14:12:01 -0700
changeset 577046 8ea012bb48b31539319d685a9041e71105196133
parent 577045 d4845037b2c404574c4892420655fc88b47d1091
child 577047 924590d5bfb6f35818e9ef89d6d2209d210ef561
push id58588
push usermaglione.k@gmail.com
push dateFri, 12 May 2017 19:00:00 +0000
reviewerserahm
bugs1361900
milestone55.0a1
Bug 1361900: Part 9 - Sort scripts by initial load time before saving. r?erahm MozReview-Commit-ID: 54UN2DVK4xM
js/xpconnect/loader/ScriptCacheActors.cpp
js/xpconnect/loader/ScriptPreloader.cpp
js/xpconnect/loader/ScriptPreloader.h
--- a/js/xpconnect/loader/ScriptCacheActors.cpp
+++ b/js/xpconnect/loader/ScriptCacheActors.cpp
@@ -42,16 +42,17 @@ ScriptCacheChild::Finalize(LinkedList<Sc
         if (!script->mSize && !script->XDREncode(jsapi.cx())) {
             continue;
         }
 
         auto data = dataArray.AppendElement();
 
         data->url() = script->mURL;
         data->cachePath() = script->mCachePath;
+        data->loadTime() = script->mLoadTime;
 
         if (script->HasBuffer()) {
             auto& xdrData = script->Buffer();
             data->xdrData().AppendElements(xdrData.begin(), xdrData.length());
             script->FreeData();
         }
     }
 
@@ -78,17 +79,17 @@ ScriptCacheParent::Recv__delete__(nsTArr
 
     // Merge the child's script data with the parent's.
     auto parent = static_cast<dom::ContentParent*>(Manager());
     auto processType = ScriptPreloader::GetChildProcessType(parent->GetRemoteType());
 
     auto& cache = ScriptPreloader::GetChildSingleton();
     for (auto& script : scripts) {
         cache.NoteScript(script.url(), script.cachePath(), processType,
-                         Move(script.xdrData()));
+                         Move(script.xdrData()), script.loadTime());
     }
 
     return IPC_OK();
 }
 
 void
 ScriptCacheParent::ActorDestroy(ActorDestroyReason aWhy)
 {}
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -493,17 +493,17 @@ ScriptPreloader::InitCacheInternal()
 
     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 &&
+            script->AsyncDecodable() &&
             JS::CanCompileOffThread(cx, options, script->mSize)) {
             DecodeScriptOffThread(cx, script);
         } else {
             script->mReadyToExecute = true;
         }
     }
 
     LOG(Info, "Initialized decoding in %fms\n",
@@ -550,18 +550,16 @@ ScriptPreloader::PrepareCacheWrite()
         if (!found) {
             mSaveComplete = true;
             return;
         }
     }
 
     AutoSafeJSAPI jsapi;
 
-    LinkedList<CachedScript> asyncScripts;
-
     for (CachedScript* next = mSavedScripts.getFirst(); next; ) {
         CachedScript* script = next;
         next = script->getNext();
 
         // 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;
@@ -574,30 +572,19 @@ ScriptPreloader::PrepareCacheWrite()
             }
         }
 
         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);
-            }
         }
     }
 
-    // Store async-decoded scripts contiguously, since they're loaded
-    // immediately at startup.
-    while (CachedScript* s = asyncScripts.popLast()) {
-        mSavedScripts.insertFront(s);
-    }
-
     mDataPrepared = true;
 }
 
 // Writes out a script cache file for the scripts accessed during early
 // startup in this session. The cache file is a little-endian binary file with
 // the following format:
 //
 // - A uint32 containing the size of the header block.
@@ -636,33 +623,42 @@ ScriptPreloader::WriteCache()
     NS_TRY(cacheFile->Exists(&exists));
     if (exists) {
         NS_TRY(cacheFile->Remove(false));
     }
 
     AutoFDClose fd;
     NS_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, &fd.rwget()));
 
+    nsTArray<CachedScript*> scripts;
+    for (auto script : mSavedScripts) {
+        scripts.AppendElement(script);
+    }
+
+    // Sort scripts by load time, with async loaded scripts before sync scripts.
+    // Since async scripts are always loaded immediately at startup, it helps to
+    // have them stored contiguously.
+    scripts.Sort(CachedScript::Comparator());
+
     OutputBuffer buf;
     size_t offset = 0;
-    for (auto script : mSavedScripts) {
+    for (auto script : scripts) {
         script->mOffset = offset;
         script->Code(buf);
 
         offset += script->mSize;
     }
 
     uint8_t headerSize[4];
     LittleEndian::writeUint32(headerSize, buf.cursor());
 
     MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
     MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
     MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
-
-    for (auto script : mSavedScripts) {
+    for (auto script : scripts) {
         MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
 
         if (script->mScript) {
             script->FreeData();
         }
     }
 
     NS_TRY(cacheFile->MoveTo(nullptr, mBaseName + NS_LITERAL_STRING(".bin")));
@@ -734,22 +730,24 @@ ScriptPreloader::NoteScript(const nsCStr
     } else if (!script) {
         script = new CachedScript(*this, url, cachePath, jsscript);
         mSavedScripts.insertBack(script);
         mScripts.Put(cachePath, script);
     } else {
         return;
     }
 
+    script->UpdateLoadTime(TimeStamp::Now());
     script->mProcessTypes += CurrentProcessType();
 }
 
 void
 ScriptPreloader::NoteScript(const nsCString& url, const nsCString& cachePath,
-                            ProcessType processType, nsTArray<uint8_t>&& xdrData)
+                            ProcessType processType, nsTArray<uint8_t>&& xdrData,
+                            TimeStamp loadTime)
 {
     CachedScript* script = mScripts.Get(cachePath);
     bool restored = script && FindScript(mRestoredScripts, cachePath);
 
     if (restored) {
         script->remove();
         mSavedScripts.insertBack(script);
 
@@ -767,16 +765,17 @@ ScriptPreloader::NoteScript(const nsCStr
             script->mSize = xdrData.Length();
             script->mXDRData.construct<nsTArray<uint8_t>>(Forward<nsTArray<uint8_t>>(xdrData));
 
             auto& data = script->Array();
             script->mXDRRange.emplace(data.Elements(), data.Length());
         }
     }
 
+    script->UpdateLoadTime(loadTime);
     script->mProcessTypes += processType;
 }
 
 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.
--- a/js/xpconnect/loader/ScriptPreloader.h
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -71,17 +71,18 @@ public:
     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);
 
     void NoteScript(const nsCString& url, const nsCString& cachePath,
-                    ProcessType processType, nsTArray<uint8_t>&& xdrData);
+                    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"));
 
     Result<Ok, nsresult> InitCache(const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild);
 
 private:
     Result<Ok, nsresult> InitCacheInternal();
@@ -143,28 +144,64 @@ private:
         {
 #ifdef DEBUG
             auto hashValue = mCache->mScripts.Get(mCachePath);
             MOZ_ASSERT_IF(hashValue, hashValue == this);
 #endif
             mCache->mScripts.Remove(mCachePath);
         }
 
+        // For use with nsTArray::Sort.
+        //
+        // Orders scripts by:
+        //
+        // 1) Async-decoded scripts before sync-decoded scripts, since the
+        //    former are needed immediately at startup, and should be stored
+        //    contiguously.
+        // 2) Script load time, so that scripts which are needed earlier are
+        //    stored earlier, and scripts needed at approximately the same
+        //    time are stored approximately contiguously.
+        struct Comparator
+        {
+            bool Equals(const CachedScript* a, const CachedScript* b) const
+            {
+              return (a->AsyncDecodable() == b->AsyncDecodable() &&
+                      a->mLoadTime == b->mLoadTime);
+            }
+
+            bool LessThan(const CachedScript* a, const CachedScript* b) const
+            {
+              if (a->AsyncDecodable() != b->AsyncDecodable()) {
+                return a->AsyncDecodable();
+              }
+              return a->mLoadTime < b->mLoadTime;
+            }
+        };
+
         void Cancel();
 
         void FreeData()
         {
             // If the script data isn't mmapped, we need to release both it
             // and the Range that points to it at the same time.
             if (!mXDRData.empty()) {
                 mXDRRange.reset();
                 mXDRData.destroy();
             }
         }
 
+        void UpdateLoadTime(const TimeStamp& loadTime)
+        {
+          if (!mLoadTime || loadTime < mLoadTime) {
+            mLoadTime = loadTime;
+          }
+        }
+
+        bool AsyncDecodable() const { return mSize > MIN_OFFTHREAD_SIZE; }
+
         // 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)
@@ -232,16 +269,18 @@ private:
         nsCString mCachePath;
 
         // The offset of this script in the cache file, from the start of the XDR
         // data block.
         uint32_t mOffset = 0;
         // The size of this script's encoded XDR data.
         uint32_t mSize = 0;
 
+        TimeStamp mLoadTime{};
+
         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;