Bug 1550108 - Change StartupCache format from zip to custom r=froydnj

I am not aware of anything that depends on StartupCache being a
zip file, and since I want to use lz4 compression because inflate
is showing up quite a lot in profiles, it's simplest to just use
a custom format. This loosely mimicks the ScriptPreloader code,
with a few diversions:

- Obviously the contents of the cache are compressed. I used lz4
  for this as I hit the same file size as deflate at a compression
  level of 1, which is what the StartupCache was using previously,
  while decompressing an order of magnitude faster. Seemed like
  the most conservative change to make. I think it's worth
  investigating what the impact of slower algs with higher ratios
  would be, but for right now I settled on this. We'd probably
  want to look at zstd next.
- I use streaming compression for this via lz4frame. This is not
  strictly necessary, but has the benefit of not requiring as
  much memory for large buffers, as well as giving us a built-in
  checksum, rather than relying on the much slower CRC that we
  were doing with the zip-based approach.
- I coded the serialization of the headers inline, since I had to
  jump back to add the offset and compressed size, which would
  make the nice Code(...) method for the ScriptPreloader stuff
  rather more complex. Open to cleaner solutions, but moving it
  out just felt like extra hoops for the reader to jump through
  to understand without the benefit of being more concise.

Differential Revision: https://phabricator.services.mozilla.com/D34652

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Doug Thayer 2019-09-27 22:15:35 +00:00
Родитель d27f7199e1
Коммит eb024f4ee6
4 изменённых файлов: 559 добавлений и 187 удалений

Просмотреть файл

@ -13,7 +13,9 @@
#include <string>
#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<class AP>
LZ4FrameCompressionContext<AP>::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<class AP>
LZ4FrameCompressionContext<AP>::~LZ4FrameCompressionContext() {
LZ4F_freeCompressionContext(mContext);
this->free_(mWriteBuffer);
}
template<class AP>
Result<Span<const char>, size_t>
LZ4FrameCompressionContext<AP>::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<char>(mWriteBufLen);
size_t headerSize =
LZ4F_compressBegin(mContext, mWriteBuffer, mWriteBufLen, &prefs);
if (LZ4F_isError(headerSize)) {
return Err(headerSize);
}
return MakeSpan(static_cast<const char*>(mWriteBuffer), headerSize);
}
template<class AP>
Result<Span<const char>, size_t>
LZ4FrameCompressionContext<AP>::ContinueCompressing(Span<const char> 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<const char*>(mWriteBuffer), outputSize);
}
template<class AP>
Result<Span<const char>, size_t> LZ4FrameCompressionContext<AP>::EndCompressing() {
size_t outputSize =
LZ4F_compressEnd(mContext, mWriteBuffer, mWriteBufLen,
/* options */ nullptr);
if (LZ4F_isError(outputSize)) {
return Err(outputSize);
}
return MakeSpan(static_cast<const char*>(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<LZ4FrameDecompressionResult, size_t>
LZ4FrameDecompressionContext::Decompress(Span<char> aOutput,
Span<const char> 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;
}

Просмотреть файл

@ -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 AllocPolicy = MallocAllocPolicy>
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<Span<const char>, 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<Span<const char>, size_t> ContinueCompressing(Span<const char> 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<Span<const char>, 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<LZ4FrameDecompressionResult, size_t> Decompress(
Span<char> aOutput, Span<const char> aInput);
private:
LZ4F_dctx_s* mContext;
bool mStableDest;
};
} /* namespace Compression */
} /* namespace mozilla */

Просмотреть файл

@ -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<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::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<LZ4FrameDecompressionContext>(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<Ok, nsresult> 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<uint8_t>();
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<uint8_t> 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<char[]>* outbuf, uint32_t* length) {
if (!zip) return NS_ERROR_NOT_AVAILABLE;
nsZipItemPtr<char> 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<char[]>* 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<char[]>* outbuf,
"Startup cache only available on main thread");
WaitOnWriteThread();
if (!mStartupWriteInitiated) {
CacheEntry* entry;
nsDependentCString idStr(id);
mTable.Get(idStr, &entry);
if (entry) {
*outbuf = MakeUnique<char[]>(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<const char> compressed = MakeSpan(
mCacheData.get<char>().get() + mCacheEntriesBaseOffset + value.mOffset,
value.mCompressedSize);
if (CanPrefetchMemory()) {
PrefetchMemory((uint8_t*)compressed.Elements(), compressed.Length());
}
value.mData = MakeUnique<char[]>(value.mUncompressedSize);
Span<char> 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<char[]>&& 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<nsIZipWriter> writer;
nsCOMPtr<nsIStringInputStream> 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<Ok, nsresult> 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<nsIZipWriter> 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<Pair<const nsCString*, StartupCacheEntry*>> 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<nsIStringInputStream> 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<const char> 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<StartupCache*>(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<StartupCache*>(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

Просмотреть файл

@ -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<char[]> data;
uint32_t size;
struct StartupCacheEntry {
UniquePtr<char[]> 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<char[]> buf, uint32_t len)
: data(std::move(buf)), size(len) {}
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) {}
~CacheEntry() {}
struct Comparator {
using Value = Pair<const nsCString*, StartupCacheEntry*>;
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<char[]>* 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<char[]>&& 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<Ok, nsresult> LoadArchive();
nsresult Init();
void WriteToDisk();
// 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 WaitOnWriteThread();
static nsresult InitSingleton();
static void WriteTimeout(nsITimer* aTimer, void* aClosure);
static void ThreadedWrite(void* aClosure);
nsClassHashtable<nsCStringHashKey, CacheEntry> mTable;
nsTArray<nsCString> mPendingWrites;
RefPtr<nsZipArchive> mArchive;
HashMap<nsCString, StartupCacheEntry, nsCStringHasher> 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;
nsCOMPtr<nsIObserverService> mObserverService;
RefPtr<StartupCacheListener> mListener;
nsCOMPtr<nsITimer> mTimer;
Atomic<bool> mDirty;
bool mStartupWriteInitiated;
bool mCurTableReferenced;
size_t mCacheEntriesBaseOffset;
static StaticRefPtr<StartupCache> gStartupCache;
static bool gShutdownInitiated;
static bool gIgnoreDiskCache;
PRThread* mWriteThread;
UniquePtr<Compression::LZ4FrameDecompressionContext> mDecompressionContext;
#ifdef DEBUG
nsTHashtable<nsISupportsHashKey> mWriteObjectMap;
#endif