/* 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 "ScriptPreloader-inl.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Monitor.h"

#include "mozilla/ScriptPreloader.h"
#include "mozilla/loader/ScriptCacheActors.h"

#include "mozilla/URLPreloader.h"

#include "mozilla/Components.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/FileUtils.h"
#include "mozilla/IOBuffers.h"
#include "mozilla/Logging.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_javascript.h"
#include "mozilla/TaskController.h"
#include "mozilla/glean/JsXpconnectMetrics.h"
#include "mozilla/glean/XpcomMetrics.h"
#include "mozilla/Try.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Document.h"
#include "mozilla/scache/StartupCache.h"
#include "mozilla/scache/StartupCacheUtils.h"

#include "crc32c.h"
#include "js/CompileOptions.h"              // JS::ReadOnlyCompileOptions
#include "js/experimental/JSStencil.h"      // JS::Stencil, JS::DecodeStencil
#include "js/experimental/CompileScript.h"  // JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize
#include "js/Transcoding.h"
#include "MainThreadUtils.h"
#include "nsDebug.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIFile.h"
#include "nsIObserverService.h"
#include "nsJSUtils.h"
#include "nsMemoryReporterManager.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"
#include "xpcpublic.h"

#if defined(XP_LINUX)
#  include <sys/mman.h>
// MADV_COLD was added in Linux 5.4 / Android API 30; define it for older SDKs
// so we can call madvise() unconditionally (the kernel will return EINVAL and
// we ignore it).
#  ifndef MADV_COLD
#    define MADV_COLD 20
#  endif
#endif

#define STARTUP_COMPLETE_TOPIC "browser-delayed-startup-finished"
#define CONTENT_DOCUMENT_LOADED_TOPIC "content-document-loaded"
#define CACHE_WRITE_TOPIC "browser-idle-startup-tasks-finished"
#define XPCOM_SHUTDOWN_TOPIC "xpcom-shutdown"
#define CACHE_INVALIDATE_TOPIC "startupcache-invalidate"

// The maximum time we'll wait for a child process to finish starting up before
// we send its script data back to the parent.
constexpr uint32_t CHILD_STARTUP_TIMEOUT_MS = 8000;

namespace mozilla {
namespace {
static LazyLogModule gLog("ScriptPreloader");

#define LOG(level, ...) MOZ_LOG(gLog, LogLevel::level, (__VA_ARGS__))
}  // namespace

using mozilla::dom::AutoJSAPI;
using mozilla::dom::ContentChild;
using mozilla::dom::ContentParent;
using namespace mozilla::loader;
using mozilla::scache::StartupCache;

using namespace JS;

ProcessType ScriptPreloader::sProcessType;

nsresult ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport,
                                         nsISupports* aData, bool aAnonymize) {
  MOZ_COLLECT_REPORT(
      "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES,
      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,
      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.");

  // Since the mem-mapped cache file is mapped into memory, we want to report
  // it as explicit memory somewhere. But since the child cache is shared
  // between all processes, we don't want to report it as explicit memory for
  // all of them. So we report it as explicit only in the parent process, and
  // non-explicit everywhere else.
  if (XRE_IsParentProcess()) {
    MOZ_COLLECT_REPORT("explicit/script-preloader/non-heap/memmapped-cache",
                       KIND_NONHEAP, UNITS_BYTES,
                       mCacheData->nonHeapSizeOfExcludingThis(),
                       "The memory-mapped startup script cache file.");
  } else {
    MOZ_COLLECT_REPORT("script-preloader-memmapped-cache", KIND_NONHEAP,
                       UNITS_BYTES, mCacheData->nonHeapSizeOfExcludingThis(),
                       "The memory-mapped startup script cache file.");
  }

  return NS_OK;
}

StaticRefPtr<ScriptPreloader> ScriptPreloader::gScriptPreloader;
StaticRefPtr<ScriptPreloader> ScriptPreloader::gChildScriptPreloader;
StaticAutoPtr<AutoMemMap> ScriptPreloader::gCacheData;
StaticAutoPtr<AutoMemMap> ScriptPreloader::gChildCacheData;

ScriptPreloader& ScriptPreloader::GetSingleton() {
  if (!gScriptPreloader) {
    AssertIsOnMainThread();
    if (XRE_IsParentProcess()) {
      gCacheData = new AutoMemMap();
      gScriptPreloader = new ScriptPreloader(gCacheData.get());
      gScriptPreloader->mChildCache = &GetChildSingleton();
      (void)gScriptPreloader->InitCache();
    } else {
      gScriptPreloader = &GetChildSingleton();
    }
  }

  return *gScriptPreloader;
}

// 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() {
  if (!gChildScriptPreloader) {
    AssertIsOnMainThread();
    gChildCacheData = new AutoMemMap();
    gChildScriptPreloader = new ScriptPreloader(gChildCacheData.get());
    if (XRE_IsParentProcess()) {
      (void)gChildScriptPreloader->InitCache(u"scriptCache-child"_ns);
    }
  }

  return *gChildScriptPreloader;
}

/* static */
void ScriptPreloader::DeleteSingleton() {
  gScriptPreloader = nullptr;
  gChildScriptPreloader = nullptr;
}

/* static */
void ScriptPreloader::DeleteCacheDataSingleton() {
  MOZ_ASSERT(!gScriptPreloader);
  MOZ_ASSERT(!gChildScriptPreloader);

  gCacheData = nullptr;
  gChildCacheData = nullptr;
}

void ScriptPreloader::InitContentChild(ContentParent& parent) {
  AssertIsOnMainThread();

  auto& cache = GetChildSingleton();
  cache.mSaveMonitor.NoteOnMainThread();

  // We want startup script data from the first process of a given type.
  // That process sends back its script data before it executes any
  // untrusted code, and then we never accept further script data for that
  // type of process for the rest of the session.
  //
  // The script data from each process type is merged with the data from the
  // parent process's frame and process scripts, and shared between all
  // content process types in the next session.
  //
  // Note that if the first process of a given type crashes or shuts down
  // before sending us its script data, we silently ignore it, and data for
  // that process type is not included in the next session's cache. This
  // should be a sufficiently rare occurrence that it's not worth trying to
  // handle specially.
  auto processType = GetChildProcessType(parent.GetRemoteType());
  bool wantScriptData =
      !cache.mRequestedChildProcessStencils.contains(processType);
  cache.mRequestedChildProcessStencils += processType;

  // If we're starting a web process (e.g. a preload process during startup),
  // make sure we receive its script data before kicking off the cache write.
  if (processType == ProcessType::Web) {
    cache.mRequiredChildProcessStencils += processType;
  }

  auto fd = cache.mCacheData->cloneFileDescriptor();
  // Don't send original cache data to new processes if the cache has been
  // invalidated.
  if (fd.IsValid() && !cache.mCacheInvalidated) {
    (void)parent.SendPScriptCacheConstructor(fd, wantScriptData);
  } else {
    (void)parent.SendPScriptCacheConstructor(NS_ERROR_FILE_NOT_FOUND,
                                             wantScriptData);
  }
}

ProcessType ScriptPreloader::GetChildProcessType(const nsACString& remoteType) {
  if (remoteType == EXTENSION_REMOTE_TYPE) {
    return ProcessType::Extension;
  }
  if (remoteType == PRIVILEGEDABOUT_REMOTE_TYPE) {
    return ProcessType::PrivilegedAbout;
  }
  return ProcessType::Web;
}

ScriptPreloader::ScriptPreloader(AutoMemMap* cacheData)
    : mCacheData(cacheData),
      mMonitor("[ScriptPreloader.mMonitor]"),
      mSaveMonitor("[ScriptPreloader.mSaveMonitor]") {
  // We do not set the process type for child processes here because the
  // remoteType in ContentChild is not ready yet.
  if (XRE_IsParentProcess()) {
    sProcessType = ProcessType::Parent;
  }

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  MOZ_RELEASE_ASSERT(obs);

  if (XRE_IsParentProcess()) {
    // In the parent process, we want to freeze the script cache as soon
    // as idle tasks for the first browser window have completed.
    obs->AddObserver(this, STARTUP_COMPLETE_TOPIC, false);
    obs->AddObserver(this, CACHE_WRITE_TOPIC, false);
  }

  obs->AddObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
  obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false);
}

ScriptPreloader::~ScriptPreloader() { Cleanup(); }

void ScriptPreloader::Cleanup() {
  mScripts.Clear();
  UnregisterWeakMemoryReporter(this);
}

void ScriptPreloader::StartCacheWriteIfReady() {
  // Check if we're the right cache. Only the this-process ScriptPreloader in
  // the parent process should do any cache writing.
  if (!mChildCache) {
    // If we don't have a child cache then we're not the right ScriptPreloader.
    return;
  }

  if (mSaveComplete || mSaveThread) {
    // We've already written the cache or are in the process of doing so.
    return;
  }

  if (!mStartupHasAdvancedToCacheWritingStage) {
    // Too early to write.
    return;
  }

  if (!mChildCache->mReceivedChildProcessStencils.contains(
          mChildCache->mRequiredChildProcessStencils)) {
    // Still missing some expected child process script data.
    return;
  }

  // Everything's ready, let's kick off the write task.
  StartCacheWrite();
}

void ScriptPreloader::StartCacheWrite() {
  MOZ_DIAGNOSTIC_ASSERT(!mSaveThread);

  (void)NS_NewNamedThread("SaveScripts", getter_AddRefs(mSaveThread), this);

  nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
  barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
                      u""_ns);
}

void ScriptPreloader::InvalidateCache() {
  {
    mMonitor.AssertNotCurrentThreadOwns();
    MonitorAutoLock mal(mMonitor);

    // Wait for pending off-thread parses to finish, since they depend on the
    // memory allocated by our CachedStencil, and can't be canceled
    // asynchronously.
    FinishPendingParses(mal);

    // Pending scripts should have been cleared by the above, and the queue
    // should have been reset.
    MOZ_ASSERT(mDecodingScripts.isEmpty());
    MOZ_ASSERT(!mDecodedStencils);

    mScripts.Clear();

    // 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 && !mSaveThread && mChildCache) {
      mSaveComplete = false;

      StartCacheWrite();
    }
  }

  {
    MonitorAutoLock saveMonitorAutoLock(mSaveMonitor.Lock());
    mSaveMonitor.NoteExclusiveAccess();

    mCacheInvalidated = true;
  }

  // If we're waiting on a timeout to finish saving, interrupt it and just save
  // immediately.
  mSaveMonitor.Lock().NotifyAll();
}

nsresult ScriptPreloader::Observe(nsISupports* subject, const char* topic,
                                  const char16_t* data) {
  AssertIsOnMainThread();

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (!strcmp(topic, STARTUP_COMPLETE_TOPIC)) {
    obs->RemoveObserver(this, STARTUP_COMPLETE_TOPIC);

    MOZ_ASSERT(XRE_IsParentProcess());

    mStartupFinished = true;
    URLPreloader::GetSingleton().SetStartupFinished();
  } else if (!strcmp(topic, CACHE_WRITE_TOPIC)) {
    obs->RemoveObserver(this, CACHE_WRITE_TOPIC);

    MOZ_ASSERT(mStartupFinished);
    MOZ_ASSERT(XRE_IsParentProcess());
    mStartupHasAdvancedToCacheWritingStage = true;

#if defined(XP_LINUX)
    // Startup is complete; mark the cache mapping cold so the kernel can
    // reclaim these pages under memory pressure without unmapping them.
    if (mCacheData->initialized()) {
      // MADV_COLD may return EINVAL on kernels older than 5.4.
      (void)madvise(mCacheData->get<uint8_t>().get(), mCacheData->size(),
                    MADV_COLD);
    }
#endif

    StartCacheWriteIfReady();
  } else if (mContentStartupFinishedTopic.Equals(topic)) {
    // If this is an uninitialized about:blank viewer or a chrome: document
    // (which should always be an XBL binding document), ignore it. We don't
    // have to worry about it loading malicious content.
    if (nsCOMPtr<dom::Document> doc = do_QueryInterface(subject)) {
      nsCOMPtr<nsIURI> uri = doc->GetDocumentURI();

      if ((NS_IsAboutBlank(uri) &&
           doc->GetReadyStateEnum() == doc->READYSTATE_UNINITIALIZED) ||
          uri->SchemeIs("chrome")) {
        return NS_OK;
      }
    }
    FinishContentStartup();
  } else if (!strcmp(topic, "timer-callback")) {
    FinishContentStartup();
  } else if (!strcmp(topic, XPCOM_SHUTDOWN_TOPIC)) {
    // Wait for any pending parses to finish at this point, to avoid creating
    // new stencils during destroying the JS runtime.
    MonitorAutoLock mal(mMonitor);
    FinishPendingParses(mal);
  } else if (!strcmp(topic, CACHE_INVALIDATE_TOPIC)) {
    InvalidateCache();
  }

  return NS_OK;
}

void ScriptPreloader::FinishContentStartup() {
  MOZ_ASSERT(XRE_IsContentProcess());

#ifdef DEBUG
  if (mContentStartupFinishedTopic.Equals(CONTENT_DOCUMENT_LOADED_TOPIC)) {
    MOZ_ASSERT(sProcessType == ProcessType::PrivilegedAbout);
  } else {
    MOZ_ASSERT(sProcessType != ProcessType::PrivilegedAbout);
  }
#endif /* DEBUG */

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  obs->RemoveObserver(this, mContentStartupFinishedTopic.get());

  mSaveTimer = nullptr;

  mStartupFinished = true;

  if (mChildActor) {
    mChildActor->SendScriptsAndFinalize(mScripts);
  }

#ifdef XP_WIN
  // Record the amount of USS at startup. This is Windows-only for now,
  // we could turn it on for Linux relatively cheaply. On macOS it can have
  // a perf impact. Only record this for non-privileged processes because
  // privileged processes record this value at a different time, leading to
  // a higher value which skews the telemetry.
  if (sProcessType != ProcessType::PrivilegedAbout) {
    mozilla::glean::memory::unique_content_startup.Accumulate(
        nsMemoryReporterManager::ResidentUnique() / 1024);
  }
#endif
}

bool ScriptPreloader::WillWriteScripts() {
  return !mDataPrepared && (XRE_IsParentProcess() || mChildActor);
}

bool ScriptPreloader::Active() const {
  if (!mCacheInitialized) {
    return false;
  }

  if (!mStartupFinished) {
    return true;
  }

  if (StaticPrefs::javascript_options_force_preloader_active() &&
      xpc::IsInAutomation()) {
    return true;
  }

  return false;
}

Result<nsCOMPtr<nsIFile>, nsresult> ScriptPreloader::GetCacheFile(
    const nsAString& suffix) {
  NS_ENSURE_TRUE(mProfD, Err(NS_ERROR_NOT_INITIALIZED));

  nsCOMPtr<nsIFile> cacheFile;
  MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile)));

  MOZ_TRY(cacheFile->AppendNative("startupCache"_ns));
  (void)cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);

  MOZ_TRY(cacheFile->Append(mBaseName + suffix));

  return std::move(cacheFile);
}

static const uint8_t MAGIC[] = "mozXDRcachev003";

Result<Ok, nsresult> ScriptPreloader::OpenCache() {
  if (StartupCache::GetIgnoreDiskCache()) {
    return Err(NS_ERROR_ABORT);
  }

  MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));

  nsCOMPtr<nsIFile> cacheFile = MOZ_TRY(GetCacheFile(u".bin"_ns));

  bool exists;
  MOZ_TRY(cacheFile->Exists(&exists));
  if (exists) {
    MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u"-current.bin"_ns));
  } else {
    MOZ_TRY(cacheFile->SetLeafName(mBaseName + u"-current.bin"_ns));
    MOZ_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(const nsAString& basePath) {
  mCacheInitialized = true;
  mBaseName = basePath;

  RegisterWeakMemoryReporter(this);

  if (!XRE_IsParentProcess()) {
    return Ok();
  }

  // Grab the compilation scope before initializing the URLPreloader, since
  // it's not safe to run component loader code during its critical section.
  AutoSafeJSAPI jsapi;
  JS::RootedObject scope(jsapi.cx(), xpc::CompilationScope());

  // 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(scope);
}

Result<Ok, nsresult> ScriptPreloader::InitCache(
    const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild) {
  MOZ_ASSERT(XRE_IsContentProcess());

  mCacheInitialized = true;
  mChildActor = cacheChild;
  sProcessType =
      GetChildProcessType(dom::ContentChild::GetSingleton()->GetRemoteType());

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  MOZ_RELEASE_ASSERT(obs);

  if (sProcessType == ProcessType::PrivilegedAbout) {
    // Since we control all of the documents loaded in the privileged content
    // process, the kBecomeUntrusted notification will never fire. Instead we
    // increase the window of active time for the ScriptPreloader to include the
    // scripts that are loaded until the first document finishes loading.
    mContentStartupFinishedTopic.AssignLiteral(CONTENT_DOCUMENT_LOADED_TOPIC);
  } else {
    // In the child process, we need to freeze the script cache before any
    // untrusted code has been executed. Use the shared kBecameUntrustedTopic
    // notification as a proxy for this.
    mContentStartupFinishedTopic = ContentChild::kBecameUntrustedTopic;
  }
  obs->AddObserver(this, mContentStartupFinishedTopic.get(), false);

  RegisterWeakMemoryReporter(this);

  auto cleanup = MakeScopeExit([&] {
    // If the parent is expecting cache data from us, make sure we send it
    // before it writes out its cache file. For normal proceses, this isn't
    // a concern, since they begin loading documents quite early. For the
    // preloaded process, we may end up waiting a long time (or, indeed,
    // never loading a document), so we need an additional timeout.
    if (cacheChild) {
      NS_NewTimerWithObserver(getter_AddRefs(mSaveTimer), this,
                              CHILD_STARTUP_TIMEOUT_MS,
                              nsITimer::TYPE_ONE_SHOT);
    }
  });

  if (cacheFile.isNothing()) {
    return Ok();
  }

  MOZ_TRY(mCacheData->init(cacheFile.ref()));

  return InitCacheInternal();
}

Result<Ok, nsresult> ScriptPreloader::InitCacheInternal(
    JS::HandleObject scope) {
  auto size = mCacheData->size();

  uint32_t headerSize;
  uint32_t crc;
  if (size < sizeof(MAGIC) + sizeof(headerSize) + sizeof(crc)) {
    return Err(NS_ERROR_UNEXPECTED);
  }

  auto data = mCacheData->get<uint8_t>();
  MOZ_RELEASE_ASSERT(JS::IsTranscodingBytecodeAligned(data.get()));

  auto end = data + size;

  if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) {
    return Err(NS_ERROR_UNEXPECTED);
  }
  data += sizeof(MAGIC);

  headerSize = LittleEndian::readUint32(data.get());
  data += sizeof(headerSize);

  crc = LittleEndian::readUint32(data.get());
  data += sizeof(crc);

  if (data + headerSize > end) {
    return Err(NS_ERROR_UNEXPECTED);
  }

  if (crc != ComputeCrc32c(~0, data.get(), headerSize)) {
    return Err(NS_ERROR_UNEXPECTED);
  }

  {
    auto cleanup = MakeScopeExit([&]() { mScripts.Clear(); });

    LinkedList<CachedStencil> scripts;

    Range<const uint8_t> header(data, data + headerSize);
    data += headerSize;

    // Reconstruct alignment padding if required.
    size_t currentOffset = data - mCacheData->get<uint8_t>();
    data += JS::AlignTranscodingBytecodeOffset(currentOffset) - currentOffset;

    InputBuffer buf(header);

    size_t offset = 0;
    while (!buf.finished()) {
      auto script = MakeUnique<CachedStencil>(*this, buf);
      MOZ_RELEASE_ASSERT(script);

      auto scriptData = data + script->mOffset;
      if (!JS::IsTranscodingBytecodeAligned(scriptData.get())) {
        return Err(NS_ERROR_UNEXPECTED);
      }

      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.
      if (script->mOffset != offset) {
        return Err(NS_ERROR_UNEXPECTED);
      }
      offset += script->mSize;

      script->mXDRRange.emplace(scriptData, scriptData + script->mSize);

      // Don't pre-decode the script unless it was used in this process type
      // during the previous session.
      if (script->mOriginalProcessTypes.contains(CurrentProcessType())) {
        scripts.insertBack(script.get());
      } else {
        script->mReadyToExecute = true;
      }

      const auto& cachePath = script->mCachePath;
      mScripts.InsertOrUpdate(cachePath, std::move(script));
    }

    if (buf.error()) {
      return Err(NS_ERROR_UNEXPECTED);
    }

    mDecodingScripts = std::move(scripts);
    cleanup.release();
  }

  StartDecodeTask(scope);
  return Ok();
}

void ScriptPreloader::PrepareCacheWriteInternal() {
  MOZ_ASSERT(NS_IsMainThread());

  mMonitor.AssertCurrentThreadOwns();

  auto cleanup = MakeScopeExit([&]() {
    if (mChildCache) {
      mChildCache->PrepareCacheWrite();
    }
  });

  if (mDataPrepared) {
    return;
  }

  JS::FrontendContext* fc = JS::NewFrontendContext();
  if (!fc) {
    return;
  }

  bool found = false;
  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.
    CachedStencil* childScript =
        mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr;
    if (childScript && !childScript->mProcessTypes.isEmpty()) {
      childScript->UpdateLoadTime(script->mLoadTime);
      childScript->mProcessTypes += script->mProcessTypes;
      script.Remove();
      continue;
    }

    if (!(script->mProcessTypes == script->mOriginalProcessTypes)) {
      // Note: EnumSet doesn't support operator!=, hence the weird form above.
      found = true;
    }

    if (!script->mSize && !script->XDREncode(fc)) {
      script.Remove();
    }
  }

  JS::DestroyFrontendContext(fc);

  if (!found) {
    mSaveComplete = true;
    return;
  }

  mDataPrepared = true;
}

void ScriptPreloader::PrepareCacheWrite() {
  MonitorAutoLock mal(mMonitor);

  PrepareCacheWriteInternal();
}

// A struct to hold reference to a CachedStencil and the snapshot of the
// CachedStencil::mLoadTime field.
// CachedStencil::mLoadTime field can be modified concurrently, and we need
// to create a snapshot, in order to sort scripts.
struct CachedStencilRefAndTime {
  using CachedStencil = ScriptPreloader::CachedStencil;
  CachedStencil* mStencil;
  TimeStamp mLoadTime;

  explicit CachedStencilRefAndTime(CachedStencil* aStencil)
      : mStencil(aStencil), mLoadTime(aStencil->mLoadTime) {}

  // For use with nsTArray::Sort.
  //
  // Orders scripts by 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 CachedStencilRefAndTime& a,
                const CachedStencilRefAndTime& b) const {
      return a.mLoadTime == b.mLoadTime;
    }

    bool LessThan(const CachedStencilRefAndTime& a,
                  const CachedStencilRefAndTime& b) const {
      return a.mLoadTime < b.mLoadTime;
    }
  };
} JS_HAZ_NON_GC_POINTER;

// 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.
//
// - A header entry for each file stored in the cache containing:
//   - The URL that the script was originally read from.
//   - Its cache key.
//   - The offset of its XDR data within the XDR data block.
//   - The size of its XDR data in the XDR data block.
//   - A bit field describing which process types the script is used in.
//
// - 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());

  if (!mDataPrepared && !mSaveComplete) {
    MonitorAutoUnlock mau(mSaveMonitor.Lock());

    NS_DispatchAndSpinEventLoopUntilComplete(
        "ScriptPreloader::PrepareCacheWrite"_ns,
        GetMainThreadSerialEventTarget(),
        NewRunnableMethod("ScriptPreloader::PrepareCacheWrite", this,
                          &ScriptPreloader::PrepareCacheWrite));
  }

  if (mSaveComplete) {
    // If we don't have anything we need to save, we're done.
    return Ok();
  }

  nsCOMPtr<nsIFile> cacheFile = MOZ_TRY(GetCacheFile(u"-new.bin"_ns));

  bool exists;
  MOZ_TRY(cacheFile->Exists(&exists));
  if (exists) {
    MOZ_TRY(cacheFile->Remove(false));
  }

  {
    AutoFDClose raiiFd;
    MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644,
                                        getter_Transfers(raiiFd)));
    const auto fd = raiiFd.get();

    // We also need to hold mMonitor while we're touching scripts in
    // mScripts, or they may be freed before we're done with them.
    mMonitor.AssertNotCurrentThreadOwns();
    MonitorAutoLock mal(mMonitor);

    nsTArray<CachedStencilRefAndTime> scriptRefs;
    for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) {
      scriptRefs.AppendElement(CachedStencilRefAndTime(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.
    scriptRefs.Sort(CachedStencilRefAndTime::Comparator());

    OutputBuffer buf;
    size_t offset = 0;
    for (auto& scriptRef : scriptRefs) {
      auto* script = scriptRef.mStencil;
      script->mOffset = offset;
      MOZ_DIAGNOSTIC_ASSERT(
          JS::IsTranscodingBytecodeOffsetAligned(script->mOffset));
      script->Code(buf);

      offset += script->mSize;
      MOZ_DIAGNOSTIC_ASSERT(
          JS::IsTranscodingBytecodeOffsetAligned(script->mSize));
    }

    uint8_t headerSize[4];
    LittleEndian::writeUint32(headerSize, buf.cursor());

    uint8_t crc[4];
    LittleEndian::writeUint32(crc, ComputeCrc32c(~0, buf.Get(), buf.cursor()));

    MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
    MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
    MOZ_TRY(Write(fd, crc, sizeof(crc)));
    MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));

    // Align the start of the scripts section to the transcode alignment.
    size_t written = sizeof(MAGIC) + sizeof(headerSize) + buf.cursor();
    size_t padding = JS::AlignTranscodingBytecodeOffset(written) - written;
    if (padding) {
      MOZ_TRY(WritePadding(fd, padding));
      written += padding;
    }

    for (auto& scriptRef : scriptRefs) {
      auto* script = scriptRef.mStencil;
      MOZ_DIAGNOSTIC_ASSERT(JS::IsTranscodingBytecodeOffsetAligned(written));
      MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));

      written += script->mSize;
      // We can only free the XDR data if the stencil isn't borrowing data from
      // it.
      if (script->mStencil && !JS::StencilIsBorrowed(script->mStencil)) {
        script->FreeData();
      }
    }
  }

  MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u".bin"_ns));

  return Ok();
}

nsresult ScriptPreloader::GetName(nsACString& aName) {
  aName.AssignLiteral("ScriptPreloader");
  return NS_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.Lock());
  mSaveMonitor.NoteLockHeld();

  // Wait about 3 seconds before saving, to avoid unnecessary IO
  // during early startup. But only if the cache hasn't been invalidated,
  // since that can trigger a new write during shutdown, and we don't want to
  // cause shutdown hangs.
  if (!mCacheInvalidated) {
    mal.Wait(TimeDuration::FromSeconds(3));
  }

  auto result = URLPreloader::GetSingleton().WriteCache();
  (void)NS_WARN_IF(result.isErr());

  result = WriteCache();
  (void)NS_WARN_IF(result.isErr());

  {
    MonitorAutoLock lock(mChildCache->mSaveMonitor.Lock());
    result = mChildCache->WriteCache();
  }
  (void)NS_WARN_IF(result.isErr());

  NS_DispatchToMainThread(
      NewRunnableMethod("ScriptPreloader::CacheWriteComplete", this,
                        &ScriptPreloader::CacheWriteComplete),
      NS_DISPATCH_NORMAL);
  return NS_OK;
}

void ScriptPreloader::CacheWriteComplete() {
  mSaveThread->AsyncShutdown();
  mSaveThread = nullptr;
  mSaveComplete = true;

  nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
  barrier->RemoveBlocker(this);
}

void ScriptPreloader::NoteStencil(const nsCString& url,
                                  const nsCString& cachePath,
                                  JS::Stencil* stencil, bool isRunOnce) {
  if (!Active()) {
    if (isRunOnce) {
      if (auto script = mScripts.Get(cachePath)) {
        script->mIsRunOnce = true;
        script->MaybeDropStencil();
      }
    }
    return;
  }

  // Don't bother trying to cache any URLs with cache-busting query
  // parameters.
  if (cachePath.FindChar('?') >= 0) {
    return;
  }

  // Don't bother caching files that belong to the mochitest harness.
  constexpr auto mochikitPrefix = "chrome://mochikit/"_ns;
  if (StringHead(url, mochikitPrefix.Length()) == mochikitPrefix) {
    return;
  }

  auto* script =
      mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, stencil);
  if (isRunOnce) {
    script->mIsRunOnce = true;
  }

  if (!script->MaybeDropStencil() && !script->mStencil) {
    MOZ_ASSERT(stencil);
    script->mStencil = stencil;
    script->mReadyToExecute = true;
  }

  script->UpdateLoadTime(TimeStamp::Now());
  script->mProcessTypes += CurrentProcessType();
}

void ScriptPreloader::NoteStencil(const nsCString& url,
                                  const nsCString& cachePath,
                                  ProcessType processType,
                                  nsTArray<uint8_t>&& xdrData,
                                  TimeStamp loadTime) {
  // After data has been prepared, there's no point in noting further scripts,
  // since the cache either has already been written, or is about to be
  // written. Any time prior to the data being prepared, we can safely mutate
  // mScripts without locking. After that point, the save thread is free to
  // access it, and we can't alter it without locking.
  if (mDataPrepared) {
    return;
  }

  auto* script =
      mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, nullptr);

  if (!script->HasRange()) {
    MOZ_ASSERT(!script->HasArray());

    script->mSize = xdrData.Length();
    script->mXDRData.construct<nsTArray<uint8_t>>(
        std::forward<nsTArray<uint8_t>>(xdrData));

    auto& data = script->Array();
    script->mXDRRange.emplace(data.Elements(), data.Length());
  }

  if (!script->mSize && !script->mStencil) {
    // If the content process is sending us an entry for a stencil
    // which was in the cache at startup, it expects us to already have this
    // script data, so it doesn't send it.
    //
    // However, the cache may have been invalidated at this point (usually
    // due to the add-on manager installing or uninstalling a legacy
    // extension during very early startup), which means we may no longer
    // have an entry for this script. Since that means we have no data to
    // write to the new cache, and no JSScript to generate it from, we need
    // to discard this entry.
    mScripts.Remove(cachePath);
    return;
  }

  script->UpdateLoadTime(loadTime);
  script->mProcessTypes += processType;
}

void ScriptPreloader::NoteReceivedAllChildStencilsForProcess(
    ProcessType aProcessType) {
  mReceivedChildProcessStencils += aProcessType;

  // We're the child cache. Tell the root cache to trigger a write if ready.
  GetSingleton().StartCacheWriteIfReady();
}

/* static */
void ScriptPreloader::FillCompileOptionsForCachedStencil(
    JS::CompileOptions& options) {
  // Users of the cache do not require return values, so inform the JS parser in
  // order for it to generate simpler bytecode.
  options.setNoScriptRval(true);

  // The ScriptPreloader trades off having bytecode available but not source
  // text. This means the JS syntax-only parser is not used. If `toString` is
  // called on functions in these scripts, the source-hook will fetch it over,
  // so using `toString` of functions should be avoided in chrome js.
  options.setSourceIsLazy(true);
}

/* static */
void ScriptPreloader::FillDecodeOptionsForCachedStencil(
    JS::DecodeOptions& options) {
  // ScriptPreloader's XDR buffer is alive during the Stencil is alive.
  // The decoded stencil can borrow from it.
  //
  // NOTE: The XDR buffer is alive during the entire browser lifetime only
  //       when it's mmapped.
  options.borrowBuffer = true;
}

already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencil(
    JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
    const nsCString& path) {
  MOZ_RELEASE_ASSERT(
      !(XRE_IsContentProcess() && !mCacheInitialized),
      "ScriptPreloader must be initialized before getting cached "
      "scripts in the content process.");

#ifdef DEBUG
  // All callers should have already checked that the script is from omni.ja
  // (Gre or App resource type) before calling GetCachedStencil.
  MOZ_ASSERT(path.Find("/resource/gre/"_ns) != kNotFound ||
                 path.Find("/resource/app/"_ns) != kNotFound,
             "GetCachedStencil should only be called for omni.ja scripts");
#endif

  // If a script is used by both the parent and the child, it's stored only
  // in the child cache.
  if (mChildCache) {
    RefPtr<JS::Stencil> stencil =
        mChildCache->GetCachedStencilInternal(cx, options, path);
    if (stencil) {
#ifndef ANDROID
      glean::script_preloader::requests
          .EnumGet(glean::script_preloader::RequestsLabel::eHitchild)
          .Add();
#endif
      return stencil.forget();
    }
  }

  RefPtr<JS::Stencil> stencil = GetCachedStencilInternal(cx, options, path);
#ifndef ANDROID
  glean::script_preloader::requests
      .EnumGet(stencil ? glean::script_preloader::RequestsLabel::eHit
                       : glean::script_preloader::RequestsLabel::eMiss)
      .Add();
#endif

  return stencil.forget();
}

already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencilInternal(
    JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
    const nsCString& path) {
  auto* cachedScript = mScripts.Get(path);
  if (cachedScript) {
    return WaitForCachedStencil(cx, options, cachedScript);
  }
  return nullptr;
}

already_AddRefed<JS::Stencil> ScriptPreloader::WaitForCachedStencil(
    JSContext* cx, const JS::ReadOnlyDecodeOptions& options,
    CachedStencil* script) {
  if (!script->mReadyToExecute) {
    // mReadyToExecute is kept as false only when off-thread decode task was
    // available (pref is set to true) and the task was successfully created.
    // See ScriptPreloader::StartDecodeTask methods.
    MOZ_ASSERT(mDecodedStencils);

    // Check for the finished operations that can contain our target.
    if (mDecodedStencils->AvailableRead() > 0) {
      FinishOffThreadDecode();
    }

    if (!script->mReadyToExecute) {
      // Our target is not yet decoded.

      // If script is small enough, we'd rather decode on main-thread than wait
      // for a decode task to complete.
      if (script->mSize < MAX_MAINTHREAD_DECODE_SIZE) {
        LOG(Info, "Script is small enough to recompile on main thread\n");

        script->mReadyToExecute = true;
        glean::script_preloader::mainthread_recompile.Add(1);
      } else {
        LOG(Info, "Must wait for async script load: %s\n", script->mURL.get());
        auto start = TimeStamp::Now();

        MonitorAutoLock mal(mMonitor);

        // Process finished tasks until our target is found.
        while (!script->mReadyToExecute) {
          if (mDecodedStencils->AvailableRead() > 0) {
            FinishOffThreadDecode();
          } else {
            MOZ_ASSERT(!mDecodingScripts.isEmpty());
            mWaitingForDecode = true;
            mal.Wait();
            mWaitingForDecode = false;
          }
        }

        TimeDuration waited = TimeStamp::Now() - start;
        glean::script_preloader::wait_time.AccumulateRawDuration(waited);
        LOG(Debug, "Waited %fms\n", waited.ToMilliseconds());
      }
    }
  }

  return script->GetStencil(cx, options);
}

void ScriptPreloader::onDecodedStencilQueued() {
  mMonitor.AssertNotCurrentThreadOwns();
  MonitorAutoLock mal(mMonitor);

  if (mWaitingForDecode) {
    // Wake up the blocked main thread.
    mal.Notify();
  }

  // NOTE: Do not perform DoFinishOffThreadDecode for partial data.
}

void ScriptPreloader::OnDecodeTaskFinished() {
  mMonitor.AssertNotCurrentThreadOwns();
  MonitorAutoLock mal(mMonitor);

  if (mWaitingForDecode) {
    // Wake up the blocked main thread.
    mal.Notify();
  } else {
    // Issue a Runnable to handle all decoded stencils, even if the next
    // WaitForCachedStencil call has not happened yet.
    NS_DispatchToMainThread(
        NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", this,
                          &ScriptPreloader::DoFinishOffThreadDecode));
  }
}

void ScriptPreloader::OnDecodeTaskFailed() {
  // NOTE: nullptr is enqueued to mDecodedStencils, and FinishOffThreadDecode
  //       handles it as failure.
  OnDecodeTaskFinished();
}

void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) {
  mMonitor.AssertCurrentThreadOwns();

  // If off-thread decoding task hasn't been started, nothing to do.
  // This can happen if the javascript.options.parallel_parsing pref was false,
  // or the decode task fails to start.
  if (!mDecodedStencils) {
    return;
  }

  // Process any pending decodes that are in flight.
  while (!mDecodingScripts.isEmpty()) {
    if (mDecodedStencils->AvailableRead() > 0) {
      FinishOffThreadDecode();
    } else {
      mWaitingForDecode = true;
      aMal.Wait();
      mWaitingForDecode = false;
    }
  }
}

void ScriptPreloader::DoFinishOffThreadDecode() {
  // NOTE: mDecodedStencils could already be reset.
  if (mDecodedStencils && mDecodedStencils->AvailableRead() > 0) {
    FinishOffThreadDecode();
  }
}

void ScriptPreloader::FinishOffThreadDecode() {
  MOZ_ASSERT(mDecodedStencils);

  while (mDecodedStencils->AvailableRead() > 0) {
    RefPtr<JS::Stencil> stencil;
    DebugOnly<int> reads = mDecodedStencils->Dequeue(&stencil, 1);
    MOZ_ASSERT(reads == 1);

    if (!stencil) {
      // DecodeTask failed.
      // Mark all remaining scripts to be decoded on the main thread.
      for (CachedStencil* next = mDecodingScripts.getFirst(); next;) {
        auto* script = next;
        next = script->getNext();

        script->mReadyToExecute = true;
        script->remove();
      }

      break;
    }

    CachedStencil* script = mDecodingScripts.getFirst();
    MOZ_ASSERT(script);

    LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get());
    script->mStencil = stencil.forget();
    script->mReadyToExecute = true;
    script->remove();
  }

  if (mDecodingScripts.isEmpty()) {
    mDecodedStencils.reset();
  }
}

void ScriptPreloader::StartDecodeTask(JS::HandleObject scope) {
  auto start = TimeStamp::Now();
  LOG(Debug, "Off-thread decoding scripts...\n");

  Vector<JS::TranscodeSource> decodingSources;

  size_t size = 0;
  for (CachedStencil* next = mDecodingScripts.getFirst(); next;) {
    auto* script = next;
    next = script->getNext();

    MOZ_ASSERT(script->IsMemMapped());

    // Skip any scripts that we decoded on the main thread rather than
    // waiting for an off-thread operation to complete.
    if (script->mReadyToExecute) {
      script->remove();
      continue;
    }
    if (!decodingSources.emplaceBack(script->Range(), script->mURL.get(), 0)) {
      break;
    }

    LOG(Debug, "Beginning off-thread decode of script %s (%u bytes)\n",
        script->mURL.get(), script->mSize);

    size += script->mSize;
  }

  MOZ_ASSERT(decodingSources.length() == mDecodingScripts.length());

  if (size == 0 && mDecodingScripts.isEmpty()) {
    return;
  }

  AutoSafeJSAPI jsapi;
  JSContext* cx = jsapi.cx();
  JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope());

  JS::CompileOptions options(cx);
  FillCompileOptionsForCachedStencil(options);

  // All XDR buffers are mmapped and live longer than JS runtime.
  // The bytecode can be borrowed from the buffer.
  options.borrowBuffer = true;
  options.usePinnedBytecode = true;

  JS::DecodeOptions decodeOptions(options);

  size_t decodingSourcesLength = decodingSources.length();

  if (!StaticPrefs::javascript_options_parallel_parsing() ||
      !StartDecodeTask(decodeOptions, std::move(decodingSources))) {
    LOG(Info, "Can't decode %lu bytes of scripts off-thread",
        (unsigned long)size);
    for (auto* script : mDecodingScripts) {
      script->mReadyToExecute = true;
    }
    return;
  }

  LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n",
      (unsigned)decodingSourcesLength, (unsigned)size,
      (TimeStamp::Now() - start).ToMilliseconds());
}

bool ScriptPreloader::StartDecodeTask(
    const JS::ReadOnlyDecodeOptions& decodeOptions,
    Vector<JS::TranscodeSource>&& decodingSources) {
  mDecodedStencils.emplace(decodingSources.length());
  MOZ_ASSERT(mDecodedStencils);

  nsCOMPtr<nsIRunnable> task =
      new DecodeTask(this, decodeOptions, std::move(decodingSources));

  nsresult rv = NS_DispatchBackgroundTask(task.forget());

  return NS_SUCCEEDED(rv);
}

NS_IMETHODIMP ScriptPreloader::DecodeTask::Run() {
  auto failure = [&]() {
    RefPtr<JS::Stencil> stencil;
    DebugOnly<int> writes = mPreloader->mDecodedStencils->Enqueue(stencil);
    MOZ_ASSERT(writes == 1);
    mPreloader->OnDecodeTaskFailed();
  };

  JS::FrontendContext* fc = JS::NewFrontendContext();
  if (!fc) {
    failure();
    return NS_OK;
  }

  auto cleanup = MakeScopeExit([&]() { JS::DestroyFrontendContext(fc); });

  size_t stackSize = TaskController::GetThreadStackSize();
  JS::SetNativeStackQuota(fc, JS::ThreadStackQuotaForSize(stackSize));

  size_t remaining = mDecodingSources.length();
  for (auto& source : mDecodingSources) {
    RefPtr<JS::Stencil> stencil;
    auto result = JS::DecodeStencil(fc, mDecodeOptions, source.range,
                                    getter_AddRefs(stencil));
    if (result != JS::TranscodeResult::Ok) {
      failure();
      return NS_OK;
    }

    DebugOnly<int> writes = mPreloader->mDecodedStencils->Enqueue(stencil);
    MOZ_ASSERT(writes == 1);

    remaining--;
    if (remaining) {
      mPreloader->onDecodedStencilQueued();
    }
  }

  mPreloader->OnDecodeTaskFinished();
  return NS_OK;
}

ScriptPreloader::CachedStencil::CachedStencil(ScriptPreloader& cache,
                                              InputBuffer& buf)
    : mCache(cache) {
  Code(buf);

  // Swap the mProcessTypes and mOriginalProcessTypes values, since we want to
  // start with an empty set of processes loaded into for this session, and
  // compare against last session's values later.
  mOriginalProcessTypes = mProcessTypes;
  mProcessTypes = {};
}

bool ScriptPreloader::CachedStencil::XDREncode(JS::FrontendContext* aFc) {
  auto cleanup = MakeScopeExit([&]() { MaybeDropStencil(); });

  mXDRData.construct<JS::TranscodeBuffer>();

  JS::TranscodeResult code = JS::EncodeStencil(aFc, mStencil, Buffer());

  if (code == JS::TranscodeResult::Ok) {
    mXDRRange.emplace(Buffer().begin(), Buffer().length());
    mSize = Range().length();
    return true;
  }
  mXDRData.destroy();
  JS::ClearFrontendErrors(aFc);
  return false;
}

already_AddRefed<JS::Stencil> ScriptPreloader::CachedStencil::GetStencil(
    JSContext* cx, const JS::ReadOnlyDecodeOptions& options) {
  MOZ_ASSERT(mReadyToExecute);
  if (mStencil) {
    return do_AddRef(mStencil);
  }

  if (!HasRange()) {
    // We've already executed the script, and thrown it away. But it wasn't
    // in the cache at startup, so we don't have any data to decode. Give
    // up.
    return nullptr;
  }

  // If we have no script at this point, the script was too small to decode
  // off-thread, or it was needed before the off-thread compilation was
  // finished, and is small enough to decode on the main thread rather than
  // wait for the off-thread decoding to finish. In either case, we decode
  // it synchronously the first time it's needed.

  auto start = TimeStamp::Now();
  LOG(Info, "Decoding stencil %s on main thread...\n", mURL.get());

  RefPtr<JS::Stencil> stencil;
  if (JS::DecodeStencil(cx, options, Range(), getter_AddRefs(stencil)) ==
      JS::TranscodeResult::Ok) {
    // Lock the monitor here to avoid data races on mScript
    // from other threads like the cache writing thread.
    //
    // It is possible that we could end up decoding the same
    // script twice, because DecodeScript isn't being guarded
    // by the monitor; however, to encourage off-thread decode
    // to proceed for other scripts we don't hold the monitor
    // while doing main thread decode, merely while updating
    // mScript.
    mCache.mMonitor.AssertNotCurrentThreadOwns();
    MonitorAutoLock mal(mCache.mMonitor);

    mStencil = stencil.forget();

    if (mCache.mSaveComplete) {
      // We can only free XDR data if the stencil isn't borrowing data out of
      // it.
      if (!JS::StencilIsBorrowed(mStencil)) {
        FreeData();
      }
    }
  }

  LOG(Debug, "Finished decoding in %fms",
      (TimeStamp::Now() - start).ToMilliseconds());

  return do_AddRef(mStencil);
}

// nsIAsyncShutdownBlocker

nsresult ScriptPreloader::GetName(nsAString& aName) {
  aName.AssignLiteral(u"ScriptPreloader: Saving bytecode cache");
  return NS_OK;
}

nsresult ScriptPreloader::GetState(nsIPropertyBag** aState) {
  *aState = nullptr;
  return NS_OK;
}

nsresult ScriptPreloader::BlockShutdown(
    nsIAsyncShutdownClient* aBarrierClient) {
  // If we're waiting on a timeout to finish saving, interrupt it and just save
  // immediately.
  mSaveMonitor.Lock().NotifyAll();
  return NS_OK;
}

already_AddRefed<nsIAsyncShutdownClient> ScriptPreloader::GetShutdownBarrier() {
  nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
  MOZ_RELEASE_ASSERT(svc);

  nsCOMPtr<nsIAsyncShutdownClient> barrier;
  (void)svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
  MOZ_RELEASE_ASSERT(barrier);

  return barrier.forget();
}

NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter,
                  nsINamed, nsIAsyncShutdownBlocker)

#undef LOG

}  // namespace mozilla
