/* -*- 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(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(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 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 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(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 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 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 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 doc = do_QueryInterface(subject)) { nsCOMPtr 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 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, nsresult> ScriptPreloader::GetCacheFile( const nsAString& suffix) { NS_ENSURE_TRUE(mProfD, Err(NS_ERROR_NOT_INITIALIZED)); nsCOMPtr 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 ScriptPreloader::OpenCache() { MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD))); nsCOMPtr 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 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 ScriptPreloader::InitCache( const Maybe& cacheFile, ScriptCacheChild* cacheChild) { MOZ_ASSERT(XRE_IsContentProcess()); mCacheInitialized = true; mChildActor = cacheChild; sProcessType = GetChildProcessType(dom::ContentChild::GetSingleton()->GetRemoteType()); nsCOMPtr 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 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(); 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 scripts; Range header(data, data + headerSize); data += headerSize; InputBuffer buf(header); size_t offset = 0; while (!buf.finished()) { auto script = MakeUnique(*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())) { // 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 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 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 scripts; for (auto& script : IterHash(mScripts, Match())) { 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 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&& 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>( std::forward>(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(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 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(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::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 ScriptPreloader::GetShutdownBarrier() { nsCOMPtr svc = components::AsyncShutdown::Service(); MOZ_RELEASE_ASSERT(svc); nsCOMPtr 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