Bug 1361900: Part 10 - Replace linked lists with a single hashtable. r?erahm draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 03 May 2017 16:57:31 -0700
changeset 577047 924590d5bfb6f35818e9ef89d6d2209d210ef561
parent 577046 8ea012bb48b31539319d685a9041e71105196133
child 628409 9e009d833675e3a8ed9ddd89a9a2a5c6510ff0b1
push id58588
push usermaglione.k@gmail.com
push dateFri, 12 May 2017 19:00:00 +0000
reviewerserahm
bugs1361900
milestone55.0a1
Bug 1361900: Part 10 - Replace linked lists with a single hashtable. r?erahm MozReview-Commit-ID: 3qXnswsP6Z0
js/xpconnect/loader/ScriptCacheActors.cpp
js/xpconnect/loader/ScriptCacheActors.h
js/xpconnect/loader/ScriptPreloader-inl.h
js/xpconnect/loader/ScriptPreloader.cpp
js/xpconnect/loader/ScriptPreloader.h
--- a/js/xpconnect/loader/ScriptCacheActors.cpp
+++ b/js/xpconnect/loader/ScriptCacheActors.cpp
@@ -26,24 +26,26 @@ ScriptCacheChild::Init(const Maybe<FileD
         // done.
         Send__delete__(this, AutoTArray<ScriptData, 0>());
     }
 }
 
 // Finalize the script cache for the content process, and send back data about
 // any scripts executed up to this point.
 void
-ScriptCacheChild::Finalize(LinkedList<ScriptPreloader::CachedScript>& scripts)
+ScriptCacheChild::SendScriptsAndFinalize(ScriptPreloader::ScriptHash& scripts)
 {
     MOZ_ASSERT(mWantCacheData);
 
     AutoSafeJSAPI jsapi;
 
+    auto matcher = ScriptPreloader::Match<ScriptPreloader::ScriptStatus::Saved>();
+
     nsTArray<ScriptData> dataArray;
-    for (auto script : scripts) {
+    for (auto& script : IterHash(scripts, matcher)) {
         if (!script->mSize && !script->XDREncode(jsapi.cx())) {
             continue;
         }
 
         auto data = dataArray.AppendElement();
 
         data->url() = script->mURL;
         data->cachePath() = script->mCachePath;
--- a/js/xpconnect/loader/ScriptCacheActors.h
+++ b/js/xpconnect/loader/ScriptCacheActors.h
@@ -44,17 +44,17 @@ class ScriptCacheChild final : public PS
 public:
     ScriptCacheChild() = default;
 
     void Init(const Maybe<FileDescriptor>& cacheFile, bool wantCacheData);
 
 protected:
     virtual void ActorDestroy(ActorDestroyReason aWhy) override;
 
-    void Finalize(LinkedList<ScriptPreloader::CachedScript>& scripts);
+    void SendScriptsAndFinalize(ScriptPreloader::ScriptHash& scripts);
 
 private:
     bool mWantCacheData = false;
 };
 
 
 } // namespace loader
 } // namespace mozilla
--- a/js/xpconnect/loader/ScriptPreloader-inl.h
+++ b/js/xpconnect/loader/ScriptPreloader-inl.h
@@ -203,12 +203,111 @@ private:
 
     bool error_ = false;
 
 public:
     const Range<uint8_t>& data;
     size_t cursor_ = 0;
 };
 
+
+template <typename T> struct Matcher;
+
+// Wraps the iterator for a nsTHashTable so that it may be used as a range
+// iterator. Each iterator result acts as a smart pointer to the hash element,
+// and has a Remove() method which will remove the element from the hash.
+//
+// It also accepts an optional Matcher instance against which to filter the
+// elements which should be iterated over.
+//
+// Example:
+//
+//    for (auto& elem : HashElemIter<HashType>(hash)) {
+//        if (elem->IsDead()) {
+//            elem.Remove();
+//        }
+//    }
+template <typename T>
+class HashElemIter
+{
+    using Iterator = typename T::Iterator;
+    using ElemType = typename T::UserDataType;
+
+    T& hash_;
+    Matcher<ElemType>* matcher_;
+    Maybe<Iterator> iter_;
+
+public:
+    explicit HashElemIter(T& hash, Matcher<ElemType>* matcher = nullptr)
+        : hash_(hash), matcher_(matcher)
+    {
+        iter_.emplace(Move(hash.Iter()));
+    }
+
+    class Elem
+    {
+        friend class HashElemIter<T>;
+
+        HashElemIter<T>& iter_;
+        bool done_;
+
+        Elem(HashElemIter& iter, bool done)
+            : iter_(iter), done_(done)
+        {}
+
+        Iterator& iter() { return iter_.iter_.ref(); }
+
+    public:
+        Elem& operator*() { return *this; }
+
+        ElemType get() { return done_ ? nullptr : iter().Data(); }
+
+        ElemType operator->() { return get(); }
+
+        operator ElemType() { return get(); }
+
+        void Remove() { iter().Remove(); }
+
+        Elem& operator++()
+        {
+            MOZ_ASSERT(!done_);
+
+            do {
+                iter().Next();
+                done_ = iter().Done();
+            } while (!done_ && iter_.matcher_ && !iter_.matcher_->Matches(get()));
+
+            return *this;
+        }
+
+        bool operator!=(Elem& other)
+        {
+            return done_ != other.done_ || this->get() != other.get();
+        }
+    };
+
+    Elem begin() { return Elem(*this, iter_->Done()); }
+
+    Elem end() { return Elem(*this, true); }
+};
+
+template <typename T>
+HashElemIter<T> IterHash(T& hash, Matcher<typename T::UserDataType>* matcher = nullptr)
+{
+    return HashElemIter<T>(hash, matcher);
+}
+
+template <typename T, typename F>
+bool
+Find(T&& iter, F&& match)
+{
+    for (auto& elem : iter) {
+        if (match(elem)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 }; // namespace loader
 }; // namespace mozilla
 
 #endif // ScriptPreloader_inl_h
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -7,16 +7,17 @@
 #include "mozilla/ScriptPreloader.h"
 #include "ScriptPreloader-inl.h"
 #include "mozilla/loader/ScriptCacheActors.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/FileUtils.h"
 #include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
 #include "mozilla/Services.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/ContentParent.h"
 
 #include "MainThreadUtils.h"
 #include "nsDebug.h"
 #include "nsDirectoryServiceUtils.h"
@@ -50,23 +51,23 @@ ProcessType ScriptPreloader::sProcessTyp
 
 
 nsresult
 ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport,
                                 nsISupports* aData, bool aAnonymize)
 {
     MOZ_COLLECT_REPORT(
         "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES,
-        SizeOfLinkedList(mSavedScripts, MallocSizeOf),
+        SizeOfHashEntries<ScriptStatus::Saved>(mScripts, MallocSizeOf),
         "Memory used to hold the scripts which have been executed in this "
         "session, and will be written to the startup script cache file.");
 
     MOZ_COLLECT_REPORT(
         "explicit/script-preloader/heap/restored-scripts", KIND_HEAP, UNITS_BYTES,
-        SizeOfLinkedList(mRestoredScripts, MallocSizeOf),
+        SizeOfHashEntries<ScriptStatus::Restored>(mScripts, MallocSizeOf),
         "Memory used to hold the scripts which have been restored from the "
         "startup script cache file, but have not been executed in this session.");
 
     MOZ_COLLECT_REPORT(
         "explicit/script-preloader/heap/other", KIND_HEAP, UNITS_BYTES,
         ShallowHeapSizeOfIncludingThis(MallocSizeOf),
         "Memory used by the script cache service itself.");
 
@@ -189,21 +190,17 @@ TraceOp(JSTracer* trc, void* data)
     preloader->Trace(trc);
 }
 
 } // anonymous namespace
 
 void
 ScriptPreloader::Trace(JSTracer* trc)
 {
-    for (auto script : mSavedScripts) {
-        JS::TraceEdge(trc, &script->mScript, "ScriptPreloader::CachedScript.mScript");
-    }
-
-    for (auto script : mRestoredScripts) {
+    for (auto& script : IterHash(mScripts)) {
         JS::TraceEdge(trc, &script->mScript, "ScriptPreloader::CachedScript.mScript");
     }
 }
 
 
 ScriptPreloader::ScriptPreloader()
   : mMonitor("[ScriptPreloader.mMonitor]")
   , mSaveMonitor("[ScriptPreloader.mSaveMonitor]")
@@ -254,50 +251,38 @@ ScriptPreloader::Cleanup()
     if (mSaveThread) {
         MonitorAutoLock mal(mSaveMonitor);
 
         while (!mSaveComplete && mSaveThread) {
             mal.Wait();
         }
     }
 
-    mSavedScripts.clear();
-    mRestoredScripts.clear();
+    mScripts.Clear();
 
     AutoSafeJSAPI jsapi;
     JS_RemoveExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
 
     UnregisterWeakMemoryReporter(this);
 }
 
 void
-ScriptPreloader::FlushScripts(LinkedList<CachedScript>& scripts)
+ScriptPreloader::FlushCache()
 {
-    for (auto next = scripts.getFirst(); next; ) {
-        auto script = next;
-        next = script->getNext();
+    MonitorAutoLock mal(mMonitor);
 
+    for (auto& script : IterHash(mScripts)) {
         // We can only purge finished scripts here. Async scripts that are
         // still being parsed off-thread have a non-refcounted reference to
         // this script, which needs to stay alive until they finish parsing.
         if (script->mReadyToExecute) {
             script->Cancel();
-            script->remove();
-            delete script;
+            script.Remove();
         }
     }
-}
-
-void
-ScriptPreloader::FlushCache()
-{
-    MonitorAutoLock mal(mMonitor);
-
-    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 && mChildCache) {
         mSaveComplete = false;
@@ -325,17 +310,17 @@ ScriptPreloader::Observe(nsISupports* su
     } else if (!strcmp(topic, DOC_ELEM_INSERTED_TOPIC)) {
         obs->RemoveObserver(this, DOC_ELEM_INSERTED_TOPIC);
 
         MOZ_ASSERT(XRE_IsContentProcess());
 
         mStartupFinished = true;
 
         if (mChildActor) {
-            mChildActor->Finalize(mSavedScripts);
+            mChildActor->SendScriptsAndFinalize(mScripts);
         }
     } else if (!strcmp(topic, SHUTDOWN_TOPIC)) {
         ForceWriteCacheFile();
     } else if (!strcmp(topic, CLEANUP_TOPIC)) {
         Cleanup();
     } else if (!strcmp(topic, CACHE_FLUSH_TOPIC)) {
         FlushCache();
     }
@@ -444,17 +429,19 @@ ScriptPreloader::InitCacheInternal()
     headerSize = LittleEndian::readUint32(data.get());
     data += sizeof(headerSize);
 
     if (data + headerSize > end) {
         return Err(NS_ERROR_UNEXPECTED);
     }
 
     {
-        AutoCleanLinkedList<CachedScript> scripts;
+        auto cleanup = MakeScopeExit([&] () {
+            mScripts.Clear();
+        });
 
         Range<uint8_t> header(data, data + headerSize);
         data += headerSize;
 
         InputBuffer buf(header);
 
         size_t offset = 0;
         while (!buf.finished()) {
@@ -469,38 +456,36 @@ ScriptPreloader::InitCacheInternal()
             // size, as a basic sanity check.
             if (script->mOffset != offset) {
                 return Err(NS_ERROR_UNEXPECTED);
             }
             offset += script->mSize;
 
             script->mXDRRange.emplace(scriptData, scriptData + script->mSize);
 
-            scripts.insertBack(script.release());
+            mScripts.Put(script->mCachePath, script.release());
         }
 
         if (buf.error()) {
             return Err(NS_ERROR_UNEXPECTED);
         }
 
-        for (auto script : scripts) {
-            mScripts.Put(script->mCachePath, script);
-        }
-        mRestoredScripts = Move(scripts);
+        cleanup.release();
     }
 
     AutoJSAPI jsapi;
     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) {
+
+    for (auto& script : IterHash(mScripts, Match<ScriptStatus::Restored>())) {
         // Only async decode scripts which have been used in this process type.
         if (script->mProcessTypes.contains(CurrentProcessType()) &&
             script->AsyncDecodable() &&
             JS::CanCompileOffThread(cx, options, script->mSize)) {
             DecodeScriptOffThread(cx, script);
         } else {
             script->mReadyToExecute = true;
         }
@@ -531,55 +516,44 @@ ScriptPreloader::PrepareCacheWrite()
             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.
-        bool found = false;
-        for (auto script : mSavedScripts) {
-            if (!script->HasRange() || script->HasArray()) {
-                found = true;
-                break;
-            }
-        }
-        if (!found) {
-            mSaveComplete = true;
-            return;
-        }
+    bool found = Find(IterHash(mScripts), [] (CachedScript* script) {
+        return (script->mStatus == ScriptStatus::Restored ||
+                !script->HasRange() || script->HasArray());
+    });
+
+    if (!found) {
+        mSaveComplete = true;
+        return;
     }
 
     AutoSafeJSAPI jsapi;
 
-    for (CachedScript* next = mSavedScripts.getFirst(); next; ) {
-        CachedScript* script = next;
-        next = script->getNext();
-
+    for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
         // 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)) {
+            if (childScript->mStatus == ScriptStatus::Saved) {
                 childScript->UpdateLoadTime(script->mLoadTime);
                 childScript->mProcessTypes += script->mProcessTypes;
             } else {
                 childScript = nullptr;
             }
         }
 
         if (childScript || (!script->mSize && !script->XDREncode(jsapi.cx()))) {
-            script->remove();
-            delete script;
+            script.Remove();
         } else {
             script->mSize = script->Range().length();
         }
     }
 
     mDataPrepared = true;
 }
 
@@ -624,17 +598,17 @@ ScriptPreloader::WriteCache()
     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) {
+    for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
         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());
 
@@ -685,85 +659,58 @@ ScriptPreloader::Run()
 
     mSaveComplete = true;
     NS_ReleaseOnMainThread(mSaveThread.forget());
 
     mal.NotifyAll();
     return NS_OK;
 }
 
-/* static */ ScriptPreloader::CachedScript*
-ScriptPreloader::FindScript(LinkedList<CachedScript>& scripts, const nsCString& cachePath)
-{
-    for (auto script : scripts) {
-        if (script->mCachePath == cachePath) {
-            return script;
-        }
-    }
-    return nullptr;
-}
-
 void
 ScriptPreloader::NoteScript(const nsCString& url, const nsCString& cachePath,
                             JS::HandleScript jsscript)
 {
     // Don't bother trying to cache any URLs with cache-busting query
     // parameters.
     if (mStartupFinished || !mCacheInitialized || 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;
     }
 
-    CachedScript* script = mScripts.Get(cachePath);
-    bool restored = script && FindScript(mRestoredScripts, cachePath);
+    auto script = mScripts.LookupOrAdd(cachePath, *this, url, cachePath, jsscript);
 
-    if (restored) {
-        script->remove();
-        mSavedScripts.insertBack(script);
+    if (script->mStatus == ScriptStatus::Restored) {
+        script->mStatus = ScriptStatus::Saved;
 
         MOZ_ASSERT(jsscript);
         script->mScript = jsscript;
         script->mReadyToExecute = true;
-    } 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,
                             TimeStamp loadTime)
 {
-    CachedScript* script = mScripts.Get(cachePath);
-    bool restored = script && FindScript(mRestoredScripts, cachePath);
+    auto script = mScripts.LookupOrAdd(cachePath, *this, url, cachePath, nullptr);
 
-    if (restored) {
-        script->remove();
-        mSavedScripts.insertBack(script);
+    if (script->mStatus == ScriptStatus::Restored) {
+        script->mStatus = ScriptStatus::Saved;
 
         script->mReadyToExecute = true;
     } else {
-        if (!script) {
-            script = new CachedScript(this, url, cachePath, nullptr);
-            mSavedScripts.insertBack(script);
-            mScripts.Put(cachePath, script);
-        }
-
         if (!script->HasRange()) {
             MOZ_ASSERT(!script->HasArray());
 
             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());
@@ -864,16 +811,17 @@ ScriptPreloader::OffThreadDecodeCallback
     script->mToken = token;
     script->mReadyToExecute = true;
 
     mal.NotifyAll();
 }
 
 ScriptPreloader::CachedScript::CachedScript(ScriptPreloader& cache, InputBuffer& buf)
     : mCache(cache)
+    , mStatus(ScriptStatus::Restored)
 {
     Code(buf);
 }
 
 bool
 ScriptPreloader::CachedScript::XDREncode(JSContext* cx)
 {
     JSAutoCompartment ac(cx, mScript);
--- a/js/xpconnect/loader/ScriptPreloader.h
+++ b/js/xpconnect/loader/ScriptPreloader.h
@@ -12,17 +12,17 @@
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/MaybeOneOf.h"
 #include "mozilla/Monitor.h"
 #include "mozilla/Range.h"
 #include "mozilla/Vector.h"
 #include "mozilla/Result.h"
 #include "mozilla/loader/AutoMemMap.h"
-#include "nsDataHashtable.h"
+#include "nsClassHashtable.h"
 #include "nsIFile.h"
 #include "nsIMemoryReporter.h"
 #include "nsIObserver.h"
 #include "nsIThread.h"
 
 #include "jsapi.h"
 
 #include <prio.h>
@@ -38,16 +38,22 @@ namespace loader {
     class InputBuffer;
     class ScriptCacheChild;
 
     enum class ProcessType : uint8_t {
         Parent,
         Web,
         Extension,
     };
+
+    template <typename T>
+    struct Matcher
+    {
+        virtual bool Matches(T) = 0;
+    };
 }
 
 using namespace mozilla::loader;
 
 class ScriptPreloader : public nsIObserver
                       , public nsIMemoryReporter
                       , public nsIRunnable
 {
@@ -96,16 +102,21 @@ public:
     }
 
     static void InitContentChild(dom::ContentParent& parent);
 
 protected:
     virtual ~ScriptPreloader() = default;
 
 private:
+    enum class ScriptStatus {
+      Restored,
+      Saved,
+    };
+
     // Represents a cached JS script, either initially read from the script
     // cache file, to be added to the next session's script cache file, or
     // both.
     //
     // A script which was read from the cache file may be in any of the
     // following states:
     //
     //  - Read from the cache, and being compiled off thread. In this case,
@@ -120,39 +131,33 @@ private:
     //    file. In this case, mReadyToExecute is true, and mScript is non-null.
     //
     // A script to be added to the next session's cache file always has a
     // non-null mScript value. If it was read from the last session's cache
     // file, it also has a non-empty mXDRRange range, which will be stored in
     // 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>
+    class CachedScript
     {
     public:
         CachedScript(CachedScript&&) = default;
 
         CachedScript(ScriptPreloader& cache, const nsCString& url, const nsCString& cachePath, JSScript* script)
             : mCache(cache)
             , mURL(url)
             , mCachePath(cachePath)
+            , mStatus(ScriptStatus::Saved)
             , mScript(script)
             , mReadyToExecute(true)
         {}
 
         inline CachedScript(ScriptPreloader& cache, InputBuffer& buf);
 
-        ~CachedScript()
-        {
-#ifdef DEBUG
-            auto hashValue = mCache->mScripts.Get(mCachePath);
-            MOZ_ASSERT_IF(hashValue, hashValue == this);
-#endif
-            mCache->mScripts.Remove(mCachePath);
-        }
+        ~CachedScript() = default;
 
         // 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.
@@ -171,16 +176,28 @@ private:
             {
               if (a->AsyncDecodable() != b->AsyncDecodable()) {
                 return a->AsyncDecodable();
               }
               return a->mLoadTime < b->mLoadTime;
             }
         };
 
+        struct StatusMatcher final : public Matcher<CachedScript*>
+        {
+            StatusMatcher(ScriptStatus status) : mStatus(status) {}
+
+            virtual bool Matches(CachedScript* script)
+            {
+                return script->mStatus == mStatus;
+            }
+
+            const ScriptStatus mStatus;
+        };
+
         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();
@@ -269,16 +286,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;
 
+        ScriptStatus mStatus;
+
         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.
@@ -296,16 +315,23 @@ private:
         // compiled during this session.
         Maybe<JS::TranscodeRange> mXDRRange;
 
         // XDR data which was generated from a script compiled during this
         // session, and will be written to the cache file.
         MaybeOneOf<JS::TranscodeBuffer, nsTArray<uint8_t>> mXDRData;
     };
 
+    template <ScriptStatus status>
+    static Matcher<CachedScript*>* Match()
+    {
+        static CachedScript::StatusMatcher matcher{status};
+        return &matcher;
+    }
+
     // There's a trade-off between the time it takes to setup an off-thread
     // decode and the time we save by doing the decode off-thread. At this
     // point, the setup is quite expensive, and 20K is about where we start to
     // see an improvement rather than a regression.
     //
     // This also means that we get much better performance loading one big
     // script than several small scripts, since the setup is per-script, and the
     // OMT compile is almost always complete by the time we need a given script.
@@ -319,71 +345,61 @@ private:
     static constexpr int MAX_MAINTHREAD_DECODE_SIZE = 50 * 1024;
 
     ScriptPreloader();
 
     void ForceWriteCacheFile();
     void Cleanup();
 
     void FlushCache();
-    void FlushScripts(LinkedList<CachedScript>& scripts);
 
     // Opens the cache file for reading.
     Result<Ok, nsresult> OpenCache();
 
     // Writes a new cache file to disk. Must not be called on the main thread.
     Result<Ok, nsresult> WriteCache();
 
     // 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 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.
     void DecodeScriptOffThread(JSContext* cx, CachedScript* script);
 
     static void OffThreadDecodeCallback(void* token, void* context);
     void CancelOffThreadParse(void* token);
 
     size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf)
     {
         return (mallocSizeOf(this) + mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) +
                 mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get()));
     }
 
-    template<typename T>
-    static size_t SizeOfLinkedList(LinkedList<T>& list, mozilla::MallocSizeOf mallocSizeOf)
+    using ScriptHash = nsClassHashtable<nsCStringHashKey, CachedScript>;
+
+    template<ScriptStatus status>
+    static size_t SizeOfHashEntries(ScriptHash& scripts, mozilla::MallocSizeOf mallocSizeOf)
     {
         size_t size = 0;
-        for (auto elem : list) {
+        for (auto elem : IterHash(scripts, Match<status>())) {
             size += elem->HeapSizeOfIncludingThis(mallocSizeOf);
         }
         return size;
     }
 
-    // The list of scripts executed during this session, and being saved for
-    // potential reuse, and to be written to the next session's cache file.
-    AutoCleanLinkedList<CachedScript> mSavedScripts;
-
-    // The list of scripts restored from the cache file at the start of this
-    // session. Scripts are removed from this list and moved to mSavedScripts
-    // the first time they're used during this session.
-    AutoCleanLinkedList<CachedScript> mRestoredScripts;
-
-    nsDataHashtable<nsCStringHashKey, CachedScript*> mScripts;
+    ScriptHash mScripts;
 
     // True after we've shown the first window, and are no longer adding new
     // scripts to the cache.
     bool mStartupFinished = false;
 
     bool mCacheInitialized = false;
     bool mSaveComplete = false;
     bool mDataPrepared = false;