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/. */
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"
23 #include "nsDirectoryServiceUtils.h"
24 #include "nsIClassInfo.h"
26 #include "nsIObserver.h"
27 #include "nsIOutputStream.h"
28 #include "nsISupports.h"
30 #include "mozilla/Omnijar.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"
48 # define SC_ENDIAN "big"
50 # define SC_ENDIAN "little"
53 #if PR_BYTES_PER_WORD == 4
54 # define SC_WORDSIZE "4"
56 # define SC_WORDSIZE "8"
59 using namespace mozilla::Compression
;
64 MOZ_DEFINE_MALLOC_SIZE_OF(StartupCacheMallocSizeOf
)
67 StartupCache::CollectReports(nsIHandleReportCallback
* aHandleReport
,
68 nsISupports
* aData
, bool aAnonymize
) {
69 MutexAutoLock
lock(mTableLock
);
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 "
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
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
,
101 if (PR_Write(fd
, data
, len
) != len
) {
102 return Err(NS_ERROR_FAILURE
);
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
);
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()) {
129 if (!gStartupCache
) {
130 if (!XRE_IsParentProcess()) {
133 #ifdef MOZ_DISABLE_STARTUPCACHE
136 StartupCache::InitSingleton();
140 return StartupCache::gStartupCache
;
143 void StartupCache::DeleteSingleton() { StartupCache::gStartupCache
= nullptr; }
145 nsresult
StartupCache::InitSingleton() {
147 StartupCache::gStartupCache
= new StartupCache();
149 rv
= StartupCache::gStartupCache
->Init();
151 StartupCache::gStartupCache
= nullptr;
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"),
167 mCurTableReferenced(false),
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"));
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
187 char* env
= PR_GetEnv("MOZ_STARTUP_CACHE");
189 rv
= NS_NewLocalFile(NS_ConvertUTF8toUTF16(env
), false,
190 getter_AddRefs(mFile
));
192 nsCOMPtr
<nsIFile
> file
;
193 rv
= NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(file
));
195 // return silently, this will fail in mochitests's xpcshell process.
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
);
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
,
225 NS_ENSURE_SUCCESS(rv
, rv
);
226 rv
= mObserverService
->AddObserver(mListener
, "startupcache-invalidate",
228 NS_ENSURE_SUCCESS(rv
, rv
);
229 rv
= mObserverService
->AddObserver(mListener
, "intl:app-locales-changed",
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!");
248 RegisterWeakMemoryReporter(this);
249 mDecompressionContext
= MakeUnique
<LZ4FrameDecompressionContext
>(true);
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 MOZ_TRY(mCacheData
.init(mFile
));
272 auto size
= mCacheData
.size();
273 if (CanPrefetchMemory()) {
274 StartPrefetchMemory();
278 if (size
< sizeof(MAGIC
) + sizeof(headerSize
)) {
279 return Err(NS_ERROR_UNEXPECTED
);
282 auto data
= mCacheData
.get
<uint8_t>();
283 auto end
= data
+ size
;
285 MMAP_FAULT_HANDLER_BEGIN_BUFFER(data
.get(), size
)
287 if (memcmp(MAGIC
, data
.get(), sizeof(MAGIC
))) {
288 return Err(NS_ERROR_UNEXPECTED
);
290 data
+= sizeof(MAGIC
);
292 headerSize
= LittleEndian::readUint32(data
.get());
293 data
+= sizeof(headerSize
);
295 if (headerSize
> end
- data
) {
296 MOZ_ASSERT(false, "StartupCache file is corrupt.");
297 return Err(NS_ERROR_UNEXPECTED
);
300 Range
<uint8_t> header(data
, data
+ headerSize
);
303 mCacheEntriesBaseOffset
= sizeof(MAGIC
) + sizeof(headerSize
) + headerSize
;
305 if (!mTable
.reserve(STARTUP_CACHE_RESERVE_CAPACITY
)) {
306 return Err(NS_ERROR_UNEXPECTED
);
308 auto cleanup
= MakeScopeExit([&]() {
309 mTableLock
.AssertCurrentThreadOwns();
314 loader::InputBuffer
buf(header
);
316 uint32_t currentOffset
= 0;
317 while (!buf
.finished()) {
319 uint32_t compressedSize
= 0;
320 uint32_t uncompressedSize
= 0;
322 buf
.codeUint32(offset
);
323 buf
.codeUint32(compressedSize
);
324 buf
.codeUint32(uncompressedSize
);
327 if (offset
+ compressedSize
> end
- data
) {
328 MOZ_ASSERT(false, "StartupCache file is corrupt.");
329 return Err(NS_ERROR_UNEXPECTED
);
332 // Make sure offsets match what we'd expect based on script ordering and
333 // size, as a basic sanity check.
334 if (offset
!= currentOffset
) {
335 return Err(NS_ERROR_UNEXPECTED
);
337 currentOffset
+= compressedSize
;
339 // We could use mTable.putNew if we knew the file we're loading weren't
340 // corrupt. However, we don't know that, so check if the key already
341 // exists. If it does, we know the file must be corrupt.
342 decltype(mTable
)::AddPtr p
= mTable
.lookupForAdd(key
);
344 return Err(NS_ERROR_UNEXPECTED
);
349 StartupCacheEntry(offset
, compressedSize
, uncompressedSize
))) {
350 return Err(NS_ERROR_UNEXPECTED
);
355 return Err(NS_ERROR_UNEXPECTED
);
361 MMAP_FAULT_HANDLER_CATCH(Err(NS_ERROR_UNEXPECTED
))
366 bool StartupCache::HasEntry(const char* id
) {
367 AUTO_PROFILER_LABEL("StartupCache::HasEntry", OTHER
);
369 MOZ_ASSERT(NS_IsMainThread(), "Startup cache only available on main thread");
371 MutexAutoLock
lock(mTableLock
);
372 return mTable
.has(nsDependentCString(id
));
375 nsresult
StartupCache::GetBuffer(const char* id
, const char** outbuf
,
377 MOZ_NO_THREAD_SAFETY_ANALYSIS
{
378 AUTO_PROFILER_LABEL("StartupCache::GetBuffer", OTHER
);
380 NS_ASSERTION(NS_IsMainThread(),
381 "Startup cache only available on main thread");
383 Telemetry::LABELS_STARTUP_CACHE_REQUESTS label
=
384 Telemetry::LABELS_STARTUP_CACHE_REQUESTS::Miss
;
386 MakeScopeExit([&label
] { Telemetry::AccumulateCategorical(label
); });
388 MutexAutoLock
lock(mTableLock
);
389 decltype(mTable
)::Ptr p
= mTable
.lookup(nsDependentCString(id
));
391 return NS_ERROR_NOT_AVAILABLE
;
394 auto& value
= p
->value();
396 label
= Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitMemory
;
398 if (!mCacheData
.initialized()) {
399 return NS_ERROR_NOT_AVAILABLE
;
401 // It is impossible for a write to be pending here. This is because
402 // we just checked mCacheData.initialized(), and this is reset before
403 // writing to the cache. It's not re-initialized unless we call
404 // LoadArchive(), either from Init() (which must have already happened) or
405 // InvalidateCache(). InvalidateCache() locks the mutex, so a write can't be
407 // Also, WriteToDisk() requires mTableLock, so while it's writing we can't
410 size_t totalRead
= 0;
411 size_t totalWritten
= 0;
412 Span
<const char> compressed
= Span(
413 mCacheData
.get
<char>().get() + mCacheEntriesBaseOffset
+ value
.mOffset
,
414 value
.mCompressedSize
);
415 value
.mData
= UniqueFreePtr
<char[]>(reinterpret_cast<char*>(
416 malloc(sizeof(char) * value
.mUncompressedSize
)));
417 Span
<char> uncompressed
= Span(value
.mData
.get(), value
.mUncompressedSize
);
418 MMAP_FAULT_HANDLER_BEGIN_BUFFER(uncompressed
.Elements(),
419 uncompressed
.Length())
420 bool finished
= false;
422 auto result
= mDecompressionContext
->Decompress(
423 uncompressed
.From(totalWritten
), compressed
.From(totalRead
));
424 if (NS_WARN_IF(result
.isErr())) {
425 value
.mData
= nullptr;
426 MutexAutoUnlock
unlock(mTableLock
);
428 return NS_ERROR_FAILURE
;
430 auto decompressionResult
= result
.unwrap();
431 totalRead
+= decompressionResult
.mSizeRead
;
432 totalWritten
+= decompressionResult
.mSizeWritten
;
433 finished
= decompressionResult
.mFinished
;
436 MMAP_FAULT_HANDLER_CATCH(NS_ERROR_FAILURE
)
438 label
= Telemetry::LABELS_STARTUP_CACHE_REQUESTS::HitDisk
;
441 if (!value
.mRequested
) {
442 value
.mRequested
= true;
443 value
.mRequestedOrder
= ++mRequestedCount
;
444 MOZ_ASSERT(mRequestedCount
<= mTable
.count(),
445 "Somehow we requested more StartupCache items than exist.");
446 ResetStartupWriteTimerCheckingReadCount();
449 // Track that something holds a reference into mTable, so we know to hold
450 // onto it in case the cache is invalidated.
451 mCurTableReferenced
= true;
452 *outbuf
= value
.mData
.get();
453 *length
= value
.mUncompressedSize
;
457 // Makes a copy of the buffer, client retains ownership of inbuf.
458 nsresult
StartupCache::PutBuffer(const char* id
, UniqueFreePtr
<char[]>&& inbuf
,
459 uint32_t len
) MOZ_NO_THREAD_SAFETY_ANALYSIS
{
460 NS_ASSERTION(NS_IsMainThread(),
461 "Startup cache only available on main thread");
462 if (StartupCache::gShutdownInitiated
) {
463 return NS_ERROR_NOT_AVAILABLE
;
466 // Try to gain the table write lock. If the background task to write the
467 // cache is running, this will fail.
468 MutexAutoTryLock
lock(mTableLock
);
470 return NS_ERROR_NOT_AVAILABLE
;
472 mTableLock
.AssertCurrentThreadOwns();
473 bool exists
= mTable
.has(nsDependentCString(id
));
475 NS_WARNING("Existing entry in StartupCache.");
476 // Double-caching is undesirable but not an error.
480 // putNew returns false on alloc failure - in the very unlikely event we hit
481 // that and aren't going to crash elsewhere, there's no reason we need to
483 if (mTable
.putNew(nsCString(id
), StartupCacheEntry(std::move(inbuf
), len
,
484 ++mRequestedCount
))) {
485 return ResetStartupWriteTimer();
487 MOZ_DIAGNOSTIC_ASSERT(mTable
.count() < STARTUP_CACHE_MAX_CAPACITY
,
488 "Too many StartupCache entries.");
492 size_t StartupCache::HeapSizeOfIncludingThis(
493 mozilla::MallocSizeOf aMallocSizeOf
) const {
494 // This function could measure more members, but they haven't been found by
495 // DMD to be significant. They can be added later if necessary.
497 size_t n
= aMallocSizeOf(this);
499 n
+= mTable
.shallowSizeOfExcludingThis(aMallocSizeOf
);
500 for (auto iter
= mTable
.iter(); !iter
.done(); iter
.next()) {
501 if (iter
.get().value().mData
) {
502 n
+= aMallocSizeOf(iter
.get().value().mData
.get());
504 n
+= iter
.get().key().SizeOfExcludingThisIfUnshared(aMallocSizeOf
);
511 * WriteToDisk writes the cache out to disk. Callers of WriteToDisk need to call
512 * WaitOnWriteComplete to make sure there isn't a write
513 * happening on another thread.
514 * We own the mTableLock here.
516 Result
<Ok
, nsresult
> StartupCache::WriteToDisk() {
517 if (!mDirty
|| mWrittenOnce
) {
522 return Err(NS_ERROR_UNEXPECTED
);
526 MOZ_TRY(mFile
->OpenNSPRFileDesc(PR_WRONLY
| PR_CREATE_FILE
| PR_TRUNCATE
,
529 nsTArray
<StartupCacheEntry::KeyValuePair
> entries(mTable
.count());
530 for (auto iter
= mTable
.iter(); !iter
.done(); iter
.next()) {
531 if (iter
.get().value().mRequested
) {
532 StartupCacheEntry::KeyValuePair
kv(&iter
.get().key(),
533 &iter
.get().value());
534 entries
.AppendElement(kv
);
538 if (entries
.IsEmpty()) {
542 entries
.Sort(StartupCacheEntry::Comparator());
543 loader::OutputBuffer buf
;
544 for (auto& e
: entries
) {
546 auto* value
= e
.second
;
547 auto uncompressedSize
= value
->mUncompressedSize
;
548 // Set the mHeaderOffsetInFile so we can go back and edit the offset.
549 value
->mHeaderOffsetInFile
= buf
.cursor();
550 // Write a 0 offset/compressed size as a placeholder until we get the real
551 // offset after compressing.
554 buf
.codeUint32(uncompressedSize
);
555 buf
.codeString(*key
);
558 uint8_t headerSize
[4];
559 LittleEndian::writeUint32(headerSize
, buf
.cursor());
561 MOZ_TRY(Write(fd
, MAGIC
, sizeof(MAGIC
)));
562 MOZ_TRY(Write(fd
, headerSize
, sizeof(headerSize
)));
563 size_t headerStart
= sizeof(MAGIC
) + sizeof(headerSize
);
564 size_t dataStart
= headerStart
+ buf
.cursor();
565 MOZ_TRY(Seek(fd
, dataStart
));
569 const size_t chunkSize
= 1024 * 16;
570 LZ4FrameCompressionContext
ctx(6, /* aCompressionLevel */
571 chunkSize
, /* aReadBufLen */
572 true, /* aChecksum */
573 true); /* aStableSrc */
574 size_t writeBufLen
= ctx
.GetRequiredWriteBufferLength();
575 auto writeBuffer
= MakeUnique
<char[]>(writeBufLen
);
576 auto writeSpan
= Span(writeBuffer
.get(), writeBufLen
);
578 for (auto& e
: entries
) {
579 auto value
= e
.second
;
580 value
->mOffset
= offset
;
581 Span
<const char> result
;
583 ctx
.BeginCompressing(writeSpan
).mapErr(MapLZ4ErrorToNsresult
));
584 MOZ_TRY(Write(fd
, result
.Elements(), result
.Length()));
585 offset
+= result
.Length();
587 for (size_t i
= 0; i
< value
->mUncompressedSize
; i
+= chunkSize
) {
588 size_t size
= std::min(chunkSize
, value
->mUncompressedSize
- i
);
589 char* uncompressed
= value
->mData
.get() + i
;
590 MOZ_TRY_VAR(result
, ctx
.ContinueCompressing(Span(uncompressed
, size
))
591 .mapErr(MapLZ4ErrorToNsresult
));
592 MOZ_TRY(Write(fd
, result
.Elements(), result
.Length()));
593 offset
+= result
.Length();
596 MOZ_TRY_VAR(result
, ctx
.EndCompressing().mapErr(MapLZ4ErrorToNsresult
));
597 MOZ_TRY(Write(fd
, result
.Elements(), result
.Length()));
598 offset
+= result
.Length();
599 value
->mCompressedSize
= offset
- value
->mOffset
;
600 MOZ_TRY(Seek(fd
, dataStart
+ offset
));
603 for (auto& e
: entries
) {
604 auto value
= e
.second
;
605 uint8_t* headerEntry
= buf
.Get() + value
->mHeaderOffsetInFile
;
606 LittleEndian::writeUint32(headerEntry
, value
->mOffset
);
607 LittleEndian::writeUint32(headerEntry
+ sizeof(value
->mOffset
),
608 value
->mCompressedSize
);
610 MOZ_TRY(Seek(fd
, headerStart
));
611 MOZ_TRY(Write(fd
, buf
.Get(), buf
.cursor()));
619 void StartupCache::InvalidateCache(bool memoryOnly
) {
621 // Ensure we're not writing using mTable...
622 MutexAutoLock
lock(mTableLock
);
624 mWrittenOnce
= false;
626 // This should only be called in tests.
627 auto writeResult
= WriteToDisk();
628 if (NS_WARN_IF(writeResult
.isErr())) {
629 gIgnoreDiskCache
= true;
633 if (mCurTableReferenced
) {
634 // There should be no way for this assert to fail other than a user manually
635 // sending startupcache-invalidate messages through the Browser Toolbox. If
636 // something knowingly invalidates the cache, the event can be counted with
637 // mAllowedInvalidationsCount.
638 MOZ_DIAGNOSTIC_ASSERT(
639 xpc::IsInAutomation() ||
640 // The allowed invalidations can grow faster than the old tables, so
641 // guard against incorrect unsigned subtraction.
642 mAllowedInvalidationsCount
> mOldTables
.Length() ||
643 // Now perform the real check.
644 mOldTables
.Length() - mAllowedInvalidationsCount
< 10,
645 "Startup cache invalidated too many times.");
646 mOldTables
.AppendElement(std::move(mTable
));
647 mCurTableReferenced
= false;
654 nsresult rv
= mFile
->Remove(false);
655 if (NS_FAILED(rv
) && rv
!= NS_ERROR_FILE_NOT_FOUND
) {
656 gIgnoreDiskCache
= true;
660 gIgnoreDiskCache
= false;
661 auto result
= LoadArchive();
662 if (NS_WARN_IF(result
.isErr())) {
663 gIgnoreDiskCache
= true;
667 void StartupCache::CountAllowedInvalidation() { mAllowedInvalidationsCount
++; }
669 void StartupCache::MaybeInitShutdownWrite() {
673 gShutdownInitiated
= true;
675 MaybeWriteOffMainThread();
678 void StartupCache::EnsureShutdownWriteComplete() {
679 MutexAutoLock
lock(mTableLock
);
680 // If we've already written or there's nothing to write,
681 // we don't need to do anything. This is the common case.
682 if (mWrittenOnce
|| (mCacheData
.initialized() && !ShouldCompactCache())) {
685 // Otherwise, ensure the write happens. The timer should have been cancelled
686 // already in MaybeInitShutdownWrite.
688 // We got the lock. Keep the following in sync with
689 // MaybeWriteOffMainThread:
693 // Most of this should be redundant given MaybeWriteOffMainThread should
694 // have run before now.
696 auto writeResult
= WriteToDisk();
697 Unused
<< NS_WARN_IF(writeResult
.isErr());
698 // We've had the lock, and `WriteToDisk()` sets mWrittenOnce and mDirty
699 // when done, and checks for them when starting, so we don't need to do
703 void StartupCache::IgnoreDiskCache() {
704 gIgnoreDiskCache
= true;
705 if (gStartupCache
) gStartupCache
->InvalidateCache();
708 bool StartupCache::GetIgnoreDiskCache() { return gIgnoreDiskCache
; }
710 void StartupCache::WaitOnPrefetch() {
711 // This can't be called from within ThreadedPrefetch()
712 MonitorAutoLock
lock(mPrefetchComplete
);
713 while (mPrefetchInProgress
) {
714 mPrefetchComplete
.Wait();
718 void StartupCache::ThreadedPrefetch(uint8_t* aStart
, size_t aSize
) {
719 // Always notify of completion, even if MMAP_FAULT_HANDLER_CATCH()
721 auto notifyPrefetchComplete
= MakeScopeExit([&] {
722 MonitorAutoLock
lock(mPrefetchComplete
);
723 mPrefetchInProgress
= false;
724 mPrefetchComplete
.NotifyAll();
727 // PrefetchMemory does madvise/equivalent, but doesn't access the memory
728 // pointed to by aStart
729 MMAP_FAULT_HANDLER_BEGIN_BUFFER(aStart
, aSize
)
730 PrefetchMemory(aStart
, aSize
);
731 MMAP_FAULT_HANDLER_CATCH()
734 // mTableLock must be held
735 bool StartupCache::ShouldCompactCache() {
736 // If we've requested less than 4/5 of the startup cache, then we should
737 // probably compact it down. This can happen quite easily after the first run,
738 // which seems to request quite a few more things than subsequent runs.
739 CheckedInt
<uint32_t> threshold
= CheckedInt
<uint32_t>(mTable
.count()) * 4 / 5;
740 MOZ_RELEASE_ASSERT(threshold
.isValid(), "Runaway StartupCache size");
741 return mRequestedCount
< threshold
.value();
745 * The write-thread is spawned on a timeout(which is reset with every write).
746 * This can avoid a slow shutdown.
748 void StartupCache::WriteTimeout(nsITimer
* aTimer
, void* aClosure
) {
750 * It is safe to use the pointer passed in aClosure to reference the
751 * StartupCache object because the timer's lifetime is tightly coupled to
752 * the lifetime of the StartupCache object; this timer is canceled in the
753 * StartupCache destructor, guaranteeing that this function runs if and only
754 * if the StartupCache object is valid.
756 StartupCache
* startupCacheObj
= static_cast<StartupCache
*>(aClosure
);
757 startupCacheObj
->MaybeWriteOffMainThread();
761 * See StartupCache::WriteTimeout above - this is just the non-static body.
763 void StartupCache::MaybeWriteOffMainThread() {
765 MutexAutoLock
lock(mTableLock
);
766 if (mWrittenOnce
|| (mCacheData
.initialized() && !ShouldCompactCache())) {
770 // Keep this code in sync with EnsureShutdownWriteComplete.
773 MutexAutoLock
lock(mTableLock
);
778 RefPtr
<StartupCache
> self
= this;
779 nsCOMPtr
<nsIRunnable
> runnable
=
780 NS_NewRunnableFunction("StartupCache::Write", [self
]() mutable {
781 MutexAutoLock
lock(self
->mTableLock
);
782 auto result
= self
->WriteToDisk();
783 Unused
<< NS_WARN_IF(result
.isErr());
785 NS_DispatchBackgroundTask(runnable
.forget(), NS_DISPATCH_EVENT_MAY_BLOCK
);
788 // We don't want to refcount StartupCache, so we'll just
789 // hold a ref to this and pass it to observerService instead.
790 NS_IMPL_ISUPPORTS(StartupCacheListener
, nsIObserver
)
792 nsresult
StartupCacheListener::Observe(nsISupports
* subject
, const char* topic
,
793 const char16_t
* data
) {
794 StartupCache
* sc
= StartupCache::GetSingleton();
795 if (!sc
) return NS_OK
;
797 if (strcmp(topic
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
) == 0) {
798 // Do not leave the thread running past xpcom shutdown
799 sc
->WaitOnPrefetch();
800 StartupCache::gShutdownInitiated
= true;
801 // Note that we don't do anything special for the background write
802 // task; we expect the threadpool to finish running any tasks already
803 // posted to it prior to shutdown. FastShutdown will call
804 // EnsureShutdownWriteComplete() to ensure any pending writes happen
806 } else if (strcmp(topic
, "startupcache-invalidate") == 0) {
807 sc
->InvalidateCache(data
&& nsCRT::strcmp(data
, u
"memoryOnly") == 0);
808 } else if (strcmp(topic
, "intl:app-locales-changed") == 0) {
809 // Live language switching invalidates the startup cache due to the history
810 // sidebar retaining localized strings in its internal SQL query. This
811 // should be a relatively rare event, but a user could do it an arbitrary
813 sc
->CountAllowedInvalidation();
818 nsresult
StartupCache::GetDebugObjectOutputStream(
819 nsIObjectOutputStream
* aStream
, nsIObjectOutputStream
** aOutStream
) {
820 NS_ENSURE_ARG_POINTER(aStream
);
822 auto* stream
= new StartupCacheDebugOutputStream(aStream
, &mWriteObjectMap
);
823 NS_ADDREF(*aOutStream
= stream
);
825 NS_ADDREF(*aOutStream
= aStream
);
831 nsresult
StartupCache::ResetStartupWriteTimerCheckingReadCount() {
834 mTimer
= NS_NewTimer();
836 rv
= mTimer
->Cancel();
837 NS_ENSURE_SUCCESS(rv
, rv
);
838 // Wait for the specified timeout, then write out the cache.
839 mTimer
->InitWithNamedFuncCallback(
840 StartupCache::WriteTimeout
, this, STARTUP_CACHE_WRITE_TIMEOUT
* 1000,
841 nsITimer::TYPE_ONE_SHOT
, "StartupCache::WriteTimeout");
845 // For test code only
846 nsresult
StartupCache::ResetStartupWriteTimerAndLock() {
847 MutexAutoLock
lock(mTableLock
);
848 return ResetStartupWriteTimer();
851 nsresult
StartupCache::ResetStartupWriteTimer() {
855 mTimer
= NS_NewTimer();
857 rv
= mTimer
->Cancel();
858 NS_ENSURE_SUCCESS(rv
, rv
);
859 // Wait for the specified timeout, then write out the cache.
860 mTimer
->InitWithNamedFuncCallback(
861 StartupCache::WriteTimeout
, this, STARTUP_CACHE_WRITE_TIMEOUT
* 1000,
862 nsITimer::TYPE_ONE_SHOT
, "StartupCache::WriteTimeout");
866 // Used only in tests:
867 bool StartupCache::StartupWriteComplete() {
868 // Need to have written to disk and not added new things since;
869 MutexAutoLock
lock(mTableLock
);
870 return !mDirty
&& mWrittenOnce
;
873 // StartupCacheDebugOutputStream implementation
875 NS_IMPL_ISUPPORTS(StartupCacheDebugOutputStream
, nsIObjectOutputStream
,
876 nsIBinaryOutputStream
, nsIOutputStream
)
878 bool StartupCacheDebugOutputStream::CheckReferences(nsISupports
* aObject
) {
881 nsCOMPtr
<nsIClassInfo
> classInfo
= do_QueryInterface(aObject
);
883 NS_ERROR("aObject must implement nsIClassInfo");
888 rv
= classInfo
->GetFlags(&flags
);
889 NS_ENSURE_SUCCESS(rv
, false);
890 if (flags
& nsIClassInfo::SINGLETON
) return true;
892 bool inserted
= mObjectMap
->EnsureInserted(aObject
);
895 "non-singleton aObject is referenced multiple times in this"
896 "serialization, we don't support that.");
902 // nsIObjectOutputStream implementation
903 nsresult
StartupCacheDebugOutputStream::WriteObject(nsISupports
* aObject
,
905 nsCOMPtr
<nsISupports
> rootObject(do_QueryInterface(aObject
));
907 NS_ASSERTION(rootObject
.get() == aObject
,
908 "bad call to WriteObject -- call WriteCompoundObject!");
909 bool check
= CheckReferences(aObject
);
910 NS_ENSURE_TRUE(check
, NS_ERROR_FAILURE
);
911 return mBinaryStream
->WriteObject(aObject
, aIsStrongRef
);
914 nsresult
StartupCacheDebugOutputStream::WriteSingleRefObject(
915 nsISupports
* aObject
) {
916 nsCOMPtr
<nsISupports
> rootObject(do_QueryInterface(aObject
));
918 NS_ASSERTION(rootObject
.get() == aObject
,
919 "bad call to WriteSingleRefObject -- call WriteCompoundObject!");
920 bool check
= CheckReferences(aObject
);
921 NS_ENSURE_TRUE(check
, NS_ERROR_FAILURE
);
922 return mBinaryStream
->WriteSingleRefObject(aObject
);
925 nsresult
StartupCacheDebugOutputStream::WriteCompoundObject(
926 nsISupports
* aObject
, const nsIID
& aIID
, bool aIsStrongRef
) {
927 nsCOMPtr
<nsISupports
> rootObject(do_QueryInterface(aObject
));
929 nsCOMPtr
<nsISupports
> roundtrip
;
930 rootObject
->QueryInterface(aIID
, getter_AddRefs(roundtrip
));
931 NS_ASSERTION(roundtrip
.get() == aObject
,
932 "bad aggregation or multiple inheritance detected by call to "
933 "WriteCompoundObject!");
935 bool check
= CheckReferences(aObject
);
936 NS_ENSURE_TRUE(check
, NS_ERROR_FAILURE
);
937 return mBinaryStream
->WriteCompoundObject(aObject
, aIID
, aIsStrongRef
);
940 nsresult
StartupCacheDebugOutputStream::WriteID(nsID
const& aID
) {
941 return mBinaryStream
->WriteID(aID
);
944 char* StartupCacheDebugOutputStream::GetBuffer(uint32_t aLength
,
945 uint32_t aAlignMask
) {
946 return mBinaryStream
->GetBuffer(aLength
, aAlignMask
);
949 void StartupCacheDebugOutputStream::PutBuffer(char* aBuffer
, uint32_t aLength
) {
950 mBinaryStream
->PutBuffer(aBuffer
, aLength
);
954 } // namespace scache
955 } // namespace mozilla