From a4368ffba182be4360e076ab5952466ea7d564b3 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 5 May 2017 16:15:04 -0700 Subject: [PATCH] Bug 1359653: Part 5 - Pre-load scripts needed during startup in a background thread. r=shu,erahm One of the things that I've noticed in profiling startup overhead is that, even with the startup cache, we spend about 130ms just loading and decoding scripts from the startup cache on my machine. I think we should be able to do better than that by doing some of that work in the background for scripts that we know we'll need during startup. With this change, we seem to consistently save about 3-5% on non-e10s startup overhead on talos. But there's a lot of room for tuning, and I think we get some considerable improvement with a few ongoing tweeks. Some notes about the approach: - Setting up the off-thread compile is fairly expensive, since we need to create a global object, and a lot of its built-in prototype objects for each compile. So in order for there to be a performance improvement for OMT compiles, the script has to be pretty large. Right now, the tipping point seems to be about 20K. There's currently no easy way to improve the per-compile setup overhead, but we should be able to combine the off-thread compiles for multiple smaller scripts into a single operation without any additional per-script overhead. - The time we spend setting up scripts for OMT compile is almost entirely CPU-bound. That means that we have a chunk of about 20-50ms where we can safely schedule thread-safe IO work during early startup, so if we schedule some of our current synchronous IO operations on background threads during the script cache setup, we basically get them for free, and can probably increase the number of scripts we compile in the background. - I went with an uncompressed mmap of the raw XDR data for a storage format. That currently occupies about 5MB of disk space. Gzipped, it's ~1.2MB, so compressing it might save some startup disk IO, but keeping it uncompressed simplifies a lot of the OMT and even main thread decoding process, but, more importantly: - We currently don't use the startup cache in content processes, for a variety of reasons. However, with this approach, I think we can safely store the cached script data from a content process before we load any untrusted code into it, and then share mmapped startup cache data between all content processes. That should speed up content process startup *a lot*, and very likely save memory, too. And: - If we're especially concerned about saving per-process memory, and we keep the cache data mapped for the lifetime of the JS runtime, I think that with some effort we can probably share the static string data from scripts between content processes, without any copying. Right now, it looks like for the main process, there's about 1.5MB of string-ish data in the XDR dumps. It's probably less for content processes, but if we could save .5MB per process this way, it might make it easier to increase the number of content processes we allow. MozReview-Commit-ID: CVJahyNktKB --HG-- extra : rebase_source : 2ec24c8b0000f9187a9bf4a096ee8d93403d7ab2 extra : absorb_source : bb9d799d664a03941447a294ac43c54f334ef6f5 --- js/xpconnect/loader/AutoMemMap.cpp | 57 ++ js/xpconnect/loader/AutoMemMap.h | 67 +++ js/xpconnect/loader/ScriptPreloader-inl.h | 169 ++++++ js/xpconnect/loader/ScriptPreloader.cpp | 667 ++++++++++++++++++++++ js/xpconnect/loader/ScriptPreloader.h | 287 ++++++++++ js/xpconnect/loader/moz.build | 10 + xpcom/build/XPCOMInit.cpp | 2 + 7 files changed, 1259 insertions(+) create mode 100644 js/xpconnect/loader/AutoMemMap.cpp create mode 100644 js/xpconnect/loader/AutoMemMap.h create mode 100644 js/xpconnect/loader/ScriptPreloader-inl.h create mode 100644 js/xpconnect/loader/ScriptPreloader.cpp create mode 100644 js/xpconnect/loader/ScriptPreloader.h diff --git a/js/xpconnect/loader/AutoMemMap.cpp b/js/xpconnect/loader/AutoMemMap.cpp new file mode 100644 index 000000000000..ecf7a8fb536b --- /dev/null +++ b/js/xpconnect/loader/AutoMemMap.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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 "AutoMemMap.h" +#include "ScriptPreloader-inl.h" + +#include "mozilla/Unused.h" +#include "nsIFile.h" + +namespace mozilla { +namespace loader { + +AutoMemMap::~AutoMemMap() +{ + if (fileMap) { + if (addr) { + Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS); + addr = nullptr; + } + + Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS); + fileMap = nullptr; + } +} + +Result +AutoMemMap::init(nsIFile* file, int flags, int mode, PRFileMapProtect prot) +{ + MOZ_ASSERT(!fd); + MOZ_ASSERT(!fileMap); + MOZ_ASSERT(!addr); + + int64_t fileSize; + NS_TRY(file->GetFileSize(&fileSize)); + + if (fileSize > UINT32_MAX) + return Err(NS_ERROR_INVALID_ARG); + + NS_TRY(file->OpenNSPRFileDesc(flags, mode, &fd.rwget())); + + fileMap = PR_CreateFileMap(fd, 0, prot); + if (!fileMap) + return Err(NS_ERROR_FAILURE); + + size_ = fileSize; + addr = PR_MemMap(fileMap, 0, size_); + if (!addr) + return Err(NS_ERROR_FAILURE); + + return Ok(); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/AutoMemMap.h b/js/xpconnect/loader/AutoMemMap.h new file mode 100644 index 000000000000..91cba0595895 --- /dev/null +++ b/js/xpconnect/loader/AutoMemMap.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4; -*- */ +/* 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/. */ + +#ifndef loader_AutoMemMap_h +#define loader_AutoMemMap_h + +#include "mozilla/FileUtils.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/RangedPtr.h" +#include "mozilla/Result.h" +#include "nsIMemoryReporter.h" + +#include + +class nsIFile; + +namespace mozilla { +namespace loader { + +class AutoMemMap +{ + public: + AutoMemMap() = default; + + ~AutoMemMap(); + + Result + init(nsIFile* file, int flags = PR_RDONLY, int mode = 0, + PRFileMapProtect prot = PR_PROT_READONLY); + + bool initialized() { return addr; } + + uint32_t size() const { MOZ_ASSERT(fd); return size_; } + + template + const RangedPtr get() + { + MOZ_ASSERT(addr); + return { static_cast(addr), size_ }; + } + + template + const RangedPtr get() const + { + MOZ_ASSERT(addr); + return { static_cast(addr), size_ }; + } + + size_t nonHeapSizeOfExcludingThis() { return size_; } + + private: + AutoFDClose fd; + PRFileMap* fileMap = nullptr; + + uint32_t size_ = 0; + void* addr = nullptr; + + AutoMemMap(const AutoMemMap&) = delete; + void operator=(const AutoMemMap&) = delete; +}; + +} // namespace loader +} // namespace mozilla + +#endif // loader_AutoMemMap_h diff --git a/js/xpconnect/loader/ScriptPreloader-inl.h b/js/xpconnect/loader/ScriptPreloader-inl.h new file mode 100644 index 000000000000..58cf352949ab --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader-inl.h @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- */ +/* 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/. */ + +#ifndef ScriptPreloader_inl_h +#define ScriptPreloader_inl_h + +#include "mozilla/Attributes.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Range.h" +#include "mozilla/Result.h" +#include "mozilla/Unused.h" +#include "nsString.h" +#include "nsTArray.h" + +#include + +namespace mozilla { +namespace loader { + +static inline Result +WrapNSResult(PRStatus aRv) +{ + if (aRv != PR_SUCCESS) { + return Err(NS_ERROR_FAILURE); + } + return Ok(); +} + +static inline Result +WrapNSResult(nsresult aRv) +{ + if (NS_FAILED(aRv)) { + return Err(aRv); + } + return Ok(); +} + +#define NS_TRY(expr) MOZ_TRY(WrapNSResult(expr)) + + +class OutputBuffer +{ +public: + OutputBuffer() + {} + + uint8_t* + write(size_t size) + { + auto buf = data.AppendElements(size); + cursor_ += size; + return buf; + } + + void + codeUint16(const uint16_t& val) + { + LittleEndian::writeUint16(write(sizeof val), val); + } + + void + codeUint32(const uint32_t& val) + { + LittleEndian::writeUint32(write(sizeof val), val); + } + + void + codeString(const nsCString& str) + { + auto len = CheckedUint16(str.Length()).value(); + + codeUint16(len); + memcpy(write(len), str.BeginReading(), len); + } + + size_t cursor() const { return cursor_; } + + + uint8_t* Get() { return data.Elements(); } + + const uint8_t* Get() const { return data.Elements(); } + +private: + nsTArray data; + size_t cursor_ = 0; +}; + +class InputBuffer +{ +public: + explicit InputBuffer(const Range& buffer) + : data(buffer) + {} + + const uint8_t* + read(size_t size) + { + MOZ_ASSERT(checkCapacity(size)); + + auto buf = &data[cursor_]; + cursor_ += size; + return buf; + } + + bool + codeUint16(uint16_t& val) + { + if (checkCapacity(sizeof val)) { + val = LittleEndian::readUint16(read(sizeof val)); + } + return !error_; + } + + bool + codeUint32(uint32_t& val) + { + if (checkCapacity(sizeof val)) { + val = LittleEndian::readUint32(read(sizeof val)); + } + return !error_; + } + + bool + codeString(nsCString& str) + { + uint16_t len; + if (codeUint16(len)) { + if (checkCapacity(len)) { + str.SetLength(len); + memcpy(str.BeginWriting(), read(len), len); + } + } + return !error_; + } + + bool error() { return error_; } + + bool finished() { return error_ || !remainingCapacity(); } + + size_t remainingCapacity() { return data.length() - cursor_; } + + size_t cursor() const { return cursor_; } + + const uint8_t* Get() const { return data.begin().get(); } + +private: + bool + checkCapacity(size_t size) + { + if (size > remainingCapacity()) { + error_ = true; + } + return !error_; + } + + bool error_ = false; + +public: + const Range& data; + size_t cursor_ = 0; +}; + +}; // namespace loader +}; // namespace mozilla + +#endif // ScriptPreloader_inl_h diff --git a/js/xpconnect/loader/ScriptPreloader.cpp b/js/xpconnect/loader/ScriptPreloader.cpp new file mode 100644 index 000000000000..8a94b24eb153 --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader.cpp @@ -0,0 +1,667 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=4 et sw=4 tw=99: */ +/* 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 "mozilla/ScriptPreloader.h" +#include "ScriptPreloader-inl.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ScriptSettings.h" + +#include "MainThreadUtils.h" +#include "nsDebug.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsJSUtils.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "xpcpublic.h" + +#define DELAYED_STARTUP_TOPIC "browser-delayed-startup-finished" +#define CLEANUP_TOPIC "xpcom-shutdown" +#define SHUTDOWN_TOPIC "quit-application-granted" + +namespace mozilla { +namespace { +static LazyLogModule gLog("ScriptPreloader"); + +#define LOG(level, ...) MOZ_LOG(gLog, LogLevel::level, (__VA_ARGS__)) +} + +using mozilla::dom::AutoJSAPI; +using namespace mozilla::loader; + +nsresult +ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) +{ + MOZ_COLLECT_REPORT( + "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES, + SizeOfLinkedList(mSavedScripts, 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, + SizeOfLinkedList(mRestoredScripts, 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."); + + MOZ_COLLECT_REPORT( + "explicit/script-preloader/non-heap/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) { + singleton = new ScriptPreloader(); + ClearOnShutdown(&singleton); + } + + return *singleton; +} + + +namespace { + +struct MOZ_RAII AutoSafeJSAPI : public AutoJSAPI +{ + AutoSafeJSAPI() { Init(); } +}; + + +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 : mSavedScripts) { + JS::TraceEdge(trc, &script->mScript, "ScriptPreloader::CachedScript.mScript"); + } + + for (auto script : mRestoredScripts) { + JS::TraceEdge(trc, &script->mScript, "ScriptPreloader::CachedScript.mScript"); + } +} + + +ScriptPreloader::ScriptPreloader() + : mMonitor("[ScriptPreloader.mMonitor]") + , mSaveMonitor("[ScriptPreloader.mSaveMonitor]") +{ + nsCOMPtr obs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(obs); + obs->AddObserver(this, DELAYED_STARTUP_TOPIC, false); + obs->AddObserver(this, SHUTDOWN_TOPIC, false); + obs->AddObserver(this, CLEANUP_TOPIC, false); + + AutoSafeJSAPI jsapi; + JS_AddExtraGCRootsTracer(jsapi.cx(), TraceOp, this); +} + +void +ScriptPreloader::ForceWriteCacheFile() +{ + if (mSaveThread) { + MonitorAutoLock mal(mSaveMonitor); + + // Unblock the save thread, so it can start saving before we get to + // XPCOM shutdown. + mal.Notify(); + } +} + +void +ScriptPreloader::Cleanup() +{ + if (mSaveThread) { + MonitorAutoLock mal(mSaveMonitor); + + while (!mSaveComplete && mSaveThread) { + mal.Wait(); + } + } + + mSavedScripts.clear(); + mRestoredScripts.clear(); + + AutoSafeJSAPI jsapi; + JS_RemoveExtraGCRootsTracer(jsapi.cx(), TraceOp, this); + + UnregisterWeakMemoryReporter(this); +} + + +nsresult +ScriptPreloader::Observe(nsISupports* subject, const char* topic, const char16_t* data) +{ + if (!strcmp(topic, DELAYED_STARTUP_TOPIC)) { + nsCOMPtr obs = services::GetObserverService(); + obs->RemoveObserver(this, DELAYED_STARTUP_TOPIC); + + mStartupFinished = true; + + if (XRE_IsParentProcess()) { + Unused << NS_NewNamedThread("SaveScripts", + getter_AddRefs(mSaveThread), this); + } + } else if (!strcmp(topic, SHUTDOWN_TOPIC)) { + ForceWriteCacheFile(); + } else if (!strcmp(topic, CLEANUP_TOPIC)) { + Cleanup(); + } + + return NS_OK; +} + + +Result, nsresult> +ScriptPreloader::GetCacheFile(const char* leafName) +{ + nsCOMPtr cacheFile; + NS_TRY(mProfD->Clone(getter_AddRefs(cacheFile))); + + NS_TRY(cacheFile->AppendNative(NS_LITERAL_CSTRING("startupCache"))); + Unused << cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777); + + NS_TRY(cacheFile->AppendNative(nsDependentCString(leafName))); + + return Move(cacheFile); +} + +static const uint8_t MAGIC[] = "mozXDRcache"; + +Result +ScriptPreloader::OpenCache() +{ + NS_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD))); + + nsCOMPtr cacheFile; + MOZ_TRY_VAR(cacheFile, GetCacheFile("scriptCache.bin")); + + bool exists; + NS_TRY(cacheFile->Exists(&exists)); + if (exists) { + NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("scriptCache-current.bin"))); + } else { + NS_TRY(cacheFile->SetLeafName(NS_LITERAL_STRING("scriptCache-current.bin"))); + NS_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() +{ + mCacheInitialized = true; + + RegisterWeakMemoryReporter(this); + + if (!XRE_IsParentProcess()) { + return Ok(); + } + + MOZ_TRY(OpenCache()); + + 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); + } + + { + AutoCleanLinkedList scripts; + + Range header(data, data + headerSize); + data += headerSize; + + InputBuffer buf(header); + + size_t offset = 0; + while (!buf.finished()) { + auto script = MakeUnique(buf); + + 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); + + scripts.insertBack(script.release()); + } + + if (buf.error()) { + return Err(NS_ERROR_UNEXPECTED); + } + + for (auto script : scripts) { + mScripts.Put(script->mCachePath, script); + } + mRestoredScripts = Move(scripts); + } + + AutoJSAPI jsapi; + MOZ_RELEASE_ASSERT(jsapi.Init(xpc::CompilationScope())); + JSContext* cx = jsapi.cx(); + + auto start = TimeStamp::Now(); + LOG(Info, "Off-thread decoding scripts...\n"); + + JS::CompileOptions options(cx, JSVERSION_LATEST); + for (auto script : mRestoredScripts) { + if (script->mSize > MIN_OFFTHREAD_SIZE && + JS::CanCompileOffThread(cx, options, script->mSize)) { + DecodeScriptOffThread(cx, script); + } else { + script->mReadyToExecute = true; + } + } + + LOG(Info, "Initialized decoding in %fms\n", + (TimeStamp::Now() - start).ToMilliseconds()); + + return Ok(); +} + +static inline Result +Write(PRFileDesc* fd, const void* data, int32_t len) +{ + if (PR_Write(fd, data, len) != len) { + return Err(NS_ERROR_FAILURE); + } + return Ok(); +} + +void +ScriptPreloader::PrepareCacheWrite() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mDataPrepared) { + return; + } + + if (mRestoredScripts.isEmpty()) { + // Check for any new scripts that we need to save. If there aren't + // any, and there aren't any saved scripts that we need to remove, + // don't bother writing out a new cache file. + bool found = false; + for (auto script : mSavedScripts) { + if (script->mXDRRange.isNothing()) { + found = true; + break; + } + } + if (!found) { + mSaveComplete = true; + return; + } + } + + AutoSafeJSAPI jsapi; + + LinkedList asyncScripts; + + for (CachedScript* next = mSavedScripts.getFirst(); next; ) { + CachedScript* script = next; + next = script->getNext(); + + if (!script->mSize && !script->XDREncode(jsapi.cx())) { + script->remove(); + delete script; + } else { + script->mSize = script->Range().length(); + + if (script->mSize > MIN_OFFTHREAD_SIZE) { + script->remove(); + asyncScripts.insertBack(script); + } + } + } + + // Store async-decoded scripts contiguously, since they're loaded + // immediately at startup. + while (CachedScript* s = asyncScripts.popLast()) { + mSavedScripts.insertFront(s); + } + + mDataPrepared = true; +} + +// 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 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(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("scriptCache-new.bin")); + + bool exists; + NS_TRY(cacheFile->Exists(&exists)); + if (exists) { + NS_TRY(cacheFile->Remove(false)); + } + + AutoFDClose fd; + NS_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, &fd.rwget())); + + OutputBuffer buf; + size_t offset = 0; + for (auto script : mSavedScripts) { + 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 : mSavedScripts) { + MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize)); + script->mXDRData.reset(); + } + + NS_TRY(cacheFile->MoveTo(nullptr, NS_LITERAL_STRING("scriptCache.bin"))); + + 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. + mal.Wait(10000); + + Unused << WriteCache(); + + mSaveComplete = true; + NS_ReleaseOnMainThread(mSaveThread.forget()); + + mal.NotifyAll(); + return NS_OK; +} + +/* static */ ScriptPreloader::CachedScript* +ScriptPreloader::FindScript(LinkedList& scripts, const nsCString& cachePath) +{ + for (auto script : scripts) { + if (script->mCachePath == cachePath) { + return script; + } + } + return nullptr; +} + +void +ScriptPreloader::NoteScript(const nsCString& url, const nsCString& cachePath, + JSScript* script) +{ + if (mStartupFinished || !mCacheInitialized) { + return; + } + // Don't bother trying to cache any URLs with cache-busting query + // parameters. + if (cachePath.FindChar('?') >= 0) { + return; + } + + bool exists = mScripts.Get(cachePath); + + CachedScript* restored = nullptr; + if (exists) { + restored = FindScript(mRestoredScripts, cachePath); + } + + if (restored) { + restored->remove(); + mSavedScripts.insertBack(restored); + + MOZ_ASSERT(script); + restored->mScript = script; + restored->mReadyToExecute = true; + } else if (!exists) { + auto cachedScript = new CachedScript(url, cachePath, script); + mSavedScripts.insertBack(cachedScript); + mScripts.Put(cachePath, cachedScript); + } +} + +JSScript* +ScriptPreloader::GetCachedScript(JSContext* cx, const nsCString& path) +{ + auto script = mScripts.Get(path); + if (script) { + return WaitForCachedScript(cx, script); + } + + return nullptr; +} + +JSScript* +ScriptPreloader::WaitForCachedScript(JSContext* cx, CachedScript* script) +{ + if (!script->mReadyToExecute) { + LOG(Info, "Must wait for async script load: %s\n", script->mURL.get()); + auto start = TimeStamp::Now(); + + MonitorAutoLock mal(mMonitor); + + if (!script->mReadyToExecute && script->mSize < MAX_MAINTHREAD_DECODE_SIZE) { + LOG(Info, "Script is small enough to recompile on main thread\n"); + + script->mReadyToExecute = true; + } else { + while (!script->mReadyToExecute) { + mal.Wait(); + } + } + + LOG(Info, "Waited %fms\n", (TimeStamp::Now() - start).ToMilliseconds()); + } + + return script->GetJSScript(cx); +} + + +void +ScriptPreloader::DecodeScriptOffThread(JSContext* cx, CachedScript* script) +{ + JS::CompileOptions options(cx, JSVERSION_LATEST); + + options.setNoScriptRval(true) + .setFileAndLine(script->mURL.get(), 1); + + if (!JS::DecodeOffThreadScript(cx, options, script->Range(), + OffThreadDecodeCallback, + static_cast(script))) { + script->mReadyToExecute = true; + } +} + +void +ScriptPreloader::CancelOffThreadParse(void* token) +{ + AutoSafeJSAPI jsapi; + JS::CancelOffThreadScriptDecoder(jsapi.cx(), token); +} + +/* static */ void +ScriptPreloader::OffThreadDecodeCallback(void* token, void* context) +{ + auto script = static_cast(context); + + MonitorAutoLock mal(GetSingleton().mMonitor); + + if (script->mReadyToExecute) { + // We've already executed this script on the main thread, and opted to + // main thread decode it rather waiting for off-thread decoding to + // finish. So just cancel the off-thread parse rather than completing + // it. + NS_DispatchToMainThread( + NewRunnableMethod(&GetSingleton(), + &ScriptPreloader::CancelOffThreadParse, + token)); + return; + } + + script->mToken = token; + script->mReadyToExecute = true; + + mal.NotifyAll(); +} + +ScriptPreloader::CachedScript::CachedScript(InputBuffer& buf) +{ + Code(buf); +} + +bool +ScriptPreloader::CachedScript::XDREncode(JSContext* cx) +{ + JSAutoCompartment ac(cx, mScript); + JS::RootedScript jsscript(cx, mScript); + + mXDRData.emplace(); + + JS::TranscodeResult code = JS::EncodeScript(cx, Data(), jsscript); + if (code == JS::TranscodeResult_Ok) { + mXDRRange.emplace(Data().begin(), Data().length()); + return true; + } + JS_ClearPendingException(cx); + return false; +} + +JSScript* +ScriptPreloader::CachedScript::GetJSScript(JSContext* cx) +{ + MOZ_ASSERT(mReadyToExecute); + if (mScript) { + return mScript; + } + + // If we have no token 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. + if (!mToken) { + MOZ_ASSERT(mXDRRange.isSome()); + + JS::RootedScript script(cx); + if (JS::DecodeScript(cx, Range(), &script)) { + mScript = script; + } + + return mScript; + } + + mScript = JS::FinishOffThreadScriptDecoder(cx, mToken); + mToken = nullptr; + return mScript; +} + +NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter) + +#undef LOG + +} // namespace mozilla diff --git a/js/xpconnect/loader/ScriptPreloader.h b/js/xpconnect/loader/ScriptPreloader.h new file mode 100644 index 000000000000..b8f02c3d5026 --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader.h @@ -0,0 +1,287 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; -*- */ +/* 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/. */ + +#ifndef ScriptPreloader_h +#define ScriptPreloader_h + +#include "mozilla/LinkedList.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Maybe.h" +#include "mozilla/Monitor.h" +#include "mozilla/Range.h" +#include "mozilla/Vector.h" +#include "mozilla/Result.h" +#include "mozilla/loader/AutoMemMap.h" +#include "nsDataHashtable.h" +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIObserver.h" +#include "nsIThread.h" + +#include "jsapi.h" + +#include + +namespace mozilla { +namespace loader { + class InputBuffer; +} + +using namespace mozilla::loader; + +class ScriptPreloader : public nsIObserver + , public nsIMemoryReporter + , public nsIRunnable +{ + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIMEMORYREPORTER + NS_DECL_NSIRUNNABLE + + static ScriptPreloader& GetSingleton(); + + // Retrieves the script with the given cache key from the script cache. + // Returns null if the script is not cached. + JSScript* GetCachedScript(JSContext* cx, const nsCString& name); + + // Notes the execution of a script with the given URL and cache key. + // Depending on the stage of startup, the script may be serialized and + // stored to the startup script cache. + void NoteScript(const nsCString& url, const nsCString& cachePath, JSScript* script); + + // Initializes the script cache from the startup script cache file. + Result InitCache(); + + void Trace(JSTracer* trc); + +protected: + virtual ~ScriptPreloader() = default; + +private: + // Represents a cached JS script, either initially read from the script + // cache file, to be added to the next session's script cache file, or + // both. + // + // A script which was read from the cache file may be in any of the + // following states: + // + // - Read from the cache, and being compiled off thread. In this case, + // mReadyToExecute is false, and mToken is null. + // - Off-thread compilation has finished, but the script has not yet been + // executed. In this case, mReadyToExecute is true, and mToken has a non-null + // value. + // - Read from the cache, but too small or needed to immediately to be + // compiled off-thread. In this case, mReadyToExecute is true, and both mToken + // and mScript are null. + // - Fully decoded, and ready to be added to the next session's cache + // file. In this case, mReadyToExecute is true, and mScript is non-null. + // + // A script to be added to the next session's cache file always has a + // non-null mScript value. If it was read from the last session's cache + // file, it also has a non-empty mXDRRange range, which will be stored in + // the next session's cache file. If it was compiled in this session, its + // mXDRRange will initially be empty, and its mXDRData buffer will be + // populated just before it is written to the cache file. + class CachedScript : public LinkedListElement + { + public: + CachedScript(CachedScript&&) = default; + + CachedScript(const nsCString& url, const nsCString& cachePath, JSScript* script) + : mURL(url) + , mCachePath(cachePath) + , mScript(script) + , mReadyToExecute(true) + {} + + explicit inline CachedScript(InputBuffer& buf); + + ~CachedScript() + { + auto& cache = GetSingleton(); +#ifdef DEBUG + auto hashValue = cache.mScripts.Get(mCachePath); + MOZ_ASSERT_IF(hashValue, hashValue == this); +#endif + cache.mScripts.Remove(mCachePath); + } + + // Encodes this script into XDR data, and stores the result in mXDRData. + // Returns true on success, false on failure. + bool XDREncode(JSContext* cx); + + // Encodes or decodes this script, in the storage format required by the + // script cache file. + template + void Code(Buffer& buffer) + { + buffer.codeString(mURL); + buffer.codeString(mCachePath); + buffer.codeUint32(mOffset); + buffer.codeUint32(mSize); + } + + // Returns the XDR data generated for this script during this session. See + // mXDRData. + JS::TranscodeBuffer& Data() + { + MOZ_ASSERT(mXDRData.isSome()); + return mXDRData.ref(); + } + + // Returns the read-only XDR data for this script. See mXDRRange. + const JS::TranscodeRange& Range() + { + MOZ_ASSERT(mXDRRange.isSome()); + return mXDRRange.ref(); + } + + JSScript* GetJSScript(JSContext* cx); + + size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) + { + auto size = mallocSizeOf(this); + if (mXDRData.isSome()) { + size += (mXDRData->sizeOfExcludingThis(mallocSizeOf) + + mURL.SizeOfExcludingThisEvenIfShared(mallocSizeOf) + + mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf)); + } + return size; + } + + // The URL from which this script was initially read and compiled. + nsCString mURL; + // A unique identifier for this script's filesystem location, used as a + // primary cache lookup value. + nsCString mCachePath; + + // The offset of this script in the cache file, from the start of the XDR + // data block. + uint32_t mOffset = 0; + // The size of this script's encoded XDR data. + uint32_t mSize = 0; + + JS::Heap mScript; + + // True if this script is ready to be executed. This means that either the + // off-thread portion of an off-thread decode has finished, or the script + // is too small to be decoded off-thread, and may be immediately decoded + // whenever it is first executed. + bool mReadyToExecute = false; + + // The off-thread decode token for a completed off-thread decode, which + // has not yet been finalized on the main thread. + void* mToken = nullptr; + + // The read-only XDR data for this script, which was either read from an + // existing cache file, or generated by encoding a script which was + // compiled during this session. + Maybe mXDRRange; + + // XDR data which was generated from a script compiled during this + // session, and will be written to the cache file. + Maybe mXDRData; + }; + + // There's a trade-off between the time it takes to setup an off-thread + // decode and the time we save by doing the decode off-thread. At this + // point, the setup is quite expensive, and 20K is about where we start to + // see an improvement rather than a regression. + // + // This also means that we get much better performance loading one big + // script than several small scripts, since the setup is per-script, and the + // OMT compile is almost always complete by the time we need a given script. + static constexpr int MIN_OFFTHREAD_SIZE = 20 * 1024; + + // The maximum size of scripts to re-decode on the main thread if off-thread + // decoding hasn't finished yet. In practice, we don't hit this very often, + // but when we do, re-decoding some smaller scripts on the main thread gives + // the background decoding a chance to catch up without blocking the main + // thread for quite as long. + static constexpr int MAX_MAINTHREAD_DECODE_SIZE = 50 * 1024; + + ScriptPreloader(); + + void ForceWriteCacheFile(); + void Cleanup(); + + // Opens the cache file for reading. + Result OpenCache(); + + // Writes a new cache file to disk. Must not be called on the main thread. + Result WriteCache(); + + // Prepares scripts for writing to the cache, serializing new scripts to + // XDR, and calculating their size-based offsets. + void PrepareCacheWrite(); + + // Returns a file pointer for the cache file with the given name in the + // current profile. + Result, nsresult> + GetCacheFile(const char* leafName); + + static CachedScript* FindScript(LinkedList& scripts, const nsCString& cachePath); + + // Waits for the given cached script to finish compiling off-thread, or + // decodes it synchronously on the main thread, as appropriate. + JSScript* WaitForCachedScript(JSContext* cx, CachedScript* script); + + // Begins decoding the given script in a background thread. + void DecodeScriptOffThread(JSContext* cx, CachedScript* script); + + static void OffThreadDecodeCallback(void* token, void* context); + void CancelOffThreadParse(void* token); + + size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) + { + return (mallocSizeOf(this) + mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) + + mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get())); + } + + template + static size_t SizeOfLinkedList(LinkedList& list, mozilla::MallocSizeOf mallocSizeOf) + { + size_t size = 0; + for (auto elem : list) { + size += elem->HeapSizeOfIncludingThis(mallocSizeOf); + } + return size; + } + + // The list of scripts executed during this session, and being saved for + // potential reuse, and to be written to the next session's cache file. + AutoCleanLinkedList mSavedScripts; + + // The list of scripts restored from the cache file at the start of this + // session. Scripts are removed from this list and moved to mSavedScripts + // the first time they're used during this session. + AutoCleanLinkedList mRestoredScripts; + + nsDataHashtable mScripts; + + // True after we've shown the first window, and are no longer adding new + // scripts to the cache. + bool mStartupFinished = false; + + bool mCacheInitialized = false; + bool mSaveComplete = false; + bool mDataPrepared = false; + + nsCOMPtr mProfD; + nsCOMPtr mSaveThread; + + // The mmapped cache data from this session's cache file. + AutoMemMap mCacheData; + + Monitor mMonitor; + Monitor mSaveMonitor; +}; + +} // namespace mozilla + +#endif // ScriptPreloader_h diff --git a/js/xpconnect/loader/moz.build b/js/xpconnect/loader/moz.build index 186a3fb10ea7..8f473602722a 100644 --- a/js/xpconnect/loader/moz.build +++ b/js/xpconnect/loader/moz.build @@ -5,9 +5,11 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. UNIFIED_SOURCES += [ + 'AutoMemMap.cpp', 'ChromeScriptLoader.cpp', 'mozJSLoaderUtils.cpp', 'mozJSSubScriptLoader.cpp', + 'ScriptPreloader.cpp', ] # mozJSComponentLoader.cpp cannot be built in unified mode because it uses @@ -16,10 +18,18 @@ SOURCES += [ 'mozJSComponentLoader.cpp' ] +EXPORTS.mozilla += [ + 'ScriptPreloader.h', +] + EXPORTS.mozilla.dom += [ 'PrecompiledScript.h', ] +EXPORTS.mozilla.loader += [ + 'AutoMemMap.h', +] + EXTRA_JS_MODULES += [ 'ISO8601DateUtils.jsm', 'XPCOMUtils.jsm', diff --git a/xpcom/build/XPCOMInit.cpp b/xpcom/build/XPCOMInit.cpp index 056376082dcf..29ef95f57000 100644 --- a/xpcom/build/XPCOMInit.cpp +++ b/xpcom/build/XPCOMInit.cpp @@ -111,6 +111,7 @@ extern nsresult nsStringInputStreamConstructor(nsISupports*, REFNSIID, void**); #include "mozilla/Services.h" #include "mozilla/Omnijar.h" #include "mozilla/HangMonitor.h" +#include "mozilla/ScriptPreloader.h" #include "mozilla/SystemGroup.h" #include "mozilla/Telemetry.h" #include "mozilla/BackgroundHangMonitor.h" @@ -709,6 +710,7 @@ NS_InitXPCOM2(nsIServiceManager** aResult, nsCOMPtr componentLoader = do_GetService("@mozilla.org/moz/jsloader;1"); + Unused << mozilla::ScriptPreloader::GetSingleton().InitCache(); mozilla::scache::StartupCache::GetSingleton(); mozilla::AvailableMemoryTracker::Activate();