/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef ScriptPreloader_h #define ScriptPreloader_h #include "mozilla/Atomics.h" #include "mozilla/CheckedInt.h" #include "mozilla/EnumSet.h" #include "mozilla/LinkedList.h" #include "mozilla/MemoryReporting.h" #include "mozilla/Maybe.h" #include "mozilla/MaybeOneOf.h" #include "mozilla/Monitor.h" #include "mozilla/Range.h" #include "mozilla/Vector.h" #include "mozilla/Result.h" #include "mozilla/loader/AutoMemMap.h" #include "nsClassHashtable.h" #include "nsIAsyncShutdown.h" #include "nsIFile.h" #include "nsIMemoryReporter.h" #include "nsIObserver.h" #include "nsIThread.h" #include "nsITimer.h" #include "js/GCAnnotations.h" // for JS_HAZ_NON_GC_POINTER #include "js/RootingAPI.h" // for Handle, Heap #include "js/Transcoding.h" // for TranscodeBuffer, TranscodeRange, TranscodeSources #include "js/TypeDecls.h" // for HandleObject, HandleScript #include namespace JS { class CompileOptions; class OffThreadToken; } // namespace JS namespace mozilla { namespace dom { class ContentParent; } namespace ipc { class FileDescriptor; } namespace loader { class InputBuffer; class ScriptCacheChild; enum class ProcessType : uint8_t { Uninitialized, Parent, Web, Extension, PrivilegedAbout, }; template struct Matcher { virtual bool Matches(T) = 0; }; } // namespace loader using namespace mozilla::loader; class ScriptPreloader : public nsIObserver, public nsIMemoryReporter, public nsIRunnable, public nsIAsyncShutdownBlocker { MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) friend class mozilla::loader::ScriptCacheChild; public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIOBSERVER NS_DECL_NSIMEMORYREPORTER NS_DECL_NSIRUNNABLE NS_DECL_NSIASYNCSHUTDOWNBLOCKER static ScriptPreloader& GetSingleton(); static ScriptPreloader& GetChildSingleton(); static ProcessType GetChildProcessType(const nsACString& remoteType); // Fill some options that should be consistent across all scripts stored // into preloader cache. static void FillCompileOptionsForCachedScript(JS::CompileOptions& options); // Retrieves the script with the given cache key from the script cache. // Returns null if the script is not cached. JSScript* GetCachedScript(JSContext* cx, const JS::ReadOnlyCompileOptions& options, const nsCString& path); // Notes the execution of a script with the given URL and cache key. // Depending on the stage of startup, the script may be serialized and // stored to the startup script cache. // // If isRunOnce is true, this script is expected to run only once per // process per browser session. A cached instance will not be kept alive // for repeated execution. void NoteScript(const nsCString& url, const nsCString& cachePath, JS::HandleScript script, bool isRunOnce = false); void NoteScript(const nsCString& url, const nsCString& cachePath, ProcessType processType, nsTArray&& xdrData, TimeStamp loadTime); // Initializes the script cache from the startup script cache file. Result InitCache(const nsAString& = u"scriptCache"_ns); Result InitCache(const Maybe& cacheFile, ScriptCacheChild* cacheChild); bool Active() { return mCacheInitialized && !mStartupFinished; } private: Result InitCacheInternal(JS::HandleObject scope = nullptr); JSScript* GetCachedScriptInternal(JSContext* cx, const JS::ReadOnlyCompileOptions& options, const nsCString& path); public: void Trace(JSTracer* trc); static ProcessType CurrentProcessType() { MOZ_ASSERT(sProcessType != ProcessType::Uninitialized); return sProcessType; } static void InitContentChild(dom::ContentParent& parent); protected: virtual ~ScriptPreloader() = default; private: enum class ScriptStatus { Restored, Saved, }; // Represents a cached JS script, either initially read from the script // cache file, to be added to the next session's script cache file, or // both. // // A script which was read from the cache file may be in any of the // following states: // // - Read from the cache, and being compiled off thread. In this case, // mReadyToExecute is false, and mToken is null. // - Off-thread compilation has finished, but the script has not yet been // executed. In this case, mReadyToExecute is true, and mToken has a // non-null value. // - Read from the cache, but too small or needed to immediately to be // compiled off-thread. In this case, mReadyToExecute is true, and both // mToken and mScript are null. // - Fully decoded, and ready to be added to the next session's cache // file. In this case, mReadyToExecute is true, and mScript is non-null. // // A script to be added to the next session's cache file always has a // non-null mScript value. If it was read from the last session's cache // file, it also has a non-empty mXDRRange range, which will be stored in // the next session's cache file. If it was compiled in this session, its // mXDRRange will initially be empty, and its mXDRData buffer will be // populated just before it is written to the cache file. class CachedScript : public LinkedListElement { public: CachedScript(CachedScript&&) = delete; CachedScript(ScriptPreloader& cache, const nsCString& url, const nsCString& cachePath, JSScript* script) : mCache(cache), mURL(url), mCachePath(cachePath), mScript(script), mReadyToExecute(true), mIsRunOnce(false) {} inline CachedScript(ScriptPreloader& cache, InputBuffer& buf); ~CachedScript() = default; ScriptStatus Status() const { return mProcessTypes.isEmpty() ? ScriptStatus::Restored : ScriptStatus::Saved; } // For use with nsTArray::Sort. // // Orders scripts by script load time, so that scripts which are needed // earlier are stored earlier, and scripts needed at approximately the // same time are stored approximately contiguously. struct Comparator { bool Equals(const CachedScript* a, const CachedScript* b) const { return a->mLoadTime == b->mLoadTime; } bool LessThan(const CachedScript* a, const CachedScript* b) const { return a->mLoadTime < b->mLoadTime; } }; struct StatusMatcher final : public Matcher { explicit StatusMatcher(ScriptStatus status) : mStatus(status) {} virtual bool Matches(CachedScript* script) override { return script->Status() == mStatus; } const ScriptStatus mStatus; }; // The purpose of this helper class is to avoid a race between // ScriptPreloader::WriteCache() and the GC on a JSScript*. // The former checks if the actual JSScript* is null on the save thread // while holding mMonitor. Aside from GC tracing, all places that mutate // the JSScript* either hold mMonitor or don't run at the same time as the // save thread. The GC can move the script, which will cause the value to // change, but this will not change whether it is null or not. // // We can't hold mMonitor while tracing, because we can end running the // GC while the current thread already holds mMonitor. Instead, this class // avoids the race by storing a separate field to indicate if the script is // null or not. To enforce this, the mutation by the GC that cannot affect // the nullness of the script is split out from other mutation. class MOZ_HEAP_CLASS ScriptHolder { public: explicit ScriptHolder(JSScript* script) : mScript(script), mHasScript(script) {} ScriptHolder() : mHasScript(false) {} // This should only be called on the main thread (either while holding // the preloader's mMonitor or while the save thread isn't running), or on // the save thread while holding the preloader's mMonitor. explicit operator bool() const { return mHasScript; } // This should only be called on the main thread. JSScript* Get() const { MOZ_ASSERT(NS_IsMainThread()); return mScript; } // This should only be called on the main thread (or from a GC thread // while the main thread is GCing). void Trace(JSTracer* trc); // These should only be called on the main thread, either while holding // the preloader's mMonitor or while the save thread isn't running. void Set(JS::HandleScript jsscript); void Clear(); private: JS::Heap mScript; bool mHasScript; // true iff mScript is non-null. }; void FreeData() { // If the script data isn't mmapped, we need to release both it // and the Range that points to it at the same time. if (!mXDRData.empty()) { mXDRRange.reset(); mXDRData.destroy(); } } void UpdateLoadTime(const TimeStamp& loadTime) { if (mLoadTime.IsNull() || loadTime < mLoadTime) { mLoadTime = loadTime; } } // Checks whether the cached JSScript for this entry will be needed // again and, if not, drops it and returns true. This is the case for // run-once scripts that do not still need to be encoded into the // cache. // // If this method returns false, callers may set mScript to a cached // JSScript instance for this entry. If it returns true, they should // not. bool MaybeDropScript() { if (mIsRunOnce && (HasRange() || !mCache.WillWriteScripts())) { mScript.Clear(); return true; } return false; } // Encodes this script into XDR data, and stores the result in mXDRData. // Returns true on success, false on failure. bool XDREncode(JSContext* cx); // Encodes or decodes this script, in the storage format required by the // script cache file. template void Code(Buffer& buffer) { buffer.codeString(mURL); buffer.codeString(mCachePath); buffer.codeUint32(mOffset); buffer.codeUint32(mSize); buffer.codeUint8(mProcessTypes); } // Returns the XDR data generated for this script during this session. See // mXDRData. JS::TranscodeBuffer& Buffer() { MOZ_ASSERT(HasBuffer()); return mXDRData.ref(); } bool HasBuffer() { return mXDRData.constructed(); } // Returns the read-only XDR data for this script. See mXDRRange. const JS::TranscodeRange& Range() { MOZ_ASSERT(HasRange()); return mXDRRange.ref(); } bool HasRange() { return mXDRRange.isSome(); } nsTArray& Array() { MOZ_ASSERT(HasArray()); return mXDRData.ref>(); } bool HasArray() { return mXDRData.constructed>(); } JSScript* GetJSScript(JSContext* cx, const JS::ReadOnlyCompileOptions& options); size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { auto size = mallocSizeOf(this); if (HasArray()) { size += Array().ShallowSizeOfExcludingThis(mallocSizeOf); } else if (HasBuffer()) { size += Buffer().sizeOfExcludingThis(mallocSizeOf); } else { return size; } // Note: mURL and mCachePath use the same string for scripts loaded // by the message manager. The following statement avoids // double-measuring in that case. size += (mURL.SizeOfExcludingThisIfUnshared(mallocSizeOf) + mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf)); return size; } ScriptPreloader& mCache; // The URL from which this script was initially read and compiled. nsCString mURL; // A unique identifier for this script's filesystem location, used as a // primary cache lookup value. nsCString mCachePath; // The offset of this script in the cache file, from the start of the XDR // data block. uint32_t mOffset = 0; // The size of this script's encoded XDR data. uint32_t mSize = 0; TimeStamp mLoadTime{}; ScriptHolder mScript; // True if this script is ready to be executed. This means that either the // off-thread portion of an off-thread decode has finished, or the script // is too small to be decoded off-thread, and may be immediately decoded // whenever it is first executed. bool mReadyToExecute = false; // True if this script is expected to run once per process. If so, its // JSScript instance will be dropped as soon as the script has // executed and been encoded into the cache. bool mIsRunOnce = false; // The set of processes in which this script has been used. EnumSet mProcessTypes{}; // The set of processes which the script was loaded into during the // last session, as read from the cache file. EnumSet mOriginalProcessTypes{}; // The read-only XDR data for this script, which was either read from an // existing cache file, or generated by encoding a script which was // compiled during this session. Maybe mXDRRange; // XDR data which was generated from a script compiled during this // session, and will be written to the cache file. MaybeOneOf> mXDRData; } JS_HAZ_NON_GC_POINTER; template static Matcher* Match() { static CachedScript::StatusMatcher matcher{status}; return &matcher; } // There's a significant setup cost for each off-thread decode operation, // so scripts are decoded in chunks to minimize the overhead. There's a // careful balancing act in choosing the size of chunks, to minimize the // number of decode operations, while also minimizing the number of buffer // underruns that require the main thread to wait for a script to finish // decoding. // // For the first chunk, we don't have much time between the start of the // decode operation and the time the first script is needed, so that chunk // needs to be fairly small. After the first chunk is finished, we have // some buffered scripts to fall back on, and a lot more breathing room, // so the chunks can be a bit bigger, but still not too big. static constexpr int OFF_THREAD_FIRST_CHUNK_SIZE = 128 * 1024; static constexpr int OFF_THREAD_CHUNK_SIZE = 512 * 1024; // Ideally, we want every chunk to be smaller than the chunk sizes // specified above. However, if we have some number of small scripts // followed by a huge script that would put us over the normal chunk size, // we're better off processing them as a single chunk. // // In order to guarantee that the JS engine will process a chunk // off-thread, it needs to be at least 100K (which is an implementation // detail that can change at any time), so make sure that we always hit at // least that size, with a bit of breathing room to be safe. static constexpr int SMALL_SCRIPT_CHUNK_THRESHOLD = 128 * 1024; // The maximum size of scripts to re-decode on the main thread if off-thread // decoding hasn't finished yet. In practice, we don't hit this very often, // but when we do, re-decoding some smaller scripts on the main thread gives // the background decoding a chance to catch up without blocking the main // thread for quite as long. static constexpr int MAX_MAINTHREAD_DECODE_SIZE = 50 * 1024; ScriptPreloader(); void Cleanup(); void FinishPendingParses(MonitorAutoLock& aMal); void InvalidateCache(); // Opens the cache file for reading. Result OpenCache(); // Writes a new cache file to disk. Must not be called on the main thread. Result WriteCache(); void StartCacheWrite(); // Prepares scripts for writing to the cache, serializing new scripts to // XDR, and calculating their size-based offsets. void PrepareCacheWrite(); void PrepareCacheWriteInternal(); void CacheWriteComplete(); void FinishContentStartup(); // Returns true if scripts added to the cache now will be encoded and // written to the cache. If we've already encoded scripts for the cache // write, or this is a content process which hasn't been asked to return // script bytecode, this will return false. bool WillWriteScripts(); // Returns a file pointer for the cache file with the given name in the // current profile. Result, nsresult> GetCacheFile(const nsAString& suffix); // Waits for the given cached script to finish compiling off-thread, or // decodes it synchronously on the main thread, as appropriate. JSScript* WaitForCachedScript(JSContext* cx, const JS::ReadOnlyCompileOptions& options, CachedScript* script); void DecodeNextBatch(size_t chunkSize, JS::HandleObject scope = nullptr); static void OffThreadDecodeCallback(JS::OffThreadToken* token, void* context); void FinishOffThreadDecode(JS::OffThreadToken* token); void DoFinishOffThreadDecode(); already_AddRefed GetShutdownBarrier(); size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { return (mallocSizeOf(this) + mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) + mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get())); } using ScriptHash = nsClassHashtable; template static size_t SizeOfHashEntries(ScriptHash& scripts, mozilla::MallocSizeOf mallocSizeOf) { size_t size = 0; for (auto elem : IterHash(scripts, Match())) { size += elem->HeapSizeOfIncludingThis(mallocSizeOf); } return size; } ScriptHash mScripts; // True after we've shown the first window, and are no longer adding new // scripts to the cache. bool mStartupFinished = false; bool mCacheInitialized = false; bool mSaveComplete = false; bool mDataPrepared = false; // May only be changed on the main thread, while `mSaveMonitor` is held. bool mCacheInvalidated = false; // The list of scripts that we read from the initial startup cache file, // but have yet to initiate a decode task for. LinkedList mPendingScripts; // The lists of scripts and their sources that make up the chunk currently // being decoded in a background thread. JS::TranscodeSources mParsingSources; Vector mParsingScripts; // The token for the completed off-thread decode task. Atomic mToken{nullptr}; // True if a runnable has been dispatched to the main thread to finish an // off-thread decode operation. Access only while 'mMonitor' is held. bool mFinishDecodeRunnablePending = false; // True is main-thread is blocked and we should notify with Monitor. Access // only while `mMonitor` is held. bool mWaitingForDecode = false; // The process type of the current process. static ProcessType sProcessType; // The process types for which remote processes have been initialized, and // are expected to send back script data. EnumSet mInitializedProcesses{}; RefPtr mChildCache; ScriptCacheChild* mChildActor = nullptr; nsString mBaseName; nsCString mContentStartupFinishedTopic; nsCOMPtr mProfD; nsCOMPtr mSaveThread; nsCOMPtr mSaveTimer; // The mmapped cache data from this session's cache file. AutoMemMap mCacheData; Monitor mMonitor; Monitor mSaveMonitor; }; } // namespace mozilla #endif // ScriptPreloader_h