Bug 1363482: Part 2 - Allow pre-loading file and JAR entry contents off-thread during startup. r?erahm draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 30 Aug 2017 15:47:17 -0700
changeset 656256 e2e35c9a6fe57882516545bb83e67146102bbe33
parent 656255 e20d79d368925aabae49ce23afb6844bfb4c1939
child 656257 27051af8e2abb40723fb768618a0551e1850c891
push id77137
push usermaglione.k@gmail.com
push dateWed, 30 Aug 2017 23:02:44 +0000
reviewerserahm
bugs1363482
milestone57.0a1
Bug 1363482: Part 2 - Allow pre-loading file and JAR entry contents off-thread during startup. r?erahm MozReview-Commit-ID: 8bKzYpXBQvT
js/xpconnect/loader/ScriptPreloader-inl.h
js/xpconnect/loader/ScriptPreloader.cpp
js/xpconnect/loader/URLPreloader.cpp
js/xpconnect/loader/URLPreloader.h
js/xpconnect/loader/moz.build
xpcom/build/FileLocation.cpp
xpcom/build/FileLocation.h
--- a/js/xpconnect/loader/ScriptPreloader-inl.h
+++ b/js/xpconnect/loader/ScriptPreloader-inl.h
@@ -35,16 +35,25 @@ public:
 
   operator nsresult() { return mErrorValue; }
 };
 
 namespace loader {
 
 using mozilla::dom::AutoJSAPI;
 
+static inline Result<Ok, nsresult>
+Write(PRFileDesc* fd, const void* data, int32_t len)
+{
+    if (PR_Write(fd, data, len) != len) {
+        return Err(NS_ERROR_FAILURE);
+    }
+    return Ok();
+}
+
 struct MOZ_RAII AutoSafeJSAPI : public AutoJSAPI
 {
     AutoSafeJSAPI() { Init(); }
 };
 
 static inline Result<Ok, nsresult>
 WrapNSResult(PRStatus aRv)
 {
@@ -289,18 +298,25 @@ public:
         ElemType get()
         {
           if (done_) {
             return nullptr;
           }
           return iter().Data();
         }
 
+        const ElemType get() const
+        {
+          return const_cast<Elem*>(this)->get();
+        }
+
         ElemType operator->() { return get(); }
 
+        const ElemType operator->() const { return get(); }
+
         operator ElemType() { return get(); }
 
         void Remove() { iter().Remove(); }
 
         Elem& operator++()
         {
             MOZ_ASSERT(!done_);
 
--- a/js/xpconnect/loader/ScriptPreloader.cpp
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -3,16 +3,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/ScriptPreloader.h"
 #include "ScriptPreloader-inl.h"
 #include "mozilla/loader/ScriptCacheActors.h"
 
+#include "mozilla/URLPreloader.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"
@@ -408,16 +410,20 @@ ScriptPreloader::InitCache(const nsAStri
     mBaseName = basePath;
 
     RegisterWeakMemoryReporter(this);
 
     if (!XRE_IsParentProcess()) {
         return Ok();
     }
 
+    // Note: Code on the main thread *must not access Omnijar in any way* until
+    // this AutoBeginReading guard is destroyed.
+    URLPreloader::AutoBeginReading abr;
+
     MOZ_TRY(OpenCache());
 
     return InitCacheInternal();
 }
 
 Result<Ok, nsresult>
 ScriptPreloader::InitCache(const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild)
 {
@@ -512,25 +518,16 @@ ScriptPreloader::InitCacheInternal()
         mPendingScripts = Move(scripts);
         cleanup.release();
     }
 
     DecodeNextBatch(OFF_THREAD_FIRST_CHUNK_SIZE);
     return Ok();
 }
 
-static inline Result<Ok, nsresult>
-Write(PRFileDesc* fd, const void* data, int32_t len)
-{
-    if (PR_Write(fd, data, len) != len) {
-        return Err(NS_ERROR_FAILURE);
-    }
-    return Ok();
-}
-
 void
 ScriptPreloader::PrepareCacheWriteInternal()
 {
     MOZ_ASSERT(NS_IsMainThread());
 
     mMonitor.AssertCurrentThreadOwns();
 
     auto cleanup = MakeScopeExit([&] () {
@@ -598,16 +595,18 @@ ScriptPreloader::PrepareCacheWrite()
 //
 // - A block of XDR data for the encoded scripts, with each script's data at
 //   an offset from the start of the block, as specified above.
 Result<Ok, nsresult>
 ScriptPreloader::WriteCache()
 {
     MOZ_ASSERT(!NS_IsMainThread());
 
+    URLPreloader::GetSingleton().WriteCache();
+
     if (!mDataPrepared && !mSaveComplete) {
         MOZ_ASSERT(!mBlockedOnSyncDispatch);
         mBlockedOnSyncDispatch = true;
 
         MonitorAutoUnlock mau(mSaveMonitor);
 
         NS_DispatchToMainThread(
           NewRunnableMethod("ScriptPreloader::PrepareCacheWrite",
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/loader/URLPreloader.cpp
@@ -0,0 +1,690 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=4 et sw=4 tw=99: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/URLPreloader.h"
+#include "mozilla/loader/AutoMemMap.h"
+#include "ScriptPreloader-inl.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/Vector.h"
+
+#include "MainThreadUtils.h"
+#include "nsPrintfCString.h"
+#include "nsDebug.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIObserverService.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsZipArchive.h"
+#include "xpcpublic.h"
+
+#include "mozilla/dom/ContentChild.h"
+
+#undef DELAYED_STARTUP_TOPIC
+#define DELAYED_STARTUP_TOPIC "sessionstore-windows-restored"
+
+namespace mozilla {
+namespace {
+static LazyLogModule gURLLog("URLPreloader");
+
+#define LOG(level, ...) MOZ_LOG(gURLLog, LogLevel::level, (__VA_ARGS__))
+
+template<typename T>
+bool
+StartsWith(const T& haystack, const T& needle)
+{
+    return StringHead(haystack, needle.Length()) == needle;
+}
+} // anonymous namespace
+
+using namespace mozilla::loader;
+
+nsresult
+URLPreloader::CollectReports(nsIHandleReportCallback* aHandleReport,
+                             nsISupports* aData, bool aAnonymize)
+{
+    MOZ_COLLECT_REPORT(
+        "explicit/url-preloader/other", KIND_HEAP, UNITS_BYTES,
+        ShallowSizeOfIncludingThis(MallocSizeOf),
+        "Memory used by the URL preloader service itself.");
+
+    for (const auto& elem : IterHash(mCachedURLs)) {
+        nsAutoCString pathName;
+        pathName.Append(elem->mPath);
+        // The backslashes will automatically be replaced with slashes in
+        // about:memory, without splitting each path component into a separate
+        // branch in the memory report tree.
+        pathName.ReplaceChar('/', '\\');
+
+        nsPrintfCString path("explicit/url-preloader/cached-urls/%s/[%s]",
+                             elem->TypeString(), pathName.get());
+
+        aHandleReport->Callback(
+            EmptyCString(), path, KIND_HEAP, UNITS_BYTES,
+            elem->SizeOfIncludingThis(MallocSizeOf),
+            NS_LITERAL_CSTRING("Memory used to hold cache data for files which "
+                               "have been read or pre-loaded during this session."),
+            aData);
+    }
+
+    return NS_OK;
+}
+
+
+URLPreloader&
+URLPreloader::GetSingleton()
+{
+    static RefPtr<URLPreloader> singleton;
+
+    if (!singleton) {
+        singleton = new URLPreloader();
+        ClearOnShutdown(&singleton);
+    }
+
+    return *singleton;
+}
+
+
+bool URLPreloader::sInitialized = false;
+
+URLPreloader::URLPreloader()
+{
+    if (InitInternal().isOk()) {
+        sInitialized = true;
+        RegisterWeakMemoryReporter(this);
+    }
+}
+
+URLPreloader::~URLPreloader()
+{
+    if (sInitialized) {
+        UnregisterWeakMemoryReporter(this);
+    }
+}
+
+Result<Ok, nsresult>
+URLPreloader::InitInternal()
+{
+    MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+    if (Omnijar::HasOmnijar(Omnijar::GRE)) {
+        NS_TRY(Omnijar::GetURIString(Omnijar::GRE, mGREPrefix));
+    }
+    if (Omnijar::HasOmnijar(Omnijar::APP)) {
+        NS_TRY(Omnijar::GetURIString(Omnijar::APP, mAppPrefix));
+    }
+
+    nsresult rv;
+    nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv);
+    NS_TRY(rv);
+
+    nsCOMPtr<nsIProtocolHandler> ph;
+    NS_TRY(ios->GetProtocolHandler("resource", getter_AddRefs(ph)));
+
+    mResProto = do_QueryInterface(ph, &rv);
+    NS_TRY(rv);
+
+    mChromeReg = services::GetChromeRegistryService();
+    if (!mChromeReg) {
+        return Err(NS_ERROR_UNEXPECTED);
+    }
+
+    if (XRE_IsParentProcess()) {
+        nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+
+        obs->AddObserver(this, DELAYED_STARTUP_TOPIC, false);
+
+        NS_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
+    } else {
+        mStartupFinished = true;
+        mReaderInitialized = true;
+    }
+
+    return Ok();
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult>
+URLPreloader::GetCacheFile(const nsAString& suffix)
+{
+    if (!mProfD) {
+        return Err(NS_ERROR_NOT_INITIALIZED);
+    }
+
+    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->Append(NS_LITERAL_STRING("urlCache") + suffix));
+
+    return Move(cacheFile);
+}
+
+static const uint8_t URL_MAGIC[] = "mozURLcachev001";
+
+Result<nsCOMPtr<nsIFile>, nsresult>
+URLPreloader::FindCacheFile()
+{
+    nsCOMPtr<nsIFile> cacheFile;
+    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("urlCache-current.bin")));
+    } else {
+        NS_TRY(cacheFile->SetLeafName(NS_LITERAL_STRING("urlCache-current.bin")));
+        NS_TRY(cacheFile->Exists(&exists));
+        if (!exists) {
+            return Err(NS_ERROR_FILE_NOT_FOUND);
+        }
+    }
+
+    return Move(cacheFile);
+}
+
+Result<Ok, nsresult>
+URLPreloader::WriteCache()
+{
+    MOZ_ASSERT(!NS_IsMainThread());
+
+    nsCOMPtr<nsIFile> cacheFile;
+    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;
+        NS_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, &fd.rwget()));
+
+        nsTArray<URLEntry*> entries;
+        for (auto& entry : IterHash(mCachedURLs)) {
+            if (entry->mReadTime) {
+                entries.AppendElement(entry);
+            }
+        }
+
+        entries.Sort(URLEntry::Comparator());
+
+        OutputBuffer buf;
+        for (auto entry : entries) {
+            entry->Code(buf);
+        }
+
+        uint8_t headerSize[4];
+        LittleEndian::writeUint32(headerSize, buf.cursor());
+
+        MOZ_TRY(Write(fd, URL_MAGIC, sizeof(URL_MAGIC)));
+        MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+        MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
+    }
+
+    NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("urlCache.bin")));
+
+    NS_DispatchToMainThread(
+        NewRunnableMethod("URLPreloader::Cleanup",
+                          this,
+                          &URLPreloader::Cleanup));
+
+    return Ok();
+}
+
+void
+URLPreloader::Cleanup()
+{
+    mCachedURLs.Clear();
+}
+
+Result<Ok, nsresult>
+URLPreloader::ReadCache(LinkedList<URLEntry>& pendingURLs)
+{
+    nsCOMPtr<nsIFile> cacheFile;
+    MOZ_TRY_VAR(cacheFile, FindCacheFile());
+
+    AutoMemMap cache;
+    MOZ_TRY(cache.init(cacheFile));
+
+    auto size = cache.size();
+
+    uint32_t headerSize;
+    if (size < sizeof(URL_MAGIC) + sizeof(headerSize)) {
+        return Err(NS_ERROR_UNEXPECTED);
+    }
+
+    auto data = cache.get<uint8_t>();
+    auto end = data + size;
+
+    if (memcmp(URL_MAGIC, data.get(), sizeof(URL_MAGIC))) {
+        return Err(NS_ERROR_UNEXPECTED);
+    }
+    data += sizeof(URL_MAGIC);
+
+    headerSize = LittleEndian::readUint32(data.get());
+    data += sizeof(headerSize);
+
+    if (data + headerSize > end) {
+        return Err(NS_ERROR_UNEXPECTED);
+    }
+
+    {
+        mMonitor.AssertCurrentThreadOwns();
+
+        auto cleanup = MakeScopeExit([&] () {
+            while (auto* elem = pendingURLs.getFirst()) {
+                elem->remove();
+            }
+            mCachedURLs.Clear();
+        });
+
+        Range<uint8_t> header(data, data + headerSize);
+        data += headerSize;
+
+        InputBuffer buf(header);
+        while (!buf.finished()) {
+            CacheKey key(buf);
+
+            auto entry = mCachedURLs.LookupOrAdd(key, key);
+            entry->mResultCode = NS_ERROR_NOT_INITIALIZED;
+
+            pendingURLs.insertBack(entry);
+        }
+
+        if (buf.error()) {
+            return Err(NS_ERROR_UNEXPECTED);
+        }
+
+        cleanup.release();
+    }
+
+    return Ok();
+}
+
+void
+URLPreloader::BackgroundReadFiles()
+{
+    Vector<nsZipCursor> cursors;
+    LinkedList<URLEntry> pendingURLs;
+
+    {
+        MonitorAutoLock mal(mMonitor);
+
+        if (ReadCache(pendingURLs).isErr()) {
+            mReaderInitialized = true;
+            mal.NotifyAll();
+            return;
+        }
+
+        int numZipEntries = 0;
+        for (auto entry : pendingURLs) {
+            if (entry->mType != entry->TypeFile) {
+                numZipEntries++;
+            }
+        }
+        MOZ_RELEASE_ASSERT(cursors.reserve(numZipEntries));
+
+        // Initialize the zip cursors for all files in Omnijar while the monitor
+        // is locked. Omnijar is not threadsafe, so the caller of
+        // AutoBeginReading guard must ensure that no code accesses Omnijar
+        // until this segment is done. Once the cursors have been initialized,
+        // the actual reading and decompression can safely be done off-thread,
+        // as is the case for thread-retargeted jar: channels.
+        for (auto entry : pendingURLs) {
+            if (entry->mType == entry->TypeFile) {
+                continue;
+            }
+
+            RefPtr<nsZipArchive> zip = entry->Archive();
+
+            auto item = zip->GetItem(entry->mPath.get());
+            if (!item) {
+                entry->mResultCode = NS_ERROR_FILE_NOT_FOUND;
+                continue;
+            }
+
+            size_t size = item->RealSize();
+
+            entry->mData.SetLength(size);
+            auto data = entry->mData.BeginWriting();
+
+            cursors.infallibleEmplaceBack(item, zip, reinterpret_cast<uint8_t*>(data),
+                                          size, true);
+        }
+
+        mReaderInitialized = true;
+        mal.NotifyAll();
+    }
+
+    // Loop over the entries, read the file's contents, store them in the
+    // entry's mData pointer, and notify any waiting threads to check for
+    // completion.
+    uint32_t i = 0;
+    for (auto entry : pendingURLs) {
+        // If there is any other error code, the entry has already failed at
+        // this point, so don't bother trying to read it again.
+        if (entry->mResultCode != NS_ERROR_NOT_INITIALIZED) {
+            continue;
+        }
+
+        nsresult rv = NS_OK;
+
+        if (entry->mType == entry->TypeFile) {
+            auto result = entry->Read();
+            if (result.isErr()) {
+                rv = result.unwrapErr();
+            }
+        } else {
+            auto& cursor = cursors[i++];
+
+            uint32_t len;
+            cursor.Copy(&len);
+            if (len != entry->mData.Length()) {
+                entry->mData.Truncate();
+                rv = NS_ERROR_FAILURE;
+            }
+        }
+
+        entry->mResultCode = rv;
+        mMonitor.NotifyAll();
+    }
+
+    // We're done reading pending entries, so clear the list.
+    pendingURLs.clear();
+
+    NS_DispatchToMainThread(
+        NewRunnableMethod("nsIThread::Shutdown",
+                          mReaderThread, &nsIThread::Shutdown));
+    mReaderThread = nullptr;
+}
+
+void
+URLPreloader::BeginBackgroundRead()
+{
+    if (!mReaderThread && !mReaderInitialized && sInitialized) {
+        nsCOMPtr<nsIRunnable> runnable =
+            NewRunnableMethod("URLPreloader::BackgroundReadFiles",
+                              this,
+                              &URLPreloader::BackgroundReadFiles);
+
+        Unused << NS_NewNamedThread(
+            "BGReadURLs", getter_AddRefs(mReaderThread), runnable);
+    }
+}
+
+
+Result<const nsCString, nsresult>
+URLPreloader::ReadInternal(const CacheKey& key, ReadType readType)
+{
+    if (mStartupFinished) {
+        URLEntry entry(key);
+
+        return entry.Read();
+    }
+
+    auto entry = mCachedURLs.LookupOrAdd(key, key);
+
+    entry->UpdateUsedTime();
+
+    return entry->ReadOrWait(readType);
+}
+
+Result<const nsCString, nsresult>
+URLPreloader::ReadURIInternal(nsIURI* uri, ReadType readType)
+{
+    CacheKey key;
+    MOZ_TRY_VAR(key, ResolveURI(uri));
+
+    return ReadInternal(key, readType);
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::Read(const CacheKey& key, ReadType readType)
+{
+    // If we're being called before the preloader has been initialized (i.e.,
+    // before the profile has been initialized), just fall back to a synchronous
+    // read. This happens when we're reading .ini and preference files that are
+    // needed to locate and initialize the profile.
+    if (!sInitialized) {
+        return URLEntry(key).Read();
+    }
+
+    return GetSingleton().ReadInternal(key, readType);
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::ReadURI(nsIURI* uri, ReadType readType)
+{
+    if (!sInitialized) {
+        return Err(NS_ERROR_NOT_INITIALIZED);
+    }
+
+    return GetSingleton().ReadURIInternal(uri, readType);
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::ReadFile(nsIFile* file, ReadType readType)
+{
+    return Read(CacheKey(file), readType);
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::ReadFile(const nsACString& path, ReadType readType)
+{
+    CacheKey key(CacheKey::TypeFile, path);
+    return Read(key, readType);
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::Read(FileLocation& location, ReadType readType)
+{
+    if (location.IsZip()) {
+        if (location.GetBaseZip()) {
+            nsCString path;
+            location.GetPath(path);
+            return ReadZip(location.GetBaseZip(), path);
+        }
+        return URLEntry::ReadLocation(location);
+    }
+
+    nsCOMPtr<nsIFile> file = location.GetBaseFile();
+    return ReadFile(file, readType);
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::ReadZip(nsZipArchive* zip, const nsACString& path, ReadType readType)
+{
+    // If the zip archive belongs to an Omnijar location, map it to a cache
+    // entry, and cache it as normal. Otherwise, simply read the entry
+    // synchronously, since other JAR archives are currently unsupported by the
+    // cache.
+    RefPtr<nsZipArchive> reader = Omnijar::GetReader(Omnijar::GRE);
+    if (zip == reader) {
+        CacheKey key(CacheKey::TypeGREJar, path);
+        return Read(key, readType);
+    }
+
+    reader = Omnijar::GetReader(Omnijar::APP);
+    if (zip == reader) {
+        CacheKey key(CacheKey::TypeAppJar, path);
+        return Read(key, readType);
+    }
+
+    // Not an Omnijar archive, so just read it directly.
+    FileLocation location(zip, path.BeginReading());
+    return URLEntry::ReadLocation(location);
+}
+
+Result<URLPreloader::CacheKey, nsresult>
+URLPreloader::ResolveURI(nsIURI* uri)
+{
+    nsCString spec;
+    nsCString scheme;
+    NS_TRY(uri->GetSpec(spec));
+    NS_TRY(uri->GetScheme(scheme));
+
+    nsCOMPtr<nsIURI> resolved;
+
+    // If the URI is a resource: or chrome: URI, first resolve it to the
+    // underlying URI that it wraps.
+    if (scheme.EqualsLiteral("resource")) {
+        NS_TRY(mResProto->ResolveURI(uri, spec));
+        NS_TRY(NS_NewURI(getter_AddRefs(resolved), spec));
+    } else if (scheme.EqualsLiteral("chrome")) {
+        NS_TRY(mChromeReg->ConvertChromeURL(uri, getter_AddRefs(resolved)));
+        NS_TRY(resolved->GetSpec(spec));
+    } else {
+        resolved = uri;
+    }
+    NS_TRY(resolved->GetScheme(scheme));
+
+    // Try the GRE and App Omnijar prefixes.
+    if (mGREPrefix.Length() && StartsWith(spec, mGREPrefix)) {
+        return CacheKey(CacheKey::TypeGREJar,
+                        Substring(spec, mGREPrefix.Length()));
+    }
+
+    if (mAppPrefix.Length() && StartsWith(spec, mAppPrefix)) {
+        return CacheKey(CacheKey::TypeAppJar,
+                        Substring(spec, mAppPrefix.Length()));
+    }
+
+    // Try for a file URI.
+    if (scheme.EqualsLiteral("file")) {
+        nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(resolved);
+        MOZ_ASSERT(fileURL);
+
+        nsCOMPtr<nsIFile> file;
+        NS_TRY(fileURL->GetFile(getter_AddRefs(file)));
+
+        nsCString path;
+        NS_TRY(file->GetNativePath(path));
+
+        return CacheKey(CacheKey::TypeFile, path);
+    }
+
+    // Not a file or Omnijar URI, so currently unsupported.
+    return Err(NS_ERROR_INVALID_ARG);
+}
+
+size_t
+URLPreloader::ShallowSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf)
+{
+    return (mallocSizeOf(this) +
+            mAppPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
+            mGREPrefix.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
+            mCachedURLs.ShallowSizeOfExcludingThis(mallocSizeOf));
+}
+
+Result<FileLocation, nsresult>
+URLPreloader::CacheKey::ToFileLocation()
+{
+    if (mType == TypeFile) {
+        nsCOMPtr<nsIFile> file;
+        NS_TRY(NS_NewNativeLocalFile(mPath, false, getter_AddRefs(file)));
+        return Move(FileLocation(file));
+    }
+
+    RefPtr<nsZipArchive> zip = Archive();
+    return Move(FileLocation(zip, mPath.get()));
+}
+
+Result<const nsCString, nsresult>
+URLPreloader::URLEntry::Read()
+{
+    FileLocation location;
+    MOZ_TRY_VAR(location, ToFileLocation());
+
+    MOZ_TRY_VAR(mData, ReadLocation(location));
+    return mData;
+}
+
+/* static */ Result<const nsCString, nsresult>
+URLPreloader::URLEntry::ReadLocation(FileLocation& location)
+{
+    FileLocation::Data data;
+    NS_TRY(location.GetData(data));
+
+    uint32_t size;
+    NS_TRY(data.GetSize(&size));
+
+    nsCString result;
+    result.SetLength(size);
+    NS_TRY(data.Copy(result.BeginWriting(), size));
+
+    return Move(result);
+}
+
+Result<const nsCString, nsresult>
+URLPreloader::URLEntry::ReadOrWait(ReadType readType)
+{
+    auto now = TimeStamp::Now();
+    LOG(Info, "Reading %s\n", mPath.get());
+    auto cleanup = MakeScopeExit([&] () {
+        LOG(Info, "Read in %fms\n", (TimeStamp::Now() - now).ToMilliseconds());
+    });
+
+    if (mResultCode == NS_ERROR_NOT_INITIALIZED) {
+        MonitorAutoLock mal(GetSingleton().mMonitor);
+
+        while (mResultCode == NS_ERROR_NOT_INITIALIZED) {
+            mal.Wait();
+        }
+    }
+
+    if (mResultCode == NS_OK && mData.IsVoid()) {
+        LOG(Info, "Reading synchronously...\n");
+        return Read();
+    }
+
+    if (NS_FAILED(mResultCode)) {
+        return Err(mResultCode);
+    }
+
+    nsCString res = mData;
+
+    if (readType == Forget) {
+        mData.SetIsVoid(true);
+    }
+    return res;
+}
+
+inline
+URLPreloader::CacheKey::CacheKey(InputBuffer& buffer)
+{
+    Code(buffer);
+}
+
+nsresult
+URLPreloader::Observe(nsISupports* subject, const char* topic, const char16_t* data)
+{
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    if (!strcmp(topic, DELAYED_STARTUP_TOPIC)) {
+        obs->RemoveObserver(this, DELAYED_STARTUP_TOPIC);
+        mStartupFinished = true;
+    }
+
+    return NS_OK;
+}
+
+
+NS_IMPL_ISUPPORTS(URLPreloader, nsIObserver, nsIMemoryReporter)
+
+#undef LOG
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/loader/URLPreloader.h
@@ -0,0 +1,325 @@
+/* -*-  Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef URLPreloader_h
+#define URLPreloader_h
+
+#include "mozilla/FileLocation.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Omnijar.h"
+#include "mozilla/Range.h"
+#include "mozilla/Vector.h"
+#include "mozilla/Result.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIChromeRegistry.h"
+#include "nsIFile.h"
+#include "nsIURI.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserver.h"
+#include "nsIResProtocolHandler.h"
+#include "nsIThread.h"
+#include "nsReadableUtils.h"
+
+class nsZipArchive;
+
+namespace mozilla {
+namespace loader {
+    class InputBuffer;
+}
+
+using namespace mozilla::loader;
+
+class ScriptPreloader;
+
+/**
+ * A singleton class to manage loading local URLs during startup, recording
+ * them, and pre-loading them during early startup in the next session. URLs
+ * that are not already loaded (or already being pre-loaded) when required are
+ * read synchronously from disk, and (if startup is not already complete)
+ * added to the pre-load list for the next session.
+ */
+class URLPreloader final : public nsIObserver
+                         , public nsIMemoryReporter
+{
+    MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+    URLPreloader();
+
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSIOBSERVER
+    NS_DECL_NSIMEMORYREPORTER
+
+    static URLPreloader& GetSingleton();
+
+    // The type of read operation to perform.
+    enum ReadType
+    {
+        // Read the file and then immediately forget its data.
+        Forget,
+        // Read the file and retain its data for the next caller.
+        Retain,
+    };
+
+    // Helpers to read the contents of files or JAR archive entries with various
+    // representations. If the preloader has not yet been initialized, or the
+    // given location is not supported by the cache, the entries will be read
+    // synchronously, and not stored in the cache.
+    static Result<const nsCString, nsresult> Read(FileLocation& location, ReadType readType = Forget);
+
+    static Result<const nsCString, nsresult> ReadURI(nsIURI* uri, ReadType readType = Forget);
+
+    static Result<const nsCString, nsresult> ReadFile(nsIFile* file, ReadType readType = Forget);
+
+    static Result<const nsCString, nsresult> ReadFile(const nsACString& path, ReadType readType = Forget);
+
+    static Result<const nsCString, nsresult> ReadZip(nsZipArchive* archive,
+                                                     const nsACString& path,
+                                                     ReadType readType = Forget);
+
+private:
+    struct CacheKey;
+
+    Result<const nsCString, nsresult> ReadInternal(const CacheKey& key, ReadType readType);
+
+    Result<const nsCString, nsresult> ReadURIInternal(nsIURI* uri, ReadType readType);
+
+    Result<const nsCString, nsresult> ReadFileInternal(nsIFile* file, ReadType readType);
+
+    static Result<const nsCString, nsresult> Read(const CacheKey& key, ReadType readType);
+
+    static bool sInitialized;
+
+protected:
+    friend class ScriptPreloader;
+
+    virtual ~URLPreloader();
+
+    Result<Ok, nsresult> WriteCache();
+
+    // Clear leftover entries after the cache has been written.
+    void Cleanup();
+
+    // Begins reading files off-thread, and ensures that initialization has
+    // completed before leaving the current scope. The caller *must* ensure that
+    // no code on the main thread access Omnijar, either directly or indirectly,
+    // for the lifetime of this guard object.
+    struct MOZ_RAII AutoBeginReading final
+    {
+        AutoBeginReading()
+        {
+            GetSingleton().BeginBackgroundRead();
+        }
+
+        ~AutoBeginReading()
+        {
+            auto& reader = GetSingleton();
+
+            MonitorAutoLock mal(reader.mMonitor);
+
+            while (!reader.mReaderInitialized && reader.sInitialized) {
+                mal.Wait();
+            }
+        }
+    };
+
+private:
+    // Represents a key for an entry in the URI cache, based on its file or JAR
+    // location.
+    struct CacheKey
+    {
+        // The type of the entry. TypeAppJar and TypeGREJar entries are in the
+        // app-specific or toolkit Omnijar files, and are handled specially.
+        // TypeFile entries are plain files in the filesystem.
+        enum EntryType : uint8_t
+        {
+            TypeAppJar,
+            TypeGREJar,
+            TypeFile,
+        };
+
+        CacheKey() = default;
+        CacheKey(const CacheKey& other) = default;
+
+        CacheKey(EntryType type, const nsACString& path)
+            : mType(type), mPath(path)
+        {}
+
+        explicit CacheKey(nsIFile* file)
+          : mType(TypeFile)
+        {
+            MOZ_ALWAYS_SUCCEEDS(file->GetNativePath(mPath));
+        }
+
+        explicit inline CacheKey(InputBuffer& buffer);
+
+        // Encodes or decodes the cache key for storage in a session cache file.
+        template <typename Buffer>
+        void Code(Buffer& buffer)
+        {
+            buffer.codeUint8(*reinterpret_cast<uint8_t*>(&mType));
+            buffer.codeString(mPath);
+        }
+
+        uint32_t Hash() const
+        {
+            return HashGeneric(mType, HashString(mPath));
+        }
+
+        bool operator==(const CacheKey& other) const
+        {
+            return mType == other.mType && mPath == other.mPath;
+        }
+
+        // Returns the Omnijar type for this entry. This may *only* be called
+        // for Omnijar entries.
+        Omnijar::Type OmnijarType()
+        {
+            switch (mType) {
+            case TypeAppJar:
+                return Omnijar::APP;
+            case TypeGREJar:
+                return Omnijar::GRE;
+            default:
+                MOZ_CRASH("Unexpected entry type");
+                return Omnijar::GRE;
+            }
+        }
+
+        const char* TypeString()
+        {
+            switch (mType) {
+            case TypeAppJar: return "AppJar";
+            case TypeGREJar: return "GREJar";
+            case TypeFile: return "File";
+            }
+            MOZ_ASSERT_UNREACHABLE("no such type");
+            return "";
+        }
+
+        already_AddRefed<nsZipArchive> Archive()
+        {
+            return Omnijar::GetReader(OmnijarType());
+        }
+
+        Result<FileLocation, nsresult> ToFileLocation();
+
+        EntryType mType = TypeFile;
+
+        // The path of the entry. For Type*Jar entries, this is the path within
+        // the Omnijar archive. For TypeFile entries, this is the full path to
+        // the file.
+        nsCString mPath{};
+    };
+
+    // Represents an entry in the URI cache.
+    struct URLEntry final : public CacheKey
+                          , public LinkedListElement<URLEntry>
+    {
+        MOZ_IMPLICIT URLEntry(const CacheKey& key)
+            : CacheKey(key)
+            , mData(NullCString())
+        {}
+
+        explicit URLEntry(nsIFile* file)
+          : CacheKey(file)
+        {}
+
+        // For use with nsTArray::Sort.
+        //
+        // Sorts entries by the time they were initially read during this
+        // session.
+        struct Comparator final
+        {
+            bool Equals(const URLEntry* a, const URLEntry* b) const
+            {
+              return a->mReadTime == b->mReadTime;
+            }
+
+            bool LessThan(const URLEntry* a, const URLEntry* b) const
+            {
+                return a->mReadTime < b->mReadTime;
+            }
+        };
+
+        // Sets the first-used time of this file to the earlier of its current
+        // first-use time or the given timestamp.
+        void UpdateUsedTime(const TimeStamp& time = TimeStamp::Now())
+        {
+          if (!mReadTime || time < mReadTime) {
+            mReadTime = time;
+          }
+        }
+
+        Result<const nsCString, nsresult> Read();
+        static Result<const nsCString, nsresult> ReadLocation(FileLocation& location);
+
+        size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const
+        {
+            return (mallocSizeOf(this) +
+                    mPath.SizeOfExcludingThisEvenIfShared(mallocSizeOf) +
+                    mData.SizeOfExcludingThisEvenIfShared(mallocSizeOf));
+        }
+
+        // Reads the contents of the file referenced by this entry, or wait for
+        // an off-thread read operation to finish if it is currently pending,
+        // and return the file's contents.
+        Result<const nsCString, nsresult> ReadOrWait(ReadType readType);
+
+        nsCString mData;
+
+        TimeStamp mReadTime{};
+
+        nsresult mResultCode = NS_OK;
+    };
+
+    // Resolves the given URI to a CacheKey, if the URI is cacheable.
+    Result<CacheKey, nsresult> ResolveURI(nsIURI* uri);
+
+    Result<Ok, nsresult> InitInternal();
+
+    // Returns a file pointer to the (possibly nonexistent) cache file with the
+    // given suffix.
+    Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix);
+    // Finds the correct cache file to use for this session.
+    Result<nsCOMPtr<nsIFile>, nsresult> FindCacheFile();
+
+    Result<Ok, nsresult> ReadCache(LinkedList<URLEntry>& pendingURLs);
+
+    void BackgroundReadFiles();
+    void BeginBackgroundRead();
+
+    using HashType = nsClassHashtable<nsGenericHashKey<CacheKey>, URLEntry>;
+
+    size_t ShallowSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf);
+
+
+    bool mStartupFinished = false;
+    bool mReaderInitialized = false;
+
+    // The prefix URLs for files in the GRE and App omni jar archives.
+    nsCString mGREPrefix;
+    nsCString mAppPrefix;
+
+    nsCOMPtr<nsIResProtocolHandler> mResProto;
+    nsCOMPtr<nsIChromeRegistry> mChromeReg;
+    nsCOMPtr<nsIFile> mProfD;
+
+    nsCOMPtr<nsIThread> mReaderThread;
+
+    // A map of URL entries which have were either read this session, or read
+    // from the last session's cache file.
+    HashType mCachedURLs;
+
+    Monitor mMonitor{"[URLPreloader::mMutex]"};
+};
+
+} // namespace mozilla
+
+#endif // URLPreloader_h
--- a/js/xpconnect/loader/moz.build
+++ b/js/xpconnect/loader/moz.build
@@ -6,30 +6,32 @@
 
 UNIFIED_SOURCES += [
     'AutoMemMap.cpp',
     'ChromeScriptLoader.cpp',
     'mozJSLoaderUtils.cpp',
     'mozJSSubScriptLoader.cpp',
     'ScriptCacheActors.cpp',
     'ScriptPreloader.cpp',
+    'URLPreloader.cpp',
 ]
 
 # mozJSComponentLoader.cpp cannot be built in unified mode because it uses
 # windows.h
 SOURCES += [
     'mozJSComponentLoader.cpp'
 ]
 
 IPDL_SOURCES += [
     'PScriptCache.ipdl',
 ]
 
 EXPORTS.mozilla += [
     'ScriptPreloader.h',
+    'URLPreloader.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'PrecompiledScript.h',
 ]
 
 EXPORTS.mozilla.loader += [
     'AutoMemMap.h',
--- a/xpcom/build/FileLocation.cpp
+++ b/xpcom/build/FileLocation.cpp
@@ -24,16 +24,31 @@ FileLocation::FileLocation(nsIFile* aFil
   Init(aFile, aPath);
 }
 
 FileLocation::FileLocation(nsZipArchive* aZip, const char* aPath)
 {
   Init(aZip, aPath);
 }
 
+FileLocation::FileLocation(const FileLocation& aOther)
+  : mBaseFile(aOther.mBaseFile)
+  , mBaseZip(aOther.mBaseZip)
+  , mPath(aOther.mPath)
+{
+}
+
+FileLocation::FileLocation(FileLocation&& aOther)
+  : mBaseFile(Move(aOther.mBaseFile))
+  , mBaseZip(Move(aOther.mBaseZip))
+  , mPath(Move(aOther.mPath))
+{
+  aOther.mPath.Truncate();
+}
+
 FileLocation::FileLocation(const FileLocation& aFile, const char* aPath)
 {
   if (aFile.IsZip()) {
     if (aFile.mBaseFile) {
       Init(aFile.mBaseFile, aFile.mPath.get());
     }
     else {
       Init(aFile.mBaseZip, aFile.mPath.get());
--- a/xpcom/build/FileLocation.h
+++ b/xpcom/build/FileLocation.h
@@ -30,33 +30,38 @@ public:
    * As such, it stores a path within an archive, as well as the archive
    * path itself, or the complete file path alone when on a filesystem.
    * When the archive is in an archive, an nsZipArchive is stored instead
    * of a file path.
    */
   FileLocation();
   ~FileLocation();
 
+  FileLocation(const FileLocation& aOther);
+  FileLocation(FileLocation&& aOther);
+
+  FileLocation& operator=(const FileLocation&) = default;
+
   /**
    * Constructor for plain files
    */
   explicit FileLocation(nsIFile* aFile);
 
   /**
    * Constructors for path within an archive. The archive can be given either
    * as nsIFile or nsZipArchive.
    */
   FileLocation(nsIFile* aZip, const char* aPath);
 
   FileLocation(nsZipArchive* aZip, const char* aPath);
 
   /**
    * Creates a new file location relative to another one.
    */
-  FileLocation(const FileLocation& aFile, const char* aPath = nullptr);
+  FileLocation(const FileLocation& aFile, const char* aPath);
 
   /**
    * Initialization functions corresponding to constructors
    */
   void Init(nsIFile* aFile);
 
   void Init(nsIFile* aZip, const char* aPath);