gecko-dev/startupcache/StartupCache.cpp
Noemi Erli b13f2bcb47 Backed out 7 changesets (bug 1627075) for causing @nsZipArchive crashes CLOSED TREE
Backed out changeset 9705b2759d45 (bug 1627075)
Backed out changeset 699212a443c3 (bug 1627075)
Backed out changeset 7ae4df10749c (bug 1627075)
Backed out changeset ece9a4223349 (bug 1627075)
Backed out changeset 6c4eedaa0d04 (bug 1627075)
Backed out changeset f5106898239f (bug 1627075)
Backed out changeset b6029c7c0016 (bug 1627075)
2020-07-02 14:05:53 +03:00

918 lines
29 KiB
C++

/* -*- 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"
#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) {
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() {
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),
mPrefetchThread(nullptr) {}
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);
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::StartPrefetchMemoryThread() {
// XXX: It would be great for this to not create its own thread, unfortunately
// there doesn't seem to be an existing thread that makes sense for this, so
// barring a coordinated global scheduling system this is the best we get.
mPrefetchThread = PR_CreateThread(
PR_USER_THREAD, StartupCache::ThreadedPrefetch, this, PR_PRIORITY_NORMAL,
PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 256 * 1024);
}
/**
* 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()) {
StartPrefetchMemoryThread();
}
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([&]() {
WaitOnPrefetchThread();
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");
return mTable.has(nsDependentCString(id));
}
nsresult StartupCache::GetBuffer(const char* id, const char** outbuf,
uint32_t* length) {
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); });
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;
}
#ifdef DEBUG
// It should be 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. Really, we want to MOZ_ASSERT(!mTableLock.IsLocked()) here,
// but there is no such method. So we hack around by attempting to gain the
// lock. This should always succeed; if it fails, someone's broken the
// assumptions.
if (!mTableLock.TryLock()) {
MOZ_ASSERT(false, "Could not gain mTableLock - should never happen!");
return NS_ERROR_NOT_AVAILABLE;
}
mTableLock.Unlock();
#endif
size_t totalRead = 0;
size_t totalWritten = 0;
Span<const char> compressed = MakeSpan(
mCacheData.get<char>().get() + mCacheEntriesBaseOffset + value.mOffset,
value.mCompressedSize);
value.mData = MakeUnique<char[]>(value.mUncompressedSize);
Span<char> uncompressed =
MakeSpan(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;
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, UniquePtr<char[]>&& inbuf,
uint32_t len) {
NS_ASSERTION(NS_IsMainThread(),
"Startup cache only available on main thread");
if (StartupCache::gShutdownInitiated) {
return NS_ERROR_NOT_AVAILABLE;
}
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;
}
// Try to gain the table write lock. If the background task to write the
// cache is running, this will fail.
if (!mTableLock.TryLock()) {
return NS_ERROR_NOT_AVAILABLE;
}
auto lockGuard = MakeScopeExit([&] { mTableLock.Unlock(); });
// 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
*/
Result<Ok, nsresult> StartupCache::WriteToDisk() {
mTableLock.AssertCurrentThreadOwns();
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 = MakeSpan(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(MakeSpan(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) {
WaitOnPrefetchThread();
// Ensure we're not writing using mTable...
MutexAutoLock unlock(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.
MOZ_DIAGNOSTIC_ASSERT(xpc::IsInAutomation() || mOldTables.Length() < 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_TARGET_DOES_NOT_EXIST &&
rv != NS_ERROR_FILE_NOT_FOUND) {
gIgnoreDiskCache = true;
return;
}
}
gIgnoreDiskCache = false;
auto result = LoadArchive();
if (NS_WARN_IF(result.isErr())) {
gIgnoreDiskCache = true;
}
}
void StartupCache::MaybeInitShutdownWrite() {
if (mTimer) {
mTimer->Cancel();
}
gShutdownInitiated = true;
MaybeWriteOffMainThread();
}
void StartupCache::EnsureShutdownWriteComplete() {
// 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.
if (!mTableLock.TryLock()) {
// Uh oh, we're writing away from the main thread. Wait to gain the lock,
// to ensure the write completes.
mTableLock.Lock();
} else {
// We got the lock. Keep the following in sync with
// MaybeWriteOffMainThread:
WaitOnPrefetchThread();
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.
}
mTableLock.Unlock();
}
void StartupCache::IgnoreDiskCache() {
gIgnoreDiskCache = true;
if (gStartupCache) gStartupCache->InvalidateCache();
}
void StartupCache::WaitOnPrefetchThread() {
if (!mPrefetchThread || mPrefetchThread == PR_GetCurrentThread()) return;
PR_JoinThread(mPrefetchThread);
mPrefetchThread = nullptr;
}
void StartupCache::ThreadedPrefetch(void* aClosure) {
AUTO_PROFILER_REGISTER_THREAD("StartupCache");
NS_SetCurrentThreadName("StartupCache");
mozilla::IOInterposer::RegisterCurrentThread();
StartupCache* startupCacheObj = static_cast<StartupCache*>(aClosure);
uint8_t* buf = startupCacheObj->mCacheData.get<uint8_t>().get();
size_t size = startupCacheObj->mCacheData.size();
MMAP_FAULT_HANDLER_BEGIN_BUFFER(buf, size)
PrefetchMemory(buf, size);
MMAP_FAULT_HANDLER_CATCH()
mozilla::IOInterposer::UnregisterCurrentThread();
}
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() {
if (mWrittenOnce) {
return;
}
if (mCacheData.initialized() && !ShouldCompactCache()) {
return;
}
// Keep this code in sync with EnsureShutdownWriteComplete.
WaitOnPrefetchThread();
mDirty = true;
mCacheData.reset();
RefPtr<StartupCache> self = this;
nsCOMPtr<nsIRunnable> runnable =
NS_NewRunnableFunction("StartupCache::Write", [self]() mutable {
MutexAutoLock unlock(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->WaitOnPrefetchThread();
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);
}
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;
}
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;
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;
nsISupportsHashKey* key = mObjectMap->GetEntry(aObject);
if (key) {
NS_ERROR(
"non-singleton aObject is referenced multiple times in this"
"serialization, we don't support that.");
return false;
}
mObjectMap->PutEntry(aObject);
return true;
}
// 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