summaryrefslogtreecommitdiffstats
path: root/js/xpconnect/loader/ScriptPreloader.h
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/xpconnect/loader/ScriptPreloader.h563
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