ChromeScriptLoader.cpp (18802B)
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 "PrecompiledScript.h" 8 9 #include "nsIIncrementalStreamLoader.h" 10 #include "nsIURI.h" 11 #include "nsIChannel.h" 12 #include "nsNetUtil.h" 13 #include "nsThreadUtils.h" 14 15 #include "jsapi.h" 16 #include "jsfriendapi.h" 17 #include "js/CompileOptions.h" // JS::CompileOptions, JS::OwningCompileOptions 18 #include "js/CompilationAndEvaluation.h" 19 #include "js/experimental/CompileScript.h" // JS::CompileGlobalScriptToStencil, JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize, JS::HadFrontendErrors, JS::ConvertFrontendErrorsToRuntimeErrors 20 #include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileGlobalScriptToStencil, JS::InstantiateGlobalStencil 21 #include "js/SourceText.h" // JS::SourceText 22 #include "js/Utility.h" 23 24 #include "mozilla/AlreadyAddRefed.h" // already_AddRefed 25 #include "mozilla/Assertions.h" // MOZ_ASSERT 26 #include "mozilla/Attributes.h" 27 #include "mozilla/ClearOnShutdown.h" // RunOnShutdown 28 #include "mozilla/EventQueue.h" // EventQueuePriority 29 #include "mozilla/Mutex.h" 30 #include "mozilla/SchedulerGroup.h" 31 #include "mozilla/StaticMutex.h" 32 #include "mozilla/StaticPrefs_javascript.h" 33 #include "mozilla/dom/ChromeUtils.h" 34 #include "mozilla/dom/Promise.h" 35 #include "mozilla/dom/ScriptLoader.h" 36 #include "mozilla/HoldDropJSObjects.h" 37 #include "mozilla/RefPtr.h" // RefPtr 38 #include "mozilla/TaskController.h" // TaskController, Task 39 #include "mozilla/ThreadSafety.h" // MOZ_GUARDED_BY 40 #include "mozilla/Utf8.h" // Utf8Unit 41 #include "mozilla/Vector.h" 42 #include "nsCCUncollectableMarker.h" 43 #include "nsCycleCollectionParticipant.h" 44 45 using namespace JS; 46 using namespace mozilla; 47 using namespace mozilla::dom; 48 49 class AsyncScriptCompileTask final : public Task { 50 static mozilla::StaticMutex sOngoingTasksMutex; 51 static Vector<AsyncScriptCompileTask*> sOngoingTasks 52 MOZ_GUARDED_BY(sOngoingTasksMutex); 53 static bool sIsShutdownRegistered; 54 55 // Compilation tasks should be cancelled before calling JS_ShutDown, in order 56 // to avoid keeping JS::Stencil and SharedImmutableString pointers alive 57 // beyond it. 58 // 59 // Cancel all ongoing tasks at ShutdownPhase::XPCOMShutdownFinal, which 60 // happens before calling JS_ShutDown. 61 static bool RegisterTask(AsyncScriptCompileTask* aTask) { 62 MOZ_ASSERT(NS_IsMainThread()); 63 64 if (!sIsShutdownRegistered) { 65 sIsShutdownRegistered = true; 66 67 RunOnShutdown([] { 68 StaticMutexAutoLock lock(sOngoingTasksMutex); 69 for (auto* task : sOngoingTasks) { 70 task->Cancel(); 71 } 72 }); 73 } 74 75 StaticMutexAutoLock lock(sOngoingTasksMutex); 76 return sOngoingTasks.append(aTask); 77 } 78 79 static void UnregisterTask(const AsyncScriptCompileTask* aTask) { 80 StaticMutexAutoLock lock(sOngoingTasksMutex); 81 sOngoingTasks.eraseIfEqual(aTask); 82 } 83 84 public: 85 explicit AsyncScriptCompileTask(JS::SourceText<Utf8Unit>&& aSrcBuf) 86 : Task(Kind::OffMainThreadOnly, EventQueuePriority::Normal), 87 mOptions(JS::OwningCompileOptions::ForFrontendContext()), 88 mSrcBuf(std::move(aSrcBuf)), 89 mMutex("AsyncScriptCompileTask") {} 90 91 ~AsyncScriptCompileTask() { 92 if (mFrontendContext) { 93 JS::DestroyFrontendContext(mFrontendContext); 94 } 95 UnregisterTask(this); 96 } 97 98 bool Init(const JS::OwningCompileOptions& aOptions) { 99 if (!RegisterTask(this)) { 100 return false; 101 } 102 103 mFrontendContext = JS::NewFrontendContext(); 104 if (!mFrontendContext) { 105 return false; 106 } 107 108 if (!mOptions.copy(mFrontendContext, aOptions)) { 109 return false; 110 } 111 112 return true; 113 } 114 115 private: 116 void Compile() { 117 // NOTE: The stack limit must be set from the same thread that compiles. 118 size_t stackSize = TaskController::GetThreadStackSize(); 119 JS::SetNativeStackQuota(mFrontendContext, 120 JS::ThreadStackQuotaForSize(stackSize)); 121 122 mStencil = 123 JS::CompileGlobalScriptToStencil(mFrontendContext, mOptions, mSrcBuf); 124 } 125 126 // Cancel the task. 127 // If the task is already running, this waits for the task to finish. 128 void Cancel() { 129 MOZ_ASSERT(NS_IsMainThread()); 130 131 MutexAutoLock lock(mMutex); 132 133 mIsCancelled = true; 134 135 mStencil = nullptr; 136 } 137 138 public: 139 TaskResult Run() override { 140 MutexAutoLock lock(mMutex); 141 142 if (mIsCancelled) { 143 return TaskResult::Complete; 144 } 145 146 Compile(); 147 return TaskResult::Complete; 148 } 149 150 already_AddRefed<JS::Stencil> StealStencil(JSContext* aCx) { 151 JS::FrontendContext* fc = mFrontendContext; 152 mFrontendContext = nullptr; 153 154 MOZ_ASSERT(fc); 155 156 if (JS::HadFrontendErrors(fc)) { 157 (void)JS::ConvertFrontendErrorsToRuntimeErrors(aCx, fc, mOptions); 158 JS::DestroyFrontendContext(fc); 159 return nullptr; 160 } 161 162 // Report warnings. 163 if (!JS::ConvertFrontendErrorsToRuntimeErrors(aCx, fc, mOptions)) { 164 JS::DestroyFrontendContext(fc); 165 return nullptr; 166 } 167 168 JS::DestroyFrontendContext(fc); 169 170 return mStencil.forget(); 171 } 172 173 #ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY 174 bool GetName(nsACString& aName) override { 175 aName.AssignLiteral("AsyncScriptCompileTask"); 176 return true; 177 } 178 #endif 179 180 private: 181 // Owning-pointer for the context associated with the script compilation. 182 // 183 // The context is allocated on main thread in Init method, and is freed on 184 // any thread in the destructor. 185 JS::FrontendContext* mFrontendContext = nullptr; 186 187 JS::OwningCompileOptions mOptions; 188 189 RefPtr<JS::Stencil> mStencil; 190 191 JS::SourceText<Utf8Unit> mSrcBuf; 192 193 // This mutex is locked during running the task or cancelling task. 194 mozilla::Mutex mMutex; 195 196 bool mIsCancelled MOZ_GUARDED_BY(mMutex) = false; 197 }; 198 199 /* static */ mozilla::StaticMutex AsyncScriptCompileTask::sOngoingTasksMutex; 200 MOZ_RUNINIT /* static */ Vector<AsyncScriptCompileTask*> 201 AsyncScriptCompileTask::sOngoingTasks; 202 /* static */ bool AsyncScriptCompileTask::sIsShutdownRegistered = false; 203 204 class AsyncScriptCompiler; 205 206 class AsyncScriptCompilationCompleteTask : public Task { 207 public: 208 AsyncScriptCompilationCompleteTask(AsyncScriptCompiler* aCompiler, 209 AsyncScriptCompileTask* aCompileTask) 210 : Task(Kind::MainThreadOnly, EventQueuePriority::Normal), 211 mCompiler(aCompiler), 212 mCompileTask(aCompileTask) { 213 MOZ_ASSERT(NS_IsMainThread()); 214 } 215 216 #ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY 217 bool GetName(nsACString& aName) override { 218 aName.AssignLiteral("AsyncScriptCompilationCompleteTask"); 219 return true; 220 } 221 #endif 222 223 TaskResult Run() override; 224 225 private: 226 // NOTE: 227 // This field is main-thread only, and this task shouldn't be freed off 228 // main thread. 229 // 230 // This is guaranteed by not having off-thread tasks which depends on this 231 // task, because otherwise the off-thread task's mDependencies can be the 232 // last reference, which results in freeing this task off main thread. 233 // 234 // If such task is added, this field must be moved to separate storage. 235 RefPtr<AsyncScriptCompiler> mCompiler; 236 237 RefPtr<AsyncScriptCompileTask> mCompileTask; 238 }; 239 240 class AsyncScriptCompiler final : public nsIIncrementalStreamLoaderObserver { 241 public: 242 // Note: References to this class are never held by cycle-collected objects. 243 // If at any point a reference is returned to a caller, please update this 244 // class to implement cycle collection. 245 NS_DECL_ISUPPORTS 246 NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER 247 248 AsyncScriptCompiler(JSContext* aCx, nsIGlobalObject* aGlobal, 249 const nsACString& aURL, Promise* aPromise) 250 : mOptions(aCx), 251 mURL(aURL), 252 mGlobalObject(aGlobal), 253 mPromise(aPromise), 254 mScriptLength(0) {} 255 256 [[nodiscard]] nsresult Start(JSContext* aCx, 257 const CompileScriptOptionsDictionary& aOptions, 258 nsIPrincipal* aPrincipal); 259 260 void OnCompilationComplete(AsyncScriptCompileTask* aCompileTask); 261 262 protected: 263 virtual ~AsyncScriptCompiler() { 264 if (mPromise->State() == Promise::PromiseState::Pending) { 265 mPromise->MaybeReject(NS_ERROR_FAILURE); 266 } 267 } 268 269 private: 270 void Reject(JSContext* aCx); 271 void Reject(JSContext* aCx, const char* aMxg); 272 273 bool StartCompile(JSContext* aCx); 274 bool StartOffThreadCompile(JS::SourceText<Utf8Unit>&& aSrcBuf); 275 void FinishCompile(JSContext* aCx); 276 void Finish(JSContext* aCx, RefPtr<JS::Stencil>&& aStencil); 277 278 OwningCompileOptions mOptions; 279 nsCString mURL; 280 nsCOMPtr<nsIGlobalObject> mGlobalObject; 281 RefPtr<Promise> mPromise; 282 nsString mCharset; 283 UniquePtr<Utf8Unit[], JS::FreePolicy> mScriptText; 284 size_t mScriptLength; 285 }; 286 287 NS_IMPL_ISUPPORTS(AsyncScriptCompiler, nsIIncrementalStreamLoaderObserver) 288 289 nsresult AsyncScriptCompiler::Start( 290 JSContext* aCx, const CompileScriptOptionsDictionary& aOptions, 291 nsIPrincipal* aPrincipal) { 292 mCharset = aOptions.mCharset; 293 294 CompileOptions options(aCx); 295 nsAutoCString filename; 296 if (aOptions.mFilename.WasPassed()) { 297 filename = NS_ConvertUTF16toUTF8(aOptions.mFilename.Value()); 298 options.setFile(filename.get()); 299 } else { 300 options.setFile(mURL.get()); 301 } 302 options.setNoScriptRval(!aOptions.mHasReturnValue); 303 304 if (!aOptions.mLazilyParse) { 305 options.setForceFullParse(); 306 } 307 308 if (NS_WARN_IF(!mOptions.copy(aCx, options))) { 309 return NS_ERROR_OUT_OF_MEMORY; 310 } 311 312 nsCOMPtr<nsIURI> uri; 313 nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL); 314 NS_ENSURE_SUCCESS(rv, rv); 315 316 nsCOMPtr<nsIChannel> channel; 317 rv = NS_NewChannel( 318 getter_AddRefs(channel), uri, aPrincipal, 319 nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 320 nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT); 321 NS_ENSURE_SUCCESS(rv, rv); 322 323 // allow deprecated HTTP request from SystemPrincipal 324 nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); 325 loadInfo->SetAllowDeprecatedSystemRequests(true); 326 nsCOMPtr<nsIIncrementalStreamLoader> loader; 327 rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), this); 328 NS_ENSURE_SUCCESS(rv, rv); 329 330 return channel->AsyncOpen(loader); 331 } 332 333 bool AsyncScriptCompiler::StartCompile(JSContext* aCx) { 334 JS::SourceText<Utf8Unit> srcBuf; 335 if (!srcBuf.init(aCx, std::move(mScriptText), mScriptLength)) { 336 return false; 337 } 338 339 // TODO: This uses the same heuristics and the same threshold as the 340 // JS::CanCompileOffThread, but the heuristics needs to be updated to 341 // reflect the change regarding the Stencil API, and also the thread 342 // management on the consumer side (bug 1846388). 343 static constexpr size_t OffThreadMinimumTextLength = 5 * 1000; 344 345 if (StaticPrefs::javascript_options_parallel_parsing() && 346 mScriptLength >= OffThreadMinimumTextLength) { 347 if (!StartOffThreadCompile(std::move(srcBuf))) { 348 return false; 349 } 350 return true; 351 } 352 353 RefPtr<Stencil> stencil = 354 JS::CompileGlobalScriptToStencil(aCx, mOptions, srcBuf); 355 if (!stencil) { 356 return false; 357 } 358 359 Finish(aCx, std::move(stencil)); 360 return true; 361 } 362 363 bool AsyncScriptCompiler::StartOffThreadCompile( 364 JS::SourceText<Utf8Unit>&& aSrcBuf) { 365 RefPtr<AsyncScriptCompileTask> compileTask = 366 new AsyncScriptCompileTask(std::move(aSrcBuf)); 367 368 RefPtr<AsyncScriptCompilationCompleteTask> complationCompleteTask = 369 new AsyncScriptCompilationCompleteTask(this, compileTask.get()); 370 371 if (!compileTask->Init(mOptions)) { 372 return false; 373 } 374 375 complationCompleteTask->AddDependency(compileTask.get()); 376 377 TaskController::Get()->AddTask(compileTask.forget()); 378 TaskController::Get()->AddTask(complationCompleteTask.forget()); 379 return true; 380 } 381 382 Task::TaskResult AsyncScriptCompilationCompleteTask::Run() { 383 mCompiler->OnCompilationComplete(mCompileTask.get()); 384 mCompiler = nullptr; 385 mCompileTask = nullptr; 386 return TaskResult::Complete; 387 } 388 389 void AsyncScriptCompiler::OnCompilationComplete( 390 AsyncScriptCompileTask* aCompileTask) { 391 AutoJSAPI jsapi; 392 if (!jsapi.Init(mGlobalObject)) { 393 mPromise->MaybeReject(NS_ERROR_FAILURE); 394 return; 395 } 396 397 JSContext* cx = jsapi.cx(); 398 RefPtr<JS::Stencil> stencil = aCompileTask->StealStencil(cx); 399 if (!stencil) { 400 Reject(cx); 401 return; 402 } 403 404 Finish(cx, std::move(stencil)); 405 return; 406 } 407 408 void AsyncScriptCompiler::Finish(JSContext* aCx, 409 RefPtr<JS::Stencil>&& aStencil) { 410 RefPtr<PrecompiledScript> result = 411 new PrecompiledScript(mGlobalObject, aStencil, mOptions); 412 413 mPromise->MaybeResolve(result); 414 } 415 416 void AsyncScriptCompiler::Reject(JSContext* aCx) { 417 RootedValue value(aCx, JS::UndefinedValue()); 418 if (JS_GetPendingException(aCx, &value)) { 419 JS_ClearPendingException(aCx); 420 } 421 mPromise->MaybeReject(value); 422 } 423 424 void AsyncScriptCompiler::Reject(JSContext* aCx, const char* aMsg) { 425 nsAutoString msg; 426 msg.AppendASCII(aMsg); 427 msg.AppendLiteral(": "); 428 nsDependentCString filename(mOptions.filename().c_str()); 429 AppendUTF8toUTF16(filename, msg); 430 431 RootedValue exn(aCx); 432 if (xpc::NonVoidStringToJsval(aCx, msg, &exn)) { 433 JS_SetPendingException(aCx, exn); 434 } 435 436 Reject(aCx); 437 } 438 439 NS_IMETHODIMP 440 AsyncScriptCompiler::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } 441 442 NS_IMETHODIMP 443 AsyncScriptCompiler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, 444 nsISupports* aContext, 445 uint32_t aDataLength, 446 const uint8_t* aData, 447 uint32_t* aConsumedData) { 448 return NS_OK; 449 } 450 451 NS_IMETHODIMP 452 AsyncScriptCompiler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, 453 nsISupports* aContext, nsresult aStatus, 454 uint32_t aLength, const uint8_t* aBuf) { 455 AutoJSAPI jsapi; 456 if (!jsapi.Init(mGlobalObject)) { 457 mPromise->MaybeReject(NS_ERROR_FAILURE); 458 return NS_OK; 459 } 460 461 JSContext* cx = jsapi.cx(); 462 463 if (NS_FAILED(aStatus)) { 464 Reject(cx, "Unable to load script"); 465 return NS_OK; 466 } 467 468 nsresult rv = ScriptLoader::ConvertToUTF8( 469 nullptr, aBuf, aLength, mCharset, nullptr, mScriptText, mScriptLength); 470 if (NS_FAILED(rv)) { 471 Reject(cx, "Unable to decode script"); 472 return NS_OK; 473 } 474 475 if (!StartCompile(cx)) { 476 Reject(cx); 477 } 478 479 return NS_OK; 480 } 481 482 namespace mozilla { 483 namespace dom { 484 485 /* static */ 486 already_AddRefed<Promise> ChromeUtils::CompileScript( 487 GlobalObject& aGlobal, const nsAString& aURL, 488 const CompileScriptOptionsDictionary& aOptions, ErrorResult& aRv) { 489 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 490 MOZ_ASSERT(global); 491 492 RefPtr<Promise> promise = Promise::Create(global, aRv); 493 if (aRv.Failed()) { 494 return nullptr; 495 } 496 497 NS_ConvertUTF16toUTF8 url(aURL); 498 RefPtr<AsyncScriptCompiler> compiler = 499 new AsyncScriptCompiler(aGlobal.Context(), global, url, promise); 500 501 nsresult rv = compiler->Start(aGlobal.Context(), aOptions, 502 aGlobal.GetSubjectPrincipal()); 503 if (NS_FAILED(rv)) { 504 promise->MaybeReject(rv); 505 } 506 507 return promise.forget(); 508 } 509 510 PrecompiledScript::PrecompiledScript(nsISupports* aParent, 511 RefPtr<JS::Stencil> aStencil, 512 JS::ReadOnlyCompileOptions& aOptions) 513 : mParent(aParent), 514 mStencil(aStencil), 515 mPublicURL(aOptions.filename().c_str()), 516 mHasReturnValue(!aOptions.noScriptRval) { 517 MOZ_ASSERT(aParent); 518 MOZ_ASSERT(aStencil); 519 #ifdef DEBUG 520 // AsyncScriptCompiler::Start can call JS::CompileOptions::setForceFullParse, 521 // but it should be compatible with the default JS::InstantiateOptions. 522 JS::InstantiateOptions options(aOptions); 523 options.assertCompatibleWithDefault(); 524 #endif 525 }; 526 527 void PrecompiledScript::ExecuteInGlobal(JSContext* aCx, HandleObject aGlobal, 528 const ExecuteInGlobalOptions& aOptions, 529 MutableHandleValue aRval, 530 ErrorResult& aRv) { 531 { 532 RootedObject targetObj(aCx, JS_FindCompilationScope(aCx, aGlobal)); 533 // Use AutoEntryScript for its ReportException method call. 534 // This will ensure notified any exception happening in the content script 535 // directly to the console, so that exceptions are flagged with the right 536 // innerWindowID. It helps these exceptions to appear in the page's web 537 // console. 538 AutoEntryScript aes(targetObj, "pre-compiled-script execution"); 539 JSContext* cx = aes.cx(); 540 541 // See assertion in constructor. 542 JS::InstantiateOptions options; 543 Rooted<JSScript*> script( 544 cx, JS::InstantiateGlobalStencil(cx, options, mStencil)); 545 if (!script) { 546 aRv.NoteJSContextException(aCx); 547 return; 548 } 549 550 if (!JS_ExecuteScript(cx, script, aRval)) { 551 JS::RootedValue exn(cx); 552 if (aOptions.mReportExceptions) { 553 // Note that ReportException will consume the exception. 554 aes.ReportException(); 555 } else { 556 // Set the exception on our caller's cx. 557 aRv.MightThrowJSException(); 558 aRv.StealExceptionFromJSContext(cx); 559 } 560 return; 561 } 562 } 563 564 JS_WrapValue(aCx, aRval); 565 } 566 567 void PrecompiledScript::GetUrl(nsAString& aUrl) { 568 CopyUTF8toUTF16(mPublicURL, aUrl); 569 } 570 571 bool PrecompiledScript::HasReturnValue() { return mHasReturnValue; } 572 573 JSObject* PrecompiledScript::WrapObject(JSContext* aCx, 574 HandleObject aGivenProto) { 575 return PrecompiledScript_Binding::Wrap(aCx, this, aGivenProto); 576 } 577 578 bool PrecompiledScript::IsBlackForCC(bool aTracingNeeded) { 579 return (nsCCUncollectableMarker::sGeneration && HasKnownLiveWrapper() && 580 (!aTracingNeeded || HasNothingToTrace(this))); 581 } 582 583 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PrecompiledScript, mParent) 584 585 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PrecompiledScript) 586 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 587 NS_INTERFACE_MAP_ENTRY(nsISupports) 588 NS_INTERFACE_MAP_END 589 590 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(PrecompiledScript) 591 return tmp->IsBlackForCC(false); 592 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END 593 594 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(PrecompiledScript) 595 return tmp->IsBlackForCC(true); 596 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END 597 598 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(PrecompiledScript) 599 return tmp->IsBlackForCC(false); 600 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END 601 602 NS_IMPL_CYCLE_COLLECTING_ADDREF(PrecompiledScript) 603 NS_IMPL_CYCLE_COLLECTING_RELEASE(PrecompiledScript) 604 605 } // namespace dom 606 } // namespace mozilla