summaryrefslogtreecommitdiffstats
path: root/startupcache
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /startupcache
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'startupcache')
-rw-r--r--startupcache/StartupCache.cpp947
-rw-r--r--startupcache/StartupCache.h289
-rw-r--r--startupcache/StartupCacheInfo.cpp50
-rw-r--r--startupcache/StartupCacheInfo.h28
-rw-r--r--startupcache/StartupCacheUtils.cpp225
-rw-r--r--startupcache/StartupCacheUtils.h77
-rw-r--r--startupcache/components.conf13
-rw-r--r--startupcache/moz.build34
-rw-r--r--startupcache/nsIStartupCacheInfo.idl38
-rw-r--r--startupcache/test/TestStartupCache.cpp188
-rw-r--r--startupcache/test/browser/browser.ini1
-rw-r--r--startupcache/test/browser/browser_startupcache_telemetry.js50
-rw-r--r--startupcache/test/moz.build10
13 files changed, 1950 insertions, 0 deletions
diff --git a/startupcache/StartupCache.cpp b/startupcache/StartupCache.cpp
new file mode 100644
index 0000000000..5c6e97b326
--- /dev/null
+++ b/startupcache/StartupCache.cpp
@@ -0,0 +1,947 @@
+/* -*- 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 "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);
+
+ mTableLock.AssertCurrentThreadOwns();
+
+ 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;
+ }
+
+ 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 fd;
+ MOZ_TRY(mFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ 0644, &fd.rwget()));
+
+ nsTArray<std::pair<const nsCString*, StartupCacheEntry*>> entries;
+ for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
+ if (iter.get().value().mRequested) {
+ entries.AppendElement(
+ std::make_pair(&iter.get().key(), &iter.get().value()));
+ }
+ }
+
+ 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
diff --git a/startupcache/StartupCache.h b/startupcache/StartupCache.h
new file mode 100644
index 0000000000..045ad05213
--- /dev/null
+++ b/startupcache/StartupCache.h
@@ -0,0 +1,289 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 StartupCache_h_
+#define StartupCache_h_
+
+#include <utility>
+
+#include "nsClassHashtable.h"
+#include "nsComponentManagerUtils.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+#include "nsTStringHasher.h" // mozilla::DefaultHasher<nsCString>
+#include "nsZipArchive.h"
+#include "nsITimer.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserverService.h"
+#include "nsIObserver.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIFile.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/AutoMemMap.h"
+#include "mozilla/Compression.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Result.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+/**
+ * The StartupCache is a persistent cache of simple key-value pairs,
+ * where the keys are null-terminated c-strings and the values are
+ * arbitrary data, passed as a (char*, size) tuple.
+ *
+ * Clients should use the GetSingleton() static method to access the cache. It
+ * will be available from the end of XPCOM init (NS_InitXPCOM3 in
+ * XPCOMInit.cpp), until XPCOM shutdown begins. The GetSingleton() method will
+ * return null if the cache is unavailable. The cache is only provided for
+ * libxul builds -- it will fail to link in non-libxul builds. The XPCOM
+ * interface is provided only to allow compiled-code tests; clients should avoid
+ * using it.
+ *
+ * The API provided is very simple: GetBuffer() returns a buffer that was
+ * previously stored in the cache (if any), and PutBuffer() inserts a buffer
+ * into the cache. GetBuffer returns a new buffer, and the caller must take
+ * ownership of it. PutBuffer will assert if the client attempts to insert a
+ * buffer with the same name as an existing entry. The cache makes a copy of the
+ * passed-in buffer, so client retains ownership.
+ *
+ * InvalidateCache() may be called if a client suspects data corruption
+ * or wishes to invalidate for any other reason. This will remove all existing
+ * cache data. Additionally, the static method IgnoreDiskCache() can be called
+ * if it is believed that the on-disk cache file is itself corrupt. This call
+ * implicitly calls InvalidateCache (if the singleton has been initialized) to
+ * ensure any data already read from disk is discarded. The cache will not load
+ * data from the disk file until a successful write occurs.
+ *
+ * Finally, getDebugObjectOutputStream() allows debug code to wrap an
+ * objectstream with a debug objectstream, to check for multiply-referenced
+ * objects. These will generally fail to deserialize correctly, unless they are
+ * stateless singletons or the client maintains their own object data map for
+ * deserialization.
+ *
+ * Writes before the final-ui-startup notification are placed in an intermediate
+ * cache in memory, then written out to disk at a later time, to get writes off
+ * the startup path. In any case, clients should not rely on being able to
+ * GetBuffer() data that is written to the cache, since it may not have been
+ * written to disk or another client may have invalidated the cache. In other
+ * words, it should be used as a cache only, and not a reliable persistent
+ * store.
+ *
+ * Some utility functions are provided in StartupCacheUtils. These functions
+ * wrap the buffers into object streams, which may be useful for serializing
+ * objects. Note the above caution about multiply-referenced objects, though --
+ * the streams are just as 'dumb' as the underlying buffers about
+ * multiply-referenced objects. They just provide some convenience in writing
+ * out data.
+ */
+
+namespace mozilla {
+
+namespace scache {
+
+struct StartupCacheEntry {
+ UniqueFreePtr<char[]> mData;
+ uint32_t mOffset;
+ uint32_t mCompressedSize;
+ uint32_t mUncompressedSize;
+ int32_t mHeaderOffsetInFile;
+ int32_t mRequestedOrder;
+ bool mRequested;
+
+ MOZ_IMPLICIT StartupCacheEntry(uint32_t aOffset, uint32_t aCompressedSize,
+ uint32_t aUncompressedSize)
+ : mData(nullptr),
+ mOffset(aOffset),
+ mCompressedSize(aCompressedSize),
+ mUncompressedSize(aUncompressedSize),
+ mHeaderOffsetInFile(0),
+ mRequestedOrder(0),
+ mRequested(false) {}
+
+ StartupCacheEntry(UniqueFreePtr<char[]> aData, size_t aLength,
+ int32_t aRequestedOrder)
+ : mData(std::move(aData)),
+ mOffset(0),
+ mCompressedSize(0),
+ mUncompressedSize(aLength),
+ mHeaderOffsetInFile(0),
+ mRequestedOrder(0),
+ mRequested(true) {}
+
+ struct Comparator {
+ using Value = std::pair<const nsCString*, StartupCacheEntry*>;
+
+ bool Equals(const Value& a, const Value& b) const {
+ return a.second->mRequestedOrder == b.second->mRequestedOrder;
+ }
+
+ bool LessThan(const Value& a, const Value& b) const {
+ return a.second->mRequestedOrder < b.second->mRequestedOrder;
+ }
+ };
+};
+
+// We don't want to refcount StartupCache, and ObserverService wants to
+// refcount its listeners, so we'll let it refcount this instead.
+class StartupCacheListener final : public nsIObserver {
+ ~StartupCacheListener() = default;
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+};
+
+class StartupCache : public nsIMemoryReporter {
+ friend class StartupCacheListener;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ // StartupCache methods. See above comments for a more detailed description.
+
+ // true if the archive has an entry for the buffer or not.
+ bool HasEntry(const char* id);
+
+ // Returns a buffer that was previously stored, caller does not take ownership
+ nsresult GetBuffer(const char* id, const char** outbuf, uint32_t* length);
+
+ // Stores a buffer. Caller yields ownership.
+ nsresult PutBuffer(const char* id, UniqueFreePtr<char[]>&& inbuf,
+ uint32_t length);
+
+ // Removes the cache file.
+ void InvalidateCache(bool memoryOnly = false);
+
+ // If some event knowingly re-generates the startup cache (like live language
+ // switching) count these events in order to allow them.
+ void CountAllowedInvalidation();
+
+ // For use during shutdown - this will write the startupcache's data
+ // to disk if the timer hasn't already gone off.
+ void MaybeInitShutdownWrite();
+
+ // For use during shutdown - ensure we complete the shutdown write
+ // before shutdown, even in the FastShutdown case.
+ void EnsureShutdownWriteComplete();
+
+ // Signal that data should not be loaded from the cache file
+ static void IgnoreDiskCache();
+
+ static bool GetIgnoreDiskCache();
+
+ // In DEBUG builds, returns a stream that will attempt to check for
+ // and disallow multiple writes of the same object.
+ nsresult GetDebugObjectOutputStream(nsIObjectOutputStream* aStream,
+ nsIObjectOutputStream** outStream);
+
+ static StartupCache* GetSingletonNoInit();
+ static StartupCache* GetSingleton();
+ static void DeleteSingleton();
+
+ // This measures all the heap memory used by the StartupCache, i.e. it
+ // excludes the mapping.
+ size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const
+ MOZ_REQUIRES(mTableLock);
+
+ bool ShouldCompactCache() MOZ_REQUIRES(mTableLock);
+ nsresult ResetStartupWriteTimerCheckingReadCount();
+ nsresult ResetStartupWriteTimerAndLock();
+ nsresult ResetStartupWriteTimer() MOZ_REQUIRES(mTableLock);
+ bool StartupWriteComplete();
+
+ private:
+ StartupCache();
+ virtual ~StartupCache();
+
+ friend class StartupCacheInfo;
+
+ Result<Ok, nsresult> LoadArchive();
+ nsresult Init();
+
+ // Returns a file pointer for the cache file with the given name in the
+ // current profile.
+ Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix);
+
+ // Opens the cache file for reading.
+ Result<Ok, nsresult> OpenCache();
+
+ // Writes the cache to disk
+ Result<Ok, nsresult> WriteToDisk() MOZ_REQUIRES(mTableLock);
+
+ void WaitOnPrefetch();
+ void StartPrefetchMemory() MOZ_REQUIRES(mTableLock);
+
+ static nsresult InitSingleton();
+ static void WriteTimeout(nsITimer* aTimer, void* aClosure);
+ void MaybeWriteOffMainThread();
+ void ThreadedPrefetch(uint8_t* aStart, size_t aSize);
+
+ Monitor mPrefetchComplete{"StartupCachePrefetch"};
+ bool mPrefetchInProgress MOZ_GUARDED_BY(mPrefetchComplete){false};
+
+ // This is normally accessed on MainThread, but WriteToDisk() can
+ // access it on other threads
+ HashMap<nsCString, StartupCacheEntry> mTable MOZ_GUARDED_BY(mTableLock);
+ // This owns references to the contents of tables which have been invalidated.
+ // In theory it grows forever if the cache is continually filled and then
+ // invalidated, but this should not happen in practice. Deleting old tables
+ // could create dangling pointers. RefPtrs could be introduced, but it would
+ // be a large amount of error-prone work to change.
+ nsTArray<decltype(mTable)> mOldTables MOZ_GUARDED_BY(mTableLock);
+ size_t mAllowedInvalidationsCount;
+ nsCOMPtr<nsIFile> mFile;
+ loader::AutoMemMap mCacheData MOZ_GUARDED_BY(mTableLock);
+ Mutex mTableLock;
+
+ nsCOMPtr<nsIObserverService> mObserverService;
+ RefPtr<StartupCacheListener> mListener;
+ nsCOMPtr<nsITimer> mTimer;
+
+ bool mDirty MOZ_GUARDED_BY(mTableLock);
+ bool mWrittenOnce MOZ_GUARDED_BY(mTableLock);
+ bool mCurTableReferenced MOZ_GUARDED_BY(mTableLock);
+
+ uint32_t mRequestedCount;
+ size_t mCacheEntriesBaseOffset;
+
+ static StaticRefPtr<StartupCache> gStartupCache;
+ static bool gShutdownInitiated;
+ static bool gIgnoreDiskCache;
+ static bool gFoundDiskCacheOnInit;
+
+ UniquePtr<Compression::LZ4FrameDecompressionContext> mDecompressionContext;
+#ifdef DEBUG
+ nsTHashSet<nsCOMPtr<nsISupports>> mWriteObjectMap;
+#endif
+};
+
+// This debug outputstream attempts to detect if clients are writing multiple
+// references to the same object. We only support that if that object
+// is a singleton.
+#ifdef DEBUG
+class StartupCacheDebugOutputStream final : public nsIObjectOutputStream {
+ ~StartupCacheDebugOutputStream() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBJECTOUTPUTSTREAM
+
+ StartupCacheDebugOutputStream(nsIObjectOutputStream* binaryStream,
+ nsTHashSet<nsCOMPtr<nsISupports>>* objectMap)
+ : mBinaryStream(binaryStream), mObjectMap(objectMap) {}
+
+ NS_FORWARD_SAFE_NSIBINARYOUTPUTSTREAM(mBinaryStream)
+ NS_FORWARD_SAFE_NSIOUTPUTSTREAM(mBinaryStream)
+
+ bool CheckReferences(nsISupports* aObject);
+
+ nsCOMPtr<nsIObjectOutputStream> mBinaryStream;
+ nsTHashSet<nsCOMPtr<nsISupports>>* mObjectMap;
+};
+#endif // DEBUG
+
+} // namespace scache
+} // namespace mozilla
+
+#endif // StartupCache_h_
diff --git a/startupcache/StartupCacheInfo.cpp b/startupcache/StartupCacheInfo.cpp
new file mode 100644
index 0000000000..973d3046d3
--- /dev/null
+++ b/startupcache/StartupCacheInfo.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "StartupCache.h"
+#include "StartupCacheInfo.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPtr.h"
+
+using namespace mozilla;
+using namespace mozilla::scache;
+
+NS_IMPL_ISUPPORTS(StartupCacheInfo, nsIStartupCacheInfo)
+
+nsresult StartupCacheInfo::GetIgnoreDiskCache(bool* aIgnore) {
+ *aIgnore = StartupCache::gIgnoreDiskCache;
+ return NS_OK;
+}
+
+nsresult StartupCacheInfo::GetFoundDiskCacheOnInit(bool* aFound) {
+ *aFound = StartupCache::gFoundDiskCacheOnInit;
+ return NS_OK;
+}
+
+nsresult StartupCacheInfo::GetWroteToDiskCache(bool* aWrote) {
+ if (!StartupCache::gStartupCache) {
+ *aWrote = false;
+ } else {
+ MutexAutoLock lock(StartupCache::gStartupCache->mTableLock);
+ *aWrote = StartupCache::gStartupCache->mWrittenOnce;
+ }
+ return NS_OK;
+}
+
+nsresult StartupCacheInfo::GetDiskCachePath(nsAString& aResult) {
+ if (!StartupCache::gStartupCache || !StartupCache::gStartupCache->mFile) {
+ return NS_OK;
+ }
+ nsAutoString path;
+ StartupCache::gStartupCache->mFile->GetPath(path);
+ aResult.Assign(path);
+ return NS_OK;
+}
+
+NS_IMPL_COMPONENT_FACTORY(nsIStartupCacheInfo) {
+ return MakeAndAddRef<StartupCacheInfo>().downcast<nsISupports>();
+}
diff --git a/startupcache/StartupCacheInfo.h b/startupcache/StartupCacheInfo.h
new file mode 100644
index 0000000000..98a342ce88
--- /dev/null
+++ b/startupcache/StartupCacheInfo.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 StartupCacheInfo_h_
+#define StartupCacheInfo_h_
+
+#include "nsIStartupCacheInfo.h"
+
+namespace mozilla {
+namespace scache {
+
+class StartupCacheInfo final : public nsIStartupCacheInfo {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTARTUPCACHEINFO
+
+ StartupCacheInfo() = default;
+
+ private:
+ ~StartupCacheInfo() = default;
+};
+
+} // namespace scache
+} // namespace mozilla
+
+#endif // StartupCache_h_
diff --git a/startupcache/StartupCacheUtils.cpp b/startupcache/StartupCacheUtils.cpp
new file mode 100644
index 0000000000..c8dcdd7f11
--- /dev/null
+++ b/startupcache/StartupCacheUtils.cpp
@@ -0,0 +1,225 @@
+/* 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 "nsCOMPtr.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIJARURI.h"
+#include "nsIResProtocolHandler.h"
+#include "nsIChromeRegistry.h"
+#include "nsStringStream.h"
+#include "StartupCacheUtils.h"
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/Omnijar.h"
+
+namespace mozilla {
+namespace scache {
+
+nsresult NewObjectInputStreamFromBuffer(const char* buffer, uint32_t len,
+ nsIObjectInputStream** stream) {
+ nsCOMPtr<nsIInputStream> stringStream;
+ nsresult rv = NS_NewByteInputStream(getter_AddRefs(stringStream),
+ Span(buffer, len), NS_ASSIGNMENT_DEPEND);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+
+ nsCOMPtr<nsIObjectInputStream> objectInput =
+ NS_NewObjectInputStream(stringStream);
+
+ objectInput.forget(stream);
+ return NS_OK;
+}
+
+nsresult NewObjectOutputWrappedStorageStream(
+ nsIObjectOutputStream** wrapperStream, nsIStorageStream** stream,
+ bool wantDebugStream) {
+ nsCOMPtr<nsIStorageStream> storageStream;
+
+ nsresult rv =
+ NS_NewStorageStream(256, UINT32_MAX, getter_AddRefs(storageStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(storageStream);
+
+ nsCOMPtr<nsIObjectOutputStream> objectOutput =
+ NS_NewObjectOutputStream(outputStream);
+
+#ifdef DEBUG
+ if (wantDebugStream) {
+ // Wrap in debug stream to detect unsupported writes of
+ // multiply-referenced non-singleton objects
+ StartupCache* sc = StartupCache::GetSingleton();
+ NS_ENSURE_TRUE(sc, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIObjectOutputStream> debugStream;
+ sc->GetDebugObjectOutputStream(objectOutput, getter_AddRefs(debugStream));
+ debugStream.forget(wrapperStream);
+ } else {
+ objectOutput.forget(wrapperStream);
+ }
+#else
+ objectOutput.forget(wrapperStream);
+#endif
+
+ storageStream.forget(stream);
+ return NS_OK;
+}
+
+nsresult NewBufferFromStorageStream(nsIStorageStream* storageStream,
+ UniqueFreePtr<char[]>* buffer,
+ uint32_t* len) {
+ nsresult rv;
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = storageStream->NewInputStream(0, getter_AddRefs(inputStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint64_t avail64;
+ rv = inputStream->Available(&avail64);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(avail64 <= UINT32_MAX, NS_ERROR_FILE_TOO_BIG);
+
+ uint32_t avail = (uint32_t)avail64;
+ auto temp = UniqueFreePtr<char[]>(
+ reinterpret_cast<char*>(malloc(sizeof(char) * avail)));
+ uint32_t read;
+ rv = inputStream->Read(temp.get(), avail, &read);
+ if (NS_SUCCEEDED(rv) && avail != read) rv = NS_ERROR_UNEXPECTED;
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ *len = avail;
+ *buffer = std::move(temp);
+ return NS_OK;
+}
+
+static const char baseName[2][5] = {"gre/", "app/"};
+
+static inline bool canonicalizeBase(nsAutoCString& spec, nsACString& out) {
+ nsAutoCString greBase, appBase;
+ nsresult rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::GRE, greBase);
+ if (NS_FAILED(rv) || !greBase.Length()) return false;
+
+ rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::APP, appBase);
+ if (NS_FAILED(rv)) return false;
+
+ bool underGre = StringBeginsWith(spec, greBase);
+ bool underApp = appBase.Length() && StringBeginsWith(spec, appBase);
+
+ if (!underGre && !underApp) return false;
+
+ /**
+ * At this point, if both underGre and underApp are true, it can be one
+ * of the two following cases:
+ * - the GRE directory points to a subdirectory of the APP directory,
+ * meaning spec points under GRE.
+ * - the APP directory points to a subdirectory of the GRE directory,
+ * meaning spec points under APP.
+ * Checking the GRE and APP path length is enough to know in which case
+ * we are.
+ */
+ if (underGre && underApp && greBase.Length() < appBase.Length())
+ underGre = false;
+
+ out.AppendLiteral("/resource/");
+ out.Append(
+ baseName[underGre ? mozilla::Omnijar::GRE : mozilla::Omnijar::APP]);
+ out.Append(Substring(spec, underGre ? greBase.Length() : appBase.Length()));
+ return true;
+}
+
+/**
+ * ResolveURI transforms a chrome: or resource: URI into the URI for its
+ * underlying resource, or returns any other URI unchanged.
+ */
+nsresult ResolveURI(nsIURI* in, nsIURI** out) {
+ nsresult rv;
+
+ // Resolve resource:// URIs. At the end of this if/else block, we
+ // have both spec and uri variables identifying the same URI.
+ if (in->SchemeIs("resource")) {
+ nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIProtocolHandler> ph;
+ rv = ioService->GetProtocolHandler("resource", getter_AddRefs(ph));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIResProtocolHandler> irph(do_QueryInterface(ph, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec;
+ rv = irph->ResolveURI(in, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return ioService->NewURI(spec, nullptr, nullptr, out);
+ }
+ if (in->SchemeIs("chrome")) {
+ nsCOMPtr<nsIChromeRegistry> chromeReg =
+ mozilla::services::GetChromeRegistry();
+ if (!chromeReg) return NS_ERROR_UNEXPECTED;
+
+ return chromeReg->ConvertChromeURL(in, out);
+ }
+
+ *out = do_AddRef(in).take();
+ return NS_OK;
+}
+
+static nsresult PathifyURIImpl(nsIURI* in, nsACString& out) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = ResolveURI(in, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!canonicalizeBase(spec, out)) {
+ if (uri->SchemeIs("file")) {
+ nsCOMPtr<nsIFileURL> baseFileURL;
+ baseFileURL = do_QueryInterface(uri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString path;
+ rv = baseFileURL->GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ out.Append(path);
+ } else if (uri->SchemeIs("jar")) {
+ nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> jarFileURI;
+ rv = jarURI->GetJARFile(getter_AddRefs(jarFileURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PathifyURIImpl(jarFileURI, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString path;
+ rv = jarURI->GetJAREntry(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ out.Append('/');
+ out.Append(path);
+ } else { // Very unlikely
+ rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ out.Append('/');
+ out.Append(spec);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult PathifyURI(const char* loaderType, size_t loaderTypeLength, nsIURI* in,
+ nsACString& out) {
+ out.AssignASCII(loaderType, loaderTypeLength);
+
+ return PathifyURIImpl(in, out);
+}
+
+} // namespace scache
+} // namespace mozilla
diff --git a/startupcache/StartupCacheUtils.h b/startupcache/StartupCacheUtils.h
new file mode 100644
index 0000000000..a59f0e4fac
--- /dev/null
+++ b/startupcache/StartupCacheUtils.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 nsStartupCacheUtils_h_
+#define nsStartupCacheUtils_h_
+
+#include "nsString.h"
+#include "nsIStorageStream.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+class nsIURI;
+
+namespace mozilla {
+namespace scache {
+
+nsresult NewObjectInputStreamFromBuffer(const char* buffer, uint32_t len,
+ nsIObjectInputStream** stream);
+
+// We can't retrieve the wrapped stream from the objectOutputStream later,
+// so we return it here. We give callers in debug builds the option
+// to wrap the outputstream in a debug stream, which will detect if
+// non-singleton objects are written out multiple times during a serialization.
+// This could cause them to be deserialized incorrectly (as multiple copies
+// instead of references).
+nsresult NewObjectOutputWrappedStorageStream(
+ nsIObjectOutputStream** wrapperStream, nsIStorageStream** stream,
+ bool wantDebugStream);
+
+// Creates a buffer for storing the stream into the cache. The buffer is
+// allocated with 'new []'. After calling this function, the caller would
+// typically call StartupCache::PutBuffer with the returned buffer.
+nsresult NewBufferFromStorageStream(nsIStorageStream* storageStream,
+ UniqueFreePtr<char[]>* buffer,
+ uint32_t* len);
+
+nsresult ResolveURI(nsIURI* in, nsIURI** out);
+
+// PathifyURI transforms uris into useful zip paths
+// to make it easier to manipulate startup cache entries
+// using standard zip tools.
+//
+// Transformations applied:
+// * resource:// URIs are resolved to their corresponding file/jar URI to
+// canonicalize resources URIs other than gre and app.
+// * Paths under GRE or APP directory have their base path replaced with
+// resource/gre or resource/app to avoid depending on install location.
+// * jar:file:///path/to/file.jar!/sub/path urls are replaced with
+// /path/to/file.jar/sub/path
+//
+// The result is concatenated with loaderType and stored into the string
+// passed in.
+//
+// For example, in the js loader (loaderType = "jsloader"):
+// resource://gre/modules/XPCOMUtils.sys.mjs or
+// file://$GRE_DIR/modules/XPCOMUtils.sys.mjs or
+// jar:file://$GRE_DIR/omni.jar!/modules/XPCOMUtils.sys.mjs becomes
+// jsloader/resource/gre/modules/XPCOMUtils.sys.mjs
+// file://$PROFILE_DIR/extensions/{uuid}/components/component.js becomes
+// jsloader/$PROFILE_DIR/extensions/%7Buuid%7D/components/component.js
+// jar:file://$PROFILE_DIR/extensions/some.xpi!/components/component.js becomes
+// jsloader/$PROFILE_DIR/extensions/some.xpi/components/component.js
+nsresult PathifyURI(const char* loaderType, size_t loaderTypeLength, nsIURI* in,
+ nsACString& out);
+
+template <int N>
+nsresult PathifyURI(const char (&loaderType)[N], nsIURI* in, nsACString& out) {
+ return PathifyURI(loaderType, N - 1, in, out);
+}
+
+} // namespace scache
+} // namespace mozilla
+
+#endif // nsStartupCacheUtils_h_
diff --git a/startupcache/components.conf b/startupcache/components.conf
new file mode 100644
index 0000000000..e85ccf192c
--- /dev/null
+++ b/startupcache/components.conf
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{a6b2f8b0-7438-11ea-bc55-0242ac130003}',
+ 'contract_ids': ['@mozilla.org/startupcacheinfo;1'],
+ 'type': 'nsIStartupCacheInfo',
+ },
+]
diff --git a/startupcache/moz.build b/startupcache/moz.build
new file mode 100644
index 0000000000..a05aebd472
--- /dev/null
+++ b/startupcache/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "XPCOM")
+
+TEST_DIRS += ["test"]
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+EXPORTS.mozilla.scache += [
+ "StartupCache.h",
+ "StartupCacheUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "StartupCache.cpp",
+ "StartupCacheInfo.cpp",
+ "StartupCacheUtils.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPIDL_MODULE = "startupcache"
+
+XPIDL_SOURCES += [
+ "nsIStartupCacheInfo.idl",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/startupcache/nsIStartupCacheInfo.idl b/startupcache/nsIStartupCacheInfo.idl
new file mode 100644
index 0000000000..d7fe0a71f7
--- /dev/null
+++ b/startupcache/nsIStartupCacheInfo.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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 "nsISupports.idl"
+
+/* NOTE: this interface is completely undesigned, not stable and likely to change */
+[scriptable, builtinclass, uuid(a6b2f8b0-7438-11ea-bc55-0242ac130003)]
+interface nsIStartupCacheInfo : nsISupports
+{
+ /**
+ * Returns true if the startup cache will not load from the cache from disk.
+ * This can happen if the cache file is corrupt or has been invalidated.
+ */
+ readonly attribute boolean IgnoreDiskCache;
+
+ /**
+ * Returns true if during initialization of the startup cache an existing
+ * cache file was found on disk. This does NOT indicate if the file loaded
+ * successfully.
+ */
+ readonly attribute boolean FoundDiskCacheOnInit;
+
+ /**
+ * Returns true once the current cache file as been written to disk at least
+ * once. If the cache was loaded from disk and never changed this may never
+ * be set to true.
+ */
+ readonly attribute boolean WroteToDiskCache;
+
+ /**
+ * The full path and filename of the startup cache file that will be stored on
+ * disk.
+ */
+ readonly attribute AString DiskCachePath;
+};
diff --git a/startupcache/test/TestStartupCache.cpp b/startupcache/test/TestStartupCache.cpp
new file mode 100644
index 0000000000..9c817ff725
--- /dev/null
+++ b/startupcache/test/TestStartupCache.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* 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 "gtest/gtest.h"
+
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/scache/StartupCacheUtils.h"
+
+#include "nsDirectoryServiceDefs.h"
+#include "nsIOutputStream.h"
+#include "nsISupports.h"
+#include "nsIStorageStream.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIURI.h"
+#include "nsThreadUtils.h"
+#include "prenv.h"
+#include "prio.h"
+#include "prprf.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Printf.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "nsNetCID.h"
+#include "nsIURIMutator.h"
+
+using namespace JS;
+
+using namespace mozilla::scache;
+using mozilla::UniquePtr;
+
+void WaitForStartupTimer() {
+ StartupCache* sc = StartupCache::GetSingleton();
+ PR_Sleep(3 * PR_TicksPerSecond());
+
+ while (true) {
+ NS_ProcessPendingEvents(nullptr);
+ if (sc->StartupWriteComplete()) {
+ return;
+ }
+ PR_Sleep(1 * PR_TicksPerSecond());
+ }
+}
+
+class TestStartupCache : public ::testing::Test {
+ protected:
+ TestStartupCache();
+ ~TestStartupCache();
+
+ nsCOMPtr<nsIFile> mSCFile;
+};
+
+TestStartupCache::TestStartupCache() {
+ NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mSCFile));
+ mSCFile->AppendNative("test-startupcache.tmp"_ns);
+#ifdef XP_WIN
+ nsAutoString env(u"MOZ_STARTUP_CACHE="_ns);
+ env.Append(mSCFile->NativePath());
+ _wputenv(env.get());
+#else
+ nsAutoCString path;
+ mSCFile->GetNativePath(path);
+ char* env = mozilla::Smprintf("MOZ_STARTUP_CACHE=%s", path.get()).release();
+ PR_SetEnv(env);
+ // We intentionally leak `env` here because it is required by PR_SetEnv
+ MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(env);
+#endif
+ StartupCache::GetSingleton()->InvalidateCache();
+}
+TestStartupCache::~TestStartupCache() {
+ PR_SetEnv("MOZ_STARTUP_CACHE=");
+ StartupCache::GetSingleton()->InvalidateCache();
+}
+
+TEST_F(TestStartupCache, StartupWriteRead) {
+ nsresult rv;
+ StartupCache* sc = StartupCache::GetSingleton();
+
+ const char* buf = "Market opportunities for BeardBook";
+ const char* id = "id";
+ const char* outbuf;
+ uint32_t len;
+
+ rv = sc->PutBuffer(id, mozilla::UniqueFreePtr<char[]>(strdup(buf)),
+ strlen(buf) + 1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = sc->GetBuffer(id, &outbuf, &len);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_STREQ(buf, outbuf);
+
+ rv = sc->ResetStartupWriteTimerAndLock();
+ EXPECT_NS_SUCCEEDED(rv);
+ WaitForStartupTimer();
+
+ rv = sc->GetBuffer(id, &outbuf, &len);
+ EXPECT_NS_SUCCEEDED(rv);
+ EXPECT_STREQ(buf, outbuf);
+}
+
+TEST_F(TestStartupCache, WriteInvalidateRead) {
+ nsresult rv;
+ const char* buf = "BeardBook competitive analysis";
+ const char* id = "id";
+ const char* outbuf;
+ uint32_t len;
+ StartupCache* sc = StartupCache::GetSingleton();
+ ASSERT_TRUE(sc);
+
+ rv = sc->PutBuffer(id, mozilla::UniqueFreePtr<char[]>(strdup(buf)),
+ strlen(buf) + 1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ sc->InvalidateCache();
+
+ rv = sc->GetBuffer(id, &outbuf, &len);
+ EXPECT_EQ(rv, NS_ERROR_NOT_AVAILABLE);
+}
+
+TEST_F(TestStartupCache, WriteObject) {
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> obj;
+
+ constexpr auto spec = "http://www.mozilla.org"_ns;
+ rv = NS_MutateURI(NS_SIMPLEURIMUTATOR_CONTRACTID).SetSpec(spec).Finalize(obj);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ StartupCache* sc = StartupCache::GetSingleton();
+
+ // Create an object stream. Usually this is done with
+ // NewObjectOutputWrappedStorageStream, but that uses
+ // StartupCache::GetSingleton in debug builds, and we
+ // don't have access to that here. Obviously.
+ const char* id = "id";
+ nsCOMPtr<nsIStorageStream> storageStream =
+ do_CreateInstance("@mozilla.org/storagestream;1");
+ ASSERT_TRUE(storageStream);
+
+ rv = storageStream->Init(256, (uint32_t)-1);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsIObjectOutputStream> objectOutput =
+ do_CreateInstance("@mozilla.org/binaryoutputstream;1");
+ ASSERT_TRUE(objectOutput);
+
+ nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(storageStream);
+
+ rv = objectOutput->SetOutputStream(outputStream);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsISupports> objQI(do_QueryInterface(obj));
+ rv = objectOutput->WriteObject(objQI, true);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ mozilla::UniqueFreePtr<char[]> buf;
+ uint32_t len;
+ NewBufferFromStorageStream(storageStream, &buf, &len);
+
+ // Since this is a post-startup write, it should be written and
+ // available.
+ rv = sc->PutBuffer(id, std::move(buf), len);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ const char* buf2;
+ uint32_t len2;
+ nsCOMPtr<nsIObjectInputStream> objectInput;
+ rv = sc->GetBuffer(id, &buf2, &len2);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = NewObjectInputStreamFromBuffer(buf2, len2, getter_AddRefs(objectInput));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsISupports> deserialized;
+ rv = objectInput->ReadObject(true, getter_AddRefs(deserialized));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsIURI> uri(do_QueryInterface(deserialized));
+ ASSERT_TRUE(uri);
+
+ nsCString outSpec;
+ rv = uri->GetSpec(outSpec);
+ EXPECT_NS_SUCCEEDED(rv);
+ ASSERT_TRUE(outSpec.Equals(spec));
+}
diff --git a/startupcache/test/browser/browser.ini b/startupcache/test/browser/browser.ini
new file mode 100644
index 0000000000..cd4a5cbbe6
--- /dev/null
+++ b/startupcache/test/browser/browser.ini
@@ -0,0 +1 @@
+[browser_startupcache_telemetry.js]
diff --git a/startupcache/test/browser/browser_startupcache_telemetry.js b/startupcache/test/browser/browser_startupcache_telemetry.js
new file mode 100644
index 0000000000..e4c8085bed
--- /dev/null
+++ b/startupcache/test/browser/browser_startupcache_telemetry.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const LABELS_STARTUP_CACHE_REQUESTS = {
+ HitMemory: 0,
+ HitDisk: 1,
+ Miss: 2,
+};
+
+add_task(async function () {
+ // Turn off tab preloading to avoid issues with RemoteController.js
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ Services.telemetry.getSnapshotForHistograms("main", true);
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let snapshot = Services.telemetry.getSnapshotForHistograms("main", true);
+ function histValue(label) {
+ return snapshot.parent.STARTUP_CACHE_REQUESTS.values[label] || 0;
+ }
+ Assert.equal(histValue(LABELS_STARTUP_CACHE_REQUESTS.HitMemory), 0);
+ Assert.equal(histValue(LABELS_STARTUP_CACHE_REQUESTS.HitDisk), 0);
+ Assert.notEqual(histValue(LABELS_STARTUP_CACHE_REQUESTS.Miss), 0);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ newWin = await BrowserTestUtils.openNewBrowserWindow();
+ snapshot = Services.telemetry.getSnapshotForHistograms("main", true);
+ Assert.notEqual(histValue(LABELS_STARTUP_CACHE_REQUESTS.HitMemory), 0);
+ Assert.equal(histValue(LABELS_STARTUP_CACHE_REQUESTS.HitDisk), 0);
+
+ // Here and below, 50 is just a nice round number that should be well over
+ // what we should observe under normal circumstances, and well under what
+ // we should see if something is seriously wrong. It won't catch everything,
+ // but it's better than nothing, and better than a test that cries wolf.
+ Assert.less(histValue(LABELS_STARTUP_CACHE_REQUESTS.Miss), 50);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ Services.obs.notifyObservers(null, "startupcache-invalidate", "memoryOnly");
+ newWin = await BrowserTestUtils.openNewBrowserWindow();
+ snapshot = Services.telemetry.getSnapshotForHistograms("main", true);
+ Assert.less(histValue(LABELS_STARTUP_CACHE_REQUESTS.HitMemory), 50);
+ Assert.notEqual(histValue(LABELS_STARTUP_CACHE_REQUESTS.HitDisk), 0);
+ // Some misses can come through - just ensure it's a small number
+ Assert.less(histValue(LABELS_STARTUP_CACHE_REQUESTS.Miss), 50);
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/startupcache/test/moz.build b/startupcache/test/moz.build
new file mode 100644
index 0000000000..0197659309
--- /dev/null
+++ b/startupcache/test/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestStartupCache.cpp",
+]
+FINAL_LIBRARY = "xul-gtest"