summaryrefslogtreecommitdiffstats
path: root/js/xpconnect/loader/ScriptPreloader.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/xpconnect/loader/ScriptPreloader.cpp')
-rw-r--r--js/xpconnect/loader/ScriptPreloader.cpp1247
1 files changed, 1247 insertions, 0 deletions
diff --git a/js/xpconnect/loader/ScriptPreloader.cpp b/js/xpconnect/loader/ScriptPreloader.cpp
new file mode 100644
index 0000000000..f53498a5e0
--- /dev/null
+++ b/js/xpconnect/loader/ScriptPreloader.cpp
@@ -0,0 +1,1247 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/ScriptPreloader.h"
+#include "mozilla/loader/ScriptCacheActors.h"
+
+#include "mozilla/URLPreloader.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/IOBuffers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+
+#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions
+#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"
+
+#define STARTUP_COMPLETE_TOPIC "browser-delayed-startup-finished"
+#define DOC_ELEM_INSERTED_TOPIC "document-element-inserted"
+#define CONTENT_DOCUMENT_LOADED_TOPIC "content-document-loaded"
+#define CACHE_WRITE_TOPIC "browser-idle-startup-tasks-finished"
+#define CLEANUP_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;
+
+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;
+}
+
+ScriptPreloader& ScriptPreloader::GetSingleton() {
+ static RefPtr<ScriptPreloader> singleton;
+
+ if (!singleton) {
+ if (XRE_IsParentProcess()) {
+ singleton = new ScriptPreloader();
+ singleton->mChildCache = &GetChildSingleton();
+ Unused << singleton->InitCache();
+ } else {
+ singleton = &GetChildSingleton();
+ }
+
+ ClearOnShutdown(&singleton);
+ }
+
+ return *singleton;
+}
+
+// The child singleton is available in all processes, including the parent, and
+// is used for scripts which are expected to be loaded into child processes
+// (such as process and frame scripts), or scripts that have already been loaded
+// into a child. The child caches are managed as follows:
+//
+// - Every startup, we open the cache file from the last session, move it to a
+// new location, and begin pre-loading the scripts that are stored in it. There
+// is a separate cache file for parent and content processes, but the parent
+// process opens both the parent and content cache files.
+//
+// - Once startup is complete, we write a new cache file for the next session,
+// containing only the scripts that were used during early startup, so we
+// don't waste pre-loading scripts that may not be needed.
+//
+// - For content processes, opening and writing the cache file is handled in the
+// parent process. The first content process of each type sends back the data
+// for scripts that were loaded in early startup, and the parent merges them
+// and writes them to a cache file.
+//
+// - Currently, content processes only benefit from the cache data written
+// during the *previous* session. Ideally, new content processes should
+// probably use the cache data written during this session if there was no
+// previous cache file, but I'd rather do that as a follow-up.
+ScriptPreloader& ScriptPreloader::GetChildSingleton() {
+ static RefPtr<ScriptPreloader> singleton;
+
+ if (!singleton) {
+ singleton = new ScriptPreloader();
+ if (XRE_IsParentProcess()) {
+ Unused << singleton->InitCache(u"scriptCache-child"_ns);
+ }
+ ClearOnShutdown(&singleton);
+ }
+
+ return *singleton;
+}
+
+void ScriptPreloader::InitContentChild(ContentParent& parent) {
+ auto& cache = GetChildSingleton();
+
+ // 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.mInitializedProcesses.contains(processType);
+ cache.mInitializedProcesses += 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) {
+ Unused << parent.SendPScriptCacheConstructor(fd, wantScriptData);
+ } else {
+ Unused << 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;
+}
+
+namespace {
+
+static void TraceOp(JSTracer* trc, void* data) {
+ auto preloader = static_cast<ScriptPreloader*>(data);
+
+ preloader->Trace(trc);
+}
+
+} // anonymous namespace
+
+void ScriptPreloader::Trace(JSTracer* trc) {
+ for (auto& script : IterHash(mScripts)) {
+ script->mScript.Trace(trc);
+ }
+}
+
+ScriptPreloader::ScriptPreloader()
+ : 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, CLEANUP_TOPIC, false);
+ obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false);
+
+ AutoSafeJSAPI jsapi;
+ JS_AddExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
+}
+
+void ScriptPreloader::Cleanup() {
+ // Wait for any pending parses to finish before clearing the mScripts
+ // hashtable, since the parse tasks depend on memory allocated by those
+ // scripts.
+ {
+ MonitorAutoLock mal(mMonitor);
+ FinishPendingParses(mal);
+
+ mScripts.Clear();
+ }
+
+ AutoSafeJSAPI jsapi;
+ JS_RemoveExtraGCRootsTracer(jsapi.cx(), TraceOp, this);
+
+ UnregisterWeakMemoryReporter(this);
+}
+
+void ScriptPreloader::StartCacheWrite() {
+ MOZ_DIAGNOSTIC_ASSERT(!mSaveThread);
+
+ Unused << 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 CachedScripts, and can't be canceled
+ // asynchronously.
+ FinishPendingParses(mal);
+
+ // Pending scripts should have been cleared by the above, and new parses
+ // should not have been queued.
+ MOZ_ASSERT(mParsingScripts.empty());
+ MOZ_ASSERT(mParsingSources.empty());
+ MOZ_ASSERT(mPendingScripts.isEmpty());
+
+ for (auto& script : IterHash(mScripts)) {
+ script.Remove();
+ }
+
+ // 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;
+
+ StartCacheWrite();
+ }
+ }
+
+ {
+ MonitorAutoLock saveMonitorAutoLock(mSaveMonitor);
+
+ mCacheInvalidated = true;
+ }
+
+ // If we're waiting on a timeout to finish saving, interrupt it and just save
+ // immediately.
+ mSaveMonitor.NotifyAll();
+}
+
+nsresult ScriptPreloader::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (!strcmp(topic, STARTUP_COMPLETE_TOPIC)) {
+ obs->RemoveObserver(this, STARTUP_COMPLETE_TOPIC);
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ mStartupFinished = true;
+ } else if (!strcmp(topic, CACHE_WRITE_TOPIC)) {
+ obs->RemoveObserver(this, CACHE_WRITE_TOPIC);
+
+ MOZ_ASSERT(mStartupFinished);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (mChildCache && !mSaveComplete && !mSaveThread) {
+ StartCacheWrite();
+ }
+ } 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, CLEANUP_TOPIC)) {
+ Cleanup();
+ } 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::Telemetry::Accumulate(
+ mozilla::Telemetry::MEMORY_UNIQUE_CONTENT_STARTUP,
+ nsMemoryReporterManager::ResidentUnique() / 1024);
+ }
+#endif
+}
+
+bool ScriptPreloader::WillWriteScripts() {
+ return !mDataPrepared && (XRE_IsParentProcess() || mChildActor);
+}
+
+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));
+ Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777);
+
+ MOZ_TRY(cacheFile->Append(mBaseName + suffix));
+
+ return std::move(cacheFile);
+}
+
+static const uint8_t MAGIC[] = "mozXDRcachev002";
+
+Result<Ok, nsresult> ScriptPreloader::OpenCache() {
+ MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD)));
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, 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, we can 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. The insertion of the first DOM
+ // document element may sometimes be earlier than is ideal, but at
+ // least it should always be safe.
+ mContentStartupFinishedTopic.AssignLiteral(DOC_ELEM_INSERTED_TOPIC);
+ }
+ 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;
+ if (size < sizeof(MAGIC) + sizeof(headerSize)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto data = mCacheData.get<uint8_t>();
+ 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);
+
+ if (data + headerSize > end) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ {
+ auto cleanup = MakeScopeExit([&]() { mScripts.Clear(); });
+
+ LinkedList<CachedScript> scripts;
+
+ Range<uint8_t> header(data, data + headerSize);
+ data += headerSize;
+
+ InputBuffer buf(header);
+
+ size_t offset = 0;
+ while (!buf.finished()) {
+ auto script = MakeUnique<CachedScript>(*this, buf);
+ MOZ_RELEASE_ASSERT(script);
+
+ auto scriptData = data + script->mOffset;
+ if (scriptData + script->mSize > end) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // Make sure offsets match what we'd expect based on script ordering and
+ // size, as a basic sanity check.
+ 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;
+ }
+
+ mScripts.Put(script->mCachePath, script.get());
+ Unused << script.release();
+ }
+
+ if (buf.error()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ mPendingScripts = std::move(scripts);
+ cleanup.release();
+ }
+
+ DecodeNextBatch(OFF_THREAD_FIRST_CHUNK_SIZE, scope);
+ return Ok();
+}
+
+void ScriptPreloader::PrepareCacheWriteInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mMonitor.AssertCurrentThreadOwns();
+
+ auto cleanup = MakeScopeExit([&]() {
+ if (mChildCache) {
+ mChildCache->PrepareCacheWrite();
+ }
+ });
+
+ if (mDataPrepared) {
+ return;
+ }
+
+ AutoSafeJSAPI jsapi;
+ 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.
+ CachedScript* 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(jsapi.cx())) {
+ script.Remove();
+ }
+ }
+
+ if (!found) {
+ mSaveComplete = true;
+ return;
+ }
+
+ mDataPrepared = true;
+}
+
+void ScriptPreloader::PrepareCacheWrite() {
+ MonitorAutoLock mal(mMonitor);
+
+ PrepareCacheWriteInternal();
+}
+
+// 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);
+
+ NS_DispatchToMainThread(
+ NewRunnableMethod("ScriptPreloader::PrepareCacheWrite", this,
+ &ScriptPreloader::PrepareCacheWrite),
+ NS_DISPATCH_SYNC);
+ }
+
+ if (mSaveComplete) {
+ // If we don't have anything we need to save, we're done.
+ return Ok();
+ }
+
+ nsCOMPtr<nsIFile> cacheFile;
+ MOZ_TRY_VAR(cacheFile, GetCacheFile(u"-new.bin"_ns));
+
+ bool exists;
+ MOZ_TRY(cacheFile->Exists(&exists));
+ if (exists) {
+ MOZ_TRY(cacheFile->Remove(false));
+ }
+
+ {
+ AutoFDClose fd;
+ MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644,
+ &fd.rwget()));
+
+ // 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<CachedScript*> scripts;
+ 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());
+
+ OutputBuffer buf;
+ size_t offset = 0;
+ for (auto script : scripts) {
+ script->mOffset = offset;
+ script->Code(buf);
+
+ offset += script->mSize;
+ }
+
+ uint8_t headerSize[4];
+ LittleEndian::writeUint32(headerSize, buf.cursor());
+
+ MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
+ MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+ MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
+ for (auto script : scripts) {
+ MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize));
+
+ if (script->mScript) {
+ script->FreeData();
+ }
+ }
+ }
+
+ MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u".bin"_ns));
+
+ return Ok();
+}
+
+// Runs in the mSaveThread thread, and writes out the cache file for the next
+// session after a reasonable delay.
+nsresult ScriptPreloader::Run() {
+ MonitorAutoLock mal(mSaveMonitor);
+
+ // Ideally wait about 10 seconds before saving, to avoid unnecessary IO
+ // during early startup. 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(10));
+ }
+
+ auto result = URLPreloader::GetSingleton().WriteCache();
+ Unused << NS_WARN_IF(result.isErr());
+
+ result = WriteCache();
+ Unused << NS_WARN_IF(result.isErr());
+
+ result = mChildCache->WriteCache();
+ Unused << 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::NoteScript(const nsCString& url,
+ const nsCString& cachePath,
+ JS::HandleScript jsscript, bool isRunOnce) {
+ if (!Active()) {
+ if (isRunOnce) {
+ if (auto script = mScripts.Get(cachePath)) {
+ script->mIsRunOnce = true;
+ script->MaybeDropScript();
+ }
+ }
+ 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.LookupOrAdd(cachePath, *this, url, cachePath, jsscript);
+ if (isRunOnce) {
+ script->mIsRunOnce = true;
+ }
+
+ if (!script->MaybeDropScript() && !script->mScript) {
+ MOZ_ASSERT(jsscript);
+ script->mScript.Set(jsscript);
+ script->mReadyToExecute = true;
+ }
+
+ script->UpdateLoadTime(TimeStamp::Now());
+ script->mProcessTypes += CurrentProcessType();
+}
+
+void ScriptPreloader::NoteScript(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.LookupOrAdd(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->mScript) {
+ // If the content process is sending us a script entry for a script
+ // 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;
+}
+
+/* static */
+void ScriptPreloader::FillCompileOptionsForCachedScript(
+ JS::CompileOptions& options) {
+ // See IsMultiDecodeCompileOptionsMatching in js/src/vm/JSScript.cpp.
+ options.setNoScriptRval(true);
+ MOZ_ASSERT(!options.selfHostingMode);
+ MOZ_ASSERT(!options.isRunOnce);
+}
+
+JSScript* ScriptPreloader::GetCachedScript(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options,
+ const nsCString& path) {
+ // If a script is used by both the parent and the child, it's stored only
+ // in the child cache.
+ if (mChildCache) {
+ RootedScript script(
+ cx, mChildCache->GetCachedScriptInternal(cx, options, path));
+ if (script) {
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::HitChild);
+ return script;
+ }
+ }
+
+ RootedScript script(cx, GetCachedScriptInternal(cx, options, path));
+ Telemetry::AccumulateCategorical(
+ script ? Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Hit
+ : Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Miss);
+ return script;
+}
+
+JSScript* ScriptPreloader::GetCachedScriptInternal(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options,
+ const nsCString& path) {
+ auto script = mScripts.Get(path);
+ if (script) {
+ return WaitForCachedScript(cx, options, script);
+ }
+
+ return nullptr;
+}
+
+JSScript* ScriptPreloader::WaitForCachedScript(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options,
+ CachedScript* script) {
+ // Always check for finished operations so that we can move on to decoding the
+ // next batch as soon as possible after the pending batch is ready. If we wait
+ // until we hit an unfinished script, we wind up having at most one batch of
+ // buffered scripts, and occasionally under-running that buffer.
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ FinishOffThreadDecode(token);
+ }
+
+ if (!script->mReadyToExecute) {
+ LOG(Info, "Must wait for async script load: %s\n", script->mURL.get());
+ auto start = TimeStamp::Now();
+
+ // If script is small enough, we'd rather recompile 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;
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::SCRIPT_PRELOADER_MAINTHREAD_RECOMPILE, 1);
+ } else {
+ MonitorAutoLock mal(mMonitor);
+
+ // Process script batches until our target is found.
+ while (!script->mReadyToExecute) {
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ MonitorAutoUnlock mau(mMonitor);
+ FinishOffThreadDecode(token);
+ } else {
+ MOZ_ASSERT(!mParsingScripts.empty());
+ mWaitingForDecode = true;
+ mal.Wait();
+ mWaitingForDecode = false;
+ }
+ }
+ }
+
+ double waitedMS = (TimeStamp::Now() - start).ToMilliseconds();
+ Telemetry::Accumulate(Telemetry::SCRIPT_PRELOADER_WAIT_TIME, int(waitedMS));
+ LOG(Debug, "Waited %fms\n", waitedMS);
+ }
+
+ return script->GetJSScript(cx, options);
+}
+
+/* static */
+void ScriptPreloader::OffThreadDecodeCallback(JS::OffThreadToken* token,
+ void* context) {
+ auto cache = static_cast<ScriptPreloader*>(context);
+
+ // Make the token available to main-thread asynchronously. The lock below is
+ // used for Wait/Notify machinery and isn't needed to update the token itself.
+ MOZ_ALWAYS_FALSE(cache->mToken.exchange(token));
+
+ cache->mMonitor.AssertNotCurrentThreadOwns();
+ MonitorAutoLock mal(cache->mMonitor);
+
+ if (cache->mWaitingForDecode) {
+ // Wake up the blocked main thread.
+ mal.Notify();
+ } else if (!cache->mFinishDecodeRunnablePending) {
+ // Issue a Runnable to ensure batches continue to decode even if the next
+ // WaitForCachedScript call has not happened yet.
+ cache->mFinishDecodeRunnablePending = true;
+ NS_DispatchToMainThread(
+ NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", cache,
+ &ScriptPreloader::DoFinishOffThreadDecode));
+ }
+}
+
+void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) {
+ mMonitor.AssertCurrentThreadOwns();
+
+ // Clear out scripts that we have not issued batch for yet.
+ mPendingScripts.clear();
+
+ // Process any pending decodes that are in flight.
+ while (!mParsingScripts.empty()) {
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ MonitorAutoUnlock mau(mMonitor);
+ FinishOffThreadDecode(token);
+ } else {
+ mWaitingForDecode = true;
+ aMal.Wait();
+ mWaitingForDecode = false;
+ }
+ }
+}
+
+void ScriptPreloader::DoFinishOffThreadDecode() {
+ {
+ MonitorAutoLock mal(mMonitor);
+ mFinishDecodeRunnablePending = false;
+ }
+
+ if (JS::OffThreadToken* token = mToken.exchange(nullptr)) {
+ FinishOffThreadDecode(token);
+ }
+}
+
+void ScriptPreloader::FinishOffThreadDecode(JS::OffThreadToken* token) {
+ mMonitor.AssertNotCurrentThreadOwns();
+ MOZ_ASSERT(token);
+
+ auto cleanup = MakeScopeExit([&]() {
+ mParsingSources.clear();
+ mParsingScripts.clear();
+
+ DecodeNextBatch(OFF_THREAD_CHUNK_SIZE);
+ });
+
+ AutoSafeJSAPI jsapi;
+ JSContext* cx = jsapi.cx();
+
+ JSAutoRealm ar(cx, xpc::CompilationScope());
+ JS::Rooted<JS::ScriptVector> jsScripts(cx, JS::ScriptVector(cx));
+
+ // If this fails, we still need to mark the scripts as finished. Any that
+ // weren't successfully compiled in this operation (which should never
+ // happen under ordinary circumstances) will be re-decoded on the main
+ // thread, and raise the appropriate errors when they're executed.
+ //
+ // The exception from the off-thread decode operation will be reported when
+ // we pop the AutoJSAPI off the stack.
+ Unused << JS::FinishMultiOffThreadScriptsDecoder(cx, token, &jsScripts);
+
+ unsigned i = 0;
+ for (auto script : mParsingScripts) {
+ LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get());
+ if (i < jsScripts.length()) {
+ script->mScript.Set(jsScripts[i++]);
+ }
+ script->mReadyToExecute = true;
+ }
+}
+
+void ScriptPreloader::DecodeNextBatch(size_t chunkSize,
+ JS::HandleObject scope) {
+ MOZ_ASSERT(mParsingSources.length() == 0);
+ MOZ_ASSERT(mParsingScripts.length() == 0);
+
+ auto cleanup = MakeScopeExit([&]() {
+ mParsingScripts.clearAndFree();
+ mParsingSources.clearAndFree();
+ });
+
+ auto start = TimeStamp::Now();
+ LOG(Debug, "Off-thread decoding scripts...\n");
+
+ size_t size = 0;
+ for (CachedScript* next = mPendingScripts.getFirst(); next;) {
+ auto script = next;
+ next = script->getNext();
+
+ // 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 we have enough data for one chunk and this script would put us
+ // over our chunk size limit, we're done.
+ if (size > SMALL_SCRIPT_CHUNK_THRESHOLD &&
+ size + script->mSize > chunkSize) {
+ break;
+ }
+ if (!mParsingScripts.append(script) ||
+ !mParsingSources.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);
+
+ script->remove();
+ size += script->mSize;
+ }
+
+ if (size == 0 && mPendingScripts.isEmpty()) {
+ return;
+ }
+
+ AutoSafeJSAPI jsapi;
+ JSContext* cx = jsapi.cx();
+ JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope());
+
+ JS::CompileOptions options(cx);
+ FillCompileOptionsForCachedScript(options);
+ options.setSourceIsLazy(true);
+
+ if (!JS::CanCompileOffThread(cx, options, size) ||
+ !JS::DecodeMultiOffThreadScripts(cx, options, mParsingSources,
+ OffThreadDecodeCallback,
+ static_cast<void*>(this))) {
+ // If we fail here, we don't move on to process the next batch, so make
+ // sure we don't have any other scripts left to process.
+ MOZ_ASSERT(mPendingScripts.isEmpty());
+ for (auto script : mPendingScripts) {
+ script->mReadyToExecute = true;
+ }
+
+ LOG(Info, "Can't decode %lu bytes of scripts off-thread",
+ (unsigned long)size);
+ for (auto script : mParsingScripts) {
+ script->mReadyToExecute = true;
+ }
+ return;
+ }
+
+ cleanup.release();
+
+ LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n",
+ (unsigned)mParsingSources.length(), (unsigned)size,
+ (TimeStamp::Now() - start).ToMilliseconds());
+}
+
+ScriptPreloader::CachedScript::CachedScript(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 = {};
+}
+
+// JS::TraceEdge() can change the value of mScript, but not whether it is
+// null, so we don't update mHasScript to avoid a race.
+void ScriptPreloader::CachedScript::ScriptHolder::Trace(JSTracer* trc) {
+ JS::TraceEdge(trc, &mScript, "ScriptPreloader::CachedScript.mScript");
+}
+
+void ScriptPreloader::CachedScript::ScriptHolder::Set(
+ JS::HandleScript jsscript) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mScript = jsscript;
+ mHasScript = mScript;
+}
+
+void ScriptPreloader::CachedScript::ScriptHolder::Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mScript = nullptr;
+ mHasScript = false;
+}
+
+bool ScriptPreloader::CachedScript::XDREncode(JSContext* cx) {
+ auto cleanup = MakeScopeExit([&]() { MaybeDropScript(); });
+
+ JSAutoRealm ar(cx, mScript.Get());
+ JS::RootedScript jsscript(cx, mScript.Get());
+
+ mXDRData.construct<JS::TranscodeBuffer>();
+
+ JS::TranscodeResult code = JS::EncodeScript(cx, Buffer(), jsscript);
+ if (code == JS::TranscodeResult_Ok) {
+ mXDRRange.emplace(Buffer().begin(), Buffer().length());
+ mSize = Range().length();
+ return true;
+ }
+ mXDRData.destroy();
+ JS_ClearPendingException(cx);
+ return false;
+}
+
+JSScript* ScriptPreloader::CachedScript::GetJSScript(
+ JSContext* cx, const JS::ReadOnlyCompileOptions& options) {
+ MOZ_ASSERT(mReadyToExecute);
+ if (mScript) {
+ if (JS::CheckCompileOptionsMatch(options, mScript.Get())) {
+ return mScript.Get();
+ }
+ LOG(Error, "Cached script %s has different options\n", mURL.get());
+ MOZ_DIAGNOSTIC_ASSERT(false, "Cached script has different options");
+ }
+
+ 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 script %s on main thread...\n", mURL.get());
+
+ JS::RootedScript script(cx);
+ if (JS::DecodeScript(cx, options, Range(), &script)) {
+ mScript.Set(script);
+
+ if (mCache.mSaveComplete) {
+ FreeData();
+ }
+ }
+
+ LOG(Debug, "Finished decoding in %fms",
+ (TimeStamp::Now() - start).ToMilliseconds());
+
+ return mScript.Get();
+}
+
+// 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.NotifyAll();
+ return NS_OK;
+}
+
+already_AddRefed<nsIAsyncShutdownClient> ScriptPreloader::GetShutdownBarrier() {
+ nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
+ MOZ_RELEASE_ASSERT(svc);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ Unused << svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
+ MOZ_RELEASE_ASSERT(barrier);
+
+ return barrier.forget();
+}
+
+NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter,
+ nsIAsyncShutdownBlocker)
+
+#undef LOG
+
+} // namespace mozilla