Bug 1845715 - Check for failure when getting RegExp match result template r=iain
[gecko.git] / startupcache / StartupCache.cpp
blob5c6e97b326bd59a3c798d6cacdc01df750a40efe
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "prio.h"
8 #include "PLDHashTable.h"
9 #include "mozilla/IOInterposer.h"
10 #include "mozilla/AutoMemMap.h"
11 #include "mozilla/IOBuffers.h"
12 #include "mozilla/MemoryReporting.h"
13 #include "mozilla/MemUtils.h"
14 #include "mozilla/MmapFaultHandler.h"
15 #include "mozilla/ResultExtensions.h"
16 #include "mozilla/scache/StartupCache.h"
17 #include "mozilla/ScopeExit.h"
19 #include "nsClassHashtable.h"
20 #include "nsComponentManagerUtils.h"
21 #include "nsCRT.h"
22 #include "nsDirectoryServiceUtils.h"
23 #include "nsIClassInfo.h"
24 #include "nsIFile.h"
25 #include "nsIObserver.h"
26 #include "nsIOutputStream.h"
27 #include "nsISupports.h"
28 #include "nsITimer.h"
29 #include "mozilla/Omnijar.h"
30 #include "prenv.h"
31 #include "mozilla/Telemetry.h"
32 #include "nsThreadUtils.h"
33 #include "nsXULAppAPI.h"
34 #include "nsIProtocolHandler.h"
35 #include "GeckoProfiler.h"
36 #include "nsAppRunner.h"
37 #include "xpcpublic.h"
38 #ifdef MOZ_BACKGROUNDTASKS
39 # include "mozilla/BackgroundTasks.h"
40 #endif
42 #if defined(XP_WIN)
43 # include <windows.h>
44 #endif
46 #ifdef IS_BIG_ENDIAN
47 # define SC_ENDIAN "big"
48 #else
49 # define SC_ENDIAN "little"
50 #endif
52 #if PR_BYTES_PER_WORD == 4
53 # define SC_WORDSIZE "4"
54 #else
55 # define SC_WORDSIZE "8"
56 #endif
58 using namespace mozilla::Compression;
60 namespace mozilla {
61 namespace scache {
63 MOZ_DEFINE_MALLOC_SIZE_OF(StartupCacheMallocSizeOf)
65 NS_IMETHODIMP
66 StartupCache::CollectReports(nsIHandleReportCallback* aHandleReport,
67 nsISupports* aData, bool aAnonymize) {
68 MutexAutoLock lock(mTableLock);
69 MOZ_COLLECT_REPORT(
70 "explicit/startup-cache/mapping", KIND_NONHEAP, UNITS_BYTES,
71 mCacheData.nonHeapSizeOfExcludingThis(),
72 "Memory used to hold the mapping of the startup cache from file. "
73 "This memory is likely to be swapped out shortly after start-up.");
75 MOZ_COLLECT_REPORT("explicit/startup-cache/data", KIND_HEAP, UNITS_BYTES,
76 HeapSizeOfIncludingThis(StartupCacheMallocSizeOf),
77 "Memory used by the startup cache for things other than "
78 "the file mapping.");
80 return NS_OK;
83 static const uint8_t MAGIC[] = "startupcache0002";
84 // This is a heuristic value for how much to reserve for mTable to avoid
85 // rehashing. This is not a hard limit in release builds, but it is in
86 // debug builds as it should be stable. If we exceed this number we should
87 // just increase it.
88 static const size_t STARTUP_CACHE_RESERVE_CAPACITY = 450;
89 // This is a hard limit which we will assert on, to ensure that we don't
90 // have some bug causing runaway cache growth.
91 static const size_t STARTUP_CACHE_MAX_CAPACITY = 5000;
93 // Not const because we change it for gtests.
94 static uint8_t STARTUP_CACHE_WRITE_TIMEOUT = 60;
96 #define STARTUP_CACHE_NAME "startupCache." SC_WORDSIZE "." SC_ENDIAN
98 static inline Result<Ok, nsresult> Write(PRFileDesc* fd, const void* data,
99 int32_t len) {
100 if (PR_Write(fd, data, len) != len) {
101 return Err(NS_ERROR_FAILURE);
103 return Ok();
106 static inline Result<Ok, nsresult> Seek(PRFileDesc* fd, int32_t offset) {
107 if (PR_Seek(fd, offset, PR_SEEK_SET) == -1) {
108 return Err(NS_ERROR_FAILURE);
110 return Ok();
113 static nsresult MapLZ4ErrorToNsresult(size_t aError) {
114 return NS_ERROR_FAILURE;
117 StartupCache* StartupCache::GetSingletonNoInit() {
118 return StartupCache::gStartupCache;
121 StartupCache* StartupCache::GetSingleton() {
122 #ifdef MOZ_BACKGROUNDTASKS
123 if (BackgroundTasks::IsBackgroundTaskMode()) {
124 return nullptr;
126 #endif
128 if (!gStartupCache) {
129 if (!XRE_IsParentProcess()) {
130 return nullptr;
132 #ifdef MOZ_DISABLE_STARTUPCACHE
133 return nullptr;
134 #else
135 StartupCache::InitSingleton();
136 #endif
139 return StartupCache::gStartupCache;
142 void StartupCache::DeleteSingleton() { StartupCache::gStartupCache = nullptr; }
144 nsresult StartupCache::InitSingleton() {
145 nsresult rv;
146 StartupCache::gStartupCache = new StartupCache();
148 rv = StartupCache::gStartupCache->Init();
149 if (NS_FAILED(rv)) {
150 StartupCache::gStartupCache = nullptr;
152 return rv;
155 StaticRefPtr<StartupCache> StartupCache::gStartupCache;
156 bool StartupCache::gShutdownInitiated;
157 bool StartupCache::gIgnoreDiskCache;
158 bool StartupCache::gFoundDiskCacheOnInit;
160 NS_IMPL_ISUPPORTS(StartupCache, nsIMemoryReporter)
162 StartupCache::StartupCache()
163 : mTableLock("StartupCache::mTableLock"),
164 mDirty(false),
165 mWrittenOnce(false),
166 mCurTableReferenced(false),
167 mRequestedCount(0),
168 mCacheEntriesBaseOffset(0) {}
170 StartupCache::~StartupCache() { UnregisterWeakMemoryReporter(this); }
172 nsresult StartupCache::Init() {
173 // workaround for bug 653936
174 nsCOMPtr<nsIProtocolHandler> jarInitializer(
175 do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "jar"));
177 nsresult rv;
179 if (mozilla::RunningGTest()) {
180 STARTUP_CACHE_WRITE_TIMEOUT = 3;
183 // This allows to override the startup cache filename
184 // which is useful from xpcshell, when there is no ProfLDS directory to keep
185 // cache in.
186 char* env = PR_GetEnv("MOZ_STARTUP_CACHE");
187 if (env && *env) {
188 rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false,
189 getter_AddRefs(mFile));
190 } else {
191 nsCOMPtr<nsIFile> file;
192 rv = NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(file));
193 if (NS_FAILED(rv)) {
194 // return silently, this will fail in mochitests's xpcshell process.
195 return rv;
198 rv = file->AppendNative("startupCache"_ns);
199 NS_ENSURE_SUCCESS(rv, rv);
201 // Try to create the directory if it's not there yet
202 rv = file->Create(nsIFile::DIRECTORY_TYPE, 0777);
203 if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) return rv;
205 rv = file->AppendNative(nsLiteralCString(STARTUP_CACHE_NAME));
207 NS_ENSURE_SUCCESS(rv, rv);
209 mFile = file;
212 NS_ENSURE_TRUE(mFile, NS_ERROR_UNEXPECTED);
214 mObserverService = do_GetService("@mozilla.org/observer-service;1");
216 if (!mObserverService) {
217 NS_WARNING("Could not get observerService.");
218 return NS_ERROR_UNEXPECTED;
221 mListener = new StartupCacheListener();
222 rv = mObserverService->AddObserver(mListener, NS_XPCOM_SHUTDOWN_OBSERVER_ID,
223 false);
224 NS_ENSURE_SUCCESS(rv, rv);
225 rv = mObserverService->AddObserver(mListener, "startupcache-invalidate",
226 false);
227 NS_ENSURE_SUCCESS(rv, rv);
228 rv = mObserverService->AddObserver(mListener, "intl:app-locales-changed",
229 false);
230 NS_ENSURE_SUCCESS(rv, rv);
233 MutexAutoLock lock(mTableLock);
234 auto result = LoadArchive();
235 rv = result.isErr() ? result.unwrapErr() : NS_OK;
238 gFoundDiskCacheOnInit = rv != NS_ERROR_FILE_NOT_FOUND;
240 // Sometimes we don't have a cache yet, that's ok.
241 // If it's corrupted, just remove it and start over.
242 if (gIgnoreDiskCache || (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND)) {
243 NS_WARNING("Failed to load startupcache file correctly, removing!");
244 InvalidateCache();
247 RegisterWeakMemoryReporter(this);
248 mDecompressionContext = MakeUnique<LZ4FrameDecompressionContext>(true);
250 return NS_OK;
253 void StartupCache::StartPrefetchMemory() {
255 MonitorAutoLock lock(mPrefetchComplete);
256 mPrefetchInProgress = true;
258 NS_DispatchBackgroundTask(NewRunnableMethod<uint8_t*, size_t>(
259 "StartupCache::ThreadedPrefetch", this, &StartupCache::ThreadedPrefetch,
260 mCacheData.get<uint8_t>().get(), mCacheData.size()));
264 * LoadArchive can only be called from the main thread.
266 Result<Ok, nsresult> StartupCache::LoadArchive() {
267 MOZ_ASSERT(NS_IsMainThread(), "Can only load startup cache on main thread");
268 if (gIgnoreDiskCache) return Err(NS_ERROR_FAILURE);
270 mTableLock.AssertCurrentThreadOwns();
272 MOZ_TRY(mCacheData.init(mFile));
273 auto size = mCacheData.size();
274 if (CanPrefetchMemory()) {
275 StartPrefetchMemory();
278 uint32_t headerSize;
279 if (size < sizeof(MAGIC) + sizeof(headerSize)) {
280 return Err(NS_ERROR_UNEXPECTED);
283 auto data = mCacheData.get<uint8_t>();
284 auto end = data + size;
286 MMAP_FAULT_HANDLER_BEGIN_BUFFER(data.get(), size)
288 if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) {
289 return Err(NS_ERROR_UNEXPECTED);
291 data += sizeof(MAGIC);
293 headerSize = LittleEndian::readUint32(data.get());
294 data += sizeof(headerSize);
296 if (headerSize > end - data) {
297 MOZ_ASSERT(false, "StartupCache file is corrupt.");
298 return Err(NS_ERROR_UNEXPECTED);
301 Range<uint8_t> header(data, data + headerSize);
302 data += headerSize;
304 mCacheEntriesBaseOffset = sizeof(MAGIC) + sizeof(headerSize) + headerSize;
306 if (!mTable.reserve(STARTUP_CACHE_RESERVE_CAPACITY)) {
307 return Err(NS_ERROR_UNEXPECTED);
309 auto cleanup = MakeScopeExit([&]() {
310 mTableLock.AssertCurrentThreadOwns();
311 WaitOnPrefetch();
312 mTable.clear();
313 mCacheData.reset();
315 loader::InputBuffer buf(header);
317 uint32_t currentOffset = 0;
318 while (!buf.finished()) {
319 uint32_t offset = 0;
320 uint32_t compressedSize = 0;
321 uint32_t uncompressedSize = 0;
322 nsCString key;
323 buf.codeUint32(offset);
324 buf.codeUint32(compressedSize);
325 buf.codeUint32(uncompressedSize);
326 buf.codeString(key);
328 if (offset + compressedSize > end - data) {
329 MOZ_ASSERT(false, "StartupCache file is corrupt.");
330 return Err(NS_ERROR_UNEXPECTED);
333 // Make sure offsets match what we'd expect based on script ordering and
334 // size, as a basic sanity check.
335 if (offset != currentOffset) {
336 return Err(NS_ERROR_UNEXPECTED);
338 currentOffset += compressedSize;
340 // We could use mTable.putNew if we knew the file we're loading weren't
341 // corrupt. However, we don't know that, so check if the key already
342 // exists. If it does, we know the file must be corrupt.
343 decltype(mTable)::AddPtr p = mTable.lookupForAdd(key);
344 if (p) {
345 return Err(NS_ERROR_UNEXPECTED);
348 if (!mTable.add(
349 p, key,
350 StartupCacheEntry(offset, compressedSize, uncompressedSize))) {
351 return Err(NS_ERROR_UNEXPECTED);
355 if (buf.error()) {
356 return Err(NS_ERROR_UNEXPECTED);
359 cleanup.release();
362 MMAP_FAULT_HANDLER_CATCH(Err(NS_ERROR_UNEXPECTED))
364 return Ok();
367 bool StartupCache::HasEntry(const char* id) {
368 AUTO_PROFILER_LABEL("StartupCache::HasEntry", OTHER);
370 MOZ_ASSERT(NS_IsMainThread(), "Startup cache only available on main thread");
372 MutexAutoLock lock(mTableLock);
373 return mTable.has(nsDependentCString(id));
376 nsresult StartupCache::GetBuffer(const char* id, const char** outbuf,
377 uint32_t* length)
378 MOZ_NO_THREAD_SAFETY_ANALYSIS {
379 AUTO_PROFILER_LABEL("StartupCache::GetBuffer", OTHER);
381 NS_ASSERTION(NS_IsMainThread(),
382 "Startup cache only available on main thread");
384 Telemetry::LABELS_STARTUP_CACHE_REQUESTS label =
385 Telemetry::LABELS_STARTUP_CACHE_REQUESTS::Miss;
386 auto telemetry =
387 MakeScopeExit([&label] { Telemetry::AccumulateCategorical(label); });
389 MutexAutoLock lock(mTableLock);
390 decltype(mTable)::Ptr p = mTable.lookup(nsDependentCString(id));
391 if (!p) {
392 return NS_ERROR_NOT_AVAILABLE;
395 auto& value = p->value();
396 if (value.mData) {
397 label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory;
398 } else {
399 if (!mCacheData.initialized()) {
400 return NS_ERROR_NOT_AVAILABLE;
403 size_t totalRead = 0;
404 size_t totalWritten = 0;
405 Span<const char> compressed = Span(
406 mCacheData.get<char>().get() + mCacheEntriesBaseOffset + value.mOffset,
407 value.mCompressedSize);
408 value.mData = UniqueFreePtr<char[]>(reinterpret_cast<char*>(
409 malloc(sizeof(char) * value.mUncompressedSize)));
410 Span<char> uncompressed = Span(value.mData.get(), value.mUncompressedSize);
411 MMAP_FAULT_HANDLER_BEGIN_BUFFER(uncompressed.Elements(),
412 uncompressed.Length())
413 bool finished = false;
414 while (!finished) {
415 auto result = mDecompressionContext->Decompress(
416 uncompressed.From(totalWritten), compressed.From(totalRead));
417 if (NS_WARN_IF(result.isErr())) {
418 value.mData = nullptr;
419 MutexAutoUnlock unlock(mTableLock);
420 InvalidateCache();
421 return NS_ERROR_FAILURE;
423 auto decompressionResult = result.unwrap();
424 totalRead += decompressionResult.mSizeRead;
425 totalWritten += decompressionResult.mSizeWritten;
426 finished = decompressionResult.mFinished;
429 MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE)
431 label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitDisk;
434 if (!value.mRequested) {
435 value.mRequested = true;
436 value.mRequestedOrder = ++mRequestedCount;
437 MOZ_ASSERT(mRequestedCount <= mTable.count(),
438 "Somehow we requested more StartupCache items than exist.");
439 ResetStartupWriteTimerCheckingReadCount();
442 // Track that something holds a reference into mTable, so we know to hold
443 // onto it in case the cache is invalidated.
444 mCurTableReferenced = true;
445 *outbuf = value.mData.get();
446 *length = value.mUncompressedSize;
447 return NS_OK;
450 // Makes a copy of the buffer, client retains ownership of inbuf.
451 nsresult StartupCache::PutBuffer(const char* id, UniqueFreePtr<char[]>&& inbuf,
452 uint32_t len) MOZ_NO_THREAD_SAFETY_ANALYSIS {
453 NS_ASSERTION(NS_IsMainThread(),
454 "Startup cache only available on main thread");
455 if (StartupCache::gShutdownInitiated) {
456 return NS_ERROR_NOT_AVAILABLE;
459 // Try to gain the table write lock. If the background task to write the
460 // cache is running, this will fail.
461 MutexAutoTryLock lock(mTableLock);
462 if (!lock) {
463 return NS_ERROR_NOT_AVAILABLE;
465 mTableLock.AssertCurrentThreadOwns();
466 bool exists = mTable.has(nsDependentCString(id));
467 if (exists) {
468 NS_WARNING("Existing entry in StartupCache.");
469 // Double-caching is undesirable but not an error.
470 return NS_OK;
473 // putNew returns false on alloc failure - in the very unlikely event we hit
474 // that and aren't going to crash elsewhere, there's no reason we need to
475 // crash here.
476 if (mTable.putNew(nsCString(id), StartupCacheEntry(std::move(inbuf), len,
477 ++mRequestedCount))) {
478 return ResetStartupWriteTimer();
480 MOZ_DIAGNOSTIC_ASSERT(mTable.count() < STARTUP_CACHE_MAX_CAPACITY,
481 "Too many StartupCache entries.");
482 return NS_OK;
485 size_t StartupCache::HeapSizeOfIncludingThis(
486 mozilla::MallocSizeOf aMallocSizeOf) const {
487 // This function could measure more members, but they haven't been found by
488 // DMD to be significant. They can be added later if necessary.
490 size_t n = aMallocSizeOf(this);
492 n += mTable.shallowSizeOfExcludingThis(aMallocSizeOf);
493 for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
494 if (iter.get().value().mData) {
495 n += aMallocSizeOf(iter.get().value().mData.get());
497 n += iter.get().key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
500 return n;
504 * WriteToDisk writes the cache out to disk. Callers of WriteToDisk need to call
505 * WaitOnWriteComplete to make sure there isn't a write
506 * happening on another thread.
507 * We own the mTableLock here.
509 Result<Ok, nsresult> StartupCache::WriteToDisk() {
510 if (!mDirty || mWrittenOnce) {
511 return Ok();
514 if (!mFile) {
515 return Err(NS_ERROR_UNEXPECTED);
518 AutoFDClose fd;
519 MOZ_TRY(mFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
520 0644, &fd.rwget()));
522 nsTArray<std::pair<const nsCString*, StartupCacheEntry*>> entries;
523 for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
524 if (iter.get().value().mRequested) {
525 entries.AppendElement(
526 std::make_pair(&iter.get().key(), &iter.get().value()));
530 if (entries.IsEmpty()) {
531 return Ok();
534 entries.Sort(StartupCacheEntry::Comparator());
535 loader::OutputBuffer buf;
536 for (auto& e : entries) {
537 auto key = e.first;
538 auto value = e.second;
539 auto uncompressedSize = value->mUncompressedSize;
540 // Set the mHeaderOffsetInFile so we can go back and edit the offset.
541 value->mHeaderOffsetInFile = buf.cursor();
542 // Write a 0 offset/compressed size as a placeholder until we get the real
543 // offset after compressing.
544 buf.codeUint32(0);
545 buf.codeUint32(0);
546 buf.codeUint32(uncompressedSize);
547 buf.codeString(*key);
550 uint8_t headerSize[4];
551 LittleEndian::writeUint32(headerSize, buf.cursor());
553 MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
554 MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
555 size_t headerStart = sizeof(MAGIC) + sizeof(headerSize);
556 size_t dataStart = headerStart + buf.cursor();
557 MOZ_TRY(Seek(fd, dataStart));
559 size_t offset = 0;
561 const size_t chunkSize = 1024 * 16;
562 LZ4FrameCompressionContext ctx(6, /* aCompressionLevel */
563 chunkSize, /* aReadBufLen */
564 true, /* aChecksum */
565 true); /* aStableSrc */
566 size_t writeBufLen = ctx.GetRequiredWriteBufferLength();
567 auto writeBuffer = MakeUnique<char[]>(writeBufLen);
568 auto writeSpan = Span(writeBuffer.get(), writeBufLen);
570 for (auto& e : entries) {
571 auto value = e.second;
572 value->mOffset = offset;
573 Span<const char> result;
574 MOZ_TRY_VAR(result,
575 ctx.BeginCompressing(writeSpan).mapErr(MapLZ4ErrorToNsresult));
576 MOZ_TRY(Write(fd, result.Elements(), result.Length()));
577 offset += result.Length();
579 for (size_t i = 0; i < value->mUncompressedSize; i += chunkSize) {
580 size_t size = std::min(chunkSize, value->mUncompressedSize - i);
581 char* uncompressed = value->mData.get() + i;
582 MOZ_TRY_VAR(result, ctx.ContinueCompressing(Span(uncompressed, size))
583 .mapErr(MapLZ4ErrorToNsresult));
584 MOZ_TRY(Write(fd, result.Elements(), result.Length()));
585 offset += result.Length();
588 MOZ_TRY_VAR(result, ctx.EndCompressing().mapErr(MapLZ4ErrorToNsresult));
589 MOZ_TRY(Write(fd, result.Elements(), result.Length()));
590 offset += result.Length();
591 value->mCompressedSize = offset - value->mOffset;
592 MOZ_TRY(Seek(fd, dataStart + offset));
595 for (auto& e : entries) {
596 auto value = e.second;
597 uint8_t* headerEntry = buf.Get() + value->mHeaderOffsetInFile;
598 LittleEndian::writeUint32(headerEntry, value->mOffset);
599 LittleEndian::writeUint32(headerEntry + sizeof(value->mOffset),
600 value->mCompressedSize);
602 MOZ_TRY(Seek(fd, headerStart));
603 MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
605 mDirty = false;
606 mWrittenOnce = true;
608 return Ok();
611 void StartupCache::InvalidateCache(bool memoryOnly) {
612 WaitOnPrefetch();
613 // Ensure we're not writing using mTable...
614 MutexAutoLock lock(mTableLock);
616 mWrittenOnce = false;
617 if (memoryOnly) {
618 // This should only be called in tests.
619 auto writeResult = WriteToDisk();
620 if (NS_WARN_IF(writeResult.isErr())) {
621 gIgnoreDiskCache = true;
622 return;
625 if (mCurTableReferenced) {
626 // There should be no way for this assert to fail other than a user manually
627 // sending startupcache-invalidate messages through the Browser Toolbox. If
628 // something knowingly invalidates the cache, the event can be counted with
629 // mAllowedInvalidationsCount.
630 MOZ_DIAGNOSTIC_ASSERT(
631 xpc::IsInAutomation() ||
632 // The allowed invalidations can grow faster than the old tables, so
633 // guard against incorrect unsigned subtraction.
634 mAllowedInvalidationsCount > mOldTables.Length() ||
635 // Now perform the real check.
636 mOldTables.Length() - mAllowedInvalidationsCount < 10,
637 "Startup cache invalidated too many times.");
638 mOldTables.AppendElement(std::move(mTable));
639 mCurTableReferenced = false;
640 } else {
641 mTable.clear();
643 mRequestedCount = 0;
644 if (!memoryOnly) {
645 mCacheData.reset();
646 nsresult rv = mFile->Remove(false);
647 if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
648 gIgnoreDiskCache = true;
649 return;
652 gIgnoreDiskCache = false;
653 auto result = LoadArchive();
654 if (NS_WARN_IF(result.isErr())) {
655 gIgnoreDiskCache = true;
659 void StartupCache::CountAllowedInvalidation() { mAllowedInvalidationsCount++; }
661 void StartupCache::MaybeInitShutdownWrite() {
662 if (mTimer) {
663 mTimer->Cancel();
665 gShutdownInitiated = true;
667 MaybeWriteOffMainThread();
670 void StartupCache::EnsureShutdownWriteComplete() {
671 MutexAutoLock lock(mTableLock);
672 // If we've already written or there's nothing to write,
673 // we don't need to do anything. This is the common case.
674 if (mWrittenOnce || (mCacheData.initialized() && !ShouldCompactCache())) {
675 return;
677 // Otherwise, ensure the write happens. The timer should have been cancelled
678 // already in MaybeInitShutdownWrite.
680 // We got the lock. Keep the following in sync with
681 // MaybeWriteOffMainThread:
682 WaitOnPrefetch();
683 mDirty = true;
684 mCacheData.reset();
685 // Most of this should be redundant given MaybeWriteOffMainThread should
686 // have run before now.
688 auto writeResult = WriteToDisk();
689 Unused << NS_WARN_IF(writeResult.isErr());
690 // We've had the lock, and `WriteToDisk()` sets mWrittenOnce and mDirty
691 // when done, and checks for them when starting, so we don't need to do
692 // anything else.
695 void StartupCache::IgnoreDiskCache() {
696 gIgnoreDiskCache = true;
697 if (gStartupCache) gStartupCache->InvalidateCache();
700 bool StartupCache::GetIgnoreDiskCache() { return gIgnoreDiskCache; }
702 void StartupCache::WaitOnPrefetch() {
703 // This can't be called from within ThreadedPrefetch()
704 MonitorAutoLock lock(mPrefetchComplete);
705 while (mPrefetchInProgress) {
706 mPrefetchComplete.Wait();
710 void StartupCache::ThreadedPrefetch(uint8_t* aStart, size_t aSize) {
711 // Always notify of completion, even if MMAP_FAULT_HANDLER_CATCH()
712 // early-returns.
713 auto notifyPrefetchComplete = MakeScopeExit([&] {
714 MonitorAutoLock lock(mPrefetchComplete);
715 mPrefetchInProgress = false;
716 mPrefetchComplete.NotifyAll();
719 // PrefetchMemory does madvise/equivalent, but doesn't access the memory
720 // pointed to by aStart
721 MMAP_FAULT_HANDLER_BEGIN_BUFFER(aStart, aSize)
722 PrefetchMemory(aStart, aSize);
723 MMAP_FAULT_HANDLER_CATCH()
726 // mTableLock must be held
727 bool StartupCache::ShouldCompactCache() {
728 // If we've requested less than 4/5 of the startup cache, then we should
729 // probably compact it down. This can happen quite easily after the first run,
730 // which seems to request quite a few more things than subsequent runs.
731 CheckedInt<uint32_t> threshold = CheckedInt<uint32_t>(mTable.count()) * 4 / 5;
732 MOZ_RELEASE_ASSERT(threshold.isValid(), "Runaway StartupCache size");
733 return mRequestedCount < threshold.value();
737 * The write-thread is spawned on a timeout(which is reset with every write).
738 * This can avoid a slow shutdown.
740 void StartupCache::WriteTimeout(nsITimer* aTimer, void* aClosure) {
742 * It is safe to use the pointer passed in aClosure to reference the
743 * StartupCache object because the timer's lifetime is tightly coupled to
744 * the lifetime of the StartupCache object; this timer is canceled in the
745 * StartupCache destructor, guaranteeing that this function runs if and only
746 * if the StartupCache object is valid.
748 StartupCache* startupCacheObj = static_cast<StartupCache*>(aClosure);
749 startupCacheObj->MaybeWriteOffMainThread();
753 * See StartupCache::WriteTimeout above - this is just the non-static body.
755 void StartupCache::MaybeWriteOffMainThread() {
757 MutexAutoLock lock(mTableLock);
758 if (mWrittenOnce || (mCacheData.initialized() && !ShouldCompactCache())) {
759 return;
762 // Keep this code in sync with EnsureShutdownWriteComplete.
763 WaitOnPrefetch();
765 MutexAutoLock lock(mTableLock);
766 mDirty = true;
767 mCacheData.reset();
770 RefPtr<StartupCache> self = this;
771 nsCOMPtr<nsIRunnable> runnable =
772 NS_NewRunnableFunction("StartupCache::Write", [self]() mutable {
773 MutexAutoLock lock(self->mTableLock);
774 auto result = self->WriteToDisk();
775 Unused << NS_WARN_IF(result.isErr());
777 NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
780 // We don't want to refcount StartupCache, so we'll just
781 // hold a ref to this and pass it to observerService instead.
782 NS_IMPL_ISUPPORTS(StartupCacheListener, nsIObserver)
784 nsresult StartupCacheListener::Observe(nsISupports* subject, const char* topic,
785 const char16_t* data) {
786 StartupCache* sc = StartupCache::GetSingleton();
787 if (!sc) return NS_OK;
789 if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
790 // Do not leave the thread running past xpcom shutdown
791 sc->WaitOnPrefetch();
792 StartupCache::gShutdownInitiated = true;
793 // Note that we don't do anything special for the background write
794 // task; we expect the threadpool to finish running any tasks already
795 // posted to it prior to shutdown. FastShutdown will call
796 // EnsureShutdownWriteComplete() to ensure any pending writes happen
797 // in that case.
798 } else if (strcmp(topic, "startupcache-invalidate") == 0) {
799 sc->InvalidateCache(data && nsCRT::strcmp(data, u"memoryOnly") == 0);
800 } else if (strcmp(topic, "intl:app-locales-changed") == 0) {
801 // Live language switching invalidates the startup cache due to the history
802 // sidebar retaining localized strings in its internal SQL query. This
803 // should be a relatively rare event, but a user could do it an arbitrary
804 // number of times.
805 sc->CountAllowedInvalidation();
807 return NS_OK;
810 nsresult StartupCache::GetDebugObjectOutputStream(
811 nsIObjectOutputStream* aStream, nsIObjectOutputStream** aOutStream) {
812 NS_ENSURE_ARG_POINTER(aStream);
813 #ifdef DEBUG
814 auto* stream = new StartupCacheDebugOutputStream(aStream, &mWriteObjectMap);
815 NS_ADDREF(*aOutStream = stream);
816 #else
817 NS_ADDREF(*aOutStream = aStream);
818 #endif
820 return NS_OK;
823 nsresult StartupCache::ResetStartupWriteTimerCheckingReadCount() {
824 nsresult rv = NS_OK;
825 if (!mTimer)
826 mTimer = NS_NewTimer();
827 else
828 rv = mTimer->Cancel();
829 NS_ENSURE_SUCCESS(rv, rv);
830 // Wait for the specified timeout, then write out the cache.
831 mTimer->InitWithNamedFuncCallback(
832 StartupCache::WriteTimeout, this, STARTUP_CACHE_WRITE_TIMEOUT * 1000,
833 nsITimer::TYPE_ONE_SHOT, "StartupCache::WriteTimeout");
834 return NS_OK;
837 // For test code only
838 nsresult StartupCache::ResetStartupWriteTimerAndLock() {
839 MutexAutoLock lock(mTableLock);
840 return ResetStartupWriteTimer();
843 nsresult StartupCache::ResetStartupWriteTimer() {
844 mDirty = true;
845 nsresult rv = NS_OK;
846 if (!mTimer)
847 mTimer = NS_NewTimer();
848 else
849 rv = mTimer->Cancel();
850 NS_ENSURE_SUCCESS(rv, rv);
851 // Wait for the specified timeout, then write out the cache.
852 mTimer->InitWithNamedFuncCallback(
853 StartupCache::WriteTimeout, this, STARTUP_CACHE_WRITE_TIMEOUT * 1000,
854 nsITimer::TYPE_ONE_SHOT, "StartupCache::WriteTimeout");
855 return NS_OK;
858 // Used only in tests:
859 bool StartupCache::StartupWriteComplete() {
860 // Need to have written to disk and not added new things since;
861 MutexAutoLock lock(mTableLock);
862 return !mDirty && mWrittenOnce;
865 // StartupCacheDebugOutputStream implementation
866 #ifdef DEBUG
867 NS_IMPL_ISUPPORTS(StartupCacheDebugOutputStream, nsIObjectOutputStream,
868 nsIBinaryOutputStream, nsIOutputStream)
870 bool StartupCacheDebugOutputStream::CheckReferences(nsISupports* aObject) {
871 nsresult rv;
873 nsCOMPtr<nsIClassInfo> classInfo = do_QueryInterface(aObject);
874 if (!classInfo) {
875 NS_ERROR("aObject must implement nsIClassInfo");
876 return false;
879 uint32_t flags;
880 rv = classInfo->GetFlags(&flags);
881 NS_ENSURE_SUCCESS(rv, false);
882 if (flags & nsIClassInfo::SINGLETON) return true;
884 bool inserted = mObjectMap->EnsureInserted(aObject);
885 if (!inserted) {
886 NS_ERROR(
887 "non-singleton aObject is referenced multiple times in this"
888 "serialization, we don't support that.");
891 return inserted;
894 // nsIObjectOutputStream implementation
895 nsresult StartupCacheDebugOutputStream::WriteObject(nsISupports* aObject,
896 bool aIsStrongRef) {
897 nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
899 NS_ASSERTION(rootObject.get() == aObject,
900 "bad call to WriteObject -- call WriteCompoundObject!");
901 bool check = CheckReferences(aObject);
902 NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
903 return mBinaryStream->WriteObject(aObject, aIsStrongRef);
906 nsresult StartupCacheDebugOutputStream::WriteSingleRefObject(
907 nsISupports* aObject) {
908 nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
910 NS_ASSERTION(rootObject.get() == aObject,
911 "bad call to WriteSingleRefObject -- call WriteCompoundObject!");
912 bool check = CheckReferences(aObject);
913 NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
914 return mBinaryStream->WriteSingleRefObject(aObject);
917 nsresult StartupCacheDebugOutputStream::WriteCompoundObject(
918 nsISupports* aObject, const nsIID& aIID, bool aIsStrongRef) {
919 nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
921 nsCOMPtr<nsISupports> roundtrip;
922 rootObject->QueryInterface(aIID, getter_AddRefs(roundtrip));
923 NS_ASSERTION(roundtrip.get() == aObject,
924 "bad aggregation or multiple inheritance detected by call to "
925 "WriteCompoundObject!");
927 bool check = CheckReferences(aObject);
928 NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
929 return mBinaryStream->WriteCompoundObject(aObject, aIID, aIsStrongRef);
932 nsresult StartupCacheDebugOutputStream::WriteID(nsID const& aID) {
933 return mBinaryStream->WriteID(aID);
936 char* StartupCacheDebugOutputStream::GetBuffer(uint32_t aLength,
937 uint32_t aAlignMask) {
938 return mBinaryStream->GetBuffer(aLength, aAlignMask);
941 void StartupCacheDebugOutputStream::PutBuffer(char* aBuffer, uint32_t aLength) {
942 mBinaryStream->PutBuffer(aBuffer, aLength);
944 #endif // DEBUG
946 } // namespace scache
947 } // namespace mozilla