/* -*- 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/DebugOnly.h" #include "mozilla/FileUtils.h" #include "mozilla/IOBuffers.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_javascript.h" #include "mozilla/TaskController.h" #include "mozilla/Telemetry.h" #include "mozilla/Try.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" // JS::Stencil, JS::DecodeStencil #include "js/experimental/CompileScript.h" // JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize #include "js/Transcoding.h" #include "MainThreadUtils.h" #include "nsDebug.h" #include "nsDirectoryServiceUtils.h" #include "nsIFile.h" #include "nsIObserverService.h" #include "nsJSUtils.h" #include "nsMemoryReporterManager.h" #include "nsNetUtil.h" #include "nsProxyRelease.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "xpcpublic.h" #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(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; } StaticRefPtr ScriptPreloader::gScriptPreloader; StaticRefPtr ScriptPreloader::gChildScriptPreloader; StaticAutoPtr ScriptPreloader::gCacheData; StaticAutoPtr ScriptPreloader::gChildCacheData; ScriptPreloader& ScriptPreloader::GetSingleton() { if (!gScriptPreloader) { if (XRE_IsParentProcess()) { gCacheData = new 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 = new 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 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 barrier = GetShutdownBarrier(); barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns); } void ScriptPreloader::InvalidateCache() { { mMonitor.AssertNotCurrentThreadOwns(); MonitorAutoLock mal(mMonitor); // Wait for pending off-thread parses to finish, since they depend on the // memory allocated by our CachedStencil, and can't be canceled // asynchronously. FinishPendingParses(mal); // Pending scripts should have been cleared by the above, and the queue // should have been reset. MOZ_ASSERT(mDecodingScripts.isEmpty()); MOZ_ASSERT(!mDecodedStencils); mScripts.Clear(); // If we've already finished saving the cache at this point, start a new // delayed save operation. This will write out an empty cache file in place // of any cache file we've already written out this session, which will // prevent us from falling back to the current session's cache file on the // next startup. if (mSaveComplete && !mSaveThread && mChildCache) { mSaveComplete = false; StartCacheWrite(); } } { 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 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 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, 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 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[] = "mozXDRcachev003"; Result ScriptPreloader::OpenCache() { if (StartupCache::GetIgnoreDiskCache()) { return Err(NS_ERROR_ABORT); } 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) { 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 ScriptPreloader::InitCache( const Maybe& cacheFile, ScriptCacheChild* cacheChild) { mSaveMonitor.AssertOnWritingThread(); 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; uint32_t crc; if (size < sizeof(MAGIC) + sizeof(headerSize) + sizeof(crc)) { return Err(NS_ERROR_UNEXPECTED); } auto data = mCacheData->get(); 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 scripts; Range header(data, data + headerSize); data += headerSize; // Reconstruct alignment padding if required. size_t currentOffset = data - mCacheData->get(); data += JS::AlignTranscodingBytecodeOffset(currentOffset) - currentOffset; 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 (!JS::IsTranscodingBytecodeAligned(scriptData.get())) { return Err(NS_ERROR_UNEXPECTED); } if (scriptData + script->mSize > end) { return Err(NS_ERROR_UNEXPECTED); } // Make sure offsets match what we'd expect based on script ordering and // size, as a basic sanity check. if (script->mOffset != offset) { return Err(NS_ERROR_UNEXPECTED); } offset += script->mSize; script->mXDRRange.emplace(scriptData, scriptData + script->mSize); // Don't pre-decode the script unless it was used in this process type // during the previous session. if (script->mOriginalProcessTypes.contains(CurrentProcessType())) { scripts.insertBack(script.get()); } else { script->mReadyToExecute = true; } const auto& cachePath = script->mCachePath; mScripts.InsertOrUpdate(cachePath, std::move(script)); } if (buf.error()) { return Err(NS_ERROR_UNEXPECTED); } mDecodingScripts = std::move(scripts); cleanup.release(); } StartDecodeTask(scope); return Ok(); } void ScriptPreloader::PrepareCacheWriteInternal() { MOZ_ASSERT(NS_IsMainThread()); mMonitor.AssertCurrentThreadOwns(); auto cleanup = MakeScopeExit([&]() { if (mChildCache) { mChildCache->PrepareCacheWrite(); } }); if (mDataPrepared) { return; } AutoSafeJSAPI jsapi; JSAutoRealm ar(jsapi.cx(), xpc::PrivilegedJunkScope()); 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. 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 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 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 raiiFd; MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, getter_Transfers(raiiFd))); const auto fd = raiiFd.get(); // We also need to hold mMonitor while we're touching scripts in // mScripts, or they may be freed before we're done with them. mMonitor.AssertNotCurrentThreadOwns(); MonitorAutoLock mal(mMonitor); nsTArray 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(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 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&& 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>( std::forward>(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 ScriptPreloader::GetCachedStencil( JSContext* cx, const JS::ReadOnlyDecodeOptions& options, const nsCString& path) { MOZ_RELEASE_ASSERT( !(XRE_IsContentProcess() && !mCacheInitialized), "ScriptPreloader must be initialized before getting cached " "scripts in the content process."); // If a script is used by both the parent and the child, it's stored only // in the child cache. if (mChildCache) { RefPtr stencil = mChildCache->GetCachedStencilInternal(cx, options, path); if (stencil) { Telemetry::AccumulateCategorical( Telemetry::LABELS_SCRIPT_PRELOADER_REQUESTS::HitChild); return stencil.forget(); } } RefPtr 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 ScriptPreloader::GetCachedStencilInternal( JSContext* cx, const JS::ReadOnlyDecodeOptions& options, const nsCString& path) { auto* cachedScript = mScripts.Get(path); if (cachedScript) { return WaitForCachedStencil(cx, options, cachedScript); } return nullptr; } already_AddRefed ScriptPreloader::WaitForCachedStencil( JSContext* cx, const JS::ReadOnlyDecodeOptions& options, CachedStencil* script) { if (!script->mReadyToExecute) { // mReadyToExecute is kept as false only when off-thread decode task was // available (pref is set to true) and the task was successfully created. // See ScriptPreloader::StartDecodeTask methods. MOZ_ASSERT(mDecodedStencils); // Check for the finished operations that can contain our target. if (mDecodedStencils->AvailableRead() > 0) { FinishOffThreadDecode(); } if (!script->mReadyToExecute) { // Our target is not yet decoded. // If script is small enough, we'd rather decode on main-thread than wait // for a decode task to complete. if (script->mSize < MAX_MAINTHREAD_DECODE_SIZE) { LOG(Info, "Script is small enough to recompile on main thread\n"); script->mReadyToExecute = true; Telemetry::ScalarAdd( Telemetry::ScalarID::SCRIPT_PRELOADER_MAINTHREAD_RECOMPILE, 1); } else { LOG(Info, "Must wait for async script load: %s\n", script->mURL.get()); auto start = TimeStamp::Now(); MonitorAutoLock mal(mMonitor); // Process finished tasks until our target is found. while (!script->mReadyToExecute) { if (mDecodedStencils->AvailableRead() > 0) { FinishOffThreadDecode(); } else { MOZ_ASSERT(!mDecodingScripts.isEmpty()); mWaitingForDecode = true; mal.Wait(); mWaitingForDecode = false; } } 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); } void ScriptPreloader::onDecodedStencilQueued() { mMonitor.AssertNotCurrentThreadOwns(); MonitorAutoLock mal(mMonitor); if (mWaitingForDecode) { // Wake up the blocked main thread. mal.Notify(); } // NOTE: Do not perform DoFinishOffThreadDecode for partial data. } void ScriptPreloader::OnDecodeTaskFinished() { mMonitor.AssertNotCurrentThreadOwns(); MonitorAutoLock mal(mMonitor); if (mWaitingForDecode) { // Wake up the blocked main thread. mal.Notify(); } else { // Issue a Runnable to handle all decoded stencils, even if the next // WaitForCachedStencil call has not happened yet. NS_DispatchToMainThread( NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", this, &ScriptPreloader::DoFinishOffThreadDecode)); } } void ScriptPreloader::OnDecodeTaskFailed() { // NOTE: nullptr is enqueued to mDecodedStencils, and FinishOffThreadDecode // handles it as failure. OnDecodeTaskFinished(); } void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) { mMonitor.AssertCurrentThreadOwns(); // If off-thread decoding task hasn't been started, nothing to do. // This can happen if the javascript.options.parallel_parsing pref was false, // or the decode task fails to start. if (!mDecodedStencils) { return; } // Process any pending decodes that are in flight. while (!mDecodingScripts.isEmpty()) { if (mDecodedStencils->AvailableRead() > 0) { FinishOffThreadDecode(); } else { mWaitingForDecode = true; aMal.Wait(); mWaitingForDecode = false; } } } void ScriptPreloader::DoFinishOffThreadDecode() { // NOTE: mDecodedStencils could already be reset. if (mDecodedStencils && mDecodedStencils->AvailableRead() > 0) { FinishOffThreadDecode(); } } void ScriptPreloader::FinishOffThreadDecode() { MOZ_ASSERT(mDecodedStencils); while (mDecodedStencils->AvailableRead() > 0) { RefPtr stencil; DebugOnly reads = mDecodedStencils->Dequeue(&stencil, 1); MOZ_ASSERT(reads == 1); if (!stencil) { // DecodeTask failed. // Mark all remaining scripts to be decoded on the main thread. for (CachedStencil* next = mDecodingScripts.getFirst(); next;) { auto* script = next; next = script->getNext(); script->mReadyToExecute = true; script->remove(); } break; } CachedStencil* script = mDecodingScripts.getFirst(); MOZ_ASSERT(script); LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get()); script->mStencil = stencil.forget(); script->mReadyToExecute = true; script->remove(); } if (mDecodingScripts.isEmpty()) { mDecodedStencils.reset(); } } void ScriptPreloader::StartDecodeTask(JS::HandleObject scope) { auto start = TimeStamp::Now(); LOG(Debug, "Off-thread decoding scripts...\n"); Vector decodingSources; size_t size = 0; for (CachedStencil* next = mDecodingScripts.getFirst(); next;) { auto* script = next; next = script->getNext(); MOZ_ASSERT(script->IsMemMapped()); // Skip any scripts that we decoded on the main thread rather than // waiting for an off-thread operation to complete. if (script->mReadyToExecute) { script->remove(); continue; } if (!decodingSources.emplaceBack(script->Range(), script->mURL.get(), 0)) { break; } LOG(Debug, "Beginning off-thread decode of script %s (%u bytes)\n", script->mURL.get(), script->mSize); size += script->mSize; } MOZ_ASSERT(decodingSources.length() == mDecodingScripts.length()); if (size == 0 && mDecodingScripts.isEmpty()) { return; } AutoSafeJSAPI jsapi; JSContext* cx = jsapi.cx(); JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope()); JS::CompileOptions options(cx); FillCompileOptionsForCachedStencil(options); // All XDR buffers are mmapped and live longer than JS runtime. // The bytecode can be borrowed from the buffer. options.borrowBuffer = true; options.usePinnedBytecode = true; JS::DecodeOptions decodeOptions(options); size_t decodingSourcesLength = decodingSources.length(); if (!StaticPrefs::javascript_options_parallel_parsing() || !StartDecodeTask(decodeOptions, std::move(decodingSources))) { LOG(Info, "Can't decode %lu bytes of scripts off-thread", (unsigned long)size); for (auto* script : mDecodingScripts) { script->mReadyToExecute = true; } return; } LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n", (unsigned)decodingSourcesLength, (unsigned)size, (TimeStamp::Now() - start).ToMilliseconds()); } bool ScriptPreloader::StartDecodeTask( const JS::ReadOnlyDecodeOptions& decodeOptions, Vector&& decodingSources) { mDecodedStencils.emplace(decodingSources.length()); MOZ_ASSERT(mDecodedStencils); nsCOMPtr task = new DecodeTask(this, decodeOptions, std::move(decodingSources)); nsresult rv = NS_DispatchBackgroundTask(task.forget()); return NS_SUCCEEDED(rv); } NS_IMETHODIMP ScriptPreloader::DecodeTask::Run() { auto failure = [&]() { RefPtr stencil; DebugOnly writes = mPreloader->mDecodedStencils->Enqueue(stencil); MOZ_ASSERT(writes == 1); mPreloader->OnDecodeTaskFailed(); }; JS::FrontendContext* fc = JS::NewFrontendContext(); if (!fc) { failure(); return NS_OK; } auto cleanup = MakeScopeExit([&]() { JS::DestroyFrontendContext(fc); }); size_t stackSize = TaskController::GetThreadStackSize(); JS::SetNativeStackQuota(fc, JS::ThreadStackQuotaForSize(stackSize)); size_t remaining = mDecodingSources.length(); for (auto& source : mDecodingSources) { RefPtr stencil; auto result = JS::DecodeStencil(fc, mDecodeOptions, source.range, getter_AddRefs(stencil)); if (result != JS::TranscodeResult::Ok) { failure(); return NS_OK; } DebugOnly writes = mPreloader->mDecodedStencils->Enqueue(stencil); MOZ_ASSERT(writes == 1); remaining--; if (remaining) { mPreloader->onDecodedStencilQueued(); } } mPreloader->OnDecodeTaskFinished(); return NS_OK; } ScriptPreloader::CachedStencil::CachedStencil(ScriptPreloader& cache, InputBuffer& buf) : mCache(cache) { Code(buf); // Swap the mProcessTypes and mOriginalProcessTypes values, since we want to // start with an empty set of processes loaded into for this session, and // compare against last session's values later. mOriginalProcessTypes = mProcessTypes; mProcessTypes = {}; } bool ScriptPreloader::CachedStencil::XDREncode(JSContext* cx) { auto cleanup = MakeScopeExit([&]() { MaybeDropStencil(); }); mXDRData.construct(); 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 ScriptPreloader::CachedStencil::GetStencil( JSContext* cx, const JS::ReadOnlyDecodeOptions& options) { MOZ_ASSERT(mReadyToExecute); if (mStencil) { return do_AddRef(mStencil); } if (!HasRange()) { // We've already executed the script, and thrown it away. But it wasn't // in the cache at startup, so we don't have any data to decode. Give // up. return nullptr; } // If we have no script at this point, the script was too small to decode // off-thread, or it was needed before the off-thread compilation was // finished, and is small enough to decode on the main thread rather than // wait for the off-thread decoding to finish. In either case, we decode // it synchronously the first time it's needed. auto start = TimeStamp::Now(); LOG(Info, "Decoding stencil %s on main thread...\n", mURL.get()); RefPtr 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 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, nsINamed, nsIAsyncShutdownBlocker) #undef LOG } // namespace mozilla