diff options
Diffstat (limited to 'startupcache')
-rw-r--r-- | startupcache/StartupCache.cpp | 917 | ||||
-rw-r--r-- | startupcache/StartupCache.h | 269 | ||||
-rw-r--r-- | startupcache/StartupCacheInfo.cpp | 47 | ||||
-rw-r--r-- | startupcache/StartupCacheInfo.h | 28 | ||||
-rw-r--r-- | startupcache/StartupCacheUtils.cpp | 241 | ||||
-rw-r--r-- | startupcache/StartupCacheUtils.h | 44 | ||||
-rw-r--r-- | startupcache/components.conf | 13 | ||||
-rw-r--r-- | startupcache/moz.build | 34 | ||||
-rw-r--r-- | startupcache/nsIStartupCacheInfo.idl | 38 | ||||
-rw-r--r-- | startupcache/test/TestStartupCache.cpp | 184 | ||||
-rw-r--r-- | startupcache/test/browser/browser.ini | 1 | ||||
-rw-r--r-- | startupcache/test/browser/browser_startupcache_telemetry.js | 50 | ||||
-rw-r--r-- | startupcache/test/moz.build | 10 |
13 files changed, 1876 insertions, 0 deletions
diff --git a/startupcache/StartupCache.cpp b/startupcache/StartupCache.cpp new file mode 100644 index 0000000000..29832ae42f --- /dev/null +++ b/startupcache/StartupCache.cpp @@ -0,0 +1,917 @@ +/* -*- 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" + +#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 = Span( + mCacheData.get<char>().get() + mCacheEntriesBaseOffset + value.mOffset, + value.mCompressedSize); + value.mData = MakeUnique<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; + 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 = 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) { + 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 diff --git a/startupcache/StartupCache.h b/startupcache/StartupCache.h new file mode 100644 index 0000000000..682894fd10 --- /dev/null +++ b/startupcache/StartupCache.h @@ -0,0 +1,269 @@ +/* -*- 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 "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/Mutex.h" +#include "mozilla/Result.h" +#include "mozilla/UniquePtr.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 { + UniquePtr<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(UniquePtr<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, UniquePtr<char[]>&& inbuf, + uint32_t length); + + // Removes the cache file. + void InvalidateCache(bool memoryOnly = false); + + // 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(); + + // 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; + + bool ShouldCompactCache(); + nsresult ResetStartupWriteTimerCheckingReadCount(); + nsresult ResetStartupWriteTimer(); + 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(); + + void WaitOnPrefetchThread(); + void StartPrefetchMemoryThread(); + + static nsresult InitSingleton(); + static void WriteTimeout(nsITimer* aTimer, void* aClosure); + void MaybeWriteOffMainThread(); + static void ThreadedPrefetch(void* aClosure); + + HashMap<nsCString, StartupCacheEntry> mTable; + // owns references to the contents of tables which have been invalidated. + // In theory grows forever if the cache is continually filled and then + // invalidated, but this should not happen in practice. + nsTArray<decltype(mTable)> mOldTables; + nsCOMPtr<nsIFile> mFile; + loader::AutoMemMap mCacheData; + Mutex mTableLock; + + nsCOMPtr<nsIObserverService> mObserverService; + RefPtr<StartupCacheListener> mListener; + nsCOMPtr<nsITimer> mTimer; + + Atomic<bool> mDirty; + Atomic<bool> mWrittenOnce; + bool mCurTableReferenced; + uint32_t mRequestedCount; + size_t mCacheEntriesBaseOffset; + + static StaticRefPtr<StartupCache> gStartupCache; + static bool gShutdownInitiated; + static bool gIgnoreDiskCache; + static bool gFoundDiskCacheOnInit; + PRThread* mPrefetchThread; + UniquePtr<Compression::LZ4FrameDecompressionContext> mDecompressionContext; +#ifdef DEBUG + nsTHashtable<nsISupportsHashKey> 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, + nsTHashtable<nsISupportsHashKey>* objectMap) + : mBinaryStream(binaryStream), mObjectMap(objectMap) {} + + NS_FORWARD_SAFE_NSIBINARYOUTPUTSTREAM(mBinaryStream) + NS_FORWARD_SAFE_NSIOUTPUTSTREAM(mBinaryStream) + + bool CheckReferences(nsISupports* aObject); + + nsCOMPtr<nsIObjectOutputStream> mBinaryStream; + nsTHashtable<nsISupportsHashKey>* 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..461948d9c2 --- /dev/null +++ b/startupcache/StartupCacheInfo.cpp @@ -0,0 +1,47 @@ +/* -*- 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 "StartupCacheInfo.h" + +#include "mozilla/Components.h" +#include "mozilla/RefPtr.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 { + *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..ca19b1d6cd --- /dev/null +++ b/startupcache/StartupCacheUtils.cpp @@ -0,0 +1,241 @@ +/* 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, + UniquePtr<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 = MakeUnique<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 = !greBase.Compare(spec.get(), false, greBase.Length()); + bool underApp = + appBase.Length() && !appBase.Compare(spec.get(), false, appBase.Length()); + + 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); + } else 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; +} + +/** + * 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 appended to the string passed in. Adding a prefix before + * calling is recommended to avoid colliding with other cache users. + * + * For example, in the js loader (string is prefixed with jsloader by caller): + * resource://gre/modules/XPCOMUtils.jsm or + * file://$GRE_DIR/modules/XPCOMUtils.jsm or + * jar:file://$GRE_DIR/omni.jar!/modules/XPCOMUtils.jsm becomes + * jsloader/resource/gre/modules/XPCOMUtils.jsm + * 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(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 = PathifyURI(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; +} + +} // namespace scache +} // namespace mozilla diff --git a/startupcache/StartupCacheUtils.h b/startupcache/StartupCacheUtils.h new file mode 100644 index 0000000000..ebd00d951c --- /dev/null +++ b/startupcache/StartupCacheUtils.h @@ -0,0 +1,44 @@ +/* -*- 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" + +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, + UniquePtr<char[]>* buffer, uint32_t* len); + +nsresult ResolveURI(nsIURI* in, nsIURI** out); + +nsresult PathifyURI(nsIURI* in, nsACString& 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..28e758c6a5 --- /dev/null +++ b/startupcache/test/TestStartupCache.cpp @@ -0,0 +1,184 @@ +/* -*- 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/Maybe.h" +#include "mozilla/Printf.h" +#include "mozilla/UniquePtr.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, UniquePtr<char[]>(strdup(buf)), strlen(buf) + 1); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + rv = sc->GetBuffer(id, &outbuf, &len); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + EXPECT_STREQ(buf, outbuf); + + rv = sc->ResetStartupWriteTimer(); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + WaitForStartupTimer(); + + rv = sc->GetBuffer(id, &outbuf, &len); + EXPECT_TRUE(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, UniquePtr<char[]>(strdup(buf)), strlen(buf) + 1); + EXPECT_TRUE(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_TRUE(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_TRUE(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_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsISupports> objQI(do_QueryInterface(obj)); + rv = objectOutput->WriteObject(objQI, true); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + UniquePtr<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_TRUE(NS_SUCCEEDED(rv)); + + const char* buf2; + uint32_t len2; + nsCOMPtr<nsIObjectInputStream> objectInput; + rv = sc->GetBuffer(id, &buf2, &len2); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + rv = NewObjectInputStreamFromBuffer(buf2, len2, getter_AddRefs(objectInput)); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsISupports> deserialized; + rv = objectInput->ReadObject(true, getter_AddRefs(deserialized)); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIURI> uri(do_QueryInterface(deserialized)); + ASSERT_TRUE(uri); + + nsCString outSpec; + rv = uri->GetSpec(outSpec); + EXPECT_TRUE(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..707bd98564 --- /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" |