diff options
Diffstat (limited to 'dom/worklet')
45 files changed, 3918 insertions, 0 deletions
diff --git a/dom/worklet/Worklet.cpp b/dom/worklet/Worklet.cpp new file mode 100644 index 0000000000..7c37bdbba9 --- /dev/null +++ b/dom/worklet/Worklet.cpp @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "Worklet.h" +#include "WorkletThread.h" + +#include "mozilla/dom/WorkletFetchHandler.h" +#include "mozilla/dom/WorkletImpl.h" +#include "xpcprivate.h" + +using JS::loader::ResolveError; +using JS::loader::ResolveErrorInfo; + +namespace mozilla::dom { +// --------------------------------------------------------------------------- +// Worklet + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Worklet) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Worklet) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwnedObject) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mImportHandlers) + tmp->mImpl->NotifyWorkletFinished(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Worklet) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwnedObject) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImportHandlers) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Worklet) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Worklet) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Worklet) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +Worklet::Worklet(nsPIDOMWindowInner* aWindow, RefPtr<WorkletImpl> aImpl, + nsISupports* aOwnedObject) + : mWindow(aWindow), mOwnedObject(aOwnedObject), mImpl(std::move(aImpl)) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(mImpl); + MOZ_ASSERT(NS_IsMainThread()); +} + +Worklet::~Worklet() { mImpl->NotifyWorkletFinished(); } + +JSObject* Worklet::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mImpl->WrapWorklet(aCx, this, aGivenProto); +} + +static bool LoadLocalizedStrings(nsTArray<nsString>& aStrings) { + // All enumes in ResolveError. + ResolveError errors[] = {ResolveError::Failure, + ResolveError::FailureMayBeBare, + ResolveError::BlockedByNullEntry, + ResolveError::BlockedByAfterPrefix, + ResolveError::BlockedByBacktrackingPrefix, + ResolveError::InvalidBareSpecifier}; + + static_assert( + ArrayLength(errors) == static_cast<size_t>(ResolveError::Length), + "The array 'errors' has missing entries in the enum class ResolveError."); + + for (auto i : errors) { + nsAutoString message; + nsresult rv = nsContentUtils::GetLocalizedString( + nsContentUtils::eDOM_PROPERTIES, ResolveErrorInfo::GetString(i), + message); + if (NS_WARN_IF(NS_FAILED(rv))) { + if (NS_WARN_IF(!aStrings.AppendElement(EmptyString(), fallible))) { + return false; + } + } else { + if (NS_WARN_IF(!aStrings.AppendElement(message, fallible))) { + return false; + } + } + } + + return true; +} + +already_AddRefed<Promise> Worklet::AddModule(JSContext* aCx, + const nsAString& aModuleURL, + const WorkletOptions& aOptions, + CallerType aCallerType, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + if (mLocalizedStrings.IsEmpty()) { + bool result = LoadLocalizedStrings(mLocalizedStrings); + if (!result) { + return nullptr; + } + } + + return WorkletFetchHandler::AddModule(this, aCx, aModuleURL, aOptions, aRv); +} + +WorkletFetchHandler* Worklet::GetImportFetchHandler(const nsACString& aURI) { + MOZ_ASSERT(NS_IsMainThread()); + return mImportHandlers.GetWeak(aURI); +} + +void Worklet::AddImportFetchHandler(const nsACString& aURI, + WorkletFetchHandler* aHandler) { + MOZ_ASSERT(aHandler); + MOZ_ASSERT(!mImportHandlers.GetWeak(aURI)); + MOZ_ASSERT(NS_IsMainThread()); + + mImportHandlers.InsertOrUpdate(aURI, RefPtr{aHandler}); +} + +} // namespace mozilla::dom diff --git a/dom/worklet/Worklet.h b/dom/worklet/Worklet.h new file mode 100644 index 0000000000..1c404da18d --- /dev/null +++ b/dom/worklet/Worklet.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_dom_Worklet_h +#define mozilla_dom_Worklet_h + +#include "mozilla/Attributes.h" +#include "nsRefPtrHashtable.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" + +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; +class WorkletImpl; + +namespace dom { + +class Promise; +class WorkletFetchHandler; +class WorkletScriptHandler; +struct WorkletOptions; +enum class CallerType : uint32_t; + +class Worklet final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Worklet) + + // |aOwnedObject| may be provided by the WorkletImpl as a parent thread + // object to keep alive and traverse for CC as long as the Worklet has + // references remaining. + Worklet(nsPIDOMWindowInner* aWindow, RefPtr<WorkletImpl> aImpl, + nsISupports* aOwnedObject = nullptr); + + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<Promise> AddModule(JSContext* aCx, + const nsAString& aModuleURL, + const WorkletOptions& aOptions, + CallerType aCallerType, ErrorResult& aRv); + + WorkletImpl* Impl() const { return mImpl; } + + const nsTArray<nsString>& GetLocalizedStrings() const { + return mLocalizedStrings; + } + + private: + ~Worklet(); + + WorkletFetchHandler* GetImportFetchHandler(const nsACString& aURI); + + void AddImportFetchHandler(const nsACString& aURI, + WorkletFetchHandler* aHandler); + + nsCOMPtr<nsPIDOMWindowInner> mWindow; + nsCOMPtr<nsISupports> mOwnedObject; + + nsRefPtrHashtable<nsCStringHashKey, WorkletFetchHandler> mImportHandlers; + + const RefPtr<WorkletImpl> mImpl; + + nsTArray<nsString> mLocalizedStrings; + + friend class WorkletFetchHandler; + friend class WorkletScriptHandler; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Worklet_h diff --git a/dom/worklet/WorkletFetchHandler.cpp b/dom/worklet/WorkletFetchHandler.cpp new file mode 100644 index 0000000000..b259d6a357 --- /dev/null +++ b/dom/worklet/WorkletFetchHandler.cpp @@ -0,0 +1,648 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#include "WorkletFetchHandler.h" + +#include "js/loader/ModuleLoadRequest.h" +#include "js/ContextOptions.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/ScriptLoadHandler.h" // ScriptDecoder +#include "mozilla/dom/Worklet.h" +#include "mozilla/dom/WorkletBinding.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "mozilla/dom/WorkletImpl.h" +#include "mozilla/dom/WorkletThread.h" +#include "mozilla/dom/worklet/WorkletModuleLoader.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/TaskQueue.h" +#include "nsIInputStreamPump.h" +#include "nsIThreadRetargetableRequest.h" +#include "xpcpublic.h" + +using JS::loader::ModuleLoadRequest; +using JS::loader::ParserMetadata; +using JS::loader::ScriptFetchOptions; +using mozilla::dom::loader::WorkletModuleLoader; + +namespace mozilla::dom { + +// A Runnable to call ModuleLoadRequest::StartModuleLoad on a worklet thread. +class StartModuleLoadRunnable final : public Runnable { + public: + StartModuleLoadRunnable( + WorkletImpl* aWorkletImpl, + const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef, + nsCOMPtr<nsIURI> aURI, nsIURI* aReferrer, + const nsTArray<nsString>& aLocalizedStrs) + : Runnable("Worklet::StartModuleLoadRunnable"), + mWorkletImpl(aWorkletImpl), + mHandlerRef(aHandlerRef), + mURI(std::move(aURI)), + mReferrer(aReferrer), + mLocalizedStrs(aLocalizedStrs), + mParentRuntime( + JS_GetParentRuntime(CycleCollectedJSContext::Get()->Context())) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mParentRuntime); + xpc::SetPrefableContextOptions(mContextOptions); + } + + ~StartModuleLoadRunnable() = default; + + NS_IMETHOD Run() override; + + private: + NS_IMETHOD RunOnWorkletThread(); + + RefPtr<WorkletImpl> mWorkletImpl; + nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrer; + const nsTArray<nsString>& mLocalizedStrs; + JSRuntime* mParentRuntime; + JS::ContextOptions mContextOptions; +}; + +NS_IMETHODIMP +StartModuleLoadRunnable::Run() { + // WorkletThread::IsOnWorkletThread() cannot be used here because it depends + // on a WorkletJSContext having been created for this thread. That does not + // happen until the global scope is created the first time + // RunOnWorkletThread() is called. + MOZ_ASSERT(!NS_IsMainThread()); + return RunOnWorkletThread(); +} + +NS_IMETHODIMP StartModuleLoadRunnable::RunOnWorkletThread() { + // This can be called on a GraphRunner thread or a DOM Worklet thread. + WorkletThread::EnsureCycleCollectedJSContext(mParentRuntime, mContextOptions); + + WorkletGlobalScope* globalScope = mWorkletImpl->GetGlobalScope(); + if (!globalScope) { + return NS_ERROR_DOM_UNKNOWN_ERR; + } + + // To fetch a worklet/module worker script graph: + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-worklet/module-worker-script-graph + // Step 1. Let options be a script fetch options whose cryptographic nonce is + // the empty string, integrity metadata is the empty string, parser metadata + // is "not-parser-inserted", credentials mode is credentials mode, referrer + // policy is the empty string, and fetch priority is "auto". + RefPtr<ScriptFetchOptions> fetchOptions = new ScriptFetchOptions( + CORSMode::CORS_NONE, /* aNonce = */ u""_ns, RequestPriority::Auto, + ParserMetadata::NotParserInserted, + /*triggeringPrincipal*/ nullptr); + + WorkletModuleLoader* moduleLoader = + static_cast<WorkletModuleLoader*>(globalScope->GetModuleLoader()); + MOZ_ASSERT(moduleLoader); + + if (!moduleLoader->HasSetLocalizedStrings()) { + moduleLoader->SetLocalizedStrings(&mLocalizedStrs); + } + + RefPtr<WorkletLoadContext> loadContext = new WorkletLoadContext(mHandlerRef); + + // Part of Step 2. This sets the Top-level flag to true + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + mURI, ReferrerPolicy::_empty, fetchOptions, SRIMetadata(), mReferrer, + loadContext, true, /* is top level */ + false, /* is dynamic import */ + moduleLoader, ModuleLoadRequest::NewVisitedSetForTopLevelImport(mURI), + nullptr); + + request->mURL = request->mURI->GetSpecOrDefault(); + request->NoCacheEntryFound(); + + return request->StartModuleLoad(); +} + +StartFetchRunnable::StartFetchRunnable( + const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef, nsIURI* aURI, + nsIURI* aReferrer) + : Runnable("Worklet::StartFetchRunnable"), + mHandlerRef(aHandlerRef), + mURI(aURI), + mReferrer(aReferrer) { + MOZ_ASSERT(!NS_IsMainThread()); +} + +NS_IMETHODIMP +StartFetchRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(mHandlerRef->mWorklet->GetParentObject()); + MOZ_ASSERT(global); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(global))) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + nsresult rv = mHandlerRef->StartFetch(cx, mURI, mReferrer); + if (NS_FAILED(rv)) { + mHandlerRef->HandleFetchFailed(mURI); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +// A Runnable to call ModuleLoadRequest::OnFetchComplete on a worklet thread. +class FetchCompleteRunnable final : public Runnable { + public: + FetchCompleteRunnable(WorkletImpl* aWorkletImpl, nsIURI* aURI, + nsresult aResult, + UniquePtr<uint8_t[]> aScriptBuffer = nullptr, + size_t aScriptLength = 0) + : Runnable("Worklet::FetchCompleteRunnable"), + mWorkletImpl(aWorkletImpl), + mURI(aURI), + mResult(aResult), + mScriptBuffer(std::move(aScriptBuffer)), + mScriptLength(aScriptLength) { + MOZ_ASSERT(NS_IsMainThread()); + } + + ~FetchCompleteRunnable() = default; + + NS_IMETHOD Run() override; + + private: + NS_IMETHOD RunOnWorkletThread(); + + RefPtr<WorkletImpl> mWorkletImpl; + nsCOMPtr<nsIURI> mURI; + nsresult mResult; + UniquePtr<uint8_t[]> mScriptBuffer; + size_t mScriptLength; +}; + +NS_IMETHODIMP +FetchCompleteRunnable::Run() { + MOZ_ASSERT(WorkletThread::IsOnWorkletThread()); + return RunOnWorkletThread(); +} + +NS_IMETHODIMP FetchCompleteRunnable::RunOnWorkletThread() { + WorkletGlobalScope* globalScope = mWorkletImpl->GetGlobalScope(); + if (!globalScope) { + return NS_ERROR_DOM_UNKNOWN_ERR; + } + + WorkletModuleLoader* moduleLoader = + static_cast<WorkletModuleLoader*>(globalScope->GetModuleLoader()); + MOZ_ASSERT(moduleLoader); + MOZ_ASSERT(mURI); + ModuleLoadRequest* request = moduleLoader->GetRequest(mURI); + MOZ_ASSERT(request); + + // Set the Source type to "text" for decoding. + request->SetTextSource(request->mLoadContext.get()); + + nsresult rv; + if (mScriptBuffer) { + UniquePtr<ScriptDecoder> decoder = MakeUnique<ScriptDecoder>( + UTF_8_ENCODING, ScriptDecoder::BOMHandling::Remove); + rv = decoder->DecodeRawData(request, mScriptBuffer.get(), mScriptLength, + true); + NS_ENSURE_SUCCESS(rv, rv); + } + + request->mBaseURL = mURI; + request->OnFetchComplete(mResult); + + if (NS_FAILED(mResult)) { + if (request->IsTopLevel()) { + request->LoadFailed(); + } else { + request->Cancel(); + } + } + + moduleLoader->RemoveRequest(mURI); + return NS_OK; +} + +////////////////////////////////////////////////////////////// +// WorkletFetchHandler +////////////////////////////////////////////////////////////// +NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletFetchHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletFetchHandler) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletFetchHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkletFetchHandler) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkletFetchHandler) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWorklet, mPromises) + tmp->mErrorToRethrow.setUndefined(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkletFetchHandler) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWorklet, mPromises) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WorkletFetchHandler) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mErrorToRethrow) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +// static +already_AddRefed<Promise> WorkletFetchHandler::AddModule( + Worklet* aWorklet, JSContext* aCx, const nsAString& aModuleURL, + const WorkletOptions& aOptions, ErrorResult& aRv) { + MOZ_ASSERT(aWorklet); + MOZ_ASSERT(NS_IsMainThread()); + + aWorklet->Impl()->OnAddModuleStarted(); + + auto promiseSettledGuard = + MakeScopeExit([&] { aWorklet->Impl()->OnAddModulePromiseSettled(); }); + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(aWorklet->GetParentObject()); + MOZ_ASSERT(global); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> window = aWorklet->GetParentObject(); + MOZ_ASSERT(window); + + nsCOMPtr<Document> doc; + doc = window->GetExtantDoc(); + if (!doc) { + promise->MaybeReject(NS_ERROR_FAILURE); + return promise.forget(); + } + + nsCOMPtr<nsIURI> resolvedURI; + nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), aModuleURL, nullptr, + doc->GetBaseURI()); + if (NS_WARN_IF(NS_FAILED(rv))) { + // https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule + // Step 3. If this fails, then return a promise rejected with a + // "SyntaxError" DOMException. + rv = NS_ERROR_DOM_SYNTAX_ERR; + + promise->MaybeReject(rv); + return promise.forget(); + } + + nsAutoCString spec; + rv = resolvedURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + rv = NS_ERROR_DOM_SYNTAX_ERR; + + promise->MaybeReject(rv); + return promise.forget(); + } + + // Maybe we already have an handler for this URI + { + WorkletFetchHandler* handler = aWorklet->GetImportFetchHandler(spec); + if (handler) { + handler->AddPromise(aCx, promise); + return promise.forget(); + } + } + + RefPtr<WorkletFetchHandler> handler = + new WorkletFetchHandler(aWorklet, promise, aOptions.mCredentials); + + nsMainThreadPtrHandle<WorkletFetchHandler> handlerRef{ + new nsMainThreadPtrHolder<WorkletFetchHandler>("FetchHandler", handler)}; + + // Determine request's Referrer + // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer + // Step 3. Switch on request’s referrer: + // "client" + // Step 1.4. Let referrerSource be document’s URL. + nsIURI* referrer = doc->GetDocumentURIAsReferrer(); + nsCOMPtr<nsIRunnable> runnable = new StartModuleLoadRunnable( + aWorklet->mImpl, handlerRef, std::move(resolvedURI), referrer, + aWorklet->GetLocalizedStrings()); + + if (NS_FAILED(aWorklet->mImpl->SendControlMessage(runnable.forget()))) { + return nullptr; + } + + promiseSettledGuard.release(); + + aWorklet->AddImportFetchHandler(spec, handler); + return promise.forget(); +} + +WorkletFetchHandler::WorkletFetchHandler(Worklet* aWorklet, Promise* aPromise, + RequestCredentials aCredentials) + : mWorklet(aWorklet), mStatus(ePending), mCredentials(aCredentials) { + MOZ_ASSERT(aWorklet); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(NS_IsMainThread()); + + mPromises.AppendElement(aPromise); +} + +WorkletFetchHandler::~WorkletFetchHandler() { mozilla::DropJSObjects(this); } + +void WorkletFetchHandler::ExecutionFailed() { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromises(NS_ERROR_DOM_ABORT_ERR); +} + +void WorkletFetchHandler::ExecutionFailed(JS::Handle<JS::Value> aError) { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromises(aError); +} + +void WorkletFetchHandler::ExecutionSucceeded() { + MOZ_ASSERT(NS_IsMainThread()); + ResolvePromises(); +} + +void WorkletFetchHandler::AddPromise(JSContext* aCx, Promise* aPromise) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(NS_IsMainThread()); + + switch (mStatus) { + case ePending: + mPromises.AppendElement(aPromise); + return; + + case eRejected: + if (mHasError) { + JS::Rooted<JS::Value> error(aCx, mErrorToRethrow); + aPromise->MaybeReject(error); + } else { + aPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + return; + + case eResolved: + aPromise->MaybeResolveWithUndefined(); + return; + } +} + +void WorkletFetchHandler::RejectPromises(nsresult aResult) { + MOZ_ASSERT(mStatus == ePending); + MOZ_ASSERT(NS_FAILED(aResult)); + MOZ_ASSERT(NS_IsMainThread()); + + mWorklet->Impl()->OnAddModulePromiseSettled(); + + for (uint32_t i = 0; i < mPromises.Length(); ++i) { + mPromises[i]->MaybeReject(aResult); + } + mPromises.Clear(); + + mStatus = eRejected; + mWorklet = nullptr; +} + +void WorkletFetchHandler::RejectPromises(JS::Handle<JS::Value> aValue) { + MOZ_ASSERT(mStatus == ePending); + MOZ_ASSERT(NS_IsMainThread()); + + mWorklet->Impl()->OnAddModulePromiseSettled(); + + for (uint32_t i = 0; i < mPromises.Length(); ++i) { + mPromises[i]->MaybeReject(aValue); + } + mPromises.Clear(); + + mHasError = true; + mErrorToRethrow = aValue; + + mozilla::HoldJSObjects(this); + + mStatus = eRejected; + mWorklet = nullptr; +} + +void WorkletFetchHandler::ResolvePromises() { + MOZ_ASSERT(mStatus == ePending); + MOZ_ASSERT(NS_IsMainThread()); + + mWorklet->Impl()->OnAddModulePromiseSettled(); + + for (uint32_t i = 0; i < mPromises.Length(); ++i) { + mPromises[i]->MaybeResolveWithUndefined(); + } + mPromises.Clear(); + + mStatus = eResolved; + mWorklet = nullptr; +} + +nsresult WorkletFetchHandler::StartFetch(JSContext* aCx, nsIURI* aURI, + nsIURI* aReferrer) { + nsAutoCString spec; + nsresult res = aURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(res))) { + return NS_ERROR_FAILURE; + } + + RequestOrUSVString requestInput; + + nsAutoString url; + CopyUTF8toUTF16(spec, url); + requestInput.SetAsUSVString().ShareOrDependUpon(url); + + RootedDictionary<RequestInit> requestInit(aCx); + requestInit.mCredentials.Construct(mCredentials); + + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script + // Step 8. mode is "cors" + requestInit.mMode.Construct(RequestMode::Cors); + + if (aReferrer) { + nsAutoString referrer; + res = aReferrer->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(res))) { + return NS_ERROR_FAILURE; + } + + CopyUTF8toUTF16(spec, referrer); + requestInit.mReferrer.Construct(referrer); + } + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(mWorklet->GetParentObject()); + MOZ_ASSERT(global); + + IgnoredErrorResult rv; + SafeRefPtr<Request> request = + Request::Constructor(global, aCx, requestInput, requestInit, rv); + if (rv.Failed()) { + return NS_ERROR_FAILURE; + } + + request->OverrideContentPolicyType(mWorklet->Impl()->ContentPolicyType()); + + RequestOrUSVString finalRequestInput; + finalRequestInput.SetAsRequest() = request.unsafeGetRawPtr(); + + RefPtr<Promise> fetchPromise = FetchRequest( + global, finalRequestInput, requestInit, CallerType::System, rv); + if (NS_WARN_IF(rv.Failed())) { + return NS_ERROR_FAILURE; + } + + RefPtr<WorkletScriptHandler> scriptHandler = + new WorkletScriptHandler(mWorklet, aURI); + fetchPromise->AppendNativeHandler(scriptHandler); + return NS_OK; +} + +void WorkletFetchHandler::HandleFetchFailed(nsIURI* aURI) { + nsCOMPtr<nsIRunnable> runnable = new FetchCompleteRunnable( + mWorklet->mImpl, aURI, NS_ERROR_FAILURE, nullptr, 0); + + if (NS_WARN_IF( + NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget())))) { + NS_WARNING("Failed to dispatch FetchCompleteRunnable to a worklet thread."); + } +} + +////////////////////////////////////////////////////////////// +// WorkletScriptHandler +////////////////////////////////////////////////////////////// +NS_IMPL_ISUPPORTS(WorkletScriptHandler, nsIStreamLoaderObserver) + +WorkletScriptHandler::WorkletScriptHandler(Worklet* aWorklet, nsIURI* aURI) + : mWorklet(aWorklet), mURI(aURI) {} + +void WorkletScriptHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aValue.isObject()) { + HandleFailure(NS_ERROR_FAILURE); + return; + } + + RefPtr<Response> response; + nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleFailure(NS_ERROR_FAILURE); + return; + } + + // https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule + // Step 6.4.1. If script is null, then: + // Step 1.1.2. Reject promise with an "AbortError" DOMException. + if (!response->Ok()) { + HandleFailure(NS_ERROR_DOM_ABORT_ERR); + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + response->GetBody(getter_AddRefs(inputStream)); + if (!inputStream) { + HandleFailure(NS_ERROR_DOM_NETWORK_ERR); + return; + } + + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleFailure(rv); + return; + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleFailure(rv); + return; + } + + rv = pump->AsyncRead(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleFailure(rv); + return; + } + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(pump); + if (rr) { + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + RefPtr<TaskQueue> queue = TaskQueue::Create( + sts.forget(), "WorkletScriptHandler STS Delivery Queue"); + rv = rr->RetargetDeliveryTo(queue); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the nsIInputStreamPump to a IO thread."); + } + } +} + +NS_IMETHODIMP WorkletScriptHandler::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, + nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(aStatus)) { + HandleFailure(aStatus); + return NS_OK; + } + + // Copy the buffer and decode it on worklet thread, as we can only access + // ModuleLoadRequest on worklet thread. + UniquePtr<uint8_t[]> scriptTextBuf = MakeUnique<uint8_t[]>(aStringLen); + memcpy(scriptTextBuf.get(), aString, aStringLen); + + nsCOMPtr<nsIRunnable> runnable = new FetchCompleteRunnable( + mWorklet->mImpl, mURI, NS_OK, std::move(scriptTextBuf), aStringLen); + + if (NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget()))) { + HandleFailure(NS_ERROR_FAILURE); + } + + return NS_OK; +} + +void WorkletScriptHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + // https://html.spec.whatwg.org/multipage/worklets.html#dom-worklet-addmodule + // Step 6.4.1. If script is null, then: + // Step 1.1.2. Reject promise with an "AbortError" DOMException. + HandleFailure(NS_ERROR_DOM_ABORT_ERR); +} + +void WorkletScriptHandler::HandleFailure(nsresult aResult) { + DispatchFetchCompleteToWorklet(aResult); +} + +void WorkletScriptHandler::DispatchFetchCompleteToWorklet(nsresult aRv) { + nsCOMPtr<nsIRunnable> runnable = + new FetchCompleteRunnable(mWorklet->mImpl, mURI, aRv, nullptr, 0); + + if (NS_WARN_IF( + NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget())))) { + NS_WARNING("Failed to dispatch FetchCompleteRunnable to a worklet thread."); + } +} + +} // namespace mozilla::dom diff --git a/dom/worklet/WorkletFetchHandler.h b/dom/worklet/WorkletFetchHandler.h new file mode 100644 index 0000000000..a380caf9eb --- /dev/null +++ b/dom/worklet/WorkletFetchHandler.h @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_dom_WorkletFetchHandler_h +#define mozilla_dom_WorkletFetchHandler_h + +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/RequestBinding.h" // RequestCredentials +#include "nsIStreamLoader.h" + +namespace mozilla::dom { +class Worklet; +struct WorkletOptions; +class WorkletScriptHandler; + +namespace loader { +class AddModuleThrowErrorRunnable; +} // namespace loader + +// WorkletFetchHandler is used to fetch the module scripts on the main thread, +// and notifies the result of addModule back to |aWorklet|. +class WorkletFetchHandler final : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WorkletFetchHandler) + + static already_AddRefed<Promise> AddModule(Worklet* aWorklet, JSContext* aCx, + const nsAString& aModuleURL, + const WorkletOptions& aOptions, + ErrorResult& aRv); + + // Load a module script on main thread. + nsresult StartFetch(JSContext* aCx, nsIURI* aURI, nsIURI* aReferrer); + + void ExecutionFailed(); + void ExecutionFailed(JS::Handle<JS::Value> aError); + + void ExecutionSucceeded(); + + void HandleFetchFailed(nsIURI* aURI); + + private: + WorkletFetchHandler(Worklet* aWorklet, Promise* aPromise, + RequestCredentials aCredentials); + + ~WorkletFetchHandler(); + + void AddPromise(JSContext* aCx, Promise* aPromise); + + void RejectPromises(nsresult aResult); + void RejectPromises(JS::Handle<JS::Value> aValue); + + void ResolvePromises(); + + friend class StartFetchRunnable; + friend class loader::AddModuleThrowErrorRunnable; + RefPtr<Worklet> mWorklet; + nsTArray<RefPtr<Promise>> mPromises; + + enum { ePending, eRejected, eResolved } mStatus; + + RequestCredentials mCredentials; + + bool mHasError = false; + JS::Heap<JS::Value> mErrorToRethrow; +}; + +// A Runnable to call WorkletFetchHandler::StartFetch on the main thread. +class StartFetchRunnable final : public Runnable { + public: + StartFetchRunnable( + const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef, + nsIURI* aURI, nsIURI* aReferrer); + ~StartFetchRunnable() = default; + + NS_IMETHOD + Run() override; + + private: + nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrer; +}; + +// WorkletScriptHandler is used to handle the result of fetching the module +// script. +class WorkletScriptHandler final : public PromiseNativeHandler, + public nsIStreamLoaderObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + WorkletScriptHandler(Worklet* aWorklet, nsIURI* aURI); + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + NS_IMETHOD + OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) override; + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void HandleFailure(nsresult aResult); + + private: + ~WorkletScriptHandler() = default; + + void DispatchFetchCompleteToWorklet(nsresult aRv); + + RefPtr<Worklet> mWorklet; + nsCOMPtr<nsIURI> mURI; +}; + +} // namespace mozilla::dom +#endif // mozilla_dom_WorkletFetchHandler_h diff --git a/dom/worklet/WorkletGlobalScope.cpp b/dom/worklet/WorkletGlobalScope.cpp new file mode 100644 index 0000000000..4437db398e --- /dev/null +++ b/dom/worklet/WorkletGlobalScope.cpp @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "WorkletGlobalScope.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/dom/WorkletGlobalScopeBinding.h" +#include "mozilla/dom/WorkletImpl.h" +#include "mozilla/dom/WorkletThread.h" +#include "mozilla/dom/worklet/WorkletModuleLoader.h" +#include "mozilla/dom/Console.h" +#include "js/RealmOptions.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "nsThreadUtils.h" +#include "nsRFPService.h" + +using JS::loader::ModuleLoaderBase; +using mozilla::dom::loader::WorkletModuleLoader; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WorkletGlobalScope) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkletGlobalScope) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mModuleLoader) + tmp->UnlinkObjectsInGlobal(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkletGlobalScope) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mModuleLoader) + tmp->TraverseObjectsInGlobal(cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletGlobalScope) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletGlobalScope) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletGlobalScope) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsIGlobalObject) + NS_INTERFACE_MAP_ENTRY(WorkletGlobalScope) +NS_INTERFACE_MAP_END + +WorkletGlobalScope::WorkletGlobalScope(WorkletImpl* aImpl) + : mImpl(aImpl), mCreationTimeStamp(TimeStamp::Now()) {} + +WorkletGlobalScope::~WorkletGlobalScope() = default; + +JSObject* WorkletGlobalScope::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + MOZ_CRASH("We should never get here!"); + return nullptr; +} + +nsISerialEventTarget* WorkletGlobalScope::SerialEventTarget() const { + WorkletThread::AssertIsOnWorkletThread(); + return NS_GetCurrentThread(); +} + +nsresult WorkletGlobalScope::Dispatch( + already_AddRefed<nsIRunnable>&& aRunnable) const { + WorkletThread::AssertIsOnWorkletThread(); + return SerialEventTarget()->Dispatch(std::move(aRunnable)); +} + +already_AddRefed<Console> WorkletGlobalScope::GetConsole(JSContext* aCx, + ErrorResult& aRv) { + if (!mConsole) { + MOZ_ASSERT(Impl()); + const WorkletLoadInfo& loadInfo = Impl()->LoadInfo(); + mConsole = Console::CreateForWorklet(aCx, this, loadInfo.OuterWindowID(), + loadInfo.InnerWindowID(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + RefPtr<Console> console = mConsole; + return console.forget(); +} + +void WorkletGlobalScope::InitModuleLoader(WorkletModuleLoader* aModuleLoader) { + MOZ_ASSERT(!mModuleLoader); + mModuleLoader = aModuleLoader; +} + +ModuleLoaderBase* WorkletGlobalScope::GetModuleLoader(JSContext* aCx) { + return mModuleLoader; +}; + +OriginTrials WorkletGlobalScope::Trials() const { return mImpl->Trials(); } + +Maybe<nsID> WorkletGlobalScope::GetAgentClusterId() const { + return mImpl->GetAgentClusterId(); +} + +bool WorkletGlobalScope::IsSharedMemoryAllowed() const { + return mImpl->IsSharedMemoryAllowed(); +} + +bool WorkletGlobalScope::ShouldResistFingerprinting(RFPTarget aTarget) const { + return mImpl->ShouldResistFingerprinting(aTarget); +} + +void WorkletGlobalScope::Dump(const Optional<nsAString>& aString) const { + WorkletThread::AssertIsOnWorkletThread(); + + if (!nsJSUtils::DumpEnabled()) { + return; + } + + if (!aString.WasPassed()) { + return; + } + + NS_ConvertUTF16toUTF8 str(aString.Value()); + + MOZ_LOG(nsContentUtils::DOMDumpLog(), mozilla::LogLevel::Debug, + ("[Worklet.Dump] %s", str.get())); +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get()); +#endif + fputs(str.get(), stdout); + fflush(stdout); +} + +JS::RealmOptions WorkletGlobalScope::CreateRealmOptions() const { + JS::RealmOptions options; + + options.creationOptions().setForceUTC( + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC)); + options.creationOptions().setAlwaysUseFdlibm( + ShouldResistFingerprinting(RFPTarget::JSMathFdlibm)); + if (ShouldResistFingerprinting(RFPTarget::JSLocale)) { + nsCString locale = nsRFPService::GetSpoofedJSLocale(); + options.creationOptions().setLocaleCopyZ(locale.get()); + } + + // The SharedArrayBuffer global constructor property should not be present in + // a fresh global object when shared memory objects aren't allowed (because + // COOP/COEP support isn't enabled, or because COOP/COEP don't act to isolate + // this worklet to a separate process). + options.creationOptions().setDefineSharedArrayBufferConstructor( + IsSharedMemoryAllowed()); + + return options; +} + +} // namespace mozilla::dom diff --git a/dom/worklet/WorkletGlobalScope.h b/dom/worklet/WorkletGlobalScope.h new file mode 100644 index 0000000000..7cb86f12d4 --- /dev/null +++ b/dom/worklet/WorkletGlobalScope.h @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_dom_WorkletGlobalScope_h +#define mozilla_dom_WorkletGlobalScope_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsDOMNavigationTiming.h" +#include "nsIGlobalObject.h" +#include "nsWrapperCache.h" + +#define WORKLET_IID \ + { \ + 0x1b3f62e7, 0xe357, 0x44be, { \ + 0xbf, 0xe0, 0xdf, 0x85, 0xe6, 0x56, 0x85, 0xac \ + } \ + } + +namespace JS { +class RealmOptions; +} + +namespace JS::loader { +class ModuleLoaderBase; +} + +namespace mozilla { + +class ErrorResult; +class WorkletImpl; + +namespace dom { + +namespace loader { +class WorkletModuleLoader; +} + +class Console; + +class WorkletGlobalScope : public nsIGlobalObject, public nsWrapperCache { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(WORKLET_IID) + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WorkletGlobalScope) + + WorkletGlobalScope(WorkletImpl*); + + nsIGlobalObject* GetParentObject() const { return nullptr; } + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; + virtual bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) = 0; + + JSObject* GetGlobalJSObject() override { return GetWrapper(); } + JSObject* GetGlobalJSObjectPreserveColor() const override { + return GetWrapperPreserveColor(); + } + + nsISerialEventTarget* SerialEventTarget() const final; + nsresult Dispatch(already_AddRefed<nsIRunnable>&&) const final; + + already_AddRefed<Console> GetConsole(JSContext* aCx, ErrorResult& aRv); + + WorkletImpl* Impl() const { return mImpl.get(); } + + void Dump(const Optional<nsAString>& aString) const; + + DOMHighResTimeStamp TimeStampToDOMHighRes(const TimeStamp& aTimeStamp) const { + MOZ_ASSERT(!aTimeStamp.IsNull()); + TimeDuration duration = aTimeStamp - mCreationTimeStamp; + return duration.ToMilliseconds(); + } + + void InitModuleLoader(loader::WorkletModuleLoader* aModuleLoader); + + JS::loader::ModuleLoaderBase* GetModuleLoader( + JSContext* aCx = nullptr) override; + + OriginTrials Trials() const override; + Maybe<nsID> GetAgentClusterId() const override; + bool IsSharedMemoryAllowed() const override; + bool ShouldResistFingerprinting(RFPTarget aTarget) const override; + + protected: + ~WorkletGlobalScope(); + + JS::RealmOptions CreateRealmOptions() const; + + const RefPtr<WorkletImpl> mImpl; + + private: + TimeStamp mCreationTimeStamp; + RefPtr<Console> mConsole; + RefPtr<loader::WorkletModuleLoader> mModuleLoader; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(WorkletGlobalScope, WORKLET_IID) + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_WorkletGlobalScope_h diff --git a/dom/worklet/WorkletImpl.cpp b/dom/worklet/WorkletImpl.cpp new file mode 100644 index 0000000000..96f7b4c1c9 --- /dev/null +++ b/dom/worklet/WorkletImpl.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "WorkletImpl.h" + +#include "Worklet.h" +#include "WorkletThread.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/RegisterWorkletBindings.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkletBinding.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "mozilla/dom/worklet/WorkletModuleLoader.h" +#include "nsGlobalWindowInner.h" + +using mozilla::dom::loader::WorkletModuleLoader; +using mozilla::dom::loader::WorkletScriptLoader; + +namespace mozilla { +// --------------------------------------------------------------------------- +// WorkletLoadInfo + +WorkletLoadInfo::WorkletLoadInfo(nsPIDOMWindowInner* aWindow) + : mInnerWindowID(aWindow->WindowID()) { + MOZ_ASSERT(NS_IsMainThread()); + nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow(); + if (outerWindow) { + mOuterWindowID = outerWindow->WindowID(); + } else { + mOuterWindowID = 0; + } +} + +// --------------------------------------------------------------------------- +// WorkletImpl + +WorkletImpl::WorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal) + : mPrincipal(NullPrincipal::CreateWithInheritedAttributes(aPrincipal)), + mWorkletLoadInfo(aWindow), + mTerminated(false), + mFinishedOnExecutionThread(false), + mIsPrivateBrowsing(false), + mTrials(OriginTrials::FromWindow(nsGlobalWindowInner::Cast(aWindow))) { + Unused << NS_WARN_IF( + NS_FAILED(ipc::PrincipalToPrincipalInfo(mPrincipal, &mPrincipalInfo))); + + if (aWindow->GetDocGroup()) { + mAgentClusterId.emplace(aWindow->GetDocGroup()->AgentClusterId()); + } + + mSharedMemoryAllowed = + nsGlobalWindowInner::Cast(aWindow)->IsSharedMemoryAllowed(); + + mShouldResistFingerprinting = aWindow->AsGlobal()->ShouldResistFingerprinting( + RFPTarget::IsAlwaysEnabledForPrecompute); + + RefPtr<dom::Document> doc = nsGlobalWindowInner::Cast(aWindow)->GetDocument(); + if (doc) { + mIsPrivateBrowsing = doc->IsInPrivateBrowsing(); + mOverriddenFingerprintingSettings = + doc->GetOverriddenFingerprintingSettings(); + } +} + +WorkletImpl::~WorkletImpl() { MOZ_ASSERT(!mGlobalScope); } + +JSObject* WorkletImpl::WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet, + JS::Handle<JSObject*> aGivenProto) { + MOZ_ASSERT(NS_IsMainThread()); + return dom::Worklet_Binding::Wrap(aCx, aWorklet, aGivenProto); +} + +dom::WorkletGlobalScope* WorkletImpl::GetGlobalScope() { + dom::WorkletThread::AssertIsOnWorkletThread(); + + if (mGlobalScope) { + return mGlobalScope; + } + if (mFinishedOnExecutionThread) { + return nullptr; + } + + dom::AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + mGlobalScope = ConstructGlobalScope(); + + JS::Rooted<JSObject*> global(cx); + NS_ENSURE_TRUE(mGlobalScope->WrapGlobalObject(cx, &global), nullptr); + + JSAutoRealm ar(cx, global); + + // Init Web IDL bindings + if (!dom::RegisterWorkletBindings(cx, global)) { + return nullptr; + } + + JS_FireOnNewGlobalObject(cx, global); + + MOZ_ASSERT(!mGlobalScope->GetModuleLoader(cx)); + + RefPtr<WorkletScriptLoader> scriptLoader = new WorkletScriptLoader(); + RefPtr<WorkletModuleLoader> moduleLoader = + new WorkletModuleLoader(scriptLoader, mGlobalScope); + mGlobalScope->InitModuleLoader(moduleLoader); + + return mGlobalScope; +} + +void WorkletImpl::NotifyWorkletFinished() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mTerminated) { + return; + } + + // Release global scope on its thread. + SendControlMessage( + NS_NewRunnableFunction("WorkletImpl::NotifyWorkletFinished", + [self = RefPtr<WorkletImpl>(this)]() { + self->mFinishedOnExecutionThread = true; + self->mGlobalScope = nullptr; + })); + + mTerminated = true; + if (mWorkletThread) { + mWorkletThread->Terminate(); + mWorkletThread = nullptr; + } +} + +nsresult WorkletImpl::SendControlMessage( + already_AddRefed<nsIRunnable> aRunnable) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<nsIRunnable> runnable = std::move(aRunnable); + + // TODO: bug 1492011 re ConsoleWorkletRunnable. + if (mTerminated) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + if (!mWorkletThread) { + // Thread creation. FIXME: this will change. + mWorkletThread = dom::WorkletThread::Create(this); + if (!mWorkletThread) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + } + + return mWorkletThread->DispatchRunnable(runnable.forget()); +} + +} // namespace mozilla diff --git a/dom/worklet/WorkletImpl.h b/dom/worklet/WorkletImpl.h new file mode 100644 index 0000000000..85dff0b8df --- /dev/null +++ b/dom/worklet/WorkletImpl.h @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_worklet_WorkletImpl_h +#define mozilla_dom_worklet_WorkletImpl_h + +#include "MainThreadUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Maybe.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/OriginTrials.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsRFPService.h" + +class nsPIDOMWindowInner; +class nsIPrincipal; +class nsIRunnable; + +namespace mozilla { + +namespace dom { + +class Worklet; +class WorkletGlobalScope; +class WorkletThread; + +} // namespace dom + +class WorkletLoadInfo { + public: + explicit WorkletLoadInfo(nsPIDOMWindowInner* aWindow); + + uint64_t OuterWindowID() const { return mOuterWindowID; } + uint64_t InnerWindowID() const { return mInnerWindowID; } + + private: + // Modified only in constructor. + uint64_t mOuterWindowID; + const uint64_t mInnerWindowID; +}; + +/** + * WorkletImpl is accessed from both the worklet's parent thread (on which the + * Worklet object lives) and the worklet's execution thread. It is owned by + * Worklet and WorkletGlobalScope. No parent thread cycle collected objects + * are owned indefinitely by WorkletImpl because WorkletImpl is not cycle + * collected. + */ +class WorkletImpl { + using RFPTarget = mozilla::RFPTarget; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WorkletImpl); + + // Methods for parent thread only: + + virtual JSObject* WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet, + JS::Handle<JSObject*> aGivenProto); + + virtual nsresult SendControlMessage(already_AddRefed<nsIRunnable> aRunnable); + + void NotifyWorkletFinished(); + + virtual nsContentPolicyType ContentPolicyType() const = 0; + + // Execution thread only. + dom::WorkletGlobalScope* GetGlobalScope(); + + // Any thread. + + const OriginTrials& Trials() const { return mTrials; } + const WorkletLoadInfo& LoadInfo() const { return mWorkletLoadInfo; } + const OriginAttributes& OriginAttributesRef() const { + return mPrincipal->OriginAttributesRef(); + } + nsIPrincipal* Principal() const { return mPrincipal; } + const ipc::PrincipalInfo& PrincipalInfo() const { return mPrincipalInfo; } + + const Maybe<nsID>& GetAgentClusterId() const { return mAgentClusterId; } + + bool IsSharedMemoryAllowed() const { return mSharedMemoryAllowed; } + bool IsSystemPrincipal() const { return mPrincipal->IsSystemPrincipal(); } + bool ShouldResistFingerprinting(RFPTarget aTarget) const { + return mShouldResistFingerprinting && + nsRFPService::IsRFPEnabledFor(mIsPrivateBrowsing, aTarget, + mOverriddenFingerprintingSettings); + } + + virtual void OnAddModuleStarted() const { + MOZ_ASSERT(NS_IsMainThread()); + // empty base impl + } + + virtual void OnAddModulePromiseSettled() const { + MOZ_ASSERT(NS_IsMainThread()); + // empty base impl + } + + protected: + WorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal); + virtual ~WorkletImpl(); + + virtual already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() = 0; + + // Modified only in constructor. + ipc::PrincipalInfo mPrincipalInfo; + nsCOMPtr<nsIPrincipal> mPrincipal; + + const WorkletLoadInfo mWorkletLoadInfo; + + // Parent thread only. + RefPtr<dom::WorkletThread> mWorkletThread; + bool mTerminated : 1; + + // Execution thread only. + RefPtr<dom::WorkletGlobalScope> mGlobalScope; + bool mFinishedOnExecutionThread : 1; + + Maybe<nsID> mAgentClusterId; + + bool mSharedMemoryAllowed : 1; + bool mShouldResistFingerprinting : 1; + bool mIsPrivateBrowsing : 1; + // The granular fingerprinting protection overrides applied to the worklet. + // This will only get populated if these is one that comes from the local + // granular override pref or WebCompat. Otherwise, a value of Nothing() + // indicates no granular overrides are present for this workerlet. + Maybe<RFPTarget> mOverriddenFingerprintingSettings; + + const OriginTrials mTrials; +}; + +} // namespace mozilla + +#endif // mozilla_dom_worklet_WorkletImpl_h diff --git a/dom/worklet/WorkletThread.cpp b/dom/worklet/WorkletThread.cpp new file mode 100644 index 0000000000..c5ad7070b0 --- /dev/null +++ b/dom/worklet/WorkletThread.cpp @@ -0,0 +1,474 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "WorkletThread.h" +#include "prthread.h" +#include "nsContentUtils.h" +#include "nsCycleCollector.h" +#include "nsJSEnvironment.h" +#include "nsJSPrincipals.h" +#include "mozilla/dom/AtomList.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/EventQueue.h" +#include "mozilla/ThreadEventQueue.h" +#include "js/ContextOptions.h" +#include "js/Exception.h" +#include "js/Initialization.h" +#include "XPCSelfHostedShmem.h" + +namespace mozilla::dom { + +namespace { + +// The size of the worklet runtime heaps in bytes. +#define WORKLET_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024 + +// The C stack size. We use the same stack size on all platforms for +// consistency. +const uint32_t kWorkletStackSize = 256 * sizeof(size_t) * 1024; + +// Half the size of the actual C stack, to be safe. +#define WORKLET_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024 + +// Helper functions + +bool PreserveWrapper(JSContext* aCx, JS::Handle<JSObject*> aObj) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aObj); + MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj)); + return mozilla::dom::TryPreserveWrapper(aObj); +} + +JSObject* Wrap(JSContext* aCx, JS::Handle<JSObject*> aExisting, + JS::Handle<JSObject*> aObj) { + if (aExisting) { + js::Wrapper::Renew(aExisting, aObj, + &js::OpaqueCrossCompartmentWrapper::singleton); + } + + return js::Wrapper::New(aCx, aObj, + &js::OpaqueCrossCompartmentWrapper::singleton); +} + +const JSWrapObjectCallbacks WrapObjectCallbacks = { + Wrap, + nullptr, +}; + +} // namespace + +// This classes control CC in the worklet thread. + +class WorkletJSRuntime final : public mozilla::CycleCollectedJSRuntime { + public: + explicit WorkletJSRuntime(JSContext* aCx) : CycleCollectedJSRuntime(aCx) {} + + ~WorkletJSRuntime() override = default; + + virtual void PrepareForForgetSkippable() override {} + + virtual void BeginCycleCollectionCallback( + mozilla::CCReason aReason) override {} + + virtual void EndCycleCollectionCallback( + CycleCollectorResults& aResults) override {} + + virtual void DispatchDeferredDeletion(bool aContinuation, + bool aPurge) override { + MOZ_ASSERT(!aContinuation); + nsCycleCollector_doDeferredDeletion(); + } + + virtual void CustomGCCallback(JSGCStatus aStatus) override { + // nsCycleCollector_collect() requires a cycle collector but + // ~WorkletJSContext calls nsCycleCollector_shutdown() and the base class + // destructor will trigger a final GC. The nsCycleCollector_collect() + // call can be skipped in this GC as ~CycleCollectedJSContext removes the + // context from |this|. + if (aStatus == JSGC_END && GetContext()) { + nsCycleCollector_collect(CCReason::GC_FINISHED, nullptr); + } + } +}; + +class WorkletJSContext final : public CycleCollectedJSContext { + public: + WorkletJSContext() { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCycleCollector_startup(); + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the + // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a + // bit of a pain. + MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkletJSContext() override { + MOZ_ASSERT(!NS_IsMainThread()); + + JSContext* cx = MaybeContext(); + if (!cx) { + return; // Initialize() must have failed + } + + nsCycleCollector_shutdown(); + } + + WorkletJSContext* GetAsWorkletJSContext() override { return this; } + + CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) override { + return new WorkletJSRuntime(aCx); + } + + nsresult Initialize(JSRuntime* aParentRuntime) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = CycleCollectedJSContext::Initialize( + aParentRuntime, WORKLET_DEFAULT_RUNTIME_HEAPSIZE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = Context(); + + js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper); + JS_InitDestroyPrincipalsCallback(cx, nsJSPrincipals::Destroy); + JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals); + JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); + JS_SetFutexCanWait(cx); + + return NS_OK; + } + + void DispatchToMicroTask( + already_AddRefed<MicroTaskRunnable> aRunnable) override { + RefPtr<MicroTaskRunnable> runnable(aRunnable); + + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(runnable); + + JSContext* cx = Context(); + MOZ_ASSERT(cx); + +#ifdef DEBUG + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + MOZ_ASSERT(global); +#endif + + JS::JobQueueMayNotBeEmpty(cx); + GetMicroTaskQueue().push_back(std::move(runnable)); + } + + bool IsSystemCaller() const override { + // Currently no support for special system worklet privileges. + return false; + } + + void ReportError(JSErrorReport* aReport, + JS::ConstUTF8CharsZ aToStringResult) override; + + uint64_t GetCurrentWorkletWindowID() { + JSObject* global = JS::CurrentGlobalOrNull(Context()); + if (NS_WARN_IF(!global)) { + return 0; + } + nsIGlobalObject* nativeGlobal = xpc::NativeGlobal(global); + nsCOMPtr<WorkletGlobalScope> workletGlobal = + do_QueryInterface(nativeGlobal); + if (NS_WARN_IF(!workletGlobal)) { + return 0; + } + return workletGlobal->Impl()->LoadInfo().InnerWindowID(); + } +}; + +void WorkletJSContext::ReportError(JSErrorReport* aReport, + JS::ConstUTF8CharsZ aToStringResult) { + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + xpcReport->Init(aReport, aToStringResult.c_str(), IsSystemCaller(), + GetCurrentWorkletWindowID()); + RefPtr<AsyncErrorReporter> reporter = new AsyncErrorReporter(xpcReport); + + JSContext* cx = Context(); + if (JS_IsExceptionPending(cx)) { + JS::ExceptionStack exnStack(cx); + if (JS::StealPendingExceptionStack(cx, &exnStack)) { + JS::Rooted<JSObject*> stack(cx); + JS::Rooted<JSObject*> stackGlobal(cx); + xpc::FindExceptionStackForConsoleReport(nullptr, exnStack.exception(), + exnStack.stack(), &stack, + &stackGlobal); + if (stack) { + reporter->SerializeStack(cx, stack); + } + } + } + + NS_DispatchToMainThread(reporter); +} + +// This is the first runnable to be dispatched. It calls the RunEventLoop() so +// basically everything happens into this runnable. The reason behind this +// approach is that, when the Worklet is terminated, it must not have any JS in +// stack, but, because we have CC, nsIThread creates an AutoNoJSAPI object by +// default. Using this runnable, CC exists only into it. +class WorkletThread::PrimaryRunnable final : public Runnable { + public: + explicit PrimaryRunnable(WorkletThread* aWorkletThread) + : Runnable("WorkletThread::PrimaryRunnable"), + mWorkletThread(aWorkletThread) { + MOZ_ASSERT(aWorkletThread); + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD + Run() override { + mWorkletThread->RunEventLoop(); + return NS_OK; + } + + private: + RefPtr<WorkletThread> mWorkletThread; +}; + +// This is the last runnable to be dispatched. It calls the TerminateInternal() +class WorkletThread::TerminateRunnable final : public Runnable { + public: + explicit TerminateRunnable(WorkletThread* aWorkletThread) + : Runnable("WorkletThread::TerminateRunnable"), + mWorkletThread(aWorkletThread) { + MOZ_ASSERT(aWorkletThread); + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD + Run() override { + mWorkletThread->TerminateInternal(); + return NS_OK; + } + + private: + RefPtr<WorkletThread> mWorkletThread; +}; + +WorkletThread::WorkletThread(WorkletImpl* aWorkletImpl) + : nsThread( + MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()), + nsThread::NOT_MAIN_THREAD, {.stackSize = kWorkletStackSize}), + mWorkletImpl(aWorkletImpl), + mExitLoop(false), + mIsTerminating(false) { + MOZ_ASSERT(NS_IsMainThread()); + nsContentUtils::RegisterShutdownObserver(this); +} + +WorkletThread::~WorkletThread() = default; + +// static +already_AddRefed<WorkletThread> WorkletThread::Create( + WorkletImpl* aWorkletImpl) { + RefPtr<WorkletThread> thread = new WorkletThread(aWorkletImpl); + if (NS_WARN_IF(NS_FAILED(thread->Init("DOM Worklet"_ns)))) { + return nullptr; + } + + RefPtr<PrimaryRunnable> runnable = new PrimaryRunnable(thread); + if (NS_WARN_IF(NS_FAILED(thread->DispatchRunnable(runnable.forget())))) { + return nullptr; + } + + return thread.forget(); +} + +nsresult WorkletThread::DispatchRunnable( + already_AddRefed<nsIRunnable> aRunnable) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); +} + +NS_IMETHODIMP +WorkletThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +WorkletThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + + // Worklet only supports asynchronous dispatch. + if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { + return NS_ERROR_UNEXPECTED; + } + + return nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); +} + +NS_IMETHODIMP +WorkletThread::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t aFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +static bool DispatchToEventLoop(void* aClosure, + JS::Dispatchable* aDispatchable) { + // This callback may execute either on the worklet thread or a random + // JS-internal helper thread. + + // See comment at JS::InitDispatchToEventLoop() below for how we know the + // thread is alive. + nsIThread* thread = static_cast<nsIThread*>(aClosure); + + nsresult rv = thread->Dispatch( + NS_NewRunnableFunction( + "WorkletThread::DispatchToEventLoop", + [aDispatchable]() { + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + if (!ccjscx) { + return; + } + + WorkletJSContext* wjc = ccjscx->GetAsWorkletJSContext(); + if (!wjc) { + return; + } + + aDispatchable->run(wjc->Context(), + JS::Dispatchable::NotShuttingDown); + }), + NS_DISPATCH_NORMAL); + + return NS_SUCCEEDED(rv); +} + +// static +void WorkletThread::EnsureCycleCollectedJSContext( + JSRuntime* aParentRuntime, const JS::ContextOptions& aOptions) { + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + if (ccjscx) { + MOZ_ASSERT(ccjscx->GetAsWorkletJSContext()); + return; + } + + WorkletJSContext* context = new WorkletJSContext(); + nsresult rv = context->Initialize(aParentRuntime); + if (NS_WARN_IF(NS_FAILED(rv))) { + // TODO: error propagation + return; + } + + JS::ContextOptionsRef(context->Context()) = aOptions; + + JS_SetGCParameter(context->Context(), JSGC_MAX_BYTES, uint32_t(-1)); + + // FIXME: JS_SetDefaultLocale + // FIXME: JSSettings + // FIXME: JS_SetSecurityCallbacks + // FIXME: JS::SetAsyncTaskCallbacks + // FIXME: JS::SetCTypesActivityCallback + // FIXME: JS_SetGCZeal + + // A thread lives strictly longer than its JSRuntime so we can safely + // store a raw pointer as the callback's closure argument on the JSRuntime. + JS::InitDispatchToEventLoop(context->Context(), DispatchToEventLoop, + NS_GetCurrentThread()); + + JS_SetNativeStackQuota(context->Context(), + WORKLET_CONTEXT_NATIVE_STACK_LIMIT); + + // When available, set the self-hosted shared memory to be read, so that we + // can decode the self-hosted content instead of parsing it. + auto& shm = xpc::SelfHostedShmem::GetSingleton(); + JS::SelfHostedCache selfHostedContent = shm.Content(); + + if (!JS::InitSelfHostedCode(context->Context(), selfHostedContent)) { + // TODO: error propagation + return; + } +} + +void WorkletThread::RunEventLoop() { + MOZ_ASSERT(!NS_IsMainThread()); + + PR_SetCurrentThreadName("worklet"); + + while (!mExitLoop) { + MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(this, /* wait: */ true)); + } + + DeleteCycleCollectedJSContext(); +} + +void WorkletThread::Terminate() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mIsTerminating) { + // nsThread::Dispatch() would leak the runnable if the event queue is no + // longer accepting runnables. + return; + } + + mIsTerminating = true; + + nsContentUtils::UnregisterShutdownObserver(this); + + RefPtr<TerminateRunnable> runnable = new TerminateRunnable(this); + DispatchRunnable(runnable.forget()); +} + +void WorkletThread::TerminateInternal() { + MOZ_ASSERT(!CycleCollectedJSContext::Get() || IsOnWorkletThread()); + + mExitLoop = true; + + nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod( + "WorkletThread::Shutdown", this, &WorkletThread::Shutdown); + NS_DispatchToMainThread(runnable); +} + +/* static */ +void WorkletThread::DeleteCycleCollectedJSContext() { + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + if (!ccjscx) { + return; + } + + // Release any MessagePort kept alive by its ipc actor. + mozilla::ipc::BackgroundChild::CloseForCurrentThread(); + + WorkletJSContext* workletjscx = ccjscx->GetAsWorkletJSContext(); + MOZ_ASSERT(workletjscx); + delete workletjscx; +} + +/* static */ +bool WorkletThread::IsOnWorkletThread() { + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + return ccjscx && ccjscx->GetAsWorkletJSContext(); +} + +/* static */ +void WorkletThread::AssertIsOnWorkletThread() { + MOZ_ASSERT(IsOnWorkletThread()); +} + +// nsIObserver +NS_IMETHODIMP +WorkletThread::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t*) { + MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0); + + // The WorkletImpl will terminate the worklet thread after sending a message + // to release worklet thread objects. + mWorkletImpl->NotifyWorkletFinished(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED(WorkletThread, nsThread, nsIObserver) + +} // namespace mozilla::dom diff --git a/dom/worklet/WorkletThread.h b/dom/worklet/WorkletThread.h new file mode 100644 index 0000000000..e6d0e414dc --- /dev/null +++ b/dom/worklet/WorkletThread.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_dom_worklet_WorkletThread_h +#define mozilla_dom_worklet_WorkletThread_h + +#include "mozilla/Attributes.h" +#include "mozilla/CondVar.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/WorkletImpl.h" +#include "nsIObserver.h" +#include "nsThread.h" + +class nsIRunnable; + +namespace JS { +class ContextOptions; +}; // namespace JS + +namespace mozilla::dom { + +class WorkletThread final : public nsThread, public nsIObserver { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIOBSERVER + + static already_AddRefed<WorkletThread> Create(WorkletImpl* aWorkletImpl); + + // Threads that call EnsureCycleCollectedJSContext must call + // DeleteCycleCollectedJSContext::Get() before terminating. Clients of + // Create() do not need to do this as Terminate() will ensure this happens. + static void EnsureCycleCollectedJSContext(JSRuntime* aParentRuntime, + const JS::ContextOptions& aOptions); + static void DeleteCycleCollectedJSContext(); + + static bool IsOnWorkletThread(); + + static void AssertIsOnWorkletThread(); + + nsresult DispatchRunnable(already_AddRefed<nsIRunnable> aRunnable); + + void Terminate(); + + private: + explicit WorkletThread(WorkletImpl* aWorkletImpl); + ~WorkletThread(); + + void RunEventLoop(); + class PrimaryRunnable; + + void TerminateInternal(); + class TerminateRunnable; + + // This should only be called by consumers that have an + // nsIEventTarget/nsIThread pointer. + NS_IMETHOD + Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) override; + + const RefPtr<WorkletImpl> mWorkletImpl; + + bool mExitLoop; // worklet execution thread + + bool mIsTerminating; // main thread +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_worklet_WorkletThread_h diff --git a/dom/worklet/loader/WorkletModuleLoader.cpp b/dom/worklet/loader/WorkletModuleLoader.cpp new file mode 100644 index 0000000000..59d2405398 --- /dev/null +++ b/dom/worklet/loader/WorkletModuleLoader.cpp @@ -0,0 +1,297 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "WorkletModuleLoader.h" + +#include "js/CompileOptions.h" // JS::InstantiateOptions +#include "js/experimental/JSStencil.h" // JS::CompileModuleScriptToStencil, JS::InstantiateModuleStencil +#include "js/loader/ModuleLoadRequest.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/Worklet.h" +#include "mozilla/dom/WorkletFetchHandler.h" +#include "nsStringBundle.h" + +using JS::loader::ModuleLoadRequest; +using JS::loader::ResolveError; + +namespace mozilla::dom::loader { + +////////////////////////////////////////////////////////////// +// WorkletScriptLoader +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletScriptLoader) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(WorkletScriptLoader) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkletScriptLoader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkletScriptLoader) + +////////////////////////////////////////////////////////////// +// WorkletModuleLoader +////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF_INHERITED(WorkletModuleLoader, ModuleLoaderBase) +NS_IMPL_RELEASE_INHERITED(WorkletModuleLoader, ModuleLoaderBase) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(WorkletModuleLoader, ModuleLoaderBase, + mFetchingRequests) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkletModuleLoader) +NS_INTERFACE_MAP_END_INHERITING(ModuleLoaderBase) + +WorkletModuleLoader::WorkletModuleLoader(WorkletScriptLoader* aScriptLoader, + nsIGlobalObject* aGlobalObject) + : ModuleLoaderBase(aScriptLoader, aGlobalObject) { + // This should be constructed on a worklet thread. + MOZ_ASSERT(!NS_IsMainThread()); +} + +already_AddRefed<ModuleLoadRequest> WorkletModuleLoader::CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) { + const nsMainThreadPtrHandle<WorkletFetchHandler>& handlerRef = + aParent->GetWorkletLoadContext()->GetHandlerRef(); + RefPtr<WorkletLoadContext> loadContext = new WorkletLoadContext(handlerRef); + + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-the-descendants-of-a-module-script + // Step 11. Perform the internal module script graph fetching procedure + // + // https://html.spec.whatwg.org/multipage/webappapis.html#internal-module-script-graph-fetching-procedure + // Step 5. Fetch a single module script with referrer is referringScript's + // base URL, + nsIURI* referrer = aParent->mURI; + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, aParent->ReferrerPolicy(), aParent->mFetchOptions, SRIMetadata(), + referrer, loadContext, false, /* is top level */ + false, /* is dynamic import */ + this, aParent->mVisitedSet, aParent->GetRootModule()); + + request->mURL = request->mURI->GetSpecOrDefault(); + request->NoCacheEntryFound(); + return request.forget(); +} + +already_AddRefed<ModuleLoadRequest> WorkletModuleLoader::CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) { + return nullptr; +} + +bool WorkletModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, + nsresult* aRvOut) { + return true; +} + +nsresult WorkletModuleLoader::StartFetch(ModuleLoadRequest* aRequest) { + InsertRequest(aRequest->mURI, aRequest); + + RefPtr<StartFetchRunnable> runnable = + new StartFetchRunnable(aRequest->GetWorkletLoadContext()->GetHandlerRef(), + aRequest->mURI, aRequest->mReferrer); + NS_DispatchToMainThread(runnable.forget()); + return NS_OK; +} + +nsresult WorkletModuleLoader::CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions, + ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleScript) { + RefPtr<JS::Stencil> stencil; + MOZ_ASSERT(aRequest->IsTextSource()); + + MaybeSourceText maybeSource; + nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource, + aRequest->mLoadContext.get()); + NS_ENSURE_SUCCESS(rv, rv); + + auto compile = [&](auto& source) { + return JS::CompileModuleScriptToStencil(aCx, aOptions, source); + }; + stencil = maybeSource.mapNonEmpty(compile); + + if (!stencil) { + return NS_ERROR_FAILURE; + } + + JS::InstantiateOptions instantiateOptions(aOptions); + aModuleScript.set( + JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil)); + return aModuleScript ? NS_OK : NS_ERROR_FAILURE; +} + +// AddModuleResultRunnable is a Runnable which will notify the result of +// Worklet::AddModule on the main thread. +class AddModuleResultRunnable final : public Runnable { + public: + explicit AddModuleResultRunnable( + const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef, + bool aSucceeded) + : Runnable("Worklet::AddModuleResultRunnable"), + mHandlerRef(aHandlerRef), + mSucceeded(aSucceeded) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + ~AddModuleResultRunnable() = default; + + NS_IMETHOD + Run() override; + + private: + nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef; + bool mSucceeded; +}; + +NS_IMETHODIMP +AddModuleResultRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + if (mSucceeded) { + mHandlerRef->ExecutionSucceeded(); + } else { + mHandlerRef->ExecutionFailed(); + } + + return NS_OK; +} + +class AddModuleThrowErrorRunnable final : public Runnable, + public StructuredCloneHolder { + public: + explicit AddModuleThrowErrorRunnable( + const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef) + : Runnable("Worklet::AddModuleThrowErrorRunnable"), + StructuredCloneHolder(CloningSupported, TransferringNotSupported, + StructuredCloneScope::SameProcess), + mHandlerRef(aHandlerRef) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + ~AddModuleThrowErrorRunnable() = default; + + NS_IMETHOD + Run() override; + + private: + nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef; +}; + +NS_IMETHODIMP +AddModuleThrowErrorRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(mHandlerRef->mWorklet->GetParentObject()); + MOZ_ASSERT(global); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(global))) { + mHandlerRef->ExecutionFailed(); + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> error(cx); + ErrorResult result; + Read(global, cx, &error, result); + Unused << NS_WARN_IF(result.Failed()); + mHandlerRef->ExecutionFailed(error); + + return NS_OK; +} + +void WorkletModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) { + if (!aRequest->IsTopLevel()) { + return; + } + + const nsMainThreadPtrHandle<WorkletFetchHandler>& handlerRef = + aRequest->GetWorkletLoadContext()->GetHandlerRef(); + + auto addModuleFailed = MakeScopeExit([&] { + RefPtr<AddModuleResultRunnable> runnable = + new AddModuleResultRunnable(handlerRef, false); + NS_DispatchToMainThread(runnable.forget()); + }); + + if (!aRequest->mModuleScript) { + return; + } + + if (!aRequest->InstantiateModuleGraph()) { + return; + } + + nsresult rv = aRequest->EvaluateModule(); + if (NS_FAILED(rv)) { + return; + } + + bool hasError = aRequest->mModuleScript->HasErrorToRethrow(); + if (hasError) { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(GetGlobalObject()))) { + return; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> error(cx, aRequest->mModuleScript->ErrorToRethrow()); + RefPtr<AddModuleThrowErrorRunnable> runnable = + new AddModuleThrowErrorRunnable(handlerRef); + ErrorResult result; + runnable->Write(cx, error, result); + if (NS_WARN_IF(result.Failed())) { + return; + } + + addModuleFailed.release(); + NS_DispatchToMainThread(runnable.forget()); + return; + } + + addModuleFailed.release(); + RefPtr<AddModuleResultRunnable> runnable = + new AddModuleResultRunnable(handlerRef, true); + NS_DispatchToMainThread(runnable.forget()); +} + +// TODO: Bug 1808301: Call FormatLocalizedString from a worklet thread. +nsresult WorkletModuleLoader::GetResolveFailureMessage( + ResolveError aError, const nsAString& aSpecifier, nsAString& aResult) { + uint8_t index = static_cast<uint8_t>(aError); + MOZ_ASSERT(index < static_cast<uint8_t>(ResolveError::Length)); + MOZ_ASSERT(mLocalizedStrs); + MOZ_ASSERT(!mLocalizedStrs->IsEmpty()); + if (!mLocalizedStrs || NS_WARN_IF(mLocalizedStrs->IsEmpty())) { + return NS_ERROR_FAILURE; + } + + const nsString& localizedStr = mLocalizedStrs->ElementAt(index); + + AutoTArray<nsString, 1> params; + params.AppendElement(aSpecifier); + + nsStringBundleBase::FormatString(localizedStr.get(), params, aResult); + return NS_OK; +} + +void WorkletModuleLoader::InsertRequest(nsIURI* aURI, + ModuleLoadRequest* aRequest) { + mFetchingRequests.InsertOrUpdate(aURI, aRequest); +} + +void WorkletModuleLoader::RemoveRequest(nsIURI* aURI) { + MOZ_ASSERT(mFetchingRequests.Remove(aURI)); +} + +ModuleLoadRequest* WorkletModuleLoader::GetRequest(nsIURI* aURI) const { + RefPtr<ModuleLoadRequest> req; + MOZ_ALWAYS_TRUE(mFetchingRequests.Get(aURI, getter_AddRefs(req))); + return req; +} + +} // namespace mozilla::dom::loader diff --git a/dom/worklet/loader/WorkletModuleLoader.h b/dom/worklet/loader/WorkletModuleLoader.h new file mode 100644 index 0000000000..3fb2a59231 --- /dev/null +++ b/dom/worklet/loader/WorkletModuleLoader.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_dom_worklet_WorkletModuleLoader_h +#define mozilla_dom_worklet_WorkletModuleLoader_h + +#include "js/loader/LoadContextBase.h" +#include "js/loader/ModuleLoaderBase.h" +#include "js/loader/ResolveResult.h" // For ResolveError +#include "mozilla/dom/WorkletFetchHandler.h" + +namespace mozilla::dom { +namespace loader { +class WorkletScriptLoader : public JS::loader::ScriptLoaderInterface { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(WorkletScriptLoader) + + nsIURI* GetBaseURI() const override { return nullptr; } + + void ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const override {} + + void ReportWarningToConsole( + ScriptLoadRequest* aRequest, const char* aMessageName, + const nsTArray<nsString>& aParams) const override {} + + nsresult FillCompileOptionsForRequest( + JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) override { + aOptions->setIntroductionType("Worklet"); + aOptions->setFileAndLine(aRequest->mURL.get(), 1); + aOptions->setIsRunOnce(true); + aOptions->setNoScriptRval(true); + return NS_OK; + } + + private: + ~WorkletScriptLoader() = default; +}; + +class WorkletModuleLoader : public JS::loader::ModuleLoaderBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WorkletModuleLoader, + JS::loader::ModuleLoaderBase) + + WorkletModuleLoader(WorkletScriptLoader* aScriptLoader, + nsIGlobalObject* aGlobalObject); + + void InsertRequest(nsIURI* aURI, JS::loader::ModuleLoadRequest* aRequest); + void RemoveRequest(nsIURI* aURI); + JS::loader::ModuleLoadRequest* GetRequest(nsIURI* aURI) const; + + bool HasSetLocalizedStrings() const { return (bool)mLocalizedStrs; } + void SetLocalizedStrings(const nsTArray<nsString>* aStrings) { + mLocalizedStrs = aStrings; + } + + private: + ~WorkletModuleLoader() = default; + + already_AddRefed<JS::loader::ModuleLoadRequest> CreateStaticImport( + nsIURI* aURI, JS::loader::ModuleLoadRequest* aParent) override; + + already_AddRefed<JS::loader::ModuleLoadRequest> CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) override; + + bool CanStartLoad(JS::loader::ModuleLoadRequest* aRequest, + nsresult* aRvOut) override; + + nsresult StartFetch(JS::loader::ModuleLoadRequest* aRequest) override; + + nsresult CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, + JS::CompileOptions& aOptions, JS::loader::ModuleLoadRequest* aRequest, + JS::MutableHandle<JSObject*> aModuleScript) override; + + void OnModuleLoadComplete(JS::loader::ModuleLoadRequest* aRequest) override; + + nsresult GetResolveFailureMessage(JS::loader::ResolveError aError, + const nsAString& aSpecifier, + nsAString& aResult) override; + + // A hashtable to map a nsIURI(from main thread) to a ModuleLoadRequest(in + // worklet thread). + nsRefPtrHashtable<nsURIHashKey, JS::loader::ModuleLoadRequest> + mFetchingRequests; + + // We get the localized strings on the main thread, and pass it to + // WorkletModuleLoader. + const nsTArray<nsString>* mLocalizedStrs = nullptr; +}; +} // namespace loader + +class WorkletLoadContext : public JS::loader::LoadContextBase { + public: + explicit WorkletLoadContext( + const nsMainThreadPtrHandle<WorkletFetchHandler>& aHandlerRef) + : JS::loader::LoadContextBase(JS::loader::ContextKind::Worklet), + mHandlerRef(aHandlerRef) {} + + const nsMainThreadPtrHandle<WorkletFetchHandler>& GetHandlerRef() const { + return mHandlerRef; + } + + private: + ~WorkletLoadContext() = default; + + nsMainThreadPtrHandle<WorkletFetchHandler> mHandlerRef; +}; +} // namespace mozilla::dom +#endif // mozilla_dom_worklet_WorkletModuleLoader_h diff --git a/dom/worklet/loader/moz.build b/dom/worklet/loader/moz.build new file mode 100644 index 0000000000..38707a81cc --- /dev/null +++ b/dom/worklet/loader/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +EXPORTS.mozilla.dom.worklet += [ + "WorkletModuleLoader.h", +] + +UNIFIED_SOURCES += [ + "WorkletModuleLoader.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/worklet/moz.build b/dom/worklet/moz.build new file mode 100644 index 0000000000..487527c488 --- /dev/null +++ b/dom/worklet/moz.build @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +DIRS += ["loader"] + +EXPORTS.mozilla.dom += [ + "Worklet.h", + "WorkletFetchHandler.h", + "WorkletGlobalScope.h", + "WorkletImpl.h", + "WorkletThread.h", +] + +UNIFIED_SOURCES += [ + "Worklet.cpp", + "WorkletFetchHandler.cpp", + "WorkletGlobalScope.cpp", + "WorkletImpl.cpp", + "WorkletThread.cpp", +] + +LOCAL_INCLUDES += [ + "/js/xpconnect/src", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] + +FINAL_LIBRARY = "xul" diff --git a/dom/worklet/tests/common.js b/dom/worklet/tests/common.js new file mode 100644 index 0000000000..3841a5f73b --- /dev/null +++ b/dom/worklet/tests/common.js @@ -0,0 +1,26 @@ +window.onload = async function () { + // We are the parent. Let's load the test. + if (parent == this || !location.search.includes("worklet_iframe")) { + SimpleTest.waitForExplicitFinish(); + + // configureTest is optional + if (window.configureTest) { + await window.configureTest(); + } + + var iframe = document.createElement("iframe"); + iframe.src = location.href + "?worklet_iframe"; + document.body.appendChild(iframe); + + return; + } + + // Here we are in the iframe. + window.SimpleTest = parent.SimpleTest; + window.is = parent.is; + window.isnot = parent.isnot; + window.ok = parent.ok; + window.info = parent.info; + + runTestInIframe(); +}; diff --git a/dom/worklet/tests/dynamic_import.js b/dom/worklet/tests/dynamic_import.js new file mode 100644 index 0000000000..a5196d212f --- /dev/null +++ b/dom/worklet/tests/dynamic_import.js @@ -0,0 +1,7 @@ +import("./empty-worklet-script.js") + .then(() => { + console.log("Fail"); + }) + .catch(e => { + console.log(e.name + ": Success"); + }); diff --git a/dom/worklet/tests/invalid_specifier.mjs b/dom/worklet/tests/invalid_specifier.mjs new file mode 100644 index 0000000000..128b60ffa6 --- /dev/null +++ b/dom/worklet/tests/invalid_specifier.mjs @@ -0,0 +1,3 @@ +/* eslint-disable import/no-unassigned-import */ +/* eslint-disable import/no-unresolved */ +import "foo"; diff --git a/dom/worklet/tests/mochitest.toml b/dom/worklet/tests/mochitest.toml new file mode 100644 index 0000000000..2515355476 --- /dev/null +++ b/dom/worklet/tests/mochitest.toml @@ -0,0 +1,58 @@ +[DEFAULT] +scheme = "https" +support-files = ["common.js"] + +["test_audioWorklet.html"] +support-files = ["worklet_audioWorklet.js"] + +["test_audioWorkletGlobalScopeRegisterProcessor.html"] +support-files = ["worklet_test_audioWorkletGlobalScopeRegisterProcessor.js"] + +["test_audioWorklet_WASM.html"] +skip-if = ["release_or_beta"] # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled +support-files = ["worklet_audioWorklet_WASM.js"] + +["test_audioWorklet_WASM_Features.html"] +support-files = ["worklet_audioWorklet_WASM_features.js"] + +["test_audioWorklet_insecureContext.html"] +scheme = "http" +skip-if = [ + "http3", + "http2", +] + +["test_audioWorklet_options.html"] +skip-if = ["release_or_beta"] # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled +support-files = ["worklet_audioWorklet_options.js"] + +["test_basic.html"] + +["test_console.html"] +support-files = ["worklet_console.js"] + +["test_dump.html"] +support-files = ["worklet_dump.js"] + +["test_dynamic_import.html"] +support-files = ["dynamic_import.js"] + +["test_exception.html"] +support-files = [ + "worklet_exception.js", + "invalid_specifier.mjs", +] + +["test_fetch_failed.html"] +support-files = ["specifier_with_user.mjs"] + +["test_import_with_cache.html"] +skip-if = ["verify"] +support-files = ["server_import_with_cache.sjs"] + +["test_paintWorklet.html"] +skip-if = ["release_or_beta"] +support-files = ["worklet_paintWorklet.js"] + +["test_promise.html"] +support-files = ["worklet_promise.js"] diff --git a/dom/worklet/tests/server_import_with_cache.sjs b/dom/worklet/tests/server_import_with_cache.sjs new file mode 100644 index 0000000000..ed34a7a72f --- /dev/null +++ b/dom/worklet/tests/server_import_with_cache.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/javascript", false); + + var state = getState("alreadySent"); + if (!state) { + setState("alreadySent", "1"); + } else { + response.setStatusLine("1.1", 404, "Not Found"); + } + + response.write("42"); +} diff --git a/dom/worklet/tests/specifier_with_user.mjs b/dom/worklet/tests/specifier_with_user.mjs new file mode 100644 index 0000000000..3d9bdb45cf --- /dev/null +++ b/dom/worklet/tests/specifier_with_user.mjs @@ -0,0 +1,3 @@ +/* eslint-disable import/no-unassigned-import */ +/* eslint-disable import/no-unresolved */ +import "http://user@example1.com/a.js"; diff --git a/dom/worklet/tests/test_audioWorklet.html b/dom/worklet/tests/test_audioWorklet.html new file mode 100644 index 0000000000..0dfb6fcf15 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for AudioWorklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> + +function configureTest() { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + consoleListener.prototype = { + observe(aSubject) { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "So far so good") { + ok(true, "Message received \\o/"); + + ConsoleAPIStorage.removeLogEventListener(this.observe); + SimpleTest.finish(); + } + } + } + + var cl = new consoleListener(); +} + +// This function is called into an iframe. +function runTestInIframe() { + ok(window.isSecureContext, "Test should run in secure context"); + var audioContext = new AudioContext(); + ok(audioContext.audioWorklet instanceof AudioWorklet, + "AudioContext.audioWorklet should be an instance of AudioWorklet"); + audioContext.audioWorklet.addModule("worklet_audioWorklet.js") +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html b/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html new file mode 100644 index 0000000000..bce6eca292 --- /dev/null +++ b/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for AudioWorklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> + +function configureTest() { + + var expected_errors = [ + "TypeError: AudioWorkletGlobalScope.registerProcessor: Argument 2 is not a constructor.", + "NotSupportedError: AudioWorkletGlobalScope.registerProcessor: Argument 1 should not be an empty string.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: Argument 2 is not an object.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: Element 0 in parameterDescriptors can't be converted to a dictionary.", + "NotSupportedError: AudioWorkletGlobalScope.registerProcessor: Argument 1 is invalid: a class with the same name is already registered.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: Missing required 'name' member of AudioParamDescriptor.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: 'defaultValue' member of AudioParamDescriptor is not a finite floating-point value.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: 'minValue' member of AudioParamDescriptor is not a finite floating-point value.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: 'maxValue' member of AudioParamDescriptor is not a finite floating-point value.", + "NotSupportedError: AudioWorkletGlobalScope.registerProcessor: Duplicated name \"test\" in parameterDescriptors.", + "TypeError: AudioWorkletGlobalScope.registerProcessor: Element 0 in parameterDescriptors can't be converted to a dictionary.", + "InvalidStateError: AudioWorkletGlobalScope.registerProcessor: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.", + "InvalidStateError: AudioWorkletGlobalScope.registerProcessor: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.", + "InvalidStateError: AudioWorkletGlobalScope.registerProcessor: In parameterDescriptors, test minValue should be smaller than maxValue.", + ]; + + var expected_errors_i = 0; + + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + consoleListener.prototype = { + observe(aSubject) { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == expected_errors[expected_errors_i]) { + ok(true, "Expected error received: " + obj.arguments[0]); + expected_errors_i++; + } + + if (expected_errors_i == expected_errors.length) { + // All errors have been received, this test has been completed + // succesfully! + ConsoleAPIStorage.removeLogEventListener(this.observe); + SimpleTest.finish(); + } + } + } + + var cl = new consoleListener(); +} + +// This function is called into an iframe. +function runTestInIframe() { + ok(window.isSecureContext, "Test should run in secure context"); + var audioContext = new AudioContext(); + ok(audioContext.audioWorklet instanceof AudioWorklet, + "AudioContext.audioWorklet should be an instance of AudioWorklet"); + audioContext.audioWorklet.addModule("worklet_test_audioWorkletGlobalScopeRegisterProcessor.js") +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_audioWorklet_WASM.html b/dom/worklet/tests/test_audioWorklet_WASM.html new file mode 100644 index 0000000000..0180465118 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_WASM.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for AudioWorklet + WASM</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [ + ["dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", true], + ["browser.tabs.remote.useCrossOriginOpenerPolicy", true], + ["browser.tabs.remote.useCrossOriginEmbedderPolicy", true], + ["javascript.options.shared_memory", true], + ]}); +} + +function create_wasmModule() { + return new Promise(resolve => { + info("Checking if we can play with WebAssembly..."); + + if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) { + resolve(null); + return; + } + + ok(WebAssembly, "WebAssembly object should exist"); + ok(WebAssembly.compile, "WebAssembly.compile function should exist"); + + const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary); + /* + js -e ' + t = wasmTextToBinary(` + (module + (func $foo (result i32) (i32.const 42)) + (export "foo" (func $foo)) + ) + `); + print(t) + ' + */ + // eslint-disable-next-line + const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]); + + WebAssembly.compile(fooModuleCode).then(m => { + ok(m instanceof WebAssembly.Module, "The WasmModule has been compiled."); + resolve(m); + }, () => { + ok(false, "The compilation of the wasmModule failed."); + resolve(null); + }); + }); +} + +function runTestInIframe() { + let audioContext = new AudioContext(); + audioContext.audioWorklet.addModule("worklet_audioWorklet_WASM.js") + .then(() => create_wasmModule()) + .then(wasmModule => { + const node = new AudioWorkletNode(audioContext, 'wasm'); + let msgId = 0; + node.port.onmessage = e => { + if (msgId++ == 0) { + ok(e.data.wasmModule instanceof WebAssembly.Module, "WasmModule received"); + } else { + ok(e.data.sab instanceof SharedArrayBuffer, "SAB received"); + SimpleTest.finish(); + } + } + + node.port.postMessage({wasmModule}); + node.port.postMessage({sab: new SharedArrayBuffer(1024)}); + node.connect(audioContext.destination); + }); +} +</script> + +</body> +</html> diff --git a/dom/worklet/tests/test_audioWorklet_WASM_Features.html b/dom/worklet/tests/test_audioWorklet_WASM_Features.html new file mode 100644 index 0000000000..944983a5d3 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_WASM_Features.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for AudioWorklet + WASM features</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +function runTestInIframe() { + let audioContext = new AudioContext(); + audioContext.audioWorklet.addModule("worklet_audioWorklet_WASM_features.js") + .then(() => { + const node = new AudioWorkletNode(audioContext, 'wasm'); + node.port.onmessage = e => { + let result = e.data; + ok(result === true, "Compilation succeeded"); + SimpleTest.finish(); + } + node.connect(audioContext.destination); + }); +} +</script> + +</body> +</html> diff --git a/dom/worklet/tests/test_audioWorklet_insecureContext.html b/dom/worklet/tests/test_audioWorklet_insecureContext.html new file mode 100644 index 0000000000..ac27578b7c --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_insecureContext.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for No AudioWorklet in insecure context</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +// This function is called into an iframe. +function runTestInIframe() { + ok(!window.isSecureContext, "Test should run in an insecure context"); + var audioContext = new AudioContext(); + ok(!("audioWorklet" in audioContext), + "AudioWorklet shouldn't be defined in AudioContext in a insecure context"); + SimpleTest.finish(); +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_audioWorklet_options.html b/dom/worklet/tests/test_audioWorklet_options.html new file mode 100644 index 0000000000..1b4b5c0077 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_options.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for AudioWorklet + Options + WASM</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> +<script type="application/javascript"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [ + ["dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", true], + ["browser.tabs.remote.useCrossOriginOpenerPolicy", true], + ["browser.tabs.remote.useCrossOriginEmbedderPolicy", true], + ["javascript.options.shared_memory", true], + ]}); +} + +function create_wasmModule() { + return new Promise(resolve => { + info("Checking if we can play with WebAssembly..."); + + if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) { + resolve(null); + return; + } + + ok(WebAssembly, "WebAssembly object should exist"); + ok(WebAssembly.compile, "WebAssembly.compile function should exist"); + + const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary); + + /* + js -e ' + t = wasmTextToBinary(` + (module + (func $foo (result i32) (i32.const 42)) + (export "foo" (func $foo)) + ) + `); + print(t) + ' + */ + // eslint-disable-next-line + const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]); + + WebAssembly.compile(fooModuleCode).then(m => { + ok(m instanceof WebAssembly.Module, "The WasmModule has been compiled."); + resolve(m); + }, () => { + ok(false, "The compilation of the wasmModule failed."); + resolve(null); + }); + }); +} + +function runTestInIframe() { + let audioContext = new AudioContext(); + audioContext.audioWorklet.addModule("worklet_audioWorklet_options.js") + .then(() => create_wasmModule()) + .then(wasmModule => { + const node = new AudioWorkletNode(audioContext, 'options', { processorOptions: { + wasmModule, sab: new SharedArrayBuffer(1024), + }}); + node.port.onmessage = e => { + ok(e.data.wasmModule instanceof WebAssembly.Module, "WasmModule received"); + ok(e.data.sab instanceof SharedArrayBuffer, "SAB received"); + SimpleTest.finish(); + } + + node.connect(audioContext.destination); + }); +} + +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_basic.html b/dom/worklet/tests/test_basic.html new file mode 100644 index 0000000000..989f6f8866 --- /dev/null +++ b/dom/worklet/tests/test_basic.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Worklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +// This function is called into an iframe. +function runTestInIframe() { + var audioContext = new AudioContext(); + ok(!!audioContext.audioWorklet, "audioContext.audioWorklet exists"); + + // First loading + audioContext.audioWorklet.addModule("common.js") + .then(() => { + ok(true, "Import should load a resource."); + }) + + // Second loading - same file + .then(() => { + return audioContext.audioWorklet.addModule("common.js") + }) + .then(() => { + ok(true, "Import should load a resource."); + }) + + // 3rd loading - a network error + .then(() => { + return audioContext.audioWorklet.addModule("404.js"); + }) + .then(() => { + ok(false, "The loading should fail."); + }, () => { + ok(true, "The loading should fail."); + }) + + // 4th loading - a network error + .then(() => { + return audioContext.audioWorklet.addModule("404.js"); + }) + .then(() => { + ok(false, "The loading should fail."); + }, () => { + ok(true, "The loading should fail."); + }) + + // done + .then(() => { + SimpleTest.finish(); + }); +} + +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_console.html b/dom/worklet/tests/test_console.html new file mode 100644 index 0000000000..5e4e43daa0 --- /dev/null +++ b/dom/worklet/tests/test_console.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Worklet - Console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +const WORKLET_SCRIPT = "worklet_console.js"; + +function configureTest() { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + consoleListener.prototype = { + observe(aSubject) { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "Hello world from a worklet") { + ok(true, "Message received \\o/"); + is(obj.filename, + new URL(WORKLET_SCRIPT, document.baseURI).toString()); + + ConsoleAPIStorage.removeLogEventListener(this.observe); + SimpleTest.finish(); + } + } + } + + var cl = new consoleListener(); +} + +// This function is called into an iframe. +function runTestInIframe() { + var audioContext = new AudioContext(); + audioContext.audioWorklet.addModule(WORKLET_SCRIPT); +} + +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_dump.html b/dom/worklet/tests/test_dump.html new file mode 100644 index 0000000000..9ee0a378b5 --- /dev/null +++ b/dom/worklet/tests/test_dump.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Worklet - Console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +// This function is called into an iframe. +function runTestInIframe() { + var audioContext = new AudioContext(); + audioContext.audioWorklet.addModule("worklet_dump.js") + .then(() => { + ok(true, "All good!"); + SimpleTest.finish(); + }); +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_dynamic_import.html b/dom/worklet/tests/test_dynamic_import.html new file mode 100644 index 0000000000..9d87a7a709 --- /dev/null +++ b/dom/worklet/tests/test_dynamic_import.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test import() should throw a TypeError for Worklets</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +const WORKLET_SCRIPT = "dynamic_import.js"; + +function configureTest() { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + // We use console API to check if a TypeError has been thrown, as worklets + // have limitations to post the result back to the main document: + // Worklets have a different global, and they don't have postMessage() APIs, + // and static import SimpleTest.js in worklets also don't work. + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + consoleListener.prototype = { + observe(aSubject) { + var obj = aSubject.wrappedJSObject; + info("Got console message:" + obj.arguments[0]); + is(TypeError.name + ": Success", obj.arguments[0], "import() should throw"); + + ConsoleAPIStorage.removeLogEventListener(this.observe); + SimpleTest.finish(); + } + } + + var cl = new consoleListener(); +} + +// This function is called into an iframe. +function runTestInIframe() { + var audioContext = new AudioContext(); + audioContext.audioWorklet.addModule(WORKLET_SCRIPT); +} + +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_exception.html b/dom/worklet/tests/test_exception.html new file mode 100644 index 0000000000..83d4ce4fd2 --- /dev/null +++ b/dom/worklet/tests/test_exception.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Exception in Worklet script</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +// This function is called into an iframe. +function runTestInIframe() { + let error; + + // This loading should fail + var audioContext = new AudioContext(); + audioContext.audioWorklet.addModule("404.js") + .then(() => { + ok(false, "We should not be called!"); + }, () => { + ok(true, "The script thrown but we are still here."); + }) + + // This should throw from JS + .then(() => { + return audioContext.audioWorklet.addModule("worklet_exception.js") + }) + .then(() => { + ok(true, "The script threw but we are still here."); + }, () => { + ok(false, "We should not be called!"); + }) + + // invalid_specifier.mjs will throw a TypeError. + .then(() => { + return audioContext.audioWorklet.addModule("invalid_specifier.mjs") + }) + .then(() => { + ok(false, "We should not be called!"); + }, (e) => { + ok(true, "The script thrown but we are still here."); + ok(e instanceof TypeError, "The error should be a TypeError."); + error = e; + }) + + // import "invalid_specifier.mjs" again, this will reuse the response from the + // previous addModule("invalid_specifier.mjs") call. + .then(() => { + return audioContext.audioWorklet.addModule("invalid_specifier.mjs") + }) + .then(() => { + ok(false, "We should not be called!"); + }, (e) => { + ok(true, "The script thrown but we are still here."); + ok (e === error, "The TypeError object should be reused."); + }) + + .then(() => { + SimpleTest.finish(); + }); +} + +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_fetch_failed.html b/dom/worklet/tests/test_fetch_failed.html new file mode 100644 index 0000000000..e60b332745 --- /dev/null +++ b/dom/worklet/tests/test_fetch_failed.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test fetch an child module script with an invalid uri for Worklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +// This function is called into an iframe. +function runTestInIframe() { + var audioContext = new AudioContext(); + ok(!!audioContext.audioWorklet, "audioContext.audioWorklet exists"); + + audioContext.audioWorklet.addModule("specifier_with_user.mjs") + .then(() => { + ok(false, "Error: load shouldn't succeed."); + }, () => { + ok(true, "OK: load should fail."); + }) + + // done + .then(() => { + SimpleTest.finish(); + }); +} + +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_import_with_cache.html b/dom/worklet/tests/test_import_with_cache.html new file mode 100644 index 0000000000..17c5be3b21 --- /dev/null +++ b/dom/worklet/tests/test_import_with_cache.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Worklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +// This function is called into an iframe. +function runTestInIframe() { + var audioContext = new AudioContext(); + function loading() { + audioContext.audioWorklet.addModule("server_import_with_cache.sjs") + .then(() => { + ok(true, "Import should load a resource."); + }, () => { + ok(false, "Import should load a resource."); + }) + .then(() => { + done(); + }); + } + + var count = 0; + const MAX = 10; + + function done() { + if (++count == MAX) { + SimpleTest.finish(); + } + } + + for (var i = 0; i < MAX; ++i) { + loading(); + } +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_paintWorklet.html b/dom/worklet/tests/test_paintWorklet.html new file mode 100644 index 0000000000..53b407e619 --- /dev/null +++ b/dom/worklet/tests/test_paintWorklet.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for PaintWorklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> + +function configureTest() { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + consoleListener.prototype = { + observe(aSubject) { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "So far so good") { + ok(true, "Message received \\o/"); + + ConsoleAPIStorage.removeLogEventListener(this.observe); + SimpleTest.finish(); + } + } + } + + var cl = new consoleListener(); + + return SpecialPowers.pushPrefEnv({ "set": [["dom.paintWorklet.enabled", true]] }); +} + +// This function is called into an iframe. +function runTestInIframe() { + paintWorklet.addModule("worklet_paintWorklet.js") +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/test_promise.html b/dom/worklet/tests/test_promise.html new file mode 100644 index 0000000000..5b438ddcc2 --- /dev/null +++ b/dom/worklet/tests/test_promise.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for promise in worklet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="common.js"></script> +</head> +<body> + +<script type="application/javascript"> +function runTestInIframe() { + if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) { + SimpleTest.finish(); + return; + } + + ok(window.isSecureContext, "Test should run in secure context"); + var audioContext = new AudioContext(); + ok(audioContext.audioWorklet instanceof AudioWorklet, + "AudioContext.audioWorklet should be an instance of AudioWorklet"); + audioContext.audioWorklet.addModule("worklet_promise.js") + .then(() => { + const node = new AudioWorkletNode(audioContext, 'promise'); + node.port.onmessage = e => { + ok(e.data instanceof WebAssembly.Module, "The WasmModule has been compiled into the worklet."); + SimpleTest.finish(); + } + + const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary); + /* + js -e ' + t = wasmTextToBinary(` + (module + (func $foo (result i32) (i32.const 42)) + (export "foo" (func $foo)) + ) + `); + print(t) + ' + */ + // eslint-disable-next-line + const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]); + + node.port.postMessage(fooModuleCode); + }); +} +</script> +</body> +</html> diff --git a/dom/worklet/tests/worklet_audioWorklet.js b/dom/worklet/tests/worklet_audioWorklet.js new file mode 100644 index 0000000000..fa916d4359 --- /dev/null +++ b/dom/worklet/tests/worklet_audioWorklet.js @@ -0,0 +1,16 @@ +class DummyProcessWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } +} + +// We need to pass a valid AudioWorkletProcessor here, otherwise, it will fail, +// and the console.log won't be executed +registerProcessor("sure!", DummyProcessWorkletProcessor); +console.log( + globalThis instanceof AudioWorkletGlobalScope ? "So far so good" : "error" +); diff --git a/dom/worklet/tests/worklet_audioWorklet_WASM.js b/dom/worklet/tests/worklet_audioWorklet_WASM.js new file mode 100644 index 0000000000..1215b4c8d0 --- /dev/null +++ b/dom/worklet/tests/worklet_audioWorklet_WASM.js @@ -0,0 +1,16 @@ +class WasmProcessWorkletProcessor extends AudioWorkletProcessor { + constructor(...args) { + super(...args); + this.port.onmessage = e => { + // Let's send it back. + this.port.postMessage(e.data); + }; + } + + process(inputs, outputs, parameters) { + // Do nothing, output silence + return true; + } +} + +registerProcessor("wasm", WasmProcessWorkletProcessor); diff --git a/dom/worklet/tests/worklet_audioWorklet_WASM_features.js b/dom/worklet/tests/worklet_audioWorklet_WASM_features.js new file mode 100644 index 0000000000..e9e4586f91 --- /dev/null +++ b/dom/worklet/tests/worklet_audioWorklet_WASM_features.js @@ -0,0 +1,44 @@ +class WasmProcessWorkletProcessor extends AudioWorkletProcessor { + constructor(...args) { + super(...args); + this.port.postMessage(testModules()); + } + + process(inputs, outputs, parameters) { + // Do nothing, output silence + return true; + } +} + +function testModule(binary) { + try { + let wasmModule = new WebAssembly.Module(binary); + } catch (error) { + if (error instanceof WebAssembly.CompileError) { + return error.message; + } + return "unknown error"; + } + return true; +} + +// TODO: test more features +function testModules() { + /* + js -e ' + t = wasmTextToBinary(` + (module + (tag) + ) + `); + print(t) + ' + */ + // eslint-disable-next-line + const exceptionHandlingCode = new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 13, 3, 1, 0, 0, + ]); + return testModule(exceptionHandlingCode); +} + +registerProcessor("wasm", WasmProcessWorkletProcessor); diff --git a/dom/worklet/tests/worklet_audioWorklet_options.js b/dom/worklet/tests/worklet_audioWorklet_options.js new file mode 100644 index 0000000000..eb7a704234 --- /dev/null +++ b/dom/worklet/tests/worklet_audioWorklet_options.js @@ -0,0 +1,12 @@ +class OptionsProcessWorkletProcessor extends AudioWorkletProcessor { + constructor(...args) { + super(...args); + this.port.postMessage(args[0].processorOptions); + } + + process(inputs, outputs, parameters) { + return true; + } +} + +registerProcessor("options", OptionsProcessWorkletProcessor); diff --git a/dom/worklet/tests/worklet_console.js b/dom/worklet/tests/worklet_console.js new file mode 100644 index 0000000000..557beb1af2 --- /dev/null +++ b/dom/worklet/tests/worklet_console.js @@ -0,0 +1 @@ +console.log("Hello world from a worklet"); diff --git a/dom/worklet/tests/worklet_dump.js b/dom/worklet/tests/worklet_dump.js new file mode 100644 index 0000000000..439d13f700 --- /dev/null +++ b/dom/worklet/tests/worklet_dump.js @@ -0,0 +1 @@ +dump("Hello world from a worklet"); diff --git a/dom/worklet/tests/worklet_exception.js b/dom/worklet/tests/worklet_exception.js new file mode 100644 index 0000000000..f3b473756e --- /dev/null +++ b/dom/worklet/tests/worklet_exception.js @@ -0,0 +1 @@ +foobar(); diff --git a/dom/worklet/tests/worklet_paintWorklet.js b/dom/worklet/tests/worklet_paintWorklet.js new file mode 100644 index 0000000000..7cf5256e51 --- /dev/null +++ b/dom/worklet/tests/worklet_paintWorklet.js @@ -0,0 +1,5 @@ +// This should work for real... at some point. +registerPaint("sure!", () => {}); +console.log( + globalThis instanceof PaintWorkletGlobalScope ? "So far so good" : "error" +); diff --git a/dom/worklet/tests/worklet_promise.js b/dom/worklet/tests/worklet_promise.js new file mode 100644 index 0000000000..8c593fd001 --- /dev/null +++ b/dom/worklet/tests/worklet_promise.js @@ -0,0 +1,22 @@ +class WasmProcessWorkletProcessor extends AudioWorkletProcessor { + constructor(...args) { + super(...args); + this.port.onmessage = e => { + WebAssembly.compile(e.data).then( + m => { + this.port.postMessage(m); + }, + () => { + this.port.postMessage("error"); + } + ); + }; + } + + process(inputs, outputs, parameters) { + // Do nothing, output silence + return true; + } +} + +registerProcessor("promise", WasmProcessWorkletProcessor); diff --git a/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js b/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js new file mode 100644 index 0000000000..cddb9524ec --- /dev/null +++ b/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js @@ -0,0 +1,384 @@ +// Define several classes. +class EmptyWorkletProcessor extends AudioWorkletProcessor {} + +class NoProcessWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } +} + +class BadDescriptorsWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return "A string"; + } +} + +class GoodDescriptorsWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "myParam", + defaultValue: 0.707, + }, + ]; + } +} + +class DummyProcessWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } +} + +class DescriptorsNoNameWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + defaultValue: 0.707, + }, + ]; + } +} + +class DescriptorsDefaultValueNotNumberWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + defaultValue: "test", + }, + ]; + } +} + +class DescriptorsMinValueNotNumberWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + minValue: "test", + }, + ]; + } +} + +class DescriptorsMaxValueNotNumberWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + maxValue: "test", + }, + ]; + } +} + +class DescriptorsDuplicatedNameWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + }, + { + name: "test", + }, + ]; + } +} + +class DescriptorsNotDictWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [42]; + } +} + +class DescriptorsOutOfRangeMinWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + defaultValue: 0, + minValue: 1, + maxValue: 2, + }, + ]; + } +} + +class DescriptorsOutOfRangeMaxWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + defaultValue: 3, + minValue: 1, + maxValue: 2, + }, + ]; + } +} + +class DescriptorsBadRangeMaxWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + } + + process() { + // Do nothing, output silence + } + + static get parameterDescriptors() { + return [ + { + name: "test", + defaultValue: 1.5, + minValue: 2, + maxValue: 1, + }, + ]; + } +} + +// Test not a constructor +// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not a constructor." +try { + registerProcessor("sure!", () => {}); +} catch (e) { + console.log(e); +} + +// Test empty name +// "NotSupportedError: Argument 1 of AudioWorkletGlobalScope.registerProcessor should not be an empty string." +try { + registerProcessor("", EmptyWorkletProcessor); +} catch (e) { + console.log(e); +} + +// Test not an object +// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not an object." +try { + registerProcessor("my-worklet-processor", ""); +} catch (e) { + console.log(e); +} + +// Test Empty class definition +registerProcessor("empty-worklet-processor", EmptyWorkletProcessor); + +// Test class with constructor but not process function +registerProcessor("no-worklet-processor", NoProcessWorkletProcessor); + +// Test class with parameterDescriptors being iterable, but the elements are not +// dictionaries. +// "TypeError: AudioWorkletGlobalScope.registerProcessor: Element 0 in parameterDescriptors can't be converted to a dictionary.", +try { + registerProcessor( + "bad-descriptors-worklet-processor", + BadDescriptorsWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// Test class with good parameterDescriptors +// No error expected here +registerProcessor( + "good-descriptors-worklet-processor", + GoodDescriptorsWorkletProcessor +); + +// Test class with constructor and process function +// No error expected here +registerProcessor("dummy-worklet-processor", DummyProcessWorkletProcessor); + +// Test class adding class with the same name twice +// "NotSupportedError: Operation is not supported: Argument 1 of AudioWorkletGlobalScope.registerProcessor is invalid: a class with the same name is already registered." +try { + registerProcessor("dummy-worklet-processor", DummyProcessWorkletProcessor); +} catch (e) { + console.log(e); +} + +// "name" is a mandatory field in descriptors +// "TypeError: Missing required 'name' member of AudioParamDescriptor." +try { + registerProcessor( + "descriptors-no-name-worklet-processor", + DescriptorsNoNameWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// "defaultValue" should be a number +// "TypeError: 'defaultValue' member of AudioParamDescriptor is not a finite floating-point value." +try { + registerProcessor( + "descriptors-default-value-not-number-worklet-processor", + DescriptorsDefaultValueNotNumberWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// "min" should be a number +// "TypeError: 'minValue' member of AudioParamDescriptor is not a finite floating-point value." +try { + registerProcessor( + "descriptors-min-value-not-number-worklet-processor", + DescriptorsMinValueNotNumberWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// "max" should be a number +// "TypeError: 'maxValue' member of AudioParamDescriptor is not a finite floating-point value." +try { + registerProcessor( + "descriptors-max-value-not-number-worklet-processor", + DescriptorsMaxValueNotNumberWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// Duplicated values are not allowed for "name" +// "NotSupportedError: Duplicated name \"test\" in parameterDescriptors" +try { + registerProcessor( + "descriptors-duplicated-name-worklet-processor", + DescriptorsDuplicatedNameWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// Descriptors' elements should be dictionnary +// "TypeError: Element 0 in parameterDescriptors can't be converted to a dictionary.", +try { + registerProcessor( + "descriptors-not-dict-worklet-processor", + DescriptorsNotDictWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// defaultValue value should be in range [minValue, maxValue]. defaultValue < minValue is not allowed +// "NotSupportedError: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.", +try { + registerProcessor( + "descriptors-out-of-range-min-worklet-processor", + DescriptorsOutOfRangeMinWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// defaultValue value should be in range [minValue, maxValue]. defaultValue > maxValue is not allowed +// "NotSupportedError: In parameterDescriptors, test defaultValue is out of the range defined by minValue and maxValue.", +try { + registerProcessor( + "descriptors-out-of-range-max-worklet-processor", + DescriptorsOutOfRangeMaxWorkletProcessor + ); +} catch (e) { + console.log(e); +} + +// We should have minValue < maxValue to define a valid range +// "NotSupportedError: In parameterDescriptors, test minValue should be smaller than maxValue.", +try { + registerProcessor( + "descriptors-bad-range-max-worklet-processor", + DescriptorsBadRangeMaxWorkletProcessor + ); +} catch (e) { + console.log(e); +} |