diff options
Diffstat (limited to '')
-rw-r--r-- | js/xpconnect/loader/ScriptPreloader.h | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/js/xpconnect/loader/ScriptPreloader.h b/js/xpconnect/loader/ScriptPreloader.h new file mode 100644 index 0000000000..7b04b7fe74 --- /dev/null +++ b/js/xpconnect/loader/ScriptPreloader.h @@ -0,0 +1,563 @@ +/* -*- 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 <prio.h> + +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 <typename T> +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<uint8_t>&& xdrData, + TimeStamp loadTime); + + // Initializes the script cache from the startup script cache file. + Result<Ok, nsresult> InitCache(const nsAString& = u"scriptCache"_ns); + + Result<Ok, nsresult> InitCache(const Maybe<ipc::FileDescriptor>& cacheFile, + ScriptCacheChild* cacheChild); + + bool Active() { return mCacheInitialized && !mStartupFinished; } + + private: + Result<Ok, nsresult> 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<CachedScript> { + 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<CachedScript*> { + 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<JSScript*> 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 <typename Buffer> + 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<JS::TranscodeBuffer>(); + } + + bool HasBuffer() { return mXDRData.constructed<JS::TranscodeBuffer>(); } + + // 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<uint8_t>& Array() { + MOZ_ASSERT(HasArray()); + return mXDRData.ref<nsTArray<uint8_t>>(); + } + + bool HasArray() { return mXDRData.constructed<nsTArray<uint8_t>>(); } + + 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<ProcessType> mProcessTypes{}; + + // The set of processes which the script was loaded into during the + // last session, as read from the cache file. + EnumSet<ProcessType> 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<JS::TranscodeRange> mXDRRange; + + // XDR data which was generated from a script compiled during this + // session, and will be written to the cache file. + MaybeOneOf<JS::TranscodeBuffer, nsTArray<uint8_t>> mXDRData; + } JS_HAZ_NON_GC_POINTER; + + template <ScriptStatus status> + static Matcher<CachedScript*>* 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<Ok, nsresult> OpenCache(); + + // Writes a new cache file to disk. Must not be called on the main thread. + Result<Ok, nsresult> 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<nsCOMPtr<nsIFile>, 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<nsIAsyncShutdownClient> GetShutdownBarrier(); + + size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { + return (mallocSizeOf(this) + + mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) + + mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get())); + } + + using ScriptHash = nsClassHashtable<nsCStringHashKey, CachedScript>; + + template <ScriptStatus status> + static size_t SizeOfHashEntries(ScriptHash& scripts, + mozilla::MallocSizeOf mallocSizeOf) { + size_t size = 0; + for (auto elem : IterHash(scripts, Match<status>())) { + 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<CachedScript> mPendingScripts; + + // The lists of scripts and their sources that make up the chunk currently + // being decoded in a background thread. + JS::TranscodeSources mParsingSources; + Vector<CachedScript*> mParsingScripts; + + // The token for the completed off-thread decode task. + Atomic<JS::OffThreadToken*, ReleaseAcquire> 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<ProcessType> mInitializedProcesses{}; + + RefPtr<ScriptPreloader> mChildCache; + ScriptCacheChild* mChildActor = nullptr; + + nsString mBaseName; + nsCString mContentStartupFinishedTopic; + + nsCOMPtr<nsIFile> mProfD; + nsCOMPtr<nsIThread> mSaveThread; + nsCOMPtr<nsITimer> mSaveTimer; + + // The mmapped cache data from this session's cache file. + AutoMemMap mCacheData; + + Monitor mMonitor; + Monitor mSaveMonitor; +}; + +} // namespace mozilla + +#endif // ScriptPreloader_h |