summaryrefslogtreecommitdiffstats
path: root/startupcache/StartupCache.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'startupcache/StartupCache.cpp')
-rw-r--r--startupcache/StartupCache.cpp956
1 files changed, 956 insertions, 0 deletions
diff --git a/startupcache/StartupCache.cpp b/startupcache/StartupCache.cpp
new file mode 100644
index 0000000000..9c066a1951
--- /dev/null
+++ b/startupcache/StartupCache.cpp
@@ -0,0 +1,956 @@
+/* -*- 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 "prio.h"
+#include "PLDHashTable.h"
+#include "mozilla/IOInterposer.h"
+#include "mozilla/AutoMemMap.h"
+#include "mozilla/IOBuffers.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/MemUtils.h"
+#include "mozilla/MmapFaultHandler.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Try.h"
+
+#include "nsClassHashtable.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCRT.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIClassInfo.h"
+#include "nsIFile.h"
+#include "nsIObserver.h"
+#include "nsIOutputStream.h"
+#include "nsISupports.h"
+#include "nsITimer.h"
+#include "mozilla/Omnijar.h"
+#include "prenv.h"
+#include "mozilla/Telemetry.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsIProtocolHandler.h"
+#include "GeckoProfiler.h"
+#include "nsAppRunner.h"
+#include "xpcpublic.h"
+#ifdef MOZ_BACKGROUNDTASKS
+# include "mozilla/BackgroundTasks.h"
+#endif
+
+#if defined(XP_WIN)
+# include <windows.h>
+#endif
+
+#ifdef IS_BIG_ENDIAN
+# define SC_ENDIAN "big"
+#else
+# define SC_ENDIAN "little"
+#endif
+
+#if PR_BYTES_PER_WORD == 4
+# define SC_WORDSIZE "4"
+#else
+# define SC_WORDSIZE "8"
+#endif
+
+using namespace mozilla::Compression;
+
+namespace mozilla {
+namespace scache {
+
+MOZ_DEFINE_MALLOC_SIZE_OF(StartupCacheMallocSizeOf)
+
+NS_IMETHODIMP
+StartupCache::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ MutexAutoLock lock(mTableLock);
+ MOZ_COLLECT_REPORT(
+ "explicit/startup-cache/mapping", KIND_NONHEAP, UNITS_BYTES,
+ mCacheData.nonHeapSizeOfExcludingThis(),
+ "Memory used to hold the mapping of the startup cache from file. "
+ "This memory is likely to be swapped out shortly after start-up.");
+
+ MOZ_COLLECT_REPORT("explicit/startup-cache/data", KIND_HEAP, UNITS_BYTES,
+ HeapSizeOfIncludingThis(StartupCacheMallocSizeOf),
+ "Memory used by the startup cache for things other than "
+ "the file mapping.");
+
+ return NS_OK;
+}
+
+static const uint8_t MAGIC[] = "startupcache0002";
+// This is a heuristic value for how much to reserve for mTable to avoid
+// rehashing. This is not a hard limit in release builds, but it is in
+// debug builds as it should be stable. If we exceed this number we should
+// just increase it.
+static const size_t STARTUP_CACHE_RESERVE_CAPACITY = 450;
+// This is a hard limit which we will assert on, to ensure that we don't
+// have some bug causing runaway cache growth.
+static const size_t STARTUP_CACHE_MAX_CAPACITY = 5000;
+
+// Not const because we change it for gtests.
+static uint8_t STARTUP_CACHE_WRITE_TIMEOUT = 60;
+
+#define STARTUP_CACHE_NAME "startupCache." SC_WORDSIZE "." SC_ENDIAN
+
+static inline Result<Ok, nsresult> Write(PRFileDesc* fd, const void* data,
+ int32_t len) {
+ if (PR_Write(fd, data, len) != len) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+static inline Result<Ok, nsresult> Seek(PRFileDesc* fd, int32_t offset) {
+ if (PR_Seek(fd, offset, PR_SEEK_SET) == -1) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+static nsresult MapLZ4ErrorToNsresult(size_t aError) {
+ return NS_ERROR_FAILURE;
+}
+
+StartupCache* StartupCache::GetSingletonNoInit() {
+ return StartupCache::gStartupCache;
+}
+
+StartupCache* StartupCache::GetSingleton() {
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ return nullptr;
+ }
+#endif
+
+ if (!gStartupCache) {
+ if (!XRE_IsParentProcess()) {
+ return nullptr;
+ }
+#ifdef MOZ_DISABLE_STARTUPCACHE
+ return nullptr;
+#else
+ StartupCache::InitSingleton();
+#endif
+ }
+
+ return StartupCache::gStartupCache;
+}
+
+void StartupCache::DeleteSingleton() { StartupCache::gStartupCache = nullptr; }
+
+nsresult StartupCache::InitSingleton() {
+ nsresult rv;
+ StartupCache::gStartupCache = new StartupCache();
+
+ rv = StartupCache::gStartupCache->Init();
+ if (NS_FAILED(rv)) {
+ StartupCache::gStartupCache = nullptr;
+ }
+ return rv;
+}
+
+StaticRefPtr<StartupCache> StartupCache::gStartupCache;
+bool StartupCache::gShutdownInitiated;
+bool StartupCache::gIgnoreDiskCache;
+bool StartupCache::gFoundDiskCacheOnInit;
+
+NS_IMPL_ISUPPORTS(StartupCache, nsIMemoryReporter)
+
+StartupCache::StartupCache()
+ : mTableLock("StartupCache::mTableLock"),
+ mDirty(false),
+ mWrittenOnce(false),
+ mCurTableReferenced(false),
+ mRequestedCount(0),
+ mCacheEntriesBaseOffset(0) {}
+
+StartupCache::~StartupCache() { UnregisterWeakMemoryReporter(this); }
+
+nsresult StartupCache::Init() {
+ // workaround for bug 653936
+ nsCOMPtr<nsIProtocolHandler> jarInitializer(
+ do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "jar"));
+
+ nsresult rv;
+
+ if (mozilla::RunningGTest()) {
+ STARTUP_CACHE_WRITE_TIMEOUT = 3;
+ }
+
+ // This allows to override the startup cache filename
+ // which is useful from xpcshell, when there is no ProfLDS directory to keep
+ // cache in.
+ char* env = PR_GetEnv("MOZ_STARTUP_CACHE");
+ if (env && *env) {
+ rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false,
+ getter_AddRefs(mFile));
+ } else {
+ nsCOMPtr<nsIFile> file;
+ rv = NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ // return silently, this will fail in mochitests's xpcshell process.
+ return rv;
+ }
+
+ rv = file->AppendNative("startupCache"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Try to create the directory if it's not there yet
+ rv = file->Create(nsIFile::DIRECTORY_TYPE, 0777);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) return rv;
+
+ rv = file->AppendNative(nsLiteralCString(STARTUP_CACHE_NAME));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFile = file;
+ }
+
+ NS_ENSURE_TRUE(mFile, NS_ERROR_UNEXPECTED);
+
+ mObserverService = do_GetService("@mozilla.org/observer-service;1");
+
+ if (!mObserverService) {
+ NS_WARNING("Could not get observerService.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mListener = new StartupCacheListener();
+ rv = mObserverService->AddObserver(mListener, NS_XPCOM_SHUTDOWN_OBSERVER_ID,
+ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mObserverService->AddObserver(mListener, "startupcache-invalidate",
+ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mObserverService->AddObserver(mListener, "intl:app-locales-changed",
+ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ {
+ MutexAutoLock lock(mTableLock);
+ auto result = LoadArchive();
+ rv = result.isErr() ? result.unwrapErr() : NS_OK;
+ }
+
+ gFoundDiskCacheOnInit = rv != NS_ERROR_FILE_NOT_FOUND;
+
+ // Sometimes we don't have a cache yet, that's ok.
+ // If it's corrupted, just remove it and start over.
+ if (gIgnoreDiskCache || (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND)) {
+ NS_WARNING("Failed to load startupcache file correctly, removing!");
+ InvalidateCache();
+ }
+
+ RegisterWeakMemoryReporter(this);
+ mDecompressionContext = MakeUnique<LZ4FrameDecompressionContext>(true);
+
+ return NS_OK;
+}
+
+void StartupCache::StartPrefetchMemory() {
+ {
+ MonitorAutoLock lock(mPrefetchComplete);
+ mPrefetchInProgress = true;
+ }
+ NS_DispatchBackgroundTask(NewRunnableMethod<uint8_t*, size_t>(
+ "StartupCache::ThreadedPrefetch", this, &StartupCache::ThreadedPrefetch,
+ mCacheData.get<uint8_t>().get(), mCacheData.size()));
+}
+
+/**
+ * LoadArchive can only be called from the main thread.
+ */
+Result<Ok, nsresult> StartupCache::LoadArchive() {
+ MOZ_ASSERT(NS_IsMainThread(), "Can only load startup cache on main thread");
+ if (gIgnoreDiskCache) return Err(NS_ERROR_FAILURE);
+
+ MOZ_TRY(mCacheData.init(mFile));
+ auto size = mCacheData.size();
+ if (CanPrefetchMemory()) {
+ StartPrefetchMemory();
+ }
+
+ uint32_t headerSize;
+ if (size < sizeof(MAGIC) + sizeof(headerSize)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto data = mCacheData.get<uint8_t>();
+ auto end = data + size;
+
+ MMAP_FAULT_HANDLER_BEGIN_BUFFER(data.get(), 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 (headerSize > end - data) {
+ MOZ_ASSERT(false, "StartupCache file is corrupt.");
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ Range<uint8_t> header(data, data + headerSize);
+ data += headerSize;
+
+ mCacheEntriesBaseOffset = sizeof(MAGIC) + sizeof(headerSize) + headerSize;
+ {
+ if (!mTable.reserve(STARTUP_CACHE_RESERVE_CAPACITY)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ auto cleanup = MakeScopeExit([&]() {
+ mTableLock.AssertCurrentThreadOwns();
+ WaitOnPrefetch();
+ mTable.clear();
+ mCacheData.reset();
+ });
+ loader::InputBuffer buf(header);
+
+ uint32_t currentOffset = 0;
+ while (!buf.finished()) {
+ uint32_t offset = 0;
+ uint32_t compressedSize = 0;
+ uint32_t uncompressedSize = 0;
+ nsCString key;
+ buf.codeUint32(offset);
+ buf.codeUint32(compressedSize);
+ buf.codeUint32(uncompressedSize);
+ buf.codeString(key);
+
+ if (offset + compressedSize > end - data) {
+ MOZ_ASSERT(false, "StartupCache file is corrupt.");
+ 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 (offset != currentOffset) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ currentOffset += compressedSize;
+
+ // We could use mTable.putNew if we knew the file we're loading weren't
+ // corrupt. However, we don't know that, so check if the key already
+ // exists. If it does, we know the file must be corrupt.
+ decltype(mTable)::AddPtr p = mTable.lookupForAdd(key);
+ if (p) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ if (!mTable.add(
+ p, key,
+ StartupCacheEntry(offset, compressedSize, uncompressedSize))) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ }
+
+ if (buf.error()) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ cleanup.release();
+ }
+
+ MMAP_FAULT_HANDLER_CATCH(Err(NS_ERROR_UNEXPECTED))
+
+ return Ok();
+}
+
+bool StartupCache::HasEntry(const char* id) {
+ AUTO_PROFILER_LABEL("StartupCache::HasEntry", OTHER);
+
+ MOZ_ASSERT(NS_IsMainThread(), "Startup cache only available on main thread");
+
+ MutexAutoLock lock(mTableLock);
+ return mTable.has(nsDependentCString(id));
+}
+
+nsresult StartupCache::GetBuffer(const char* id, const char** outbuf,
+ uint32_t* length)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ AUTO_PROFILER_LABEL("StartupCache::GetBuffer", OTHER);
+
+ NS_ASSERTION(NS_IsMainThread(),
+ "Startup cache only available on main thread");
+
+ Telemetry::LABELS_STARTUP_CACHE_REQUESTS label =
+ Telemetry::LABELS_STARTUP_CACHE_REQUESTS::Miss;
+ auto telemetry =
+ MakeScopeExit([&label] { Telemetry::AccumulateCategorical(label); });
+
+ MutexAutoLock lock(mTableLock);
+ decltype(mTable)::Ptr p = mTable.lookup(nsDependentCString(id));
+ if (!p) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ auto& value = p->value();
+ if (value.mData) {
+ label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory;
+ } else {
+ if (!mCacheData.initialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ // It is impossible for a write to be pending here. This is because
+ // we just checked mCacheData.initialized(), and this is reset before
+ // writing to the cache. It's not re-initialized unless we call
+ // LoadArchive(), either from Init() (which must have already happened) or
+ // InvalidateCache(). InvalidateCache() locks the mutex, so a write can't be
+ // happening.
+ // Also, WriteToDisk() requires mTableLock, so while it's writing we can't
+ // be here.
+
+ size_t totalRead = 0;
+ size_t totalWritten = 0;
+ Span<const char> compressed = Span(
+ mCacheData.get<char>().get() + mCacheEntriesBaseOffset + value.mOffset,
+ value.mCompressedSize);
+ value.mData = UniqueFreePtr<char[]>(reinterpret_cast<char*>(
+ malloc(sizeof(char) * value.mUncompressedSize)));
+ Span<char> uncompressed = Span(value.mData.get(), value.mUncompressedSize);
+ MMAP_FAULT_HANDLER_BEGIN_BUFFER(uncompressed.Elements(),
+ uncompressed.Length())
+ bool finished = false;
+ while (!finished) {
+ auto result = mDecompressionContext->Decompress(
+ uncompressed.From(totalWritten), compressed.From(totalRead));
+ if (NS_WARN_IF(result.isErr())) {
+ value.mData = nullptr;
+ MutexAutoUnlock unlock(mTableLock);
+ InvalidateCache();
+ return NS_ERROR_FAILURE;
+ }
+ auto decompressionResult = result.unwrap();
+ totalRead += decompressionResult.mSizeRead;
+ totalWritten += decompressionResult.mSizeWritten;
+ finished = decompressionResult.mFinished;
+ }
+
+ MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE)
+
+ label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitDisk;
+ }
+
+ if (!value.mRequested) {
+ value.mRequested = true;
+ value.mRequestedOrder = ++mRequestedCount;
+ MOZ_ASSERT(mRequestedCount <= mTable.count(),
+ "Somehow we requested more StartupCache items than exist.");
+ ResetStartupWriteTimerCheckingReadCount();
+ }
+
+ // Track that something holds a reference into mTable, so we know to hold
+ // onto it in case the cache is invalidated.
+ mCurTableReferenced = true;
+ *outbuf = value.mData.get();
+ *length = value.mUncompressedSize;
+ return NS_OK;
+}
+
+// Makes a copy of the buffer, client retains ownership of inbuf.
+nsresult StartupCache::PutBuffer(const char* id, UniqueFreePtr<char[]>&& inbuf,
+ uint32_t len) MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ NS_ASSERTION(NS_IsMainThread(),
+ "Startup cache only available on main thread");
+ if (StartupCache::gShutdownInitiated) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Try to gain the table write lock. If the background task to write the
+ // cache is running, this will fail.
+ MutexAutoTryLock lock(mTableLock);
+ if (!lock) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mTableLock.AssertCurrentThreadOwns();
+ bool exists = mTable.has(nsDependentCString(id));
+ if (exists) {
+ NS_WARNING("Existing entry in StartupCache.");
+ // Double-caching is undesirable but not an error.
+ return NS_OK;
+ }
+
+ // putNew returns false on alloc failure - in the very unlikely event we hit
+ // that and aren't going to crash elsewhere, there's no reason we need to
+ // crash here.
+ if (mTable.putNew(nsCString(id), StartupCacheEntry(std::move(inbuf), len,
+ ++mRequestedCount))) {
+ return ResetStartupWriteTimer();
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mTable.count() < STARTUP_CACHE_MAX_CAPACITY,
+ "Too many StartupCache entries.");
+ return NS_OK;
+}
+
+size_t StartupCache::HeapSizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ // This function could measure more members, but they haven't been found by
+ // DMD to be significant. They can be added later if necessary.
+
+ size_t n = aMallocSizeOf(this);
+
+ n += mTable.shallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
+ if (iter.get().value().mData) {
+ n += aMallocSizeOf(iter.get().value().mData.get());
+ }
+ n += iter.get().key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+/**
+ * WriteToDisk writes the cache out to disk. Callers of WriteToDisk need to call
+ * WaitOnWriteComplete to make sure there isn't a write
+ * happening on another thread.
+ * We own the mTableLock here.
+ */
+Result<Ok, nsresult> StartupCache::WriteToDisk() {
+ if (!mDirty || mWrittenOnce) {
+ return Ok();
+ }
+
+ if (!mFile) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ AutoFDClose raiiFd;
+ MOZ_TRY(mFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ 0644, getter_Transfers(raiiFd)));
+ const auto fd = raiiFd.get();
+
+ nsTArray<StartupCacheEntry::KeyValuePair> entries(mTable.count());
+ for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
+ if (iter.get().value().mRequested) {
+ StartupCacheEntry::KeyValuePair kv(&iter.get().key(),
+ &iter.get().value());
+ entries.AppendElement(kv);
+ }
+ }
+
+ if (entries.IsEmpty()) {
+ return Ok();
+ }
+
+ entries.Sort(StartupCacheEntry::Comparator());
+ loader::OutputBuffer buf;
+ for (auto& e : entries) {
+ auto* key = e.first;
+ auto* value = e.second;
+ auto uncompressedSize = value->mUncompressedSize;
+ // Set the mHeaderOffsetInFile so we can go back and edit the offset.
+ value->mHeaderOffsetInFile = buf.cursor();
+ // Write a 0 offset/compressed size as a placeholder until we get the real
+ // offset after compressing.
+ buf.codeUint32(0);
+ buf.codeUint32(0);
+ buf.codeUint32(uncompressedSize);
+ buf.codeString(*key);
+ }
+
+ uint8_t headerSize[4];
+ LittleEndian::writeUint32(headerSize, buf.cursor());
+
+ MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
+ MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
+ size_t headerStart = sizeof(MAGIC) + sizeof(headerSize);
+ size_t dataStart = headerStart + buf.cursor();
+ MOZ_TRY(Seek(fd, dataStart));
+
+ size_t offset = 0;
+
+ const size_t chunkSize = 1024 * 16;
+ LZ4FrameCompressionContext ctx(6, /* aCompressionLevel */
+ chunkSize, /* aReadBufLen */
+ true, /* aChecksum */
+ true); /* aStableSrc */
+ size_t writeBufLen = ctx.GetRequiredWriteBufferLength();
+ auto writeBuffer = MakeUnique<char[]>(writeBufLen);
+ auto writeSpan = Span(writeBuffer.get(), writeBufLen);
+
+ for (auto& e : entries) {
+ auto value = e.second;
+ value->mOffset = offset;
+ Span<const char> result;
+ MOZ_TRY_VAR(result,
+ ctx.BeginCompressing(writeSpan).mapErr(MapLZ4ErrorToNsresult));
+ MOZ_TRY(Write(fd, result.Elements(), result.Length()));
+ offset += result.Length();
+
+ for (size_t i = 0; i < value->mUncompressedSize; i += chunkSize) {
+ size_t size = std::min(chunkSize, value->mUncompressedSize - i);
+ char* uncompressed = value->mData.get() + i;
+ MOZ_TRY_VAR(result, ctx.ContinueCompressing(Span(uncompressed, size))
+ .mapErr(MapLZ4ErrorToNsresult));
+ MOZ_TRY(Write(fd, result.Elements(), result.Length()));
+ offset += result.Length();
+ }
+
+ MOZ_TRY_VAR(result, ctx.EndCompressing().mapErr(MapLZ4ErrorToNsresult));
+ MOZ_TRY(Write(fd, result.Elements(), result.Length()));
+ offset += result.Length();
+ value->mCompressedSize = offset - value->mOffset;
+ MOZ_TRY(Seek(fd, dataStart + offset));
+ }
+
+ for (auto& e : entries) {
+ auto value = e.second;
+ uint8_t* headerEntry = buf.Get() + value->mHeaderOffsetInFile;
+ LittleEndian::writeUint32(headerEntry, value->mOffset);
+ LittleEndian::writeUint32(headerEntry + sizeof(value->mOffset),
+ value->mCompressedSize);
+ }
+ MOZ_TRY(Seek(fd, headerStart));
+ MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
+
+ mDirty = false;
+ mWrittenOnce = true;
+
+ return Ok();
+}
+
+void StartupCache::InvalidateCache(bool memoryOnly) {
+ WaitOnPrefetch();
+ // Ensure we're not writing using mTable...
+ MutexAutoLock lock(mTableLock);
+
+ mWrittenOnce = false;
+ if (memoryOnly) {
+ // This should only be called in tests.
+ auto writeResult = WriteToDisk();
+ if (NS_WARN_IF(writeResult.isErr())) {
+ gIgnoreDiskCache = true;
+ return;
+ }
+ }
+ if (mCurTableReferenced) {
+ // There should be no way for this assert to fail other than a user manually
+ // sending startupcache-invalidate messages through the Browser Toolbox. If
+ // something knowingly invalidates the cache, the event can be counted with
+ // mAllowedInvalidationsCount.
+ MOZ_DIAGNOSTIC_ASSERT(
+ xpc::IsInAutomation() ||
+ // The allowed invalidations can grow faster than the old tables, so
+ // guard against incorrect unsigned subtraction.
+ mAllowedInvalidationsCount > mOldTables.Length() ||
+ // Now perform the real check.
+ mOldTables.Length() - mAllowedInvalidationsCount < 10,
+ "Startup cache invalidated too many times.");
+ mOldTables.AppendElement(std::move(mTable));
+ mCurTableReferenced = false;
+ } else {
+ mTable.clear();
+ }
+ mRequestedCount = 0;
+ if (!memoryOnly) {
+ mCacheData.reset();
+ nsresult rv = mFile->Remove(false);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
+ gIgnoreDiskCache = true;
+ return;
+ }
+ }
+ gIgnoreDiskCache = false;
+ auto result = LoadArchive();
+ if (NS_WARN_IF(result.isErr())) {
+ gIgnoreDiskCache = true;
+ }
+}
+
+void StartupCache::CountAllowedInvalidation() { mAllowedInvalidationsCount++; }
+
+void StartupCache::MaybeInitShutdownWrite() {
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ gShutdownInitiated = true;
+
+ MaybeWriteOffMainThread();
+}
+
+void StartupCache::EnsureShutdownWriteComplete() {
+ MutexAutoLock lock(mTableLock);
+ // If we've already written or there's nothing to write,
+ // we don't need to do anything. This is the common case.
+ if (mWrittenOnce || (mCacheData.initialized() && !ShouldCompactCache())) {
+ return;
+ }
+ // Otherwise, ensure the write happens. The timer should have been cancelled
+ // already in MaybeInitShutdownWrite.
+
+ // We got the lock. Keep the following in sync with
+ // MaybeWriteOffMainThread:
+ WaitOnPrefetch();
+ mDirty = true;
+ mCacheData.reset();
+ // Most of this should be redundant given MaybeWriteOffMainThread should
+ // have run before now.
+
+ auto writeResult = WriteToDisk();
+ Unused << NS_WARN_IF(writeResult.isErr());
+ // We've had the lock, and `WriteToDisk()` sets mWrittenOnce and mDirty
+ // when done, and checks for them when starting, so we don't need to do
+ // anything else.
+}
+
+void StartupCache::IgnoreDiskCache() {
+ gIgnoreDiskCache = true;
+ if (gStartupCache) gStartupCache->InvalidateCache();
+}
+
+bool StartupCache::GetIgnoreDiskCache() { return gIgnoreDiskCache; }
+
+void StartupCache::WaitOnPrefetch() {
+ // This can't be called from within ThreadedPrefetch()
+ MonitorAutoLock lock(mPrefetchComplete);
+ while (mPrefetchInProgress) {
+ mPrefetchComplete.Wait();
+ }
+}
+
+void StartupCache::ThreadedPrefetch(uint8_t* aStart, size_t aSize) {
+ // Always notify of completion, even if MMAP_FAULT_HANDLER_CATCH()
+ // early-returns.
+ auto notifyPrefetchComplete = MakeScopeExit([&] {
+ MonitorAutoLock lock(mPrefetchComplete);
+ mPrefetchInProgress = false;
+ mPrefetchComplete.NotifyAll();
+ });
+
+ // PrefetchMemory does madvise/equivalent, but doesn't access the memory
+ // pointed to by aStart
+ MMAP_FAULT_HANDLER_BEGIN_BUFFER(aStart, aSize)
+ PrefetchMemory(aStart, aSize);
+ MMAP_FAULT_HANDLER_CATCH()
+}
+
+// mTableLock must be held
+bool StartupCache::ShouldCompactCache() {
+ // If we've requested less than 4/5 of the startup cache, then we should
+ // probably compact it down. This can happen quite easily after the first run,
+ // which seems to request quite a few more things than subsequent runs.
+ CheckedInt<uint32_t> threshold = CheckedInt<uint32_t>(mTable.count()) * 4 / 5;
+ MOZ_RELEASE_ASSERT(threshold.isValid(), "Runaway StartupCache size");
+ return mRequestedCount < threshold.value();
+}
+
+/*
+ * The write-thread is spawned on a timeout(which is reset with every write).
+ * This can avoid a slow shutdown.
+ */
+void StartupCache::WriteTimeout(nsITimer* aTimer, void* aClosure) {
+ /*
+ * It is safe to use the pointer passed in aClosure to reference the
+ * StartupCache object because the timer's lifetime is tightly coupled to
+ * the lifetime of the StartupCache object; this timer is canceled in the
+ * StartupCache destructor, guaranteeing that this function runs if and only
+ * if the StartupCache object is valid.
+ */
+ StartupCache* startupCacheObj = static_cast<StartupCache*>(aClosure);
+ startupCacheObj->MaybeWriteOffMainThread();
+}
+
+/*
+ * See StartupCache::WriteTimeout above - this is just the non-static body.
+ */
+void StartupCache::MaybeWriteOffMainThread() {
+ {
+ MutexAutoLock lock(mTableLock);
+ if (mWrittenOnce || (mCacheData.initialized() && !ShouldCompactCache())) {
+ return;
+ }
+ }
+ // Keep this code in sync with EnsureShutdownWriteComplete.
+ WaitOnPrefetch();
+ {
+ MutexAutoLock lock(mTableLock);
+ mDirty = true;
+ mCacheData.reset();
+ }
+
+ RefPtr<StartupCache> self = this;
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction("StartupCache::Write", [self]() mutable {
+ MutexAutoLock lock(self->mTableLock);
+ auto result = self->WriteToDisk();
+ Unused << NS_WARN_IF(result.isErr());
+ });
+ NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+}
+
+// We don't want to refcount StartupCache, so we'll just
+// hold a ref to this and pass it to observerService instead.
+NS_IMPL_ISUPPORTS(StartupCacheListener, nsIObserver)
+
+nsresult StartupCacheListener::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ StartupCache* sc = StartupCache::GetSingleton();
+ if (!sc) return NS_OK;
+
+ if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+ // Do not leave the thread running past xpcom shutdown
+ sc->WaitOnPrefetch();
+ StartupCache::gShutdownInitiated = true;
+ // Note that we don't do anything special for the background write
+ // task; we expect the threadpool to finish running any tasks already
+ // posted to it prior to shutdown. FastShutdown will call
+ // EnsureShutdownWriteComplete() to ensure any pending writes happen
+ // in that case.
+ } else if (strcmp(topic, "startupcache-invalidate") == 0) {
+ sc->InvalidateCache(data && nsCRT::strcmp(data, u"memoryOnly") == 0);
+ } else if (strcmp(topic, "intl:app-locales-changed") == 0) {
+ // Live language switching invalidates the startup cache due to the history
+ // sidebar retaining localized strings in its internal SQL query. This
+ // should be a relatively rare event, but a user could do it an arbitrary
+ // number of times.
+ sc->CountAllowedInvalidation();
+ }
+ return NS_OK;
+}
+
+nsresult StartupCache::GetDebugObjectOutputStream(
+ nsIObjectOutputStream* aStream, nsIObjectOutputStream** aOutStream) {
+ NS_ENSURE_ARG_POINTER(aStream);
+#ifdef DEBUG
+ auto* stream = new StartupCacheDebugOutputStream(aStream, &mWriteObjectMap);
+ NS_ADDREF(*aOutStream = stream);
+#else
+ NS_ADDREF(*aOutStream = aStream);
+#endif
+
+ return NS_OK;
+}
+
+nsresult StartupCache::ResetStartupWriteTimerCheckingReadCount() {
+ nsresult rv = NS_OK;
+ if (!mTimer)
+ mTimer = NS_NewTimer();
+ else
+ rv = mTimer->Cancel();
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Wait for the specified timeout, then write out the cache.
+ mTimer->InitWithNamedFuncCallback(
+ StartupCache::WriteTimeout, this, STARTUP_CACHE_WRITE_TIMEOUT * 1000,
+ nsITimer::TYPE_ONE_SHOT, "StartupCache::WriteTimeout");
+ return NS_OK;
+}
+
+// For test code only
+nsresult StartupCache::ResetStartupWriteTimerAndLock() {
+ MutexAutoLock lock(mTableLock);
+ return ResetStartupWriteTimer();
+}
+
+nsresult StartupCache::ResetStartupWriteTimer() {
+ mDirty = true;
+ nsresult rv = NS_OK;
+ if (!mTimer)
+ mTimer = NS_NewTimer();
+ else
+ rv = mTimer->Cancel();
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Wait for the specified timeout, then write out the cache.
+ mTimer->InitWithNamedFuncCallback(
+ StartupCache::WriteTimeout, this, STARTUP_CACHE_WRITE_TIMEOUT * 1000,
+ nsITimer::TYPE_ONE_SHOT, "StartupCache::WriteTimeout");
+ return NS_OK;
+}
+
+// Used only in tests:
+bool StartupCache::StartupWriteComplete() {
+ // Need to have written to disk and not added new things since;
+ MutexAutoLock lock(mTableLock);
+ return !mDirty && mWrittenOnce;
+}
+
+// StartupCacheDebugOutputStream implementation
+#ifdef DEBUG
+NS_IMPL_ISUPPORTS(StartupCacheDebugOutputStream, nsIObjectOutputStream,
+ nsIBinaryOutputStream, nsIOutputStream)
+
+bool StartupCacheDebugOutputStream::CheckReferences(nsISupports* aObject) {
+ nsresult rv;
+
+ nsCOMPtr<nsIClassInfo> classInfo = do_QueryInterface(aObject);
+ if (!classInfo) {
+ NS_ERROR("aObject must implement nsIClassInfo");
+ return false;
+ }
+
+ uint32_t flags;
+ rv = classInfo->GetFlags(&flags);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (flags & nsIClassInfo::SINGLETON) return true;
+
+ bool inserted = mObjectMap->EnsureInserted(aObject);
+ if (!inserted) {
+ NS_ERROR(
+ "non-singleton aObject is referenced multiple times in this"
+ "serialization, we don't support that.");
+ }
+
+ return inserted;
+}
+
+// nsIObjectOutputStream implementation
+nsresult StartupCacheDebugOutputStream::WriteObject(nsISupports* aObject,
+ bool aIsStrongRef) {
+ nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
+
+ NS_ASSERTION(rootObject.get() == aObject,
+ "bad call to WriteObject -- call WriteCompoundObject!");
+ bool check = CheckReferences(aObject);
+ NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
+ return mBinaryStream->WriteObject(aObject, aIsStrongRef);
+}
+
+nsresult StartupCacheDebugOutputStream::WriteSingleRefObject(
+ nsISupports* aObject) {
+ nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
+
+ NS_ASSERTION(rootObject.get() == aObject,
+ "bad call to WriteSingleRefObject -- call WriteCompoundObject!");
+ bool check = CheckReferences(aObject);
+ NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
+ return mBinaryStream->WriteSingleRefObject(aObject);
+}
+
+nsresult StartupCacheDebugOutputStream::WriteCompoundObject(
+ nsISupports* aObject, const nsIID& aIID, bool aIsStrongRef) {
+ nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
+
+ nsCOMPtr<nsISupports> roundtrip;
+ rootObject->QueryInterface(aIID, getter_AddRefs(roundtrip));
+ NS_ASSERTION(roundtrip.get() == aObject,
+ "bad aggregation or multiple inheritance detected by call to "
+ "WriteCompoundObject!");
+
+ bool check = CheckReferences(aObject);
+ NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
+ return mBinaryStream->WriteCompoundObject(aObject, aIID, aIsStrongRef);
+}
+
+nsresult StartupCacheDebugOutputStream::WriteID(nsID const& aID) {
+ return mBinaryStream->WriteID(aID);
+}
+
+char* StartupCacheDebugOutputStream::GetBuffer(uint32_t aLength,
+ uint32_t aAlignMask) {
+ return mBinaryStream->GetBuffer(aLength, aAlignMask);
+}
+
+void StartupCacheDebugOutputStream::PutBuffer(char* aBuffer, uint32_t aLength) {
+ mBinaryStream->PutBuffer(aBuffer, aLength);
+}
+#endif // DEBUG
+
+} // namespace scache
+} // namespace mozilla