ScriptPreloader.cpp (46924B)
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/. */ 6 7 #include "ScriptPreloader-inl.h" 8 #include "mozilla/AlreadyAddRefed.h" 9 #include "mozilla/Monitor.h" 10 11 #include "mozilla/ScriptPreloader.h" 12 #include "mozilla/loader/ScriptCacheActors.h" 13 14 #include "mozilla/URLPreloader.h" 15 16 #include "mozilla/Components.h" 17 #include "mozilla/DebugOnly.h" 18 #include "mozilla/FileUtils.h" 19 #include "mozilla/IOBuffers.h" 20 #include "mozilla/Logging.h" 21 #include "mozilla/ScopeExit.h" 22 #include "mozilla/Services.h" 23 #include "mozilla/StaticPrefs_javascript.h" 24 #include "mozilla/TaskController.h" 25 #include "mozilla/glean/JsXpconnectMetrics.h" 26 #include "mozilla/glean/XpcomMetrics.h" 27 #include "mozilla/Try.h" 28 #include "mozilla/dom/ContentChild.h" 29 #include "mozilla/dom/ContentParent.h" 30 #include "mozilla/dom/Document.h" 31 #include "mozilla/scache/StartupCache.h" 32 33 #include "crc32c.h" 34 #include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions 35 #include "js/experimental/JSStencil.h" // JS::Stencil, JS::DecodeStencil 36 #include "js/experimental/CompileScript.h" // JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize 37 #include "js/Transcoding.h" 38 #include "MainThreadUtils.h" 39 #include "nsDebug.h" 40 #include "nsDirectoryServiceUtils.h" 41 #include "nsIFile.h" 42 #include "nsIObserverService.h" 43 #include "nsJSUtils.h" 44 #include "nsMemoryReporterManager.h" 45 #include "nsNetUtil.h" 46 #include "nsProxyRelease.h" 47 #include "nsThreadUtils.h" 48 #include "nsXULAppAPI.h" 49 #include "xpcpublic.h" 50 51 #define STARTUP_COMPLETE_TOPIC "browser-delayed-startup-finished" 52 #define DOC_ELEM_INSERTED_TOPIC "document-element-inserted" 53 #define CONTENT_DOCUMENT_LOADED_TOPIC "content-document-loaded" 54 #define CACHE_WRITE_TOPIC "browser-idle-startup-tasks-finished" 55 #define XPCOM_SHUTDOWN_TOPIC "xpcom-shutdown" 56 #define CACHE_INVALIDATE_TOPIC "startupcache-invalidate" 57 58 // The maximum time we'll wait for a child process to finish starting up before 59 // we send its script data back to the parent. 60 constexpr uint32_t CHILD_STARTUP_TIMEOUT_MS = 8000; 61 62 namespace mozilla { 63 namespace { 64 static LazyLogModule gLog("ScriptPreloader"); 65 66 #define LOG(level, ...) MOZ_LOG(gLog, LogLevel::level, (__VA_ARGS__)) 67 } // namespace 68 69 using mozilla::dom::AutoJSAPI; 70 using mozilla::dom::ContentChild; 71 using mozilla::dom::ContentParent; 72 using namespace mozilla::loader; 73 using mozilla::scache::StartupCache; 74 75 using namespace JS; 76 77 ProcessType ScriptPreloader::sProcessType; 78 79 nsresult ScriptPreloader::CollectReports(nsIHandleReportCallback* aHandleReport, 80 nsISupports* aData, bool aAnonymize) { 81 MOZ_COLLECT_REPORT( 82 "explicit/script-preloader/heap/saved-scripts", KIND_HEAP, UNITS_BYTES, 83 SizeOfHashEntries<ScriptStatus::Saved>(mScripts, MallocSizeOf), 84 "Memory used to hold the scripts which have been executed in this " 85 "session, and will be written to the startup script cache file."); 86 87 MOZ_COLLECT_REPORT( 88 "explicit/script-preloader/heap/restored-scripts", KIND_HEAP, UNITS_BYTES, 89 SizeOfHashEntries<ScriptStatus::Restored>(mScripts, MallocSizeOf), 90 "Memory used to hold the scripts which have been restored from the " 91 "startup script cache file, but have not been executed in this session."); 92 93 MOZ_COLLECT_REPORT("explicit/script-preloader/heap/other", KIND_HEAP, 94 UNITS_BYTES, ShallowHeapSizeOfIncludingThis(MallocSizeOf), 95 "Memory used by the script cache service itself."); 96 97 // Since the mem-mapped cache file is mapped into memory, we want to report 98 // it as explicit memory somewhere. But since the child cache is shared 99 // between all processes, we don't want to report it as explicit memory for 100 // all of them. So we report it as explicit only in the parent process, and 101 // non-explicit everywhere else. 102 if (XRE_IsParentProcess()) { 103 MOZ_COLLECT_REPORT("explicit/script-preloader/non-heap/memmapped-cache", 104 KIND_NONHEAP, UNITS_BYTES, 105 mCacheData->nonHeapSizeOfExcludingThis(), 106 "The memory-mapped startup script cache file."); 107 } else { 108 MOZ_COLLECT_REPORT("script-preloader-memmapped-cache", KIND_NONHEAP, 109 UNITS_BYTES, mCacheData->nonHeapSizeOfExcludingThis(), 110 "The memory-mapped startup script cache file."); 111 } 112 113 return NS_OK; 114 } 115 116 StaticRefPtr<ScriptPreloader> ScriptPreloader::gScriptPreloader; 117 StaticRefPtr<ScriptPreloader> ScriptPreloader::gChildScriptPreloader; 118 StaticAutoPtr<AutoMemMap> ScriptPreloader::gCacheData; 119 StaticAutoPtr<AutoMemMap> ScriptPreloader::gChildCacheData; 120 121 ScriptPreloader& ScriptPreloader::GetSingleton() { 122 if (!gScriptPreloader) { 123 AssertIsOnMainThread(); 124 if (XRE_IsParentProcess()) { 125 gCacheData = new AutoMemMap(); 126 gScriptPreloader = new ScriptPreloader(gCacheData.get()); 127 gScriptPreloader->mChildCache = &GetChildSingleton(); 128 (void)gScriptPreloader->InitCache(); 129 } else { 130 gScriptPreloader = &GetChildSingleton(); 131 } 132 } 133 134 return *gScriptPreloader; 135 } 136 137 // The child singleton is available in all processes, including the parent, and 138 // is used for scripts which are expected to be loaded into child processes 139 // (such as process and frame scripts), or scripts that have already been loaded 140 // into a child. The child caches are managed as follows: 141 // 142 // - Every startup, we open the cache file from the last session, move it to a 143 // new location, and begin pre-loading the scripts that are stored in it. There 144 // is a separate cache file for parent and content processes, but the parent 145 // process opens both the parent and content cache files. 146 // 147 // - Once startup is complete, we write a new cache file for the next session, 148 // containing only the scripts that were used during early startup, so we 149 // don't waste pre-loading scripts that may not be needed. 150 // 151 // - For content processes, opening and writing the cache file is handled in the 152 // parent process. The first content process of each type sends back the data 153 // for scripts that were loaded in early startup, and the parent merges them 154 // and writes them to a cache file. 155 // 156 // - Currently, content processes only benefit from the cache data written 157 // during the *previous* session. Ideally, new content processes should 158 // probably use the cache data written during this session if there was no 159 // previous cache file, but I'd rather do that as a follow-up. 160 ScriptPreloader& ScriptPreloader::GetChildSingleton() { 161 if (!gChildScriptPreloader) { 162 AssertIsOnMainThread(); 163 gChildCacheData = new AutoMemMap(); 164 gChildScriptPreloader = new ScriptPreloader(gChildCacheData.get()); 165 if (XRE_IsParentProcess()) { 166 (void)gChildScriptPreloader->InitCache(u"scriptCache-child"_ns); 167 } 168 } 169 170 return *gChildScriptPreloader; 171 } 172 173 /* static */ 174 void ScriptPreloader::DeleteSingleton() { 175 gScriptPreloader = nullptr; 176 gChildScriptPreloader = nullptr; 177 } 178 179 /* static */ 180 void ScriptPreloader::DeleteCacheDataSingleton() { 181 MOZ_ASSERT(!gScriptPreloader); 182 MOZ_ASSERT(!gChildScriptPreloader); 183 184 gCacheData = nullptr; 185 gChildCacheData = nullptr; 186 } 187 188 void ScriptPreloader::InitContentChild(ContentParent& parent) { 189 AssertIsOnMainThread(); 190 191 auto& cache = GetChildSingleton(); 192 cache.mSaveMonitor.NoteOnMainThread(); 193 194 // We want startup script data from the first process of a given type. 195 // That process sends back its script data before it executes any 196 // untrusted code, and then we never accept further script data for that 197 // type of process for the rest of the session. 198 // 199 // The script data from each process type is merged with the data from the 200 // parent process's frame and process scripts, and shared between all 201 // content process types in the next session. 202 // 203 // Note that if the first process of a given type crashes or shuts down 204 // before sending us its script data, we silently ignore it, and data for 205 // that process type is not included in the next session's cache. This 206 // should be a sufficiently rare occurrence that it's not worth trying to 207 // handle specially. 208 auto processType = GetChildProcessType(parent.GetRemoteType()); 209 bool wantScriptData = !cache.mInitializedProcesses.contains(processType); 210 cache.mInitializedProcesses += processType; 211 212 auto fd = cache.mCacheData->cloneFileDescriptor(); 213 // Don't send original cache data to new processes if the cache has been 214 // invalidated. 215 if (fd.IsValid() && !cache.mCacheInvalidated) { 216 (void)parent.SendPScriptCacheConstructor(fd, wantScriptData); 217 } else { 218 (void)parent.SendPScriptCacheConstructor(NS_ERROR_FILE_NOT_FOUND, 219 wantScriptData); 220 } 221 } 222 223 ProcessType ScriptPreloader::GetChildProcessType(const nsACString& remoteType) { 224 if (remoteType == EXTENSION_REMOTE_TYPE) { 225 return ProcessType::Extension; 226 } 227 if (remoteType == PRIVILEGEDABOUT_REMOTE_TYPE) { 228 return ProcessType::PrivilegedAbout; 229 } 230 return ProcessType::Web; 231 } 232 233 ScriptPreloader::ScriptPreloader(AutoMemMap* cacheData) 234 : mCacheData(cacheData), 235 mMonitor("[ScriptPreloader.mMonitor]"), 236 mSaveMonitor("[ScriptPreloader.mSaveMonitor]") { 237 // We do not set the process type for child processes here because the 238 // remoteType in ContentChild is not ready yet. 239 if (XRE_IsParentProcess()) { 240 sProcessType = ProcessType::Parent; 241 } 242 243 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 244 MOZ_RELEASE_ASSERT(obs); 245 246 if (XRE_IsParentProcess()) { 247 // In the parent process, we want to freeze the script cache as soon 248 // as idle tasks for the first browser window have completed. 249 obs->AddObserver(this, STARTUP_COMPLETE_TOPIC, false); 250 obs->AddObserver(this, CACHE_WRITE_TOPIC, false); 251 } 252 253 obs->AddObserver(this, XPCOM_SHUTDOWN_TOPIC, false); 254 obs->AddObserver(this, CACHE_INVALIDATE_TOPIC, false); 255 } 256 257 ScriptPreloader::~ScriptPreloader() { Cleanup(); } 258 259 void ScriptPreloader::Cleanup() { 260 mScripts.Clear(); 261 UnregisterWeakMemoryReporter(this); 262 } 263 264 void ScriptPreloader::StartCacheWrite() { 265 MOZ_DIAGNOSTIC_ASSERT(!mSaveThread); 266 267 (void)NS_NewNamedThread("SaveScripts", getter_AddRefs(mSaveThread), this); 268 269 nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); 270 barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, 271 u""_ns); 272 } 273 274 void ScriptPreloader::InvalidateCache() { 275 { 276 mMonitor.AssertNotCurrentThreadOwns(); 277 MonitorAutoLock mal(mMonitor); 278 279 // Wait for pending off-thread parses to finish, since they depend on the 280 // memory allocated by our CachedStencil, and can't be canceled 281 // asynchronously. 282 FinishPendingParses(mal); 283 284 // Pending scripts should have been cleared by the above, and the queue 285 // should have been reset. 286 MOZ_ASSERT(mDecodingScripts.isEmpty()); 287 MOZ_ASSERT(!mDecodedStencils); 288 289 mScripts.Clear(); 290 291 // If we've already finished saving the cache at this point, start a new 292 // delayed save operation. This will write out an empty cache file in place 293 // of any cache file we've already written out this session, which will 294 // prevent us from falling back to the current session's cache file on the 295 // next startup. 296 if (mSaveComplete && !mSaveThread && mChildCache) { 297 mSaveComplete = false; 298 299 StartCacheWrite(); 300 } 301 } 302 303 { 304 MonitorAutoLock saveMonitorAutoLock(mSaveMonitor.Lock()); 305 mSaveMonitor.NoteExclusiveAccess(); 306 307 mCacheInvalidated = true; 308 } 309 310 // If we're waiting on a timeout to finish saving, interrupt it and just save 311 // immediately. 312 mSaveMonitor.Lock().NotifyAll(); 313 } 314 315 nsresult ScriptPreloader::Observe(nsISupports* subject, const char* topic, 316 const char16_t* data) { 317 AssertIsOnMainThread(); 318 319 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 320 if (!strcmp(topic, STARTUP_COMPLETE_TOPIC)) { 321 obs->RemoveObserver(this, STARTUP_COMPLETE_TOPIC); 322 323 MOZ_ASSERT(XRE_IsParentProcess()); 324 325 mStartupFinished = true; 326 URLPreloader::GetSingleton().SetStartupFinished(); 327 } else if (!strcmp(topic, CACHE_WRITE_TOPIC)) { 328 obs->RemoveObserver(this, CACHE_WRITE_TOPIC); 329 330 MOZ_ASSERT(mStartupFinished); 331 MOZ_ASSERT(XRE_IsParentProcess()); 332 333 if (mChildCache && !mSaveComplete && !mSaveThread) { 334 StartCacheWrite(); 335 } 336 } else if (mContentStartupFinishedTopic.Equals(topic)) { 337 // If this is an uninitialized about:blank viewer or a chrome: document 338 // (which should always be an XBL binding document), ignore it. We don't 339 // have to worry about it loading malicious content. 340 if (nsCOMPtr<dom::Document> doc = do_QueryInterface(subject)) { 341 nsCOMPtr<nsIURI> uri = doc->GetDocumentURI(); 342 343 if ((NS_IsAboutBlank(uri) && 344 doc->GetReadyStateEnum() == doc->READYSTATE_UNINITIALIZED) || 345 uri->SchemeIs("chrome")) { 346 return NS_OK; 347 } 348 } 349 FinishContentStartup(); 350 } else if (!strcmp(topic, "timer-callback")) { 351 FinishContentStartup(); 352 } else if (!strcmp(topic, XPCOM_SHUTDOWN_TOPIC)) { 353 // Wait for any pending parses to finish at this point, to avoid creating 354 // new stencils during destroying the JS runtime. 355 MonitorAutoLock mal(mMonitor); 356 FinishPendingParses(mal); 357 } else if (!strcmp(topic, CACHE_INVALIDATE_TOPIC)) { 358 InvalidateCache(); 359 } 360 361 return NS_OK; 362 } 363 364 void ScriptPreloader::FinishContentStartup() { 365 MOZ_ASSERT(XRE_IsContentProcess()); 366 367 #ifdef DEBUG 368 if (mContentStartupFinishedTopic.Equals(CONTENT_DOCUMENT_LOADED_TOPIC)) { 369 MOZ_ASSERT(sProcessType == ProcessType::PrivilegedAbout); 370 } else { 371 MOZ_ASSERT(sProcessType != ProcessType::PrivilegedAbout); 372 } 373 #endif /* DEBUG */ 374 375 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 376 obs->RemoveObserver(this, mContentStartupFinishedTopic.get()); 377 378 mSaveTimer = nullptr; 379 380 mStartupFinished = true; 381 382 if (mChildActor) { 383 mChildActor->SendScriptsAndFinalize(mScripts); 384 } 385 386 #ifdef XP_WIN 387 // Record the amount of USS at startup. This is Windows-only for now, 388 // we could turn it on for Linux relatively cheaply. On macOS it can have 389 // a perf impact. Only record this for non-privileged processes because 390 // privileged processes record this value at a different time, leading to 391 // a higher value which skews the telemetry. 392 if (sProcessType != ProcessType::PrivilegedAbout) { 393 mozilla::glean::memory::unique_content_startup.Accumulate( 394 nsMemoryReporterManager::ResidentUnique() / 1024); 395 } 396 #endif 397 } 398 399 bool ScriptPreloader::WillWriteScripts() { 400 return !mDataPrepared && (XRE_IsParentProcess() || mChildActor); 401 } 402 403 Result<nsCOMPtr<nsIFile>, nsresult> ScriptPreloader::GetCacheFile( 404 const nsAString& suffix) { 405 NS_ENSURE_TRUE(mProfD, Err(NS_ERROR_NOT_INITIALIZED)); 406 407 nsCOMPtr<nsIFile> cacheFile; 408 MOZ_TRY(mProfD->Clone(getter_AddRefs(cacheFile))); 409 410 MOZ_TRY(cacheFile->AppendNative("startupCache"_ns)); 411 (void)cacheFile->Create(nsIFile::DIRECTORY_TYPE, 0777); 412 413 MOZ_TRY(cacheFile->Append(mBaseName + suffix)); 414 415 return std::move(cacheFile); 416 } 417 418 static const uint8_t MAGIC[] = "mozXDRcachev003"; 419 420 Result<Ok, nsresult> ScriptPreloader::OpenCache() { 421 if (StartupCache::GetIgnoreDiskCache()) { 422 return Err(NS_ERROR_ABORT); 423 } 424 425 MOZ_TRY(NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(mProfD))); 426 427 nsCOMPtr<nsIFile> cacheFile = MOZ_TRY(GetCacheFile(u".bin"_ns)); 428 429 bool exists; 430 MOZ_TRY(cacheFile->Exists(&exists)); 431 if (exists) { 432 MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u"-current.bin"_ns)); 433 } else { 434 MOZ_TRY(cacheFile->SetLeafName(mBaseName + u"-current.bin"_ns)); 435 MOZ_TRY(cacheFile->Exists(&exists)); 436 if (!exists) { 437 return Err(NS_ERROR_FILE_NOT_FOUND); 438 } 439 } 440 441 MOZ_TRY(mCacheData->init(cacheFile)); 442 443 return Ok(); 444 } 445 446 // Opens the script cache file for this session, and initializes the script 447 // cache based on its contents. See WriteCache for details of the cache file. 448 Result<Ok, nsresult> ScriptPreloader::InitCache(const nsAString& basePath) { 449 mCacheInitialized = true; 450 mBaseName = basePath; 451 452 RegisterWeakMemoryReporter(this); 453 454 if (!XRE_IsParentProcess()) { 455 return Ok(); 456 } 457 458 // Grab the compilation scope before initializing the URLPreloader, since 459 // it's not safe to run component loader code during its critical section. 460 AutoSafeJSAPI jsapi; 461 JS::RootedObject scope(jsapi.cx(), xpc::CompilationScope()); 462 463 // Note: Code on the main thread *must not access Omnijar in any way* until 464 // this AutoBeginReading guard is destroyed. 465 URLPreloader::AutoBeginReading abr; 466 467 MOZ_TRY(OpenCache()); 468 469 return InitCacheInternal(scope); 470 } 471 472 Result<Ok, nsresult> ScriptPreloader::InitCache( 473 const Maybe<ipc::FileDescriptor>& cacheFile, ScriptCacheChild* cacheChild) { 474 MOZ_ASSERT(XRE_IsContentProcess()); 475 476 mCacheInitialized = true; 477 mChildActor = cacheChild; 478 sProcessType = 479 GetChildProcessType(dom::ContentChild::GetSingleton()->GetRemoteType()); 480 481 nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); 482 MOZ_RELEASE_ASSERT(obs); 483 484 if (sProcessType == ProcessType::PrivilegedAbout) { 485 // Since we control all of the documents loaded in the privileged 486 // content process, we can increase the window of active time for the 487 // ScriptPreloader to include the scripts that are loaded until the 488 // first document finishes loading. 489 mContentStartupFinishedTopic.AssignLiteral(CONTENT_DOCUMENT_LOADED_TOPIC); 490 } else { 491 // In the child process, we need to freeze the script cache before any 492 // untrusted code has been executed. The insertion of the first DOM 493 // document element may sometimes be earlier than is ideal, but at 494 // least it should always be safe. 495 mContentStartupFinishedTopic.AssignLiteral(DOC_ELEM_INSERTED_TOPIC); 496 } 497 obs->AddObserver(this, mContentStartupFinishedTopic.get(), false); 498 499 RegisterWeakMemoryReporter(this); 500 501 auto cleanup = MakeScopeExit([&] { 502 // If the parent is expecting cache data from us, make sure we send it 503 // before it writes out its cache file. For normal proceses, this isn't 504 // a concern, since they begin loading documents quite early. For the 505 // preloaded process, we may end up waiting a long time (or, indeed, 506 // never loading a document), so we need an additional timeout. 507 if (cacheChild) { 508 NS_NewTimerWithObserver(getter_AddRefs(mSaveTimer), this, 509 CHILD_STARTUP_TIMEOUT_MS, 510 nsITimer::TYPE_ONE_SHOT); 511 } 512 }); 513 514 if (cacheFile.isNothing()) { 515 return Ok(); 516 } 517 518 MOZ_TRY(mCacheData->init(cacheFile.ref())); 519 520 return InitCacheInternal(); 521 } 522 523 Result<Ok, nsresult> ScriptPreloader::InitCacheInternal( 524 JS::HandleObject scope) { 525 auto size = mCacheData->size(); 526 527 uint32_t headerSize; 528 uint32_t crc; 529 if (size < sizeof(MAGIC) + sizeof(headerSize) + sizeof(crc)) { 530 return Err(NS_ERROR_UNEXPECTED); 531 } 532 533 auto data = mCacheData->get<uint8_t>(); 534 MOZ_RELEASE_ASSERT(JS::IsTranscodingBytecodeAligned(data.get())); 535 536 auto end = data + size; 537 538 if (memcmp(MAGIC, data.get(), sizeof(MAGIC))) { 539 return Err(NS_ERROR_UNEXPECTED); 540 } 541 data += sizeof(MAGIC); 542 543 headerSize = LittleEndian::readUint32(data.get()); 544 data += sizeof(headerSize); 545 546 crc = LittleEndian::readUint32(data.get()); 547 data += sizeof(crc); 548 549 if (data + headerSize > end) { 550 return Err(NS_ERROR_UNEXPECTED); 551 } 552 553 if (crc != ComputeCrc32c(~0, data.get(), headerSize)) { 554 return Err(NS_ERROR_UNEXPECTED); 555 } 556 557 { 558 auto cleanup = MakeScopeExit([&]() { mScripts.Clear(); }); 559 560 LinkedList<CachedStencil> scripts; 561 562 Range<const uint8_t> header(data, data + headerSize); 563 data += headerSize; 564 565 // Reconstruct alignment padding if required. 566 size_t currentOffset = data - mCacheData->get<uint8_t>(); 567 data += JS::AlignTranscodingBytecodeOffset(currentOffset) - currentOffset; 568 569 InputBuffer buf(header); 570 571 size_t offset = 0; 572 while (!buf.finished()) { 573 auto script = MakeUnique<CachedStencil>(*this, buf); 574 MOZ_RELEASE_ASSERT(script); 575 576 auto scriptData = data + script->mOffset; 577 if (!JS::IsTranscodingBytecodeAligned(scriptData.get())) { 578 return Err(NS_ERROR_UNEXPECTED); 579 } 580 581 if (scriptData + script->mSize > end) { 582 return Err(NS_ERROR_UNEXPECTED); 583 } 584 585 // Make sure offsets match what we'd expect based on script ordering and 586 // size, as a basic sanity check. 587 if (script->mOffset != offset) { 588 return Err(NS_ERROR_UNEXPECTED); 589 } 590 offset += script->mSize; 591 592 script->mXDRRange.emplace(scriptData, scriptData + script->mSize); 593 594 // Don't pre-decode the script unless it was used in this process type 595 // during the previous session. 596 if (script->mOriginalProcessTypes.contains(CurrentProcessType())) { 597 scripts.insertBack(script.get()); 598 } else { 599 script->mReadyToExecute = true; 600 } 601 602 const auto& cachePath = script->mCachePath; 603 mScripts.InsertOrUpdate(cachePath, std::move(script)); 604 } 605 606 if (buf.error()) { 607 return Err(NS_ERROR_UNEXPECTED); 608 } 609 610 mDecodingScripts = std::move(scripts); 611 cleanup.release(); 612 } 613 614 StartDecodeTask(scope); 615 return Ok(); 616 } 617 618 void ScriptPreloader::PrepareCacheWriteInternal() { 619 MOZ_ASSERT(NS_IsMainThread()); 620 621 mMonitor.AssertCurrentThreadOwns(); 622 623 auto cleanup = MakeScopeExit([&]() { 624 if (mChildCache) { 625 mChildCache->PrepareCacheWrite(); 626 } 627 }); 628 629 if (mDataPrepared) { 630 return; 631 } 632 633 JS::FrontendContext* fc = JS::NewFrontendContext(); 634 if (!fc) { 635 return; 636 } 637 638 bool found = false; 639 for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) { 640 // Don't write any scripts that are also in the child cache. They'll be 641 // loaded from the child cache in that case, so there's no need to write 642 // them twice. 643 CachedStencil* childScript = 644 mChildCache ? mChildCache->mScripts.Get(script->mCachePath) : nullptr; 645 if (childScript && !childScript->mProcessTypes.isEmpty()) { 646 childScript->UpdateLoadTime(script->mLoadTime); 647 childScript->mProcessTypes += script->mProcessTypes; 648 script.Remove(); 649 continue; 650 } 651 652 if (!(script->mProcessTypes == script->mOriginalProcessTypes)) { 653 // Note: EnumSet doesn't support operator!=, hence the weird form above. 654 found = true; 655 } 656 657 if (!script->mSize && !script->XDREncode(fc)) { 658 script.Remove(); 659 } 660 } 661 662 JS::DestroyFrontendContext(fc); 663 664 if (!found) { 665 mSaveComplete = true; 666 return; 667 } 668 669 mDataPrepared = true; 670 } 671 672 void ScriptPreloader::PrepareCacheWrite() { 673 MonitorAutoLock mal(mMonitor); 674 675 PrepareCacheWriteInternal(); 676 } 677 678 // A struct to hold reference to a CachedStencil and the snapshot of the 679 // CachedStencil::mLoadTime field. 680 // CachedStencil::mLoadTime field can be modified concurrently, and we need 681 // to create a snapshot, in order to sort scripts. 682 struct CachedStencilRefAndTime { 683 using CachedStencil = ScriptPreloader::CachedStencil; 684 CachedStencil* mStencil; 685 TimeStamp mLoadTime; 686 687 explicit CachedStencilRefAndTime(CachedStencil* aStencil) 688 : mStencil(aStencil), mLoadTime(aStencil->mLoadTime) {} 689 690 // For use with nsTArray::Sort. 691 // 692 // Orders scripts by script load time, so that scripts which are needed 693 // earlier are stored earlier, and scripts needed at approximately the 694 // same time are stored approximately contiguously. 695 struct Comparator { 696 bool Equals(const CachedStencilRefAndTime& a, 697 const CachedStencilRefAndTime& b) const { 698 return a.mLoadTime == b.mLoadTime; 699 } 700 701 bool LessThan(const CachedStencilRefAndTime& a, 702 const CachedStencilRefAndTime& b) const { 703 return a.mLoadTime < b.mLoadTime; 704 } 705 }; 706 } JS_HAZ_NON_GC_POINTER; 707 708 // Writes out a script cache file for the scripts accessed during early 709 // startup in this session. The cache file is a little-endian binary file with 710 // the following format: 711 // 712 // - A uint32 containing the size of the header block. 713 // 714 // - A header entry for each file stored in the cache containing: 715 // - The URL that the script was originally read from. 716 // - Its cache key. 717 // - The offset of its XDR data within the XDR data block. 718 // - The size of its XDR data in the XDR data block. 719 // - A bit field describing which process types the script is used in. 720 // 721 // - A block of XDR data for the encoded scripts, with each script's data at 722 // an offset from the start of the block, as specified above. 723 Result<Ok, nsresult> ScriptPreloader::WriteCache() { 724 MOZ_ASSERT(!NS_IsMainThread()); 725 726 if (!mDataPrepared && !mSaveComplete) { 727 MonitorAutoUnlock mau(mSaveMonitor.Lock()); 728 729 NS_DispatchAndSpinEventLoopUntilComplete( 730 "ScriptPreloader::PrepareCacheWrite"_ns, 731 GetMainThreadSerialEventTarget(), 732 NewRunnableMethod("ScriptPreloader::PrepareCacheWrite", this, 733 &ScriptPreloader::PrepareCacheWrite)); 734 } 735 736 if (mSaveComplete) { 737 // If we don't have anything we need to save, we're done. 738 return Ok(); 739 } 740 741 nsCOMPtr<nsIFile> cacheFile = MOZ_TRY(GetCacheFile(u"-new.bin"_ns)); 742 743 bool exists; 744 MOZ_TRY(cacheFile->Exists(&exists)); 745 if (exists) { 746 MOZ_TRY(cacheFile->Remove(false)); 747 } 748 749 { 750 AutoFDClose raiiFd; 751 MOZ_TRY(cacheFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, 0644, 752 getter_Transfers(raiiFd))); 753 const auto fd = raiiFd.get(); 754 755 // We also need to hold mMonitor while we're touching scripts in 756 // mScripts, or they may be freed before we're done with them. 757 mMonitor.AssertNotCurrentThreadOwns(); 758 MonitorAutoLock mal(mMonitor); 759 760 nsTArray<CachedStencilRefAndTime> scriptRefs; 761 for (auto& script : IterHash(mScripts, Match<ScriptStatus::Saved>())) { 762 scriptRefs.AppendElement(CachedStencilRefAndTime(script)); 763 } 764 765 // Sort scripts by load time, with async loaded scripts before sync scripts. 766 // Since async scripts are always loaded immediately at startup, it helps to 767 // have them stored contiguously. 768 scriptRefs.Sort(CachedStencilRefAndTime::Comparator()); 769 770 OutputBuffer buf; 771 size_t offset = 0; 772 for (auto& scriptRef : scriptRefs) { 773 auto* script = scriptRef.mStencil; 774 script->mOffset = offset; 775 MOZ_DIAGNOSTIC_ASSERT( 776 JS::IsTranscodingBytecodeOffsetAligned(script->mOffset)); 777 script->Code(buf); 778 779 offset += script->mSize; 780 MOZ_DIAGNOSTIC_ASSERT( 781 JS::IsTranscodingBytecodeOffsetAligned(script->mSize)); 782 } 783 784 uint8_t headerSize[4]; 785 LittleEndian::writeUint32(headerSize, buf.cursor()); 786 787 uint8_t crc[4]; 788 LittleEndian::writeUint32(crc, ComputeCrc32c(~0, buf.Get(), buf.cursor())); 789 790 MOZ_TRY(Write(fd, MAGIC, sizeof(MAGIC))); 791 MOZ_TRY(Write(fd, headerSize, sizeof(headerSize))); 792 MOZ_TRY(Write(fd, crc, sizeof(crc))); 793 MOZ_TRY(Write(fd, buf.Get(), buf.cursor())); 794 795 // Align the start of the scripts section to the transcode alignment. 796 size_t written = sizeof(MAGIC) + sizeof(headerSize) + buf.cursor(); 797 size_t padding = JS::AlignTranscodingBytecodeOffset(written) - written; 798 if (padding) { 799 MOZ_TRY(WritePadding(fd, padding)); 800 written += padding; 801 } 802 803 for (auto& scriptRef : scriptRefs) { 804 auto* script = scriptRef.mStencil; 805 MOZ_DIAGNOSTIC_ASSERT(JS::IsTranscodingBytecodeOffsetAligned(written)); 806 MOZ_TRY(Write(fd, script->Range().begin().get(), script->mSize)); 807 808 written += script->mSize; 809 // We can only free the XDR data if the stencil isn't borrowing data from 810 // it. 811 if (script->mStencil && !JS::StencilIsBorrowed(script->mStencil)) { 812 script->FreeData(); 813 } 814 } 815 } 816 817 MOZ_TRY(cacheFile->MoveTo(nullptr, mBaseName + u".bin"_ns)); 818 819 return Ok(); 820 } 821 822 nsresult ScriptPreloader::GetName(nsACString& aName) { 823 aName.AssignLiteral("ScriptPreloader"); 824 return NS_OK; 825 } 826 827 // Runs in the mSaveThread thread, and writes out the cache file for the next 828 // session after a reasonable delay. 829 nsresult ScriptPreloader::Run() { 830 MonitorAutoLock mal(mSaveMonitor.Lock()); 831 mSaveMonitor.NoteLockHeld(); 832 833 // Ideally wait about 10 seconds before saving, to avoid unnecessary IO 834 // during early startup. But only if the cache hasn't been invalidated, 835 // since that can trigger a new write during shutdown, and we don't want to 836 // cause shutdown hangs. 837 if (!mCacheInvalidated) { 838 mal.Wait(TimeDuration::FromSeconds(10)); 839 } 840 841 auto result = URLPreloader::GetSingleton().WriteCache(); 842 (void)NS_WARN_IF(result.isErr()); 843 844 result = WriteCache(); 845 (void)NS_WARN_IF(result.isErr()); 846 847 { 848 MonitorAutoLock lock(mChildCache->mSaveMonitor.Lock()); 849 result = mChildCache->WriteCache(); 850 } 851 (void)NS_WARN_IF(result.isErr()); 852 853 NS_DispatchToMainThread( 854 NewRunnableMethod("ScriptPreloader::CacheWriteComplete", this, 855 &ScriptPreloader::CacheWriteComplete), 856 NS_DISPATCH_NORMAL); 857 return NS_OK; 858 } 859 860 void ScriptPreloader::CacheWriteComplete() { 861 mSaveThread->AsyncShutdown(); 862 mSaveThread = nullptr; 863 mSaveComplete = true; 864 865 nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); 866 barrier->RemoveBlocker(this); 867 } 868 869 void ScriptPreloader::NoteStencil(const nsCString& url, 870 const nsCString& cachePath, 871 JS::Stencil* stencil, bool isRunOnce) { 872 if (!Active()) { 873 if (isRunOnce) { 874 if (auto script = mScripts.Get(cachePath)) { 875 script->mIsRunOnce = true; 876 script->MaybeDropStencil(); 877 } 878 } 879 return; 880 } 881 882 // Don't bother trying to cache any URLs with cache-busting query 883 // parameters. 884 if (cachePath.FindChar('?') >= 0) { 885 return; 886 } 887 888 // Don't bother caching files that belong to the mochitest harness. 889 constexpr auto mochikitPrefix = "chrome://mochikit/"_ns; 890 if (StringHead(url, mochikitPrefix.Length()) == mochikitPrefix) { 891 return; 892 } 893 894 auto* script = 895 mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, stencil); 896 if (isRunOnce) { 897 script->mIsRunOnce = true; 898 } 899 900 if (!script->MaybeDropStencil() && !script->mStencil) { 901 MOZ_ASSERT(stencil); 902 script->mStencil = stencil; 903 script->mReadyToExecute = true; 904 } 905 906 script->UpdateLoadTime(TimeStamp::Now()); 907 script->mProcessTypes += CurrentProcessType(); 908 } 909 910 void ScriptPreloader::NoteStencil(const nsCString& url, 911 const nsCString& cachePath, 912 ProcessType processType, 913 nsTArray<uint8_t>&& xdrData, 914 TimeStamp loadTime) { 915 // After data has been prepared, there's no point in noting further scripts, 916 // since the cache either has already been written, or is about to be 917 // written. Any time prior to the data being prepared, we can safely mutate 918 // mScripts without locking. After that point, the save thread is free to 919 // access it, and we can't alter it without locking. 920 if (mDataPrepared) { 921 return; 922 } 923 924 auto* script = 925 mScripts.GetOrInsertNew(cachePath, *this, url, cachePath, nullptr); 926 927 if (!script->HasRange()) { 928 MOZ_ASSERT(!script->HasArray()); 929 930 script->mSize = xdrData.Length(); 931 script->mXDRData.construct<nsTArray<uint8_t>>( 932 std::forward<nsTArray<uint8_t>>(xdrData)); 933 934 auto& data = script->Array(); 935 script->mXDRRange.emplace(data.Elements(), data.Length()); 936 } 937 938 if (!script->mSize && !script->mStencil) { 939 // If the content process is sending us an entry for a stencil 940 // which was in the cache at startup, it expects us to already have this 941 // script data, so it doesn't send it. 942 // 943 // However, the cache may have been invalidated at this point (usually 944 // due to the add-on manager installing or uninstalling a legacy 945 // extension during very early startup), which means we may no longer 946 // have an entry for this script. Since that means we have no data to 947 // write to the new cache, and no JSScript to generate it from, we need 948 // to discard this entry. 949 mScripts.Remove(cachePath); 950 return; 951 } 952 953 script->UpdateLoadTime(loadTime); 954 script->mProcessTypes += processType; 955 } 956 957 /* static */ 958 void ScriptPreloader::FillCompileOptionsForCachedStencil( 959 JS::CompileOptions& options) { 960 // Users of the cache do not require return values, so inform the JS parser in 961 // order for it to generate simpler bytecode. 962 options.setNoScriptRval(true); 963 964 // The ScriptPreloader trades off having bytecode available but not source 965 // text. This means the JS syntax-only parser is not used. If `toString` is 966 // called on functions in these scripts, the source-hook will fetch it over, 967 // so using `toString` of functions should be avoided in chrome js. 968 options.setSourceIsLazy(true); 969 } 970 971 /* static */ 972 void ScriptPreloader::FillDecodeOptionsForCachedStencil( 973 JS::DecodeOptions& options) { 974 // ScriptPreloader's XDR buffer is alive during the Stencil is alive. 975 // The decoded stencil can borrow from it. 976 // 977 // NOTE: The XDR buffer is alive during the entire browser lifetime only 978 // when it's mmapped. 979 options.borrowBuffer = true; 980 } 981 982 already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencil( 983 JSContext* cx, const JS::ReadOnlyDecodeOptions& options, 984 const nsCString& path) { 985 MOZ_RELEASE_ASSERT( 986 !(XRE_IsContentProcess() && !mCacheInitialized), 987 "ScriptPreloader must be initialized before getting cached " 988 "scripts in the content process."); 989 990 // If a script is used by both the parent and the child, it's stored only 991 // in the child cache. 992 if (mChildCache) { 993 RefPtr<JS::Stencil> stencil = 994 mChildCache->GetCachedStencilInternal(cx, options, path); 995 if (stencil) { 996 glean::script_preloader::requests 997 .EnumGet(glean::script_preloader::RequestsLabel::eHitchild) 998 .Add(); 999 return stencil.forget(); 1000 } 1001 } 1002 1003 RefPtr<JS::Stencil> stencil = GetCachedStencilInternal(cx, options, path); 1004 glean::script_preloader::requests 1005 .EnumGet(stencil ? glean::script_preloader::RequestsLabel::eHit 1006 : glean::script_preloader::RequestsLabel::eMiss) 1007 .Add(); 1008 1009 return stencil.forget(); 1010 } 1011 1012 already_AddRefed<JS::Stencil> ScriptPreloader::GetCachedStencilInternal( 1013 JSContext* cx, const JS::ReadOnlyDecodeOptions& options, 1014 const nsCString& path) { 1015 auto* cachedScript = mScripts.Get(path); 1016 if (cachedScript) { 1017 return WaitForCachedStencil(cx, options, cachedScript); 1018 } 1019 return nullptr; 1020 } 1021 1022 already_AddRefed<JS::Stencil> ScriptPreloader::WaitForCachedStencil( 1023 JSContext* cx, const JS::ReadOnlyDecodeOptions& options, 1024 CachedStencil* script) { 1025 if (!script->mReadyToExecute) { 1026 // mReadyToExecute is kept as false only when off-thread decode task was 1027 // available (pref is set to true) and the task was successfully created. 1028 // See ScriptPreloader::StartDecodeTask methods. 1029 MOZ_ASSERT(mDecodedStencils); 1030 1031 // Check for the finished operations that can contain our target. 1032 if (mDecodedStencils->AvailableRead() > 0) { 1033 FinishOffThreadDecode(); 1034 } 1035 1036 if (!script->mReadyToExecute) { 1037 // Our target is not yet decoded. 1038 1039 // If script is small enough, we'd rather decode on main-thread than wait 1040 // for a decode task to complete. 1041 if (script->mSize < MAX_MAINTHREAD_DECODE_SIZE) { 1042 LOG(Info, "Script is small enough to recompile on main thread\n"); 1043 1044 script->mReadyToExecute = true; 1045 glean::script_preloader::mainthread_recompile.Add(1); 1046 } else { 1047 LOG(Info, "Must wait for async script load: %s\n", script->mURL.get()); 1048 auto start = TimeStamp::Now(); 1049 1050 MonitorAutoLock mal(mMonitor); 1051 1052 // Process finished tasks until our target is found. 1053 while (!script->mReadyToExecute) { 1054 if (mDecodedStencils->AvailableRead() > 0) { 1055 FinishOffThreadDecode(); 1056 } else { 1057 MOZ_ASSERT(!mDecodingScripts.isEmpty()); 1058 mWaitingForDecode = true; 1059 mal.Wait(); 1060 mWaitingForDecode = false; 1061 } 1062 } 1063 1064 TimeDuration waited = TimeStamp::Now() - start; 1065 glean::script_preloader::wait_time.AccumulateRawDuration(waited); 1066 LOG(Debug, "Waited %fms\n", waited.ToMilliseconds()); 1067 } 1068 } 1069 } 1070 1071 return script->GetStencil(cx, options); 1072 } 1073 1074 void ScriptPreloader::onDecodedStencilQueued() { 1075 mMonitor.AssertNotCurrentThreadOwns(); 1076 MonitorAutoLock mal(mMonitor); 1077 1078 if (mWaitingForDecode) { 1079 // Wake up the blocked main thread. 1080 mal.Notify(); 1081 } 1082 1083 // NOTE: Do not perform DoFinishOffThreadDecode for partial data. 1084 } 1085 1086 void ScriptPreloader::OnDecodeTaskFinished() { 1087 mMonitor.AssertNotCurrentThreadOwns(); 1088 MonitorAutoLock mal(mMonitor); 1089 1090 if (mWaitingForDecode) { 1091 // Wake up the blocked main thread. 1092 mal.Notify(); 1093 } else { 1094 // Issue a Runnable to handle all decoded stencils, even if the next 1095 // WaitForCachedStencil call has not happened yet. 1096 NS_DispatchToMainThread( 1097 NewRunnableMethod("ScriptPreloader::DoFinishOffThreadDecode", this, 1098 &ScriptPreloader::DoFinishOffThreadDecode)); 1099 } 1100 } 1101 1102 void ScriptPreloader::OnDecodeTaskFailed() { 1103 // NOTE: nullptr is enqueued to mDecodedStencils, and FinishOffThreadDecode 1104 // handles it as failure. 1105 OnDecodeTaskFinished(); 1106 } 1107 1108 void ScriptPreloader::FinishPendingParses(MonitorAutoLock& aMal) { 1109 mMonitor.AssertCurrentThreadOwns(); 1110 1111 // If off-thread decoding task hasn't been started, nothing to do. 1112 // This can happen if the javascript.options.parallel_parsing pref was false, 1113 // or the decode task fails to start. 1114 if (!mDecodedStencils) { 1115 return; 1116 } 1117 1118 // Process any pending decodes that are in flight. 1119 while (!mDecodingScripts.isEmpty()) { 1120 if (mDecodedStencils->AvailableRead() > 0) { 1121 FinishOffThreadDecode(); 1122 } else { 1123 mWaitingForDecode = true; 1124 aMal.Wait(); 1125 mWaitingForDecode = false; 1126 } 1127 } 1128 } 1129 1130 void ScriptPreloader::DoFinishOffThreadDecode() { 1131 // NOTE: mDecodedStencils could already be reset. 1132 if (mDecodedStencils && mDecodedStencils->AvailableRead() > 0) { 1133 FinishOffThreadDecode(); 1134 } 1135 } 1136 1137 void ScriptPreloader::FinishOffThreadDecode() { 1138 MOZ_ASSERT(mDecodedStencils); 1139 1140 while (mDecodedStencils->AvailableRead() > 0) { 1141 RefPtr<JS::Stencil> stencil; 1142 DebugOnly<int> reads = mDecodedStencils->Dequeue(&stencil, 1); 1143 MOZ_ASSERT(reads == 1); 1144 1145 if (!stencil) { 1146 // DecodeTask failed. 1147 // Mark all remaining scripts to be decoded on the main thread. 1148 for (CachedStencil* next = mDecodingScripts.getFirst(); next;) { 1149 auto* script = next; 1150 next = script->getNext(); 1151 1152 script->mReadyToExecute = true; 1153 script->remove(); 1154 } 1155 1156 break; 1157 } 1158 1159 CachedStencil* script = mDecodingScripts.getFirst(); 1160 MOZ_ASSERT(script); 1161 1162 LOG(Debug, "Finished off-thread decode of %s\n", script->mURL.get()); 1163 script->mStencil = stencil.forget(); 1164 script->mReadyToExecute = true; 1165 script->remove(); 1166 } 1167 1168 if (mDecodingScripts.isEmpty()) { 1169 mDecodedStencils.reset(); 1170 } 1171 } 1172 1173 void ScriptPreloader::StartDecodeTask(JS::HandleObject scope) { 1174 auto start = TimeStamp::Now(); 1175 LOG(Debug, "Off-thread decoding scripts...\n"); 1176 1177 Vector<JS::TranscodeSource> decodingSources; 1178 1179 size_t size = 0; 1180 for (CachedStencil* next = mDecodingScripts.getFirst(); next;) { 1181 auto* script = next; 1182 next = script->getNext(); 1183 1184 MOZ_ASSERT(script->IsMemMapped()); 1185 1186 // Skip any scripts that we decoded on the main thread rather than 1187 // waiting for an off-thread operation to complete. 1188 if (script->mReadyToExecute) { 1189 script->remove(); 1190 continue; 1191 } 1192 if (!decodingSources.emplaceBack(script->Range(), script->mURL.get(), 0)) { 1193 break; 1194 } 1195 1196 LOG(Debug, "Beginning off-thread decode of script %s (%u bytes)\n", 1197 script->mURL.get(), script->mSize); 1198 1199 size += script->mSize; 1200 } 1201 1202 MOZ_ASSERT(decodingSources.length() == mDecodingScripts.length()); 1203 1204 if (size == 0 && mDecodingScripts.isEmpty()) { 1205 return; 1206 } 1207 1208 AutoSafeJSAPI jsapi; 1209 JSContext* cx = jsapi.cx(); 1210 JSAutoRealm ar(cx, scope ? scope : xpc::CompilationScope()); 1211 1212 JS::CompileOptions options(cx); 1213 FillCompileOptionsForCachedStencil(options); 1214 1215 // All XDR buffers are mmapped and live longer than JS runtime. 1216 // The bytecode can be borrowed from the buffer. 1217 options.borrowBuffer = true; 1218 options.usePinnedBytecode = true; 1219 1220 JS::DecodeOptions decodeOptions(options); 1221 1222 size_t decodingSourcesLength = decodingSources.length(); 1223 1224 if (!StaticPrefs::javascript_options_parallel_parsing() || 1225 !StartDecodeTask(decodeOptions, std::move(decodingSources))) { 1226 LOG(Info, "Can't decode %lu bytes of scripts off-thread", 1227 (unsigned long)size); 1228 for (auto* script : mDecodingScripts) { 1229 script->mReadyToExecute = true; 1230 } 1231 return; 1232 } 1233 1234 LOG(Debug, "Initialized decoding of %u scripts (%u bytes) in %fms\n", 1235 (unsigned)decodingSourcesLength, (unsigned)size, 1236 (TimeStamp::Now() - start).ToMilliseconds()); 1237 } 1238 1239 bool ScriptPreloader::StartDecodeTask( 1240 const JS::ReadOnlyDecodeOptions& decodeOptions, 1241 Vector<JS::TranscodeSource>&& decodingSources) { 1242 mDecodedStencils.emplace(decodingSources.length()); 1243 MOZ_ASSERT(mDecodedStencils); 1244 1245 nsCOMPtr<nsIRunnable> task = 1246 new DecodeTask(this, decodeOptions, std::move(decodingSources)); 1247 1248 nsresult rv = NS_DispatchBackgroundTask(task.forget()); 1249 1250 return NS_SUCCEEDED(rv); 1251 } 1252 1253 NS_IMETHODIMP ScriptPreloader::DecodeTask::Run() { 1254 auto failure = [&]() { 1255 RefPtr<JS::Stencil> stencil; 1256 DebugOnly<int> writes = mPreloader->mDecodedStencils->Enqueue(stencil); 1257 MOZ_ASSERT(writes == 1); 1258 mPreloader->OnDecodeTaskFailed(); 1259 }; 1260 1261 JS::FrontendContext* fc = JS::NewFrontendContext(); 1262 if (!fc) { 1263 failure(); 1264 return NS_OK; 1265 } 1266 1267 auto cleanup = MakeScopeExit([&]() { JS::DestroyFrontendContext(fc); }); 1268 1269 size_t stackSize = TaskController::GetThreadStackSize(); 1270 JS::SetNativeStackQuota(fc, JS::ThreadStackQuotaForSize(stackSize)); 1271 1272 size_t remaining = mDecodingSources.length(); 1273 for (auto& source : mDecodingSources) { 1274 RefPtr<JS::Stencil> stencil; 1275 auto result = JS::DecodeStencil(fc, mDecodeOptions, source.range, 1276 getter_AddRefs(stencil)); 1277 if (result != JS::TranscodeResult::Ok) { 1278 failure(); 1279 return NS_OK; 1280 } 1281 1282 DebugOnly<int> writes = mPreloader->mDecodedStencils->Enqueue(stencil); 1283 MOZ_ASSERT(writes == 1); 1284 1285 remaining--; 1286 if (remaining) { 1287 mPreloader->onDecodedStencilQueued(); 1288 } 1289 } 1290 1291 mPreloader->OnDecodeTaskFinished(); 1292 return NS_OK; 1293 } 1294 1295 ScriptPreloader::CachedStencil::CachedStencil(ScriptPreloader& cache, 1296 InputBuffer& buf) 1297 : mCache(cache) { 1298 Code(buf); 1299 1300 // Swap the mProcessTypes and mOriginalProcessTypes values, since we want to 1301 // start with an empty set of processes loaded into for this session, and 1302 // compare against last session's values later. 1303 mOriginalProcessTypes = mProcessTypes; 1304 mProcessTypes = {}; 1305 } 1306 1307 bool ScriptPreloader::CachedStencil::XDREncode(JS::FrontendContext* aFc) { 1308 auto cleanup = MakeScopeExit([&]() { MaybeDropStencil(); }); 1309 1310 mXDRData.construct<JS::TranscodeBuffer>(); 1311 1312 JS::TranscodeResult code = JS::EncodeStencil(aFc, mStencil, Buffer()); 1313 1314 if (code == JS::TranscodeResult::Ok) { 1315 mXDRRange.emplace(Buffer().begin(), Buffer().length()); 1316 mSize = Range().length(); 1317 return true; 1318 } 1319 mXDRData.destroy(); 1320 JS::ClearFrontendErrors(aFc); 1321 return false; 1322 } 1323 1324 already_AddRefed<JS::Stencil> ScriptPreloader::CachedStencil::GetStencil( 1325 JSContext* cx, const JS::ReadOnlyDecodeOptions& options) { 1326 MOZ_ASSERT(mReadyToExecute); 1327 if (mStencil) { 1328 return do_AddRef(mStencil); 1329 } 1330 1331 if (!HasRange()) { 1332 // We've already executed the script, and thrown it away. But it wasn't 1333 // in the cache at startup, so we don't have any data to decode. Give 1334 // up. 1335 return nullptr; 1336 } 1337 1338 // If we have no script at this point, the script was too small to decode 1339 // off-thread, or it was needed before the off-thread compilation was 1340 // finished, and is small enough to decode on the main thread rather than 1341 // wait for the off-thread decoding to finish. In either case, we decode 1342 // it synchronously the first time it's needed. 1343 1344 auto start = TimeStamp::Now(); 1345 LOG(Info, "Decoding stencil %s on main thread...\n", mURL.get()); 1346 1347 RefPtr<JS::Stencil> stencil; 1348 if (JS::DecodeStencil(cx, options, Range(), getter_AddRefs(stencil)) == 1349 JS::TranscodeResult::Ok) { 1350 // Lock the monitor here to avoid data races on mScript 1351 // from other threads like the cache writing thread. 1352 // 1353 // It is possible that we could end up decoding the same 1354 // script twice, because DecodeScript isn't being guarded 1355 // by the monitor; however, to encourage off-thread decode 1356 // to proceed for other scripts we don't hold the monitor 1357 // while doing main thread decode, merely while updating 1358 // mScript. 1359 mCache.mMonitor.AssertNotCurrentThreadOwns(); 1360 MonitorAutoLock mal(mCache.mMonitor); 1361 1362 mStencil = stencil.forget(); 1363 1364 if (mCache.mSaveComplete) { 1365 // We can only free XDR data if the stencil isn't borrowing data out of 1366 // it. 1367 if (!JS::StencilIsBorrowed(mStencil)) { 1368 FreeData(); 1369 } 1370 } 1371 } 1372 1373 LOG(Debug, "Finished decoding in %fms", 1374 (TimeStamp::Now() - start).ToMilliseconds()); 1375 1376 return do_AddRef(mStencil); 1377 } 1378 1379 // nsIAsyncShutdownBlocker 1380 1381 nsresult ScriptPreloader::GetName(nsAString& aName) { 1382 aName.AssignLiteral(u"ScriptPreloader: Saving bytecode cache"); 1383 return NS_OK; 1384 } 1385 1386 nsresult ScriptPreloader::GetState(nsIPropertyBag** aState) { 1387 *aState = nullptr; 1388 return NS_OK; 1389 } 1390 1391 nsresult ScriptPreloader::BlockShutdown( 1392 nsIAsyncShutdownClient* aBarrierClient) { 1393 // If we're waiting on a timeout to finish saving, interrupt it and just save 1394 // immediately. 1395 mSaveMonitor.Lock().NotifyAll(); 1396 return NS_OK; 1397 } 1398 1399 already_AddRefed<nsIAsyncShutdownClient> ScriptPreloader::GetShutdownBarrier() { 1400 nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service(); 1401 MOZ_RELEASE_ASSERT(svc); 1402 1403 nsCOMPtr<nsIAsyncShutdownClient> barrier; 1404 (void)svc->GetXpcomWillShutdown(getter_AddRefs(barrier)); 1405 MOZ_RELEASE_ASSERT(barrier); 1406 1407 return barrier.forget(); 1408 } 1409 1410 NS_IMPL_ISUPPORTS(ScriptPreloader, nsIObserver, nsIRunnable, nsIMemoryReporter, 1411 nsINamed, nsIAsyncShutdownBlocker) 1412 1413 #undef LOG 1414 1415 } // namespace mozilla