diff --git a/mfbt/Compression.cpp b/mfbt/Compression.cpp index 7264889adf59..8b93c0b1961f 100644 --- a/mfbt/Compression.cpp +++ b/mfbt/Compression.cpp @@ -13,7 +13,9 @@ #include #include "lz4/lz4.h" +#include "lz4/lz4frame.h" +using namespace mozilla; using namespace mozilla::Compression; /* Our wrappers */ @@ -78,3 +80,111 @@ bool LZ4::decompressPartial(const char* aSource, size_t aInputSize, char* aDest, *aOutputSize = 0; return false; } + +template +LZ4FrameCompressionContext::LZ4FrameCompressionContext(int aCompressionLevel, + size_t aMaxSrcSize, + bool aChecksum, + bool aStableSrc) + : mContext(nullptr), + mCompressionLevel(aCompressionLevel), + mGenerateChecksum(aChecksum), + mStableSrc(aStableSrc), + mMaxSrcSize(aMaxSrcSize), + mWriteBufLen(0), + mWriteBuffer(nullptr) { + LZ4F_errorCode_t err = LZ4F_createCompressionContext(&mContext, LZ4F_VERSION); + MOZ_RELEASE_ASSERT(!LZ4F_isError(err)); +} + +template +LZ4FrameCompressionContext::~LZ4FrameCompressionContext() { + LZ4F_freeCompressionContext(mContext); + this->free_(mWriteBuffer); +} + +template +Result, size_t> +LZ4FrameCompressionContext::BeginCompressing() { + LZ4F_contentChecksum_t checksum = + mGenerateChecksum ? LZ4F_contentChecksumEnabled : LZ4F_noContentChecksum; + LZ4F_preferences_t prefs = { + { + LZ4F_max256KB, + LZ4F_blockLinked, + checksum, + }, + mCompressionLevel, + }; + mWriteBufLen = LZ4F_compressBound(mMaxSrcSize, &prefs); + mWriteBuffer = this->template pod_malloc(mWriteBufLen); + + size_t headerSize = + LZ4F_compressBegin(mContext, mWriteBuffer, mWriteBufLen, &prefs); + if (LZ4F_isError(headerSize)) { + return Err(headerSize); + } + + return MakeSpan(static_cast(mWriteBuffer), headerSize); +} + +template +Result, size_t> +LZ4FrameCompressionContext::ContinueCompressing(Span aInput) { + LZ4F_compressOptions_t opts = {}; + opts.stableSrc = (uint32_t)mStableSrc; + size_t outputSize = + LZ4F_compressUpdate(mContext, mWriteBuffer, mWriteBufLen, + aInput.Elements(), aInput.Length(), + &opts); + if (LZ4F_isError(outputSize)) { + return Err(outputSize); + } + + return MakeSpan(static_cast(mWriteBuffer), outputSize); +} + +template +Result, size_t> LZ4FrameCompressionContext::EndCompressing() { + size_t outputSize = + LZ4F_compressEnd(mContext, mWriteBuffer, mWriteBufLen, + /* options */ nullptr); + if (LZ4F_isError(outputSize)) { + return Err(outputSize); + } + + return MakeSpan(static_cast(mWriteBuffer), outputSize); +} + +LZ4FrameDecompressionContext::LZ4FrameDecompressionContext(bool aStableDest) + : mContext(nullptr), + mStableDest(aStableDest) { + LZ4F_errorCode_t err = + LZ4F_createDecompressionContext(&mContext, LZ4F_VERSION); + MOZ_RELEASE_ASSERT(!LZ4F_isError(err)); +} + +LZ4FrameDecompressionContext::~LZ4FrameDecompressionContext() { + LZ4F_freeDecompressionContext(mContext); +} + +Result +LZ4FrameDecompressionContext::Decompress(Span aOutput, + Span aInput) { + LZ4F_decompressOptions_t opts = {}; + opts.stableDst = (uint32_t)mStableDest; + size_t outBytes = aOutput.Length(); + size_t inBytes = aInput.Length(); + size_t result = LZ4F_decompress(mContext, aOutput.Elements(), &outBytes, + aInput.Elements(), &inBytes, + &opts); + if (LZ4F_isError(result)) { + return Err(result); + } + + LZ4FrameDecompressionResult decompressionResult = {}; + decompressionResult.mFinished = !result; + decompressionResult.mSizeRead = inBytes; + decompressionResult.mSizeWritten = outBytes; + return decompressionResult; +} diff --git a/mfbt/Compression.h b/mfbt/Compression.h index 7b61638c3cb0..101cd7c7f689 100644 --- a/mfbt/Compression.h +++ b/mfbt/Compression.h @@ -9,8 +9,14 @@ #ifndef mozilla_Compression_h_ #define mozilla_Compression_h_ +#include "mozilla/AllocPolicy.h" #include "mozilla/Assertions.h" #include "mozilla/Types.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" + +struct LZ4F_cctx_s; // compression context +struct LZ4F_dctx_s; // decompression context namespace mozilla { namespace Compression { @@ -137,6 +143,87 @@ class LZ4 { } }; +/** + * Context for LZ4 Frame-based streaming compression. Use this if you + * want to incrementally compress something or if you want to compress + * something such that another application can read it. + */ +template +class LZ4FrameCompressionContext final : private AllocPolicy { + public: + MFBT_API LZ4FrameCompressionContext(int aCompressionLevel, size_t aMaxSrcSize, + bool aChecksum, bool aStableSrc = false); + MFBT_API ~LZ4FrameCompressionContext(); + + + /** + * Begin streaming frame-based compression. + * + * @return a Result with a Span containing the frame header, or an lz4 error + * code (size_t). + */ + MFBT_API Result, size_t> BeginCompressing(); + + /** + * Continue streaming frame-based compression with the provided input. + * + * @param aInput input buffer to be compressed. + * @return a Result with a Span containing compressed output, or an lz4 error + * code (size_t). + */ + MFBT_API Result, size_t> ContinueCompressing(Span aInput); + + /** + * Finalize streaming frame-based compression with the provided input. + * + * @return a Result with a Span containing compressed output and the frame + * footer, or an lz4 error code (size_t). + */ + MFBT_API Result, size_t> EndCompressing(); + + private: + LZ4F_cctx_s* mContext; + int mCompressionLevel; + bool mGenerateChecksum; + bool mStableSrc; + size_t mMaxSrcSize; + size_t mWriteBufLen; + char* mWriteBuffer; +}; + +struct LZ4FrameDecompressionResult { + size_t mSizeRead; + size_t mSizeWritten; + bool mFinished; +}; + +/** + * Context for LZ4 Frame-based streaming decompression. Use this if you + * want to decompress something compressed by LZ4FrameCompressionContext + * or by another application. + */ +class LZ4FrameDecompressionContext final { + public: + explicit MFBT_API LZ4FrameDecompressionContext(bool aStableDest = false); + MFBT_API ~LZ4FrameDecompressionContext(); + + /** + * Decompress a buffer/part of a buffer compressed with + * LZ4FrameCompressionContext or another application. + * + * @param aOutput output buffer to be write results into. + * @param aInput input buffer to be decompressed. + * @return a Result with information on bytes read/written and whether we + * completely decompressed the input into the output, or an lz4 error code (size_t). + */ + MFBT_API Result Decompress( + Span aOutput, Span aInput); + + private: + LZ4F_dctx_s* mContext; + bool mStableDest; +}; + } /* namespace Compression */ } /* namespace mozilla */ diff --git a/startupcache/StartupCache.cpp b/startupcache/StartupCache.cpp index 309e7e44adc4..4ef7f013d35f 100644 --- a/startupcache/StartupCache.cpp +++ b/startupcache/StartupCache.cpp @@ -7,8 +7,13 @@ #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/ResultExtensions.h" #include "mozilla/scache/StartupCache.h" +#include "mozilla/ScopeExit.h" #include "nsAutoPtr.h" #include "nsClassHashtable.h" @@ -48,6 +53,8 @@ # define SC_WORDSIZE "8" #endif +using namespace mozilla::Compression; + namespace mozilla { namespace scache { @@ -58,7 +65,7 @@ StartupCache::CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, bool aAnonymize) { MOZ_COLLECT_REPORT( "explicit/startup-cache/mapping", KIND_NONHEAP, UNITS_BYTES, - SizeOfMapping(), + 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."); @@ -70,8 +77,34 @@ StartupCache::CollectReports(nsIHandleReportCallback* aHandleReport, 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_CAPACITY = 450; + #define STARTUP_CACHE_NAME "startupCache." SC_WORDSIZE "." SC_ENDIAN +static inline Result 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 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::GetSingleton() { if (!gStartupCache) { if (!XRE_IsParentProcess()) { @@ -107,7 +140,11 @@ bool StartupCache::gIgnoreDiskCache; NS_IMPL_ISUPPORTS(StartupCache, nsIMemoryReporter) StartupCache::StartupCache() - : mArchive(nullptr), mStartupWriteInitiated(false), mWriteThread(nullptr) {} + : mDirty(false), + mStartupWriteInitiated(false), + mCurTableReferenced(false), + mCacheEntriesBaseOffset(0), + mWriteThread(nullptr) {} StartupCache::~StartupCache() { if (mTimer) { @@ -123,8 +160,9 @@ StartupCache::~StartupCache() { // it on the main thread and block the shutdown we simply wont update // the startup cache. Always do this if the file doesn't exist since // we use it part of the package step. - if (!mArchive) { - WriteToDisk(); + if (mDirty) { + auto result = WriteToDisk(); + Unused << NS_WARN_IF(result.isErr()); } UnregisterWeakMemoryReporter(this); @@ -183,7 +221,8 @@ nsresult StartupCache::Init() { false); NS_ENSURE_SUCCESS(rv, rv); - rv = LoadArchive(); + auto result = LoadArchive(); + rv = result.isErr() ? result.unwrapErr() : NS_OK; // Sometimes we don't have a cache yet, that's ok. // If it's corrupted, just remove it and start over. @@ -193,6 +232,7 @@ nsresult StartupCache::Init() { } RegisterWeakMemoryReporter(this); + mDecompressionContext = MakeUnique(true); return NS_OK; } @@ -201,53 +241,91 @@ nsresult StartupCache::Init() { * LoadArchive can be called from the main thread or while reloading cache on * write thread. */ -nsresult StartupCache::LoadArchive() { - if (gIgnoreDiskCache) return NS_ERROR_FAILURE; +Result StartupCache::LoadArchive() { + if (gIgnoreDiskCache) return Err(NS_ERROR_FAILURE); - mArchive = new nsZipArchive(); - nsresult rv = mArchive->OpenArchive(mFile); + MOZ_TRY(mCacheData.init(mFile)); + auto size = mCacheData.size(); - if (NS_FAILED(rv)) { - mArchive = nullptr; + uint32_t headerSize; + if (size < sizeof(MAGIC) + sizeof(headerSize)) { + return Err(NS_ERROR_UNEXPECTED); } - return rv; + auto data = mCacheData.get(); + auto end = data + 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 (data + headerSize > end) { + return Err(NS_ERROR_UNEXPECTED); + } + + Range header(data, data + headerSize); + data += headerSize; + + mCacheEntriesBaseOffset = sizeof(MAGIC) + sizeof(headerSize) + headerSize; + { + if (!mTable.reserve(STARTUP_CACHE_CAPACITY)) { + return Err(NS_ERROR_UNEXPECTED); + } + auto cleanup = MakeScopeExit([&]() { mTable.clear(); }); + 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 (data + offset + compressedSize > end) { + 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; + + if (!mTable.putNew(key, StartupCacheEntry(offset, compressedSize, + uncompressedSize))) { + return Err(NS_ERROR_UNEXPECTED); + } + } + + if (buf.error()) { + return Err(NS_ERROR_UNEXPECTED); + } + + cleanup.release(); + } + + return Ok(); } -namespace { - -nsresult GetBufferFromZipArchive(nsZipArchive* zip, bool doCRC, const char* id, - UniquePtr* outbuf, uint32_t* length) { - if (!zip) return NS_ERROR_NOT_AVAILABLE; - - nsZipItemPtr zipItem(zip, id, doCRC); - if (!zipItem) return NS_ERROR_NOT_AVAILABLE; - - *outbuf = zipItem.Forget(); - *length = zipItem.Length(); - return NS_OK; -} - -} /* anonymous namespace */ - bool StartupCache::HasEntry(const char* id) { AUTO_PROFILER_LABEL("StartupCache::HasEntry", OTHER); MOZ_ASSERT(NS_IsMainThread(), "Startup cache only available on main thread"); WaitOnWriteThread(); - if (!mStartupWriteInitiated) { - CacheEntry* entry; - mTable.Get(nsDependentCString(id), &entry); - return !!entry; - } - - return mArchive && mArchive->GetItem(id); + return mTable.has(nsDependentCString(id)); } -// NOTE: this will not find a new entry until it has been written to disk! -// Consumer should take ownership of the resulting buffer. -nsresult StartupCache::GetBuffer(const char* id, UniquePtr* outbuf, +nsresult StartupCache::GetBuffer(const char* id, const char** outbuf, uint32_t* length) { AUTO_PROFILER_LABEL("StartupCache::GetBuffer", OTHER); @@ -255,25 +333,65 @@ nsresult StartupCache::GetBuffer(const char* id, UniquePtr* outbuf, "Startup cache only available on main thread"); WaitOnWriteThread(); - if (!mStartupWriteInitiated) { - CacheEntry* entry; - nsDependentCString idStr(id); - mTable.Get(idStr, &entry); - if (entry) { - *outbuf = MakeUnique(entry->size); - memcpy(outbuf->get(), entry->data.get(), entry->size); - *length = entry->size; - Telemetry::AccumulateCategorical( - Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory); - return NS_OK; - } + 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; } - nsresult rv = GetBufferFromZipArchive(mArchive, true, id, outbuf, length); - Telemetry::AccumulateCategorical( - NS_SUCCEEDED(rv) ? Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitDisk - : Telemetry::LABELS_STARTUP_CACHE_REQUESTS::Miss); - return rv; + auto& value = p->value(); + if (value.mData) { + label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory; + } else { + if (!mCacheData.initialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + size_t totalRead = 0; + size_t totalWritten = 0; + Span compressed = MakeSpan( + mCacheData.get().get() + mCacheEntriesBaseOffset + value.mOffset, + value.mCompressedSize); + if (CanPrefetchMemory()) { + PrefetchMemory((uint8_t*)compressed.Elements(), compressed.Length()); + } + value.mData = MakeUnique(value.mUncompressedSize); + Span uncompressed = + MakeSpan(value.mData.get(), value.mUncompressedSize); + 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; + } + + label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitDisk; + } + + if (!value.mRequested) { + value.mRequested = true; + value.mRequestedOrder = ++mRequestedCount; + } + + // Track that something holds a reference into mTable, so we know to hold + // onto it in case the cache is invalidated. + mCurTableReferenced = true; + *outbuf = p->value().Elements(); + *length = p->value().Length(); + return NS_OK; } // Makes a copy of the buffer, client retains ownership of inbuf. @@ -286,31 +404,22 @@ nsresult StartupCache::PutBuffer(const char* id, UniquePtr&& inbuf, return NS_ERROR_NOT_AVAILABLE; } - nsDependentCString idStr(id); - // Cache it for now, we'll write all together later. - auto entry = mTable.LookupForAdd(idStr); + bool exists = mTable.has(nsDependentCString(id)); - if (entry) { + if (exists) { NS_WARNING("Existing entry in StartupCache."); // Double-caching is undesirable but not an error. return NS_OK; } -#ifdef DEBUG - if (mArchive) { - nsZipItem* zipItem = mArchive->GetItem(id); - NS_ASSERTION(zipItem == nullptr, "Existing entry in disk StartupCache."); + // 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(); } -#endif - - entry.OrInsert( - [&inbuf, &len]() { return new CacheEntry(std::move(inbuf), len); }); - mPendingWrites.AppendElement(idStr); - return ResetStartupWriteTimer(); -} - -size_t StartupCache::SizeOfMapping() { - return mArchive ? mArchive->SizeOfMapping() : 0; + return NS_OK; } size_t StartupCache::HeapSizeOfIncludingThis( @@ -320,125 +429,130 @@ size_t StartupCache::HeapSizeOfIncludingThis( size_t n = aMallocSizeOf(this); - n += mTable.ShallowSizeOfExcludingThis(aMallocSizeOf); - for (auto iter = mTable.ConstIter(); !iter.Done(); iter.Next()) { - n += iter.Data()->SizeOfIncludingThis(aMallocSizeOf); - } - - n += mPendingWrites.ShallowSizeOfExcludingThis(aMallocSizeOf); + n += mTable.shallowSizeOfExcludingThis(aMallocSizeOf); return n; } -struct CacheWriteHolder { - nsCOMPtr writer; - nsCOMPtr stream; - PRTime time; -}; - -static void CacheCloseHelper(const nsACString& key, const CacheEntry* data, - const CacheWriteHolder* holder) { - MOZ_ASSERT(data); // assert key was found in mTable. - - nsresult rv; - nsIStringInputStream* stream = holder->stream; - nsIZipWriter* writer = holder->writer; - - stream->ShareData(data->data.get(), data->size); - -#ifdef DEBUG - bool hasEntry; - rv = writer->HasEntry(key, &hasEntry); - NS_ASSERTION(NS_SUCCEEDED(rv) && hasEntry == false, - "Existing entry in disk StartupCache."); -#endif - rv = writer->AddEntryStream(key, holder->time, true, stream, false); - - if (NS_FAILED(rv)) { - NS_WARNING("cache entry deleted but not written to disk."); - } -} - /** * WriteToDisk writes the cache out to disk. Callers of WriteToDisk need to call * WaitOnWriteThread to make sure there isn't a write happening on another * thread */ -void StartupCache::WriteToDisk() { - nsresult rv; +Result StartupCache::WriteToDisk() { mStartupWriteInitiated = true; + if (!mDirty) return Ok(); - if (mTable.Count() == 0) return; + AutoFDClose fd; + MOZ_TRY(mFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + 0644, &fd.rwget())); - nsCOMPtr zipW = do_CreateInstance("@mozilla.org/zipwriter;1"); - if (!zipW) return; - - rv = zipW->Open(mFile, PR_RDWR | PR_CREATE_FILE); - if (NS_FAILED(rv)) { - NS_WARNING("could not open zipfile for write"); - return; + nsTArray> entries; + for (auto iter = mTable.iter(); !iter.done(); iter.next()) { + entries.AppendElement(MakePair(&iter.get().key(), &iter.get().value())); } - // If we didn't have an mArchive member, that means that we failed to - // open the startup cache for reading. Therefore, we need to record - // the time of creation in a zipfile comment; this has been useful for - // Telemetry statistics. - PRTime now = PR_Now(); - if (!mArchive) { - nsCString comment; - comment.Assign((char*)&now, sizeof(now)); - zipW->SetComment(comment); + 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); } - nsCOMPtr stream = - do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); - if (NS_FAILED(rv)) { - NS_WARNING("Couldn't create string input stream."); - return; + 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; + for (auto& e : entries) { + auto value = e.second(); + value->mOffset = offset; + const size_t chunkSize = 1024 * 16; + LZ4FrameCompressionContext ctx(6, /* aCompressionLevel */ + chunkSize, /* aReadBufLen */ + true, /* aChecksum */ + true); /* aStableSrc */ + Span result; + MOZ_TRY_VAR(result, ctx.BeginCompressing().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)); } - CacheWriteHolder holder; - holder.stream = stream; - holder.writer = zipW; - holder.time = now; - - for (auto& key : mPendingWrites) { - CacheCloseHelper(key, mTable.Get(key), &holder); + 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); } - mPendingWrites.Clear(); - mTable.Clear(); + MOZ_TRY(Seek(fd, headerStart)); + MOZ_TRY(Write(fd, buf.Get(), buf.cursor())); - // Close the archive so Windows doesn't choke. - mArchive = nullptr; - zipW->Close(); + mDirty = false; - // We succesfully wrote the archive to disk; mark the disk file as trusted - gIgnoreDiskCache = false; - - // Our reader's view of the archive is outdated now, reload it. - LoadArchive(); + return Ok(); } void StartupCache::InvalidateCache(bool memoryOnly) { - if (memoryOnly) { - // The memoryOnly option is just for testing purposes. We want to ensure - // that we're nuking the in-memory form but that we preserve everything - // on disk. - WriteToDisk(); - return; - } WaitOnWriteThread(); - mPendingWrites.Clear(); - mTable.Clear(); - mArchive = nullptr; - 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; + if (memoryOnly) { + 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; + } + if (!memoryOnly) { + 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; - LoadArchive(); + auto result = LoadArchive(); + if (NS_WARN_IF(result.isErr())) { + gIgnoreDiskCache = true; + } } void StartupCache::IgnoreDiskCache() { @@ -473,7 +587,8 @@ void StartupCache::ThreadedWrite(void* aClosure) { * if the StartupCache object is valid. */ StartupCache* startupCacheObj = static_cast(aClosure); - startupCacheObj->WriteToDisk(); + auto result = startupCacheObj->WriteToDisk(); + Unused << NS_WARN_IF(result.isErr()); mozilla::IOInterposer::UnregisterCurrentThread(); } @@ -491,6 +606,9 @@ void StartupCache::WriteTimeout(nsITimer* aTimer, void* aClosure) { * if the StartupCache object is valid. */ StartupCache* startupCacheObj = static_cast(aClosure); + startupCacheObj->mStartupWriteInitiated = false; + startupCacheObj->mDirty = true; + startupCacheObj->mCacheData.reset(); startupCacheObj->mWriteThread = PR_CreateThread( PR_USER_THREAD, StartupCache::ThreadedWrite, startupCacheObj, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); @@ -530,6 +648,7 @@ nsresult StartupCache::GetDebugObjectOutputStream( nsresult StartupCache::ResetStartupWriteTimer() { mStartupWriteInitiated = false; + mDirty = true; nsresult rv = NS_OK; if (!mTimer) mTimer = NS_NewTimer(); @@ -545,7 +664,7 @@ nsresult StartupCache::ResetStartupWriteTimer() { bool StartupCache::StartupWriteComplete() { WaitOnWriteThread(); - return mStartupWriteInitiated && mTable.Count() == 0; + return mStartupWriteInitiated && !mDirty; } // StartupCacheDebugOutputStream implementation diff --git a/startupcache/StartupCache.h b/startupcache/StartupCache.h index 1ae23581401b..4dcb836ca6c8 100644 --- a/startupcache/StartupCache.h +++ b/startupcache/StartupCache.h @@ -18,7 +18,11 @@ #include "nsIOutputStream.h" #include "nsIFile.h" #include "mozilla/Attributes.h" +#include "mozilla/AutoMemMap.h" +#include "mozilla/Compression.h" #include "mozilla/MemoryReporting.h" +#include "mozilla/Pair.h" +#include "mozilla/Result.h" #include "mozilla/UniquePtr.h" /** @@ -75,20 +79,58 @@ namespace mozilla { namespace scache { -struct CacheEntry { - UniquePtr data; - uint32_t size; +struct StartupCacheEntry { + UniquePtr mData; + uint32_t mOffset; + uint32_t mCompressedSize; + uint32_t mUncompressedSize; + int32_t mHeaderOffsetInFile; + int32_t mRequestedOrder; + bool mRequested; - CacheEntry() : size(0) {} + 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) {} - // Takes possession of buf - CacheEntry(UniquePtr buf, uint32_t len) - : data(std::move(buf)), size(len) {} + StartupCacheEntry(UniquePtr aData, size_t aLength, + int32_t aRequestedOrder) + : mData(std::move(aData)), + mOffset(0), + mCompressedSize(0), + mUncompressedSize(aLength), + mHeaderOffsetInFile(0), + mRequestedOrder(0), + mRequested(true) {} - ~CacheEntry() {} + struct Comparator { + using Value = Pair; - size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { - return mallocSizeOf(this) + mallocSizeOf(data.get()); + 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; + } + }; +}; + +struct nsCStringHasher { + using Key = nsCString; + using Lookup = nsCString; + + static HashNumber hash(const Lookup& aLookup) { + return HashString(aLookup.get()); + } + + static bool match(const Key& aKey, const Lookup& aLookup) { + return aKey.Equals(aLookup); } }; @@ -112,9 +154,8 @@ class StartupCache : public nsIMemoryReporter { // 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 takes ownership. - nsresult GetBuffer(const char* id, UniquePtr* outbuf, - uint32_t* length); + // 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&& inbuf, @@ -138,8 +179,6 @@ class StartupCache : public nsIMemoryReporter { // excludes the mapping. size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; - size_t SizeOfMapping(); - // FOR TESTING ONLY nsresult ResetStartupWriteTimer(); bool StartupWriteComplete(); @@ -148,30 +187,47 @@ class StartupCache : public nsIMemoryReporter { StartupCache(); virtual ~StartupCache(); - nsresult LoadArchive(); + Result LoadArchive(); nsresult Init(); - void WriteToDisk(); + + // Returns a file pointer for the cache file with the given name in the + // current profile. + Result, nsresult> GetCacheFile(const nsAString& suffix); + + // Opens the cache file for reading. + Result OpenCache(); + + // Writes the cache to disk + Result WriteToDisk(); + void WaitOnWriteThread(); static nsresult InitSingleton(); static void WriteTimeout(nsITimer* aTimer, void* aClosure); static void ThreadedWrite(void* aClosure); - nsClassHashtable mTable; - nsTArray mPendingWrites; - RefPtr mArchive; + HashMap 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 mOldTables; nsCOMPtr mFile; + loader::AutoMemMap mCacheData; nsCOMPtr mObserverService; RefPtr mListener; nsCOMPtr mTimer; + Atomic mDirty; bool mStartupWriteInitiated; + bool mCurTableReferenced; + size_t mCacheEntriesBaseOffset; static StaticRefPtr gStartupCache; static bool gShutdownInitiated; static bool gIgnoreDiskCache; PRThread* mWriteThread; + UniquePtr mDecompressionContext; #ifdef DEBUG nsTHashtable mWriteObjectMap; #endif