/* -*- 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/. */
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;
}
staticconst 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. staticconst 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. staticconst size_t STARTUP_CACHE_MAX_CAPACITY = 5000;
// Not const because we change it for gtests. static uint8_t STARTUP_CACHE_WRITE_TIMEOUT = 60;
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) {
MOZ_TRY(
NS_NewNativeLocalFile(nsDependentCString(env), 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;
}
// 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;
// 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();
}
/** * LoadArchive can only be called from the main thread.
*/
Result<Ok, nsresult> StartupCache::LoadArchive() {
MOZ_ASSERT(NS_IsMainThread(), "Can only load startup cache on main thread"); if (gIgnoreDiskCache) return Err(NS_ERROR_FAILURE);
MOZ_TRY(mCacheData.init(mFile)); auto size = mCacheData.size(); if (CanPrefetchMemory()) {
StartPrefetchMemory();
}
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);
}
MutexAutoLock lock(mTableLock);
decltype(mTable)::Ptr p = mTable.lookup(nsDependentCString(id)); if (!p) { return NS_ERROR_NOT_AVAILABLE;
}
auto& value = p->value(); if (value.mData) {
label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory;
} else { if (!mCacheData.initialized()) { return NS_ERROR_NOT_AVAILABLE;
} // It is impossible for a write to be pending here. This is because // we just checked mCacheData.initialized(), and this is reset before // writing to the cache. It's not re-initialized unless we call // LoadArchive(), either from Init() (which must have already happened) or // InvalidateCache(). InvalidateCache() locks the mutex, so a write can't be // happening. // Also, WriteToDisk() requires mTableLock, so while it's writing we can't // be here.
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(constchar* 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();
}
nsTArray<StartupCacheEntry::KeyValuePair> entries(mTable.count()); for (auto iter = mTable.iter(); !iter.done(); iter.next()) { if (iter.get().value().mRequested) {
StartupCacheEntry::KeyValuePair kv(&iter.get().key(),
&iter.get().value());
entries.AppendElement(kv);
}
}
if (entries.IsEmpty()) { return Ok();
}
entries.Sort(StartupCacheEntry::Comparator());
loader::OutputBuffer buf; for (auto& e : entries) { auto* key = e.first; auto* value = e.second; auto uncompressedSize = value->mUncompressedSize; // Set the mHeaderOffsetInFile so we can go back and edit the offset.
value->mHeaderOffsetInFile = buf.cursor(); // Write a 0 offset/compressed size as a placeholder until we get the real // offset after compressing.
buf.codeUint32(0);
buf.codeUint32(0);
buf.codeUint32(uncompressedSize);
buf.codeString(*key);
}
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::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();
}
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();
}
// 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)
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.
} elseif (strcmp(topic, "startupcache-invalidate") == 0) {
sc->InvalidateCache(data && nsCRT::strcmp(data, u"memoryOnly") == 0);
} elseif (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::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;
}
bool inserted = mObjectMap->EnsureInserted(aObject); if (!inserted) {
NS_ERROR( "non-singleton aObject is referenced multiple times in this" "serialization, we don't support that.");
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.