Bug 1860073 [wpt PR 42637] - Update wpt metadata, a=testonly
[gecko.git] / startupcache / StartupCache.cpp
blob8fd3564974a7d33f38ac416c46289749a2668c6e
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"
18 #include "mozilla/Try.h"
20 #include "nsClassHashtable.h"
21 #include "nsComponentManagerUtils.h"
22 #include "nsCRT.h"
23 #include "nsDirectoryServiceUtils.h"
24 #include "nsIClassInfo.h"
25 #include "nsIFile.h"
26 #include "nsIObserver.h"
27 #include "nsIOutputStream.h"
28 #include "nsISupports.h"
29 #include "nsITimer.h"
30 #include "mozilla/Omnijar.h"
31 #include "prenv.h"
32 #include "mozilla/Telemetry.h"
33 #include "nsThreadUtils.h"
34 #include "nsXULAppAPI.h"
35 #include "nsIProtocolHandler.h"
36 #include "GeckoProfiler.h"
37 #include "nsAppRunner.h"
38 #include "xpcpublic.h"
39 #ifdef MOZ_BACKGROUNDTASKS
40 # include "mozilla/BackgroundTasks.h"
41 #endif
43 #if defined(XP_WIN)
44 # include <windows.h>
45 #endif
47 #ifdef IS_BIG_ENDIAN
48 # define SC_ENDIAN "big"
49 #else
50 # define SC_ENDIAN "little"
51 #endif
53 #if PR_BYTES_PER_WORD == 4
54 # define SC_WORDSIZE "4"
55 #else
56 # define SC_WORDSIZE "8"
57 #endif
59 using namespace mozilla::Compression;
61 namespace mozilla {
62 namespace scache {
64 MOZ_DEFINE_MALLOC_SIZE_OF(StartupCacheMallocSizeOf)
66 NS_IMETHODIMP
67 StartupCache::CollectReports(nsIHandleReportCallback* aHandleReport,
68 nsISupports* aData, bool aAnonymize) {
69 MutexAutoLock lock(mTableLock);
70 MOZ_COLLECT_REPORT(
71 "explicit/startup-cache/mapping", KIND_NONHEAP, UNITS_BYTES,
72 mCacheData.nonHeapSizeOfExcludingThis(),
73 "Memory used to hold the mapping of the startup cache from file. "
74 "This memory is likely to be swapped out shortly after start-up.");
76 MOZ_COLLECT_REPORT("explicit/startup-cache/data", KIND_HEAP, UNITS_BYTES,
77 HeapSizeOfIncludingThis(StartupCacheMallocSizeOf),
78 "Memory used by the startup cache for things other than "
79 "the file mapping.");
81 return NS_OK;
84 static const uint8_t MAGIC[] = "startupcache0002";
85 // This is a heuristic value for how much to reserve for mTable to avoid
86 // rehashing. This is not a hard limit in release builds, but it is in
87 // debug builds as it should be stable. If we exceed this number we should
88 // just increase it.
89 static const size_t STARTUP_CACHE_RESERVE_CAPACITY = 450;
90 // This is a hard limit which we will assert on, to ensure that we don't
91 // have some bug causing runaway cache growth.
92 static const size_t STARTUP_CACHE_MAX_CAPACITY = 5000;
94 // Not const because we change it for gtests.
95 static uint8_t STARTUP_CACHE_WRITE_TIMEOUT = 60;
97 #define STARTUP_CACHE_NAME "startupCache." SC_WORDSIZE "." SC_ENDIAN
99 static inline Result<Ok, nsresult> Write(PRFileDesc* fd, const void* data,
100 int32_t len) {
101 if (PR_Write(fd, data, len) != len) {
102 return Err(NS_ERROR_FAILURE);
104 return Ok();
107 static inline Result<Ok, nsresult> Seek(PRFileDesc* fd, int32_t offset) {
108 if (PR_Seek(fd, offset, PR_SEEK_SET) == -1) {
109 return Err(NS_ERROR_FAILURE);
111 return Ok();
114 static nsresult MapLZ4ErrorToNsresult(size_t aError) {
115 return NS_ERROR_FAILURE;
118 StartupCache* StartupCache::GetSingletonNoInit() {
119 return StartupCache::gStartupCache;
122 StartupCache* StartupCache::GetSingleton() {
123 #ifdef MOZ_BACKGROUNDTASKS
124 if (BackgroundTasks::IsBackgroundTaskMode()) {
125 return nullptr;
127 #endif
129 if (!gStartupCache) {
130 if (!XRE_IsParentProcess()) {
131 return nullptr;
133 #ifdef MOZ_DISABLE_STARTUPCACHE
134 return nullptr;
135 #else
136 StartupCache::InitSingleton();
137 #endif
140 return StartupCache::gStartupCache;
143 void StartupCache::DeleteSingleton() { StartupCache::gStartupCache = nullptr; }
145 nsresult StartupCache::InitSingleton() {
146 nsresult rv;
147 StartupCache::gStartupCache = new StartupCache();
149 rv = StartupCache::gStartupCache->Init();
150 if (NS_FAILED(rv)) {
151 StartupCache::gStartupCache = nullptr;
153 return rv;
156 StaticRefPtr<StartupCache> StartupCache::gStartupCache;
157 bool StartupCache::gShutdownInitiated;
158 bool StartupCache::gIgnoreDiskCache;
159 bool StartupCache::gFoundDiskCacheOnInit;
161 NS_IMPL_ISUPPORTS(StartupCache, nsIMemoryReporter)
163 StartupCache::StartupCache()
164 : mTableLock("StartupCache::mTableLock"),
165 mDirty(false),
166 mWrittenOnce(false),
167 mCurTableReferenced(false),
168 mRequestedCount(0),
169 mCacheEntriesBaseOffset(0) {}
171 StartupCache::~StartupCache() { UnregisterWeakMemoryReporter(this); }
173 nsresult StartupCache::Init() {
174 // workaround for bug 653936
175 nsCOMPtr<nsIProtocolHandler> jarInitializer(
176 do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "jar"));
178 nsresult rv;
180 if (mozilla::RunningGTest()) {
181 STARTUP_CACHE_WRITE_TIMEOUT = 3;
184 // This allows to override the startup cache filename
185 // which is useful from xpcshell, when there is no ProfLDS directory to keep
186 // cache in.
187 char* env = PR_GetEnv("MOZ_STARTUP_CACHE");
188 if (env && *env) {
189 rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(env), false,
190 getter_AddRefs(mFile));
191 } else {
192 nsCOMPtr<nsIFile> file;
193 rv = NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(file));
194 if (NS_FAILED(rv)) {
195 // return silently, this will fail in mochitests's xpcshell process.
196 return rv;
199 rv = file->AppendNative("startupCache"_ns);
200 NS_ENSURE_SUCCESS(rv, rv);
202 // Try to create the directory if it's not there yet
203 rv = file->Create(nsIFile::DIRECTORY_TYPE, 0777);
204 if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) return rv;
206 rv = file->AppendNative(nsLiteralCString(STARTUP_CACHE_NAME));
208 NS_ENSURE_SUCCESS(rv, rv);
210 mFile = file;
213 NS_ENSURE_TRUE(mFile, NS_ERROR_UNEXPECTED);
215 mObserverService = do_GetService("@mozilla.org/observer-service;1");
217 if (!mObserverService) {
218 NS_WARNING("Could not get observerService.");
219 return NS_ERROR_UNEXPECTED;
222 mListener = new StartupCacheListener();
223 rv = mObserverService->AddObserver(mListener, NS_XPCOM_SHUTDOWN_OBSERVER_ID,
224 false);
225 NS_ENSURE_SUCCESS(rv, rv);
226 rv = mObserverService->AddObserver(mListener, "startupcache-invalidate",
227 false);
228 NS_ENSURE_SUCCESS(rv, rv);
229 rv = mObserverService->AddObserver(mListener, "intl:app-locales-changed",
230 false);
231 NS_ENSURE_SUCCESS(rv, rv);
234 MutexAutoLock lock(mTableLock);
235 auto result = LoadArchive();
236 rv = result.isErr() ? result.unwrapErr() : NS_OK;
239 gFoundDiskCacheOnInit = rv != NS_ERROR_FILE_NOT_FOUND;
241 // Sometimes we don't have a cache yet, that's ok.
242 // If it's corrupted, just remove it and start over.
243 if (gIgnoreDiskCache || (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND)) {
244 NS_WARNING("Failed to load startupcache file correctly, removing!");
245 InvalidateCache();
248 RegisterWeakMemoryReporter(this);
249 mDecompressionContext = MakeUnique<LZ4FrameDecompressionContext>(true);
251 return NS_OK;
254 void StartupCache::StartPrefetchMemory() {
256 MonitorAutoLock lock(mPrefetchComplete);
257 mPrefetchInProgress = true;
259 NS_DispatchBackgroundTask(NewRunnableMethod<uint8_t*, size_t>(
260 "StartupCache::ThreadedPrefetch", this, &StartupCache::ThreadedPrefetch,
261 mCacheData.get<uint8_t>().get(), mCacheData.size()));
265 * LoadArchive can only be called from the main thread.
267 Result<Ok, nsresult> StartupCache::LoadArchive() {
268 MOZ_ASSERT(NS_IsMainThread(), "Can only load startup cache on main thread");
269 if (gIgnoreDiskCache) return Err(NS_ERROR_FAILURE);
271 mTableLock.AssertCurrentThreadOwns();
273 MOZ_TRY(mCacheData.init(mFile));
274 auto size = mCacheData.size();
275 if (CanPrefetchMemory()) {
276 StartPrefetchMemory();
279 uint32_t headerSize;
280 if (size < sizeof(MAGIC) + sizeof(headerSize)) {
281 return Err(NS_ERROR_UNEXPECTED);
284 auto data = mCacheData.get<uint8_t>();
285 auto end = data + size;
287 MMAP_FAULT_HANDLER_BEGIN_BUFFER(data.get(), size)
289 if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) {
290 return Err(NS_ERROR_UNEXPECTED);
292 data += sizeof(MAGIC);
294 headerSize = LittleEndian::readUint32(data.get());
295 data += sizeof(headerSize);
297 if (headerSize > end - data) {
298 MOZ_ASSERT(false, "StartupCache file is corrupt.");
299 return Err(NS_ERROR_UNEXPECTED);
302 Range<uint8_t> header(data, data + headerSize);
303 data += headerSize;
305 mCacheEntriesBaseOffset = sizeof(MAGIC) + sizeof(headerSize) + headerSize;
307 if (!mTable.reserve(STARTUP_CACHE_RESERVE_CAPACITY)) {
308 return Err(NS_ERROR_UNEXPECTED);
310 auto cleanup = MakeScopeExit([&]() {
311 mTableLock.AssertCurrentThreadOwns();
312 WaitOnPrefetch();
313 mTable.clear();
314 mCacheData.reset();
316 loader::InputBuffer buf(header);
318 uint32_t currentOffset = 0;
319 while (!buf.finished()) {
320 uint32_t offset = 0;
321 uint32_t compressedSize = 0;
322 uint32_t uncompressedSize = 0;
323 nsCString key;
324 buf.codeUint32(offset);
325 buf.codeUint32(compressedSize);
326 buf.codeUint32(uncompressedSize);
327 buf.codeString(key);
329 if (offset + compressedSize > end - data) {
330 MOZ_ASSERT(false, "StartupCache file is corrupt.");
331 return Err(NS_ERROR_UNEXPECTED);
334 // Make sure offsets match what we'd expect based on script ordering and
335 // size, as a basic sanity check.
336 if (offset != currentOffset) {
337 return Err(NS_ERROR_UNEXPECTED);
339 currentOffset += compressedSize;
341 // We could use mTable.putNew if we knew the file we're loading weren't
342 // corrupt. However, we don't know that, so check if the key already
343 // exists. If it does, we know the file must be corrupt.
344 decltype(mTable)::AddPtr p = mTable.lookupForAdd(key);
345 if (p) {
346 return Err(NS_ERROR_UNEXPECTED);
349 if (!mTable.add(
350 p, key,
351 StartupCacheEntry(offset, compressedSize, uncompressedSize))) {
352 return Err(NS_ERROR_UNEXPECTED);
356 if (buf.error()) {
357 return Err(NS_ERROR_UNEXPECTED);
360 cleanup.release();
363 MMAP_FAULT_HANDLER_CATCH(Err(NS_ERROR_UNEXPECTED))
365 return Ok();
368 bool StartupCache::HasEntry(const char* id) {
369 AUTO_PROFILER_LABEL("StartupCache::HasEntry", OTHER);
371 MOZ_ASSERT(NS_IsMainThread(), "Startup cache only available on main thread");
373 MutexAutoLock lock(mTableLock);
374 return mTable.has(nsDependentCString(id));
377 nsresult StartupCache::GetBuffer(const char* id, const char** outbuf,
378 uint32_t* length)
379 MOZ_NO_THREAD_SAFETY_ANALYSIS {
380 AUTO_PROFILER_LABEL("StartupCache::GetBuffer", OTHER);
382 NS_ASSERTION(NS_IsMainThread(),
383 "Startup cache only available on main thread");
385 Telemetry::LABELS_STARTUP_CACHE_REQUESTS label =
386 Telemetry::LABELS_STARTUP_CACHE_REQUESTS::Miss;
387 auto telemetry =
388 MakeScopeExit([&label] { Telemetry::AccumulateCategorical(label); });
390 MutexAutoLock lock(mTableLock);
391 decltype(mTable)::Ptr p = mTable.lookup(nsDependentCString(id));
392 if (!p) {
393 return NS_ERROR_NOT_AVAILABLE;
396 auto& value = p->value();
397 if (value.mData) {
398 label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory;
399 } else {
400 if (!mCacheData.initialized()) {
401 return NS_ERROR_NOT_AVAILABLE;
404 size_t totalRead = 0;
405 size_t totalWritten = 0;
406 Span<const char> compressed = Span(
407 mCacheData.get<char>().get() + mCacheEntriesBaseOffset + value.mOffset,
408 value.mCompressedSize);
409 value.mData = UniqueFreePtr<char[]>(reinterpret_cast<char*>(
410 malloc(sizeof(char) * value.mUncompressedSize)));
411 Span<char> uncompressed = Span(value.mData.get(), value.mUncompressedSize);
412 MMAP_FAULT_HANDLER_BEGIN_BUFFER(uncompressed.Elements(),
413 uncompressed.Length())
414 bool finished = false;
415 while (!finished) {
416 auto result = mDecompressionContext->Decompress(
417 uncompressed.From(totalWritten), compressed.From(totalRead));
418 if (NS_WARN_IF(result.isErr())) {
419 value.mData = nullptr;
420 MutexAutoUnlock unlock(mTableLock);
421 InvalidateCache();
422 return NS_ERROR_FAILURE;
424 auto decompressionResult = result.unwrap();
425 totalRead += decompressionResult.mSizeRead;
426 totalWritten += decompressionResult.mSizeWritten;
427 finished = decompressionResult.mFinished;
430 MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE)
432 label = Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitDisk;
435 if (!value.mRequested) {
436 value.mRequested = true;
437 value.mRequestedOrder = ++mRequestedCount;
438 MOZ_ASSERT(mRequestedCount <= mTable.count(),
439 "Somehow we requested more StartupCache items than exist.");
440 ResetStartupWriteTimerCheckingReadCount();
443 // Track that something holds a reference into mTable, so we know to hold
444 // onto it in case the cache is invalidated.
445 mCurTableReferenced = true;
446 *outbuf = value.mData.get();
447 *length = value.mUncompressedSize;
448 return NS_OK;
451 // Makes a copy of the buffer, client retains ownership of inbuf.
452 nsresult StartupCache::PutBuffer(const char* id, UniqueFreePtr<char[]>&& inbuf,
453 uint32_t len) MOZ_NO_THREAD_SAFETY_ANALYSIS {
454 NS_ASSERTION(NS_IsMainThread(),
455 "Startup cache only available on main thread");
456 if (StartupCache::gShutdownInitiated) {
457 return NS_ERROR_NOT_AVAILABLE;
460 // Try to gain the table write lock. If the background task to write the
461 // cache is running, this will fail.
462 MutexAutoTryLock lock(mTableLock);
463 if (!lock) {
464 return NS_ERROR_NOT_AVAILABLE;
466 mTableLock.AssertCurrentThreadOwns();
467 bool exists = mTable.has(nsDependentCString(id));
468 if (exists) {
469 NS_WARNING("Existing entry in StartupCache.");
470 // Double-caching is undesirable but not an error.
471 return NS_OK;
474 // putNew returns false on alloc failure - in the very unlikely event we hit
475 // that and aren't going to crash elsewhere, there's no reason we need to
476 // crash here.
477 if (mTable.putNew(nsCString(id), StartupCacheEntry(std::move(inbuf), len,
478 ++mRequestedCount))) {
479 return ResetStartupWriteTimer();
481 MOZ_DIAGNOSTIC_ASSERT(mTable.count() < STARTUP_CACHE_MAX_CAPACITY,
482 "Too many StartupCache entries.");
483 return NS_OK;
486 size_t StartupCache::HeapSizeOfIncludingThis(
487 mozilla::MallocSizeOf aMallocSizeOf) const {
488 // This function could measure more members, but they haven't been found by
489 // DMD to be significant. They can be added later if necessary.
491 size_t n = aMallocSizeOf(this);
493 n += mTable.shallowSizeOfExcludingThis(aMallocSizeOf);
494 for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
495 if (iter.get().value().mData) {
496 n += aMallocSizeOf(iter.get().value().mData.get());
498 n += iter.get().key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
501 return n;
505 * WriteToDisk writes the cache out to disk. Callers of WriteToDisk need to call
506 * WaitOnWriteComplete to make sure there isn't a write
507 * happening on another thread.
508 * We own the mTableLock here.
510 Result<Ok, nsresult> StartupCache::WriteToDisk() {
511 if (!mDirty || mWrittenOnce) {
512 return Ok();
515 if (!mFile) {
516 return Err(NS_ERROR_UNEXPECTED);
519 AutoFDClose fd;
520 MOZ_TRY(mFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
521 0644, &fd.rwget()));
523 nsTArray<std::pair<const nsCString*, StartupCacheEntry*>> entries;
524 for (auto iter = mTable.iter(); !iter.done(); iter.next()) {
525 if (iter.get().value().mRequested) {
526 entries.AppendElement(
527 std::make_pair(&iter.get().key(), &iter.get().value()));
531 if (entries.IsEmpty()) {
532 return Ok();
535 entries.Sort(StartupCacheEntry::Comparator());
536 loader::OutputBuffer buf;
537 for (auto& e : entries) {
538 auto key = e.first;
539 auto value = e.second;
540 auto uncompressedSize = value->mUncompressedSize;
541 // Set the mHeaderOffsetInFile so we can go back and edit the offset.
542 value->mHeaderOffsetInFile = buf.cursor();
543 // Write a 0 offset/compressed size as a placeholder until we get the real
544 // offset after compressing.
545 buf.codeUint32(0);
546 buf.codeUint32(0);
547 buf.codeUint32(uncompressedSize);
548 buf.codeString(*key);
551 uint8_t headerSize[4];
552 LittleEndian::writeUint32(headerSize, buf.cursor());
554 MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC)));
555 MOZ_TRY(Write(fd, headerSize, sizeof(headerSize)));
556 size_t headerStart = sizeof(MAGIC) + sizeof(headerSize);
557 size_t dataStart = headerStart + buf.cursor();
558 MOZ_TRY(Seek(fd, dataStart));
560 size_t offset = 0;
562 const size_t chunkSize = 1024 * 16;
563 LZ4FrameCompressionContext ctx(6, /* aCompressionLevel */
564 chunkSize, /* aReadBufLen */
565 true, /* aChecksum */
566 true); /* aStableSrc */
567 size_t writeBufLen = ctx.GetRequiredWriteBufferLength();
568 auto writeBuffer = MakeUnique<char[]>(writeBufLen);
569 auto writeSpan = Span(writeBuffer.get(), writeBufLen);
571 for (auto& e : entries) {
572 auto value = e.second;
573 value->mOffset = offset;
574 Span<const char> result;
575 MOZ_TRY_VAR(result,
576 ctx.BeginCompressing(writeSpan).mapErr(MapLZ4ErrorToNsresult));
577 MOZ_TRY(Write(fd, result.Elements(), result.Length()));
578 offset += result.Length();
580 for (size_t i = 0; i < value->mUncompressedSize; i += chunkSize) {
581 size_t size = std::min(chunkSize, value->mUncompressedSize - i);
582 char* uncompressed = value->mData.get() + i;
583 MOZ_TRY_VAR(result, ctx.ContinueCompressing(Span(uncompressed, size))
584 .mapErr(MapLZ4ErrorToNsresult));
585 MOZ_TRY(Write(fd, result.Elements(), result.Length()));
586 offset += result.Length();
589 MOZ_TRY_VAR(result, ctx.EndCompressing().mapErr(MapLZ4ErrorToNsresult));
590 MOZ_TRY(Write(fd, result.Elements(), result.Length()));
591 offset += result.Length();
592 value->mCompressedSize = offset - value->mOffset;
593 MOZ_TRY(Seek(fd, dataStart + offset));
596 for (auto& e : entries) {
597 auto value = e.second;
598 uint8_t* headerEntry = buf.Get() + value->mHeaderOffsetInFile;
599 LittleEndian::writeUint32(headerEntry, value->mOffset);
600 LittleEndian::writeUint32(headerEntry + sizeof(value->mOffset),
601 value->mCompressedSize);
603 MOZ_TRY(Seek(fd, headerStart));
604 MOZ_TRY(Write(fd, buf.Get(), buf.cursor()));
606 mDirty = false;
607 mWrittenOnce = true;
609 return Ok();
612 void StartupCache::InvalidateCache(bool memoryOnly) {
613 WaitOnPrefetch();
614 // Ensure we're not writing using mTable...
615 MutexAutoLock lock(mTableLock);
617 mWrittenOnce = false;
618 if (memoryOnly) {
619 // This should only be called in tests.
620 auto writeResult = WriteToDisk();
621 if (NS_WARN_IF(writeResult.isErr())) {
622 gIgnoreDiskCache = true;
623 return;
626 if (mCurTableReferenced) {
627 // There should be no way for this assert to fail other than a user manually
628 // sending startupcache-invalidate messages through the Browser Toolbox. If
629 // something knowingly invalidates the cache, the event can be counted with
630 // mAllowedInvalidationsCount.
631 MOZ_DIAGNOSTIC_ASSERT(
632 xpc::IsInAutomation() ||
633 // The allowed invalidations can grow faster than the old tables, so
634 // guard against incorrect unsigned subtraction.
635 mAllowedInvalidationsCount > mOldTables.Length() ||
636 // Now perform the real check.
637 mOldTables.Length() - mAllowedInvalidationsCount < 10,
638 "Startup cache invalidated too many times.");
639 mOldTables.AppendElement(std::move(mTable));
640 mCurTableReferenced = false;
641 } else {
642 mTable.clear();
644 mRequestedCount = 0;
645 if (!memoryOnly) {
646 mCacheData.reset();
647 nsresult rv = mFile->Remove(false);
648 if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
649 gIgnoreDiskCache = true;
650 return;
653 gIgnoreDiskCache = false;
654 auto result = LoadArchive();
655 if (NS_WARN_IF(result.isErr())) {
656 gIgnoreDiskCache = true;
660 void StartupCache::CountAllowedInvalidation() { mAllowedInvalidationsCount++; }
662 void StartupCache::MaybeInitShutdownWrite() {
663 if (mTimer) {
664 mTimer->Cancel();
666 gShutdownInitiated = true;
668 MaybeWriteOffMainThread();
671 void StartupCache::EnsureShutdownWriteComplete() {
672 MutexAutoLock lock(mTableLock);
673 // If we've already written or there's nothing to write,
674 // we don't need to do anything. This is the common case.
675 if (mWrittenOnce || (mCacheData.initialized() && !ShouldCompactCache())) {
676 return;
678 // Otherwise, ensure the write happens. The timer should have been cancelled
679 // already in MaybeInitShutdownWrite.
681 // We got the lock. Keep the following in sync with
682 // MaybeWriteOffMainThread:
683 WaitOnPrefetch();
684 mDirty = true;
685 mCacheData.reset();
686 // Most of this should be redundant given MaybeWriteOffMainThread should
687 // have run before now.
689 auto writeResult = WriteToDisk();
690 Unused << NS_WARN_IF(writeResult.isErr());
691 // We've had the lock, and `WriteToDisk()` sets mWrittenOnce and mDirty
692 // when done, and checks for them when starting, so we don't need to do
693 // anything else.
696 void StartupCache::IgnoreDiskCache() {
697 gIgnoreDiskCache = true;
698 if (gStartupCache) gStartupCache->InvalidateCache();
701 bool StartupCache::GetIgnoreDiskCache() { return gIgnoreDiskCache; }
703 void StartupCache::WaitOnPrefetch() {
704 // This can't be called from within ThreadedPrefetch()
705 MonitorAutoLock lock(mPrefetchComplete);
706 while (mPrefetchInProgress) {
707 mPrefetchComplete.Wait();
711 void StartupCache::ThreadedPrefetch(uint8_t* aStart, size_t aSize) {
712 // Always notify of completion, even if MMAP_FAULT_HANDLER_CATCH()
713 // early-returns.
714 auto notifyPrefetchComplete = MakeScopeExit([&] {
715 MonitorAutoLock lock(mPrefetchComplete);
716 mPrefetchInProgress = false;
717 mPrefetchComplete.NotifyAll();
720 // PrefetchMemory does madvise/equivalent, but doesn't access the memory
721 // pointed to by aStart
722 MMAP_FAULT_HANDLER_BEGIN_BUFFER(aStart, aSize)
723 PrefetchMemory(aStart, aSize);
724 MMAP_FAULT_HANDLER_CATCH()
727 // mTableLock must be held
728 bool StartupCache::ShouldCompactCache() {
729 // If we've requested less than 4/5 of the startup cache, then we should
730 // probably compact it down. This can happen quite easily after the first run,
731 // which seems to request quite a few more things than subsequent runs.
732 CheckedInt<uint32_t> threshold = CheckedInt<uint32_t>(mTable.count()) * 4 / 5;
733 MOZ_RELEASE_ASSERT(threshold.isValid(), "Runaway StartupCache size");
734 return mRequestedCount < threshold.value();
738 * The write-thread is spawned on a timeout(which is reset with every write).
739 * This can avoid a slow shutdown.
741 void StartupCache::WriteTimeout(nsITimer* aTimer, void* aClosure) {
743 * It is safe to use the pointer passed in aClosure to reference the
744 * StartupCache object because the timer's lifetime is tightly coupled to
745 * the lifetime of the StartupCache object; this timer is canceled in the
746 * StartupCache destructor, guaranteeing that this function runs if and only
747 * if the StartupCache object is valid.
749 StartupCache* startupCacheObj = static_cast<StartupCache*>(aClosure);
750 startupCacheObj->MaybeWriteOffMainThread();
754 * See StartupCache::WriteTimeout above - this is just the non-static body.
756 void StartupCache::MaybeWriteOffMainThread() {
758 MutexAutoLock lock(mTableLock);
759 if (mWrittenOnce || (mCacheData.initialized() && !ShouldCompactCache())) {
760 return;
763 // Keep this code in sync with EnsureShutdownWriteComplete.
764 WaitOnPrefetch();
766 MutexAutoLock lock(mTableLock);
767 mDirty = true;
768 mCacheData.reset();
771 RefPtr<StartupCache> self = this;
772 nsCOMPtr<nsIRunnable> runnable =
773 NS_NewRunnableFunction("StartupCache::Write", [self]() mutable {
774 MutexAutoLock lock(self->mTableLock);
775 auto result = self->WriteToDisk();
776 Unused << NS_WARN_IF(result.isErr());
778 NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
781 // We don't want to refcount StartupCache, so we'll just
782 // hold a ref to this and pass it to observerService instead.
783 NS_IMPL_ISUPPORTS(StartupCacheListener, nsIObserver)
785 nsresult StartupCacheListener::Observe(nsISupports* subject, const char* topic,
786 const char16_t* data) {
787 StartupCache* sc = StartupCache::GetSingleton();
788 if (!sc) return NS_OK;
790 if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
791 // Do not leave the thread running past xpcom shutdown
792 sc->WaitOnPrefetch();
793 StartupCache::gShutdownInitiated = true;
794 // Note that we don't do anything special for the background write
795 // task; we expect the threadpool to finish running any tasks already
796 // posted to it prior to shutdown. FastShutdown will call
797 // EnsureShutdownWriteComplete() to ensure any pending writes happen
798 // in that case.
799 } else if (strcmp(topic, "startupcache-invalidate") == 0) {
800 sc->InvalidateCache(data && nsCRT::strcmp(data, u"memoryOnly") == 0);
801 } else if (strcmp(topic, "intl:app-locales-changed") == 0) {
802 // Live language switching invalidates the startup cache due to the history
803 // sidebar retaining localized strings in its internal SQL query. This
804 // should be a relatively rare event, but a user could do it an arbitrary
805 // number of times.
806 sc->CountAllowedInvalidation();
808 return NS_OK;
811 nsresult StartupCache::GetDebugObjectOutputStream(
812 nsIObjectOutputStream* aStream, nsIObjectOutputStream** aOutStream) {
813 NS_ENSURE_ARG_POINTER(aStream);
814 #ifdef DEBUG
815 auto* stream = new StartupCacheDebugOutputStream(aStream, &mWriteObjectMap);
816 NS_ADDREF(*aOutStream = stream);
817 #else
818 NS_ADDREF(*aOutStream = aStream);
819 #endif
821 return NS_OK;
824 nsresult StartupCache::ResetStartupWriteTimerCheckingReadCount() {
825 nsresult rv = NS_OK;
826 if (!mTimer)
827 mTimer = NS_NewTimer();
828 else
829 rv = mTimer->Cancel();
830 NS_ENSURE_SUCCESS(rv, rv);
831 // Wait for the specified timeout, then write out the cache.
832 mTimer->InitWithNamedFuncCallback(
833 StartupCache::WriteTimeout, this, STARTUP_CACHE_WRITE_TIMEOUT * 1000,
834 nsITimer::TYPE_ONE_SHOT, "StartupCache::WriteTimeout");
835 return NS_OK;
838 // For test code only
839 nsresult StartupCache::ResetStartupWriteTimerAndLock() {
840 MutexAutoLock lock(mTableLock);
841 return ResetStartupWriteTimer();
844 nsresult StartupCache::ResetStartupWriteTimer() {
845 mDirty = true;
846 nsresult rv = NS_OK;
847 if (!mTimer)
848 mTimer = NS_NewTimer();
849 else
850 rv = mTimer->Cancel();
851 NS_ENSURE_SUCCESS(rv, rv);
852 // Wait for the specified timeout, then write out the cache.
853 mTimer->InitWithNamedFuncCallback(
854 StartupCache::WriteTimeout, this, STARTUP_CACHE_WRITE_TIMEOUT * 1000,
855 nsITimer::TYPE_ONE_SHOT, "StartupCache::WriteTimeout");
856 return NS_OK;
859 // Used only in tests:
860 bool StartupCache::StartupWriteComplete() {
861 // Need to have written to disk and not added new things since;
862 MutexAutoLock lock(mTableLock);
863 return !mDirty && mWrittenOnce;
866 // StartupCacheDebugOutputStream implementation
867 #ifdef DEBUG
868 NS_IMPL_ISUPPORTS(StartupCacheDebugOutputStream, nsIObjectOutputStream,
869 nsIBinaryOutputStream, nsIOutputStream)
871 bool StartupCacheDebugOutputStream::CheckReferences(nsISupports* aObject) {
872 nsresult rv;
874 nsCOMPtr<nsIClassInfo> classInfo = do_QueryInterface(aObject);
875 if (!classInfo) {
876 NS_ERROR("aObject must implement nsIClassInfo");
877 return false;
880 uint32_t flags;
881 rv = classInfo->GetFlags(&flags);
882 NS_ENSURE_SUCCESS(rv, false);
883 if (flags & nsIClassInfo::SINGLETON) return true;
885 bool inserted = mObjectMap->EnsureInserted(aObject);
886 if (!inserted) {
887 NS_ERROR(
888 "non-singleton aObject is referenced multiple times in this"
889 "serialization, we don't support that.");
892 return inserted;
895 // nsIObjectOutputStream implementation
896 nsresult StartupCacheDebugOutputStream::WriteObject(nsISupports* aObject,
897 bool aIsStrongRef) {
898 nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
900 NS_ASSERTION(rootObject.get() == aObject,
901 "bad call to WriteObject -- call WriteCompoundObject!");
902 bool check = CheckReferences(aObject);
903 NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
904 return mBinaryStream->WriteObject(aObject, aIsStrongRef);
907 nsresult StartupCacheDebugOutputStream::WriteSingleRefObject(
908 nsISupports* aObject) {
909 nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
911 NS_ASSERTION(rootObject.get() == aObject,
912 "bad call to WriteSingleRefObject -- call WriteCompoundObject!");
913 bool check = CheckReferences(aObject);
914 NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
915 return mBinaryStream->WriteSingleRefObject(aObject);
918 nsresult StartupCacheDebugOutputStream::WriteCompoundObject(
919 nsISupports* aObject, const nsIID& aIID, bool aIsStrongRef) {
920 nsCOMPtr<nsISupports> rootObject(do_QueryInterface(aObject));
922 nsCOMPtr<nsISupports> roundtrip;
923 rootObject->QueryInterface(aIID, getter_AddRefs(roundtrip));
924 NS_ASSERTION(roundtrip.get() == aObject,
925 "bad aggregation or multiple inheritance detected by call to "
926 "WriteCompoundObject!");
928 bool check = CheckReferences(aObject);
929 NS_ENSURE_TRUE(check, NS_ERROR_FAILURE);
930 return mBinaryStream->WriteCompoundObject(aObject, aIID, aIsStrongRef);
933 nsresult StartupCacheDebugOutputStream::WriteID(nsID const& aID) {
934 return mBinaryStream->WriteID(aID);
937 char* StartupCacheDebugOutputStream::GetBuffer(uint32_t aLength,
938 uint32_t aAlignMask) {
939 return mBinaryStream->GetBuffer(aLength, aAlignMask);
942 void StartupCacheDebugOutputStream::PutBuffer(char* aBuffer, uint32_t aLength) {
943 mBinaryStream->PutBuffer(aBuffer, aLength);
945 #endif // DEBUG
947 } // namespace scache
948 } // namespace mozilla