ScriptPreloader.h (18671B)
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #ifndef ScriptPreloader_h 7 #define ScriptPreloader_h 8 9 #include "mozilla/EnumSet.h" 10 #include "mozilla/EventTargetAndLockCapability.h" 11 #include "mozilla/LinkedList.h" 12 #include "mozilla/MemoryReporting.h" 13 #include "mozilla/Maybe.h" 14 #include "mozilla/MaybeOneOf.h" 15 #include "mozilla/Monitor.h" 16 #include "mozilla/Range.h" 17 #include "mozilla/Result.h" 18 #include "mozilla/SPSCQueue.h" 19 #include "mozilla/StaticPtr.h" 20 #include "mozilla/Vector.h" 21 #include "mozilla/loader/AutoMemMap.h" 22 #include "MainThreadUtils.h" 23 #include "nsClassHashtable.h" 24 #include "nsThreadUtils.h" 25 #include "nsIAsyncShutdown.h" 26 #include "nsIFile.h" 27 #include "nsIMemoryReporter.h" 28 #include "nsIObserver.h" 29 #include "nsIThread.h" 30 #include "nsITimer.h" 31 32 #include "js/CompileOptions.h" // JS::DecodeOptions, JS::ReadOnlyDecodeOptions 33 #include "js/experimental/CompileScript.h" // JS::FrontendContext 34 #include "js/experimental/JSStencil.h" // JS::Stencil 35 #include "js/GCAnnotations.h" // for JS_HAZ_NON_GC_POINTER 36 #include "js/RootingAPI.h" // for Handle, Heap 37 #include "js/Transcoding.h" // for TranscodeBuffer, TranscodeRange, TranscodeSource 38 #include "js/TypeDecls.h" // for HandleObject, HandleScript 39 40 #include <prio.h> 41 42 namespace mozilla { 43 namespace dom { 44 class ContentParent; 45 } 46 namespace ipc { 47 class FileDescriptor; 48 } 49 namespace loader { 50 class InputBuffer; 51 class ScriptCacheChild; 52 53 enum class ProcessType : uint8_t { 54 Uninitialized, 55 Parent, 56 Web, 57 Extension, 58 PrivilegedAbout, 59 }; 60 61 template <typename T> 62 struct Matcher { 63 virtual bool Matches(T) = 0; 64 }; 65 } // namespace loader 66 67 using namespace mozilla::loader; 68 69 struct CachedStencilRefAndTime; 70 71 class ScriptPreloader : public nsIObserver, 72 public nsIMemoryReporter, 73 public nsIRunnable, 74 public nsINamed, 75 public nsIAsyncShutdownBlocker { 76 MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) 77 78 friend class mozilla::loader::ScriptCacheChild; 79 80 public: 81 NS_DECL_THREADSAFE_ISUPPORTS 82 NS_DECL_NSIOBSERVER 83 NS_DECL_NSIMEMORYREPORTER 84 NS_DECL_NSIRUNNABLE 85 NS_DECL_NSINAMED 86 NS_DECL_NSIASYNCSHUTDOWNBLOCKER 87 88 private: 89 static StaticRefPtr<ScriptPreloader> gScriptPreloader; 90 static StaticRefPtr<ScriptPreloader> gChildScriptPreloader; 91 static StaticAutoPtr<AutoMemMap> gCacheData; 92 static StaticAutoPtr<AutoMemMap> gChildCacheData; 93 94 public: 95 static ScriptPreloader& GetSingleton(); 96 static ScriptPreloader& GetChildSingleton(); 97 98 static void DeleteSingleton(); 99 static void DeleteCacheDataSingleton(); 100 101 static ProcessType GetChildProcessType(const nsACString& remoteType); 102 103 // Fill some options that should be consistent across all scripts stored 104 // into preloader cache. 105 static void FillCompileOptionsForCachedStencil(JS::CompileOptions& options); 106 static void FillDecodeOptionsForCachedStencil(JS::DecodeOptions& options); 107 108 // Retrieves the stencil with the given cache key from the cache. 109 // Returns null if the stencil is not cached. 110 already_AddRefed<JS::Stencil> GetCachedStencil( 111 JSContext* cx, const JS::ReadOnlyDecodeOptions& options, 112 const nsCString& path); 113 114 // Notes the execution of a script with the given URL and cache key. 115 // Depending on the stage of startup, the script may be serialized and 116 // stored to the startup script cache. 117 // 118 // If isRunOnce is true, this script is expected to run only once per 119 // process per browser session. A cached instance will not be kept alive 120 // for repeated execution. 121 void NoteStencil(const nsCString& url, const nsCString& cachePath, 122 JS::Stencil* stencil, bool isRunOnce = false); 123 124 // Notes the IPC arrival of the XDR data of a stencil compiled by some 125 // child process. See ScriptCacheChild::SendScriptsAndFinalize. 126 void NoteStencil(const nsCString& url, const nsCString& cachePath, 127 ProcessType processType, nsTArray<uint8_t>&& xdrData, 128 TimeStamp loadTime); 129 130 // Initializes the script cache from the startup script cache file. 131 Result<Ok, nsresult> InitCache(const nsAString& = u"scriptCache"_ns) 132 MOZ_REQUIRES(sMainThreadCapability); 133 134 Result<Ok, nsresult> InitCache(const Maybe<ipc::FileDescriptor>& cacheFile, 135 ScriptCacheChild* cacheChild) 136 MOZ_REQUIRES(sMainThreadCapability); 137 138 bool Active() const { return mCacheInitialized && !mStartupFinished; } 139 140 private: 141 Result<Ok, nsresult> InitCacheInternal(JS::Handle<JSObject*> scope = nullptr); 142 already_AddRefed<JS::Stencil> GetCachedStencilInternal( 143 JSContext* cx, const JS::ReadOnlyDecodeOptions& options, 144 const nsCString& path); 145 146 public: 147 static ProcessType CurrentProcessType() { 148 MOZ_ASSERT(sProcessType != ProcessType::Uninitialized); 149 return sProcessType; 150 } 151 152 static void InitContentChild(dom::ContentParent& parent); 153 154 protected: 155 virtual ~ScriptPreloader(); 156 157 private: 158 enum class ScriptStatus { 159 Restored, 160 Saved, 161 }; 162 163 // Represents a cached script stencil, either initially read from the 164 // cache file, to be added to the next session's stencil cache file, or 165 // both. 166 // 167 // - Read from the cache, and being decoded off thread. In this case: 168 // - mReadyToExecute is false 169 // - mDecodingScripts contains the CachedStencil 170 // - mDecodedStencils have never contained the stencil 171 // - mStencil is null 172 // 173 // - Off-thread decode for the stencil has finished, but the stencil has not 174 // yet been dequeued nor executed. In this case: 175 // - mReadyToExecute is true 176 // - mDecodingScripts contains the CachedStencil 177 // - mDecodedStencils contains the decoded stencil 178 // - mStencil is null 179 // 180 // - Off-thread decode for the stencil has finished, and the stencil has 181 // been dequeued, but has not yet been executed. In this case: 182 // - mReadyToExecute is true 183 // - mDecodingScripts no longer contains the CachedStencil 184 // - mDecodedStencils no longer contains the decoded stencil 185 // - mStencil is non-null 186 // 187 // - Fully decoded, and ready to be added to the next session's cache 188 // file. In this case: 189 // - mReadyToExecute is true 190 // - mStencil is non-null 191 // 192 // A stencil to be added to the next session's cache file always has a 193 // non-null mStencil value. If it was read from the last session's cache 194 // file, it also has a non-empty mXDRRange range, which will be stored in 195 // the next session's cache file. If it was compiled in this session, its 196 // mXDRRange will initially be empty, and its mXDRData buffer will be 197 // populated just before it is written to the cache file. 198 class CachedStencil : public LinkedListElement<CachedStencil> { 199 public: 200 CachedStencil(CachedStencil&&) = delete; 201 202 CachedStencil(ScriptPreloader& cache, const nsCString& url, 203 const nsCString& cachePath, JS::Stencil* stencil) 204 : mCache(cache), 205 mURL(url), 206 mCachePath(cachePath), 207 mStencil(stencil), 208 mReadyToExecute(true), 209 mIsRunOnce(false) {} 210 211 inline CachedStencil(ScriptPreloader& cache, InputBuffer& buf); 212 213 ~CachedStencil() = default; 214 215 ScriptStatus Status() const { 216 return mProcessTypes.isEmpty() ? ScriptStatus::Restored 217 : ScriptStatus::Saved; 218 } 219 220 struct StatusMatcher final : public Matcher<CachedStencil*> { 221 explicit StatusMatcher(ScriptStatus status) : mStatus(status) {} 222 223 virtual bool Matches(CachedStencil* script) override { 224 return script->Status() == mStatus; 225 } 226 227 const ScriptStatus mStatus; 228 }; 229 230 void FreeData() { 231 // If the script data isn't mmapped, we need to release both it 232 // and the Range that points to it at the same time. 233 if (!IsMemMapped()) { 234 mXDRRange.reset(); 235 mXDRData.destroy(); 236 } 237 } 238 239 void UpdateLoadTime(const TimeStamp& loadTime) { 240 if (mLoadTime.IsNull() || loadTime < mLoadTime) { 241 mLoadTime = loadTime; 242 } 243 } 244 245 // Checks whether the cached JSScript for this entry will be needed 246 // again and, if not, drops it and returns true. This is the case for 247 // run-once scripts that do not still need to be encoded into the 248 // cache. 249 // 250 // If this method returns false, callers may set mScript to a cached 251 // JSScript instance for this entry. If it returns true, they should 252 // not. 253 bool MaybeDropStencil() { 254 if (mIsRunOnce && (HasRange() || !mCache.WillWriteScripts())) { 255 mStencil = nullptr; 256 return true; 257 } 258 return false; 259 } 260 261 // Encodes this script into XDR data, and stores the result in mXDRData. 262 // Returns true on success, false on failure. 263 bool XDREncode(JS::FrontendContext* cx); 264 265 // Encodes or decodes this script, in the storage format required by the 266 // script cache file. 267 template <typename Buffer> 268 void Code(Buffer& buffer) { 269 buffer.codeString(mURL); 270 buffer.codeString(mCachePath); 271 buffer.codeUint32(mOffset); 272 buffer.codeUint32(mSize); 273 buffer.codeUint8(mProcessTypes); 274 } 275 276 // Returns the XDR data generated for this script during this session. See 277 // mXDRData. 278 JS::TranscodeBuffer& Buffer() { 279 MOZ_ASSERT(HasBuffer()); 280 return mXDRData.ref<JS::TranscodeBuffer>(); 281 } 282 283 bool HasBuffer() { return mXDRData.constructed<JS::TranscodeBuffer>(); } 284 285 // Returns the read-only XDR data for this script. See mXDRRange. 286 const JS::TranscodeRange& Range() { 287 MOZ_ASSERT(HasRange()); 288 return mXDRRange.ref(); 289 } 290 291 bool HasRange() { return mXDRRange.isSome(); } 292 293 bool IsMemMapped() const { return mXDRData.empty(); } 294 295 nsTArray<uint8_t>& Array() { 296 MOZ_ASSERT(HasArray()); 297 return mXDRData.ref<nsTArray<uint8_t>>(); 298 } 299 300 bool HasArray() { return mXDRData.constructed<nsTArray<uint8_t>>(); } 301 302 already_AddRefed<JS::Stencil> GetStencil( 303 JSContext* cx, const JS::ReadOnlyDecodeOptions& options); 304 305 size_t HeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { 306 auto size = mallocSizeOf(this); 307 308 if (HasArray()) { 309 size += Array().ShallowSizeOfExcludingThis(mallocSizeOf); 310 } else if (HasBuffer()) { 311 size += Buffer().sizeOfExcludingThis(mallocSizeOf); 312 } 313 314 if (mStencil) { 315 size += JS::SizeOfStencil(mStencil, mallocSizeOf); 316 } 317 318 // Note: mURL and mCachePath use the same string for scripts loaded 319 // by the message manager. The following statement avoids 320 // double-measuring in that case. 321 size += (mURL.SizeOfExcludingThisIfUnshared(mallocSizeOf) + 322 mCachePath.SizeOfExcludingThisEvenIfShared(mallocSizeOf)); 323 324 return size; 325 } 326 327 ScriptPreloader& mCache; 328 329 // The URL from which this script was initially read and compiled. 330 nsCString mURL; 331 // A unique identifier for this script's filesystem location, used as a 332 // primary cache lookup value. 333 nsCString mCachePath; 334 335 // The offset of this script in the cache file, from the start of the XDR 336 // data block. 337 uint32_t mOffset = 0; 338 // The size of this script's encoded XDR data. 339 uint32_t mSize = 0; 340 341 TimeStamp mLoadTime{}; 342 343 RefPtr<JS::Stencil> mStencil; 344 345 // True if this script is ready to be executed. This means that either the 346 // off-thread portion of an off-thread decode has finished, or the 347 // off-thread decode failed, and may be immediately decoded 348 // whenever it is first executed. 349 bool mReadyToExecute = false; 350 351 // True if this script is expected to run once per process. If so, its 352 // JSScript instance will be dropped as soon as the script has 353 // executed and been encoded into the cache. 354 bool mIsRunOnce = false; 355 356 // The set of processes in which this script has been used. 357 EnumSet<ProcessType> mProcessTypes{}; 358 359 // The set of processes which the script was loaded into during the 360 // last session, as read from the cache file. 361 EnumSet<ProcessType> mOriginalProcessTypes{}; 362 363 // The read-only XDR data for this script, which was either read from an 364 // existing cache file, or generated by encoding a script which was 365 // compiled during this session. 366 Maybe<JS::TranscodeRange> mXDRRange; 367 368 // XDR data which was generated from a script compiled during this 369 // session, and will be written to the cache file. 370 // 371 // The format is JS::TranscodeBuffer if the script was XDR'd as part 372 // of this process, or nsTArray<> if the script was transfered by IPC 373 // from a child process. 374 MaybeOneOf<JS::TranscodeBuffer, nsTArray<uint8_t>> mXDRData; 375 } JS_HAZ_NON_GC_POINTER; 376 377 friend struct CachedStencilRefAndTime; 378 379 template <ScriptStatus status> 380 static Matcher<CachedStencil*>* Match() { 381 static CachedStencil::StatusMatcher matcher{status}; 382 return &matcher; 383 } 384 385 // The maximum size of scripts to re-decode on the main thread if off-thread 386 // decoding hasn't finished yet. In practice, we don't hit this very often, 387 // but when we do, re-decoding some smaller scripts on the main thread gives 388 // the background decoding a chance to catch up without blocking the main 389 // thread for quite as long. 390 static constexpr int MAX_MAINTHREAD_DECODE_SIZE = 50 * 1024; 391 392 explicit ScriptPreloader(AutoMemMap* cacheData); 393 394 void Cleanup(); 395 396 void FinishPendingParses(MonitorAutoLock& aMal); 397 void InvalidateCache() MOZ_REQUIRES(sMainThreadCapability); 398 399 // Opens the cache file for reading. 400 Result<Ok, nsresult> OpenCache(); 401 402 // Writes a new cache file to disk. Must not be called on the main thread. 403 Result<Ok, nsresult> WriteCache() MOZ_REQUIRES(mSaveMonitor.Lock()); 404 405 void StartCacheWrite(); 406 407 // Prepares scripts for writing to the cache, serializing new scripts to 408 // XDR, and calculating their size-based offsets. 409 void PrepareCacheWrite(); 410 411 void PrepareCacheWriteInternal(); 412 413 void CacheWriteComplete(); 414 415 void FinishContentStartup(); 416 417 // Returns true if scripts added to the cache now will be encoded and 418 // written to the cache. If we've already encoded scripts for the cache 419 // write, or this is a content process which hasn't been asked to return 420 // script bytecode, this will return false. 421 bool WillWriteScripts(); 422 423 // Returns a file pointer for the cache file with the given name in the 424 // current profile. 425 Result<nsCOMPtr<nsIFile>, nsresult> GetCacheFile(const nsAString& suffix); 426 427 // Waits for the given cached script to finish compiling off-thread, or 428 // decodes it synchronously on the main thread, as appropriate. 429 already_AddRefed<JS::Stencil> WaitForCachedStencil( 430 JSContext* cx, const JS::ReadOnlyDecodeOptions& options, 431 CachedStencil* script); 432 433 void StartDecodeTask(JS::Handle<JSObject*> scope); 434 435 private: 436 bool StartDecodeTask(const JS::ReadOnlyDecodeOptions& decodeOptions, 437 Vector<JS::TranscodeSource>&& decodingSources); 438 439 class DecodeTask : public Runnable { 440 ScriptPreloader* mPreloader; 441 JS::OwningDecodeOptions mDecodeOptions; 442 Vector<JS::TranscodeSource> mDecodingSources; 443 444 public: 445 DecodeTask(ScriptPreloader* preloader, 446 const JS::ReadOnlyDecodeOptions& decodeOptions, 447 Vector<JS::TranscodeSource>&& decodingSources) 448 : Runnable("ScriptPreloaderDecodeTask"), 449 mPreloader(preloader), 450 mDecodingSources(std::move(decodingSources)) { 451 mDecodeOptions.infallibleCopy(decodeOptions); 452 } 453 454 NS_IMETHOD Run() override; 455 }; 456 457 friend class DecodeTask; 458 459 void onDecodedStencilQueued(); 460 void OnDecodeTaskFinished(); 461 void OnDecodeTaskFailed(); 462 463 public: 464 void FinishOffThreadDecode(); 465 void DoFinishOffThreadDecode(); 466 467 already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier(); 468 469 size_t ShallowHeapSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { 470 return (mallocSizeOf(this) + 471 mScripts.ShallowSizeOfExcludingThis(mallocSizeOf) + 472 mallocSizeOf(mSaveThread.get()) + mallocSizeOf(mProfD.get())); 473 } 474 475 using ScriptHash = nsClassHashtable<nsCStringHashKey, CachedStencil>; 476 477 template <ScriptStatus status> 478 static size_t SizeOfHashEntries(ScriptHash& scripts, 479 mozilla::MallocSizeOf mallocSizeOf) { 480 size_t size = 0; 481 for (auto elem : IterHash(scripts, Match<status>())) { 482 size += elem->HeapSizeOfIncludingThis(mallocSizeOf); 483 } 484 return size; 485 } 486 487 ScriptHash mScripts; 488 489 // True after we've shown the first window, and are no longer adding new 490 // scripts to the cache. 491 bool mStartupFinished = false; 492 493 bool mCacheInitialized = false; 494 bool mSaveComplete = false; 495 bool mDataPrepared = false; 496 // May only be changed on the main thread, while `mSaveMonitor.Lock()` is 497 // held. 498 bool mCacheInvalidated MOZ_GUARDED_BY(mSaveMonitor) = false; 499 500 // The list of scripts currently being decoded in a background thread. 501 LinkedList<CachedStencil> mDecodingScripts; 502 503 // The result of the decode task. 504 // 505 // This is emplaced when starting the decode task, with the capacity equal 506 // to the number of sources. 507 // 508 // If the decode task failed, nullptr is enqueued. 509 Maybe<SPSCQueue<RefPtr<JS::Stencil>>> mDecodedStencils; 510 511 // True is main-thread is blocked and we should notify with Monitor. Access 512 // only while `mMonitor` is held. 513 bool mWaitingForDecode MOZ_GUARDED_BY(mMonitor) = false; 514 515 // The process type of the current process. 516 static ProcessType sProcessType; 517 518 // The process types for which remote processes have been initialized, and 519 // are expected to send back script data. 520 EnumSet<ProcessType> mInitializedProcesses{}; 521 522 RefPtr<ScriptPreloader> mChildCache; 523 ScriptCacheChild* mChildActor = nullptr; 524 525 nsString mBaseName; 526 nsCString mContentStartupFinishedTopic; 527 528 nsCOMPtr<nsIFile> mProfD; 529 nsCOMPtr<nsIThread> mSaveThread; 530 nsCOMPtr<nsITimer> mSaveTimer; 531 532 // The mmapped cache data from this session's cache file. 533 // The instance is held by either `gCacheData` or `gChildCacheData` static 534 // fields, and its lifetime is guaranteed to be longer than ScriptPreloader 535 // instance. 536 AutoMemMap* mCacheData; 537 538 Monitor mMonitor MOZ_ACQUIRED_AFTER(mSaveMonitor.Lock()); 539 MainThreadAndLockCapability<Monitor> mSaveMonitor; 540 }; 541 542 } // namespace mozilla 543 544 #endif // ScriptPreloader_h