diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/xpconnect/loader/ScriptPreloader.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | js/xpconnect/loader/ScriptPreloader.cpp | 1319 |
1 files changed, 1319 insertions, 0 deletions
diff --git a/js/xpconnect/loader/ScriptPreloader.cpp b/js/xpconnect/loader/ScriptPreloader.cpp new file mode 100644 index 0000000000..22b788127c --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader.cpp @@ -0,0 +1,1319 @@ +/* -*- 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/AlreadyAddRefed.h" +#include "mozilla/Monitor.h" + +#include "mozilla/ScriptPreloader.h" +#include "mozilla/loader/ScriptCacheActors.h" + +#include "mozilla/URLPreloader.h" + +#include "mozilla/ArrayUtils.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 "mozilla/scache/StartupCache.h" + +#include "crc32c.h" +#include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions +#include "js/experimental/JSStencil.h" +#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" + +#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 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; +UniquePtr<AutoMemMap> ScriptPreloader::gCacheData; +UniquePtr<AutoMemMap> ScriptPreloader::gChildCacheData; + +ScriptPreloader& ScriptPreloader::GetSingleton() { + if (!gScriptPreloader) { + if (XRE_IsParentProcess()) { + gCacheData = MakeUnique<AutoMemMap>(); + gScriptPreloader = new ScriptPreloader(gCacheData.get()); + gScriptPreloader->mChildCache = &GetChildSingleton(); + Unused << 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) { + gChildCacheData = MakeUnique<AutoMemMap>(); + gChildScriptPreloader = new ScriptPreloader(gChildCacheData.get()); + if (XRE_IsParentProcess()) { + Unused << 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) { + auto& cache = GetChildSingleton(); + cache.mSaveMonitor.AssertOnWritingThread(); + + // 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; +} + +ScriptPreloader::ScriptPreloader(AutoMemMap* cacheData) + : mCacheData(cacheData), + mMonitor("[ScriptPreloader.mMonitor]"), + mSaveMonitor("[ScriptPreloader.mSaveMonitor]", this) { + // 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::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()); + + 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(); + } + } + + { + MonitorSingleWriterAutoLock 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; + URLPreloader::GetSingleton().SetStartupFinished(); + } 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, 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::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[] = "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_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) { + mSaveMonitor.AssertOnWritingThread(); + 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) { + mSaveMonitor.AssertOnWritingThread(); + 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; + 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<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); + } + + 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; + JSAutoRealm ar(jsapi.cx(), xpc::PrivilegedJunkScope()); + 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(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()); + mSaveMonitor.AssertCurrentThreadOwns(); + + if (!mDataPrepared && !mSaveComplete) { + MonitorSingleWriterAutoUnlock mau(mSaveMonitor); + + 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_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<CachedStencil*> 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(CachedStencil::Comparator()); + + OutputBuffer buf; + size_t offset = 0; + for (auto script : scripts) { + 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 script : scripts) { + 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() { + MonitorSingleWriterAutoLock 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()); + + { + MonitorSingleWriterAutoLock lock(mChildCache->mSaveMonitor); + 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::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; +} + +/* 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::DecodeOptions& options, const nsCString& path) { + MOZ_RELEASE_ASSERT( + !(XRE_IsContentProcess() && !mCacheInitialized), + "ScriptPreloader must be initialized before getting cached " + "scripts in the content process."); + + // 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) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::HitChild); + return stencil.forget(); + } + } + + RefPtr<JS::Stencil> stencil = GetCachedStencilInternal(cx, options, path); + Telemetry::AccumulateCategorical( + stencil ? Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Hit + : Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::Miss); + return stencil.forget(); +} + +already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencilInternal( + JSContext* cx, const JS::DecodeOptions& 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::DecodeOptions& options, CachedStencil* 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->GetStencil(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()); + Vector<RefPtr<JS::Stencil>> stencils; + + // 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::FinishDecodeMultiStencilsOffThread(cx, token, &stencils); + + unsigned i = 0; + for (auto script : mParsingScripts) { + LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get()); + if (i < stencils.length()) { + script->mStencil = stencils[i++].forget(); + } + 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 (CachedStencil* next = mPendingScripts.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 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); + 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); + + if (!JS::CanDecodeOffThread(cx, decodeOptions, size) || + !JS::DecodeMultiStencilsOffThread(cx, decodeOptions, 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::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(JSContext* cx) { + auto cleanup = MakeScopeExit([&]() { MaybeDropStencil(); }); + + mXDRData.construct<JS::TranscodeBuffer>(); + + JS::TranscodeResult code = JS::EncodeStencil(cx, mStencil, Buffer()); + if (code == JS::TranscodeResult::Ok) { + mXDRRange.emplace(Buffer().begin(), Buffer().length()); + mSize = Range().length(); + return true; + } + mXDRData.destroy(); + JS_ClearPendingException(cx); + return false; +} + +already_AddRefed<JS::Stencil> ScriptPreloader::CachedStencil::GetStencil( + JSContext* cx, const JS::DecodeOptions& 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.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, + nsINamed, nsIAsyncShutdownBlocker) + +#undef LOG + +} // namespace mozilla |