diff options
Diffstat (limited to 'dom/worklet')
36 files changed, 2845 insertions, 0 deletions
diff --git a/dom/worklet/Worklet.cpp b/dom/worklet/Worklet.cpp new file mode 100644 index 0000000000..edbfe99060 --- /dev/null +++ b/dom/worklet/Worklet.cpp @@ -0,0 +1,492 @@ +/* -*- 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/WorkletBinding.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/WorkletImpl.h" +#include "js/Modules.h" +#include "js/SourceText.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamLoader.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIInputStreamPump.h" +#include "nsNetUtil.h" +#include "xpcprivate.h" + +namespace mozilla { +namespace dom { + +class ExecutionRunnable final : public Runnable { + public: + ExecutionRunnable(WorkletFetchHandler* aHandler, WorkletImpl* aWorkletImpl, + JS::UniqueTwoByteChars aScriptBuffer, size_t aScriptLength) + : Runnable("Worklet::ExecutionRunnable"), + mHandler(aHandler), + mWorkletImpl(aWorkletImpl), + mScriptBuffer(std::move(aScriptBuffer)), + mScriptLength(aScriptLength), + mParentRuntime( + JS_GetParentRuntime(CycleCollectedJSContext::Get()->Context())), + mResult(NS_ERROR_FAILURE) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mParentRuntime); + } + + NS_IMETHOD + Run() override; + + private: + void RunOnWorkletThread(); + + void RunOnMainThread(); + + bool ParseAndLinkModule(JSContext* aCx, JS::MutableHandle<JSObject*> aModule); + + RefPtr<WorkletFetchHandler> mHandler; + RefPtr<WorkletImpl> mWorkletImpl; + JS::UniqueTwoByteChars mScriptBuffer; + size_t mScriptLength; + JSRuntime* mParentRuntime; + nsresult mResult; +}; + +// --------------------------------------------------------------------------- +// WorkletFetchHandler + +class WorkletFetchHandler final : public PromiseNativeHandler, + public nsIStreamLoaderObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + static already_AddRefed<Promise> Fetch(Worklet* aWorklet, JSContext* aCx, + const nsAString& aModuleURL, + const WorkletOptions& aOptions, + ErrorResult& aRv) { + MOZ_ASSERT(aWorklet); + MOZ_ASSERT(NS_IsMainThread()); + + 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))) { + promise->MaybeReject(rv); + return promise.forget(); + } + + nsAutoCString spec; + rv = resolvedURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->MaybeReject(rv); + return promise.forget(); + } + + // Maybe we already have an handler for this URI + { + WorkletFetchHandler* handler = aWorklet->GetImportFetchHandler(spec); + if (handler) { + handler->AddPromise(promise); + return promise.forget(); + } + } + + RequestOrUSVString requestInput; + requestInput.SetAsUSVString().ShareOrDependUpon(aModuleURL); + + RequestInit requestInit; + requestInit.mCredentials.Construct(aOptions.mCredentials); + + SafeRefPtr<Request> request = + Request::Constructor(global, aCx, requestInput, requestInit, aRv); + if (aRv.Failed()) { + return nullptr; + } + + request->OverrideContentPolicyType(aWorklet->Impl()->ContentPolicyType()); + + RequestOrUSVString finalRequestInput; + finalRequestInput.SetAsRequest() = request.unsafeGetRawPtr(); + + RefPtr<Promise> fetchPromise = FetchRequest( + global, finalRequestInput, requestInit, CallerType::System, aRv); + if (NS_WARN_IF(aRv.Failed())) { + // OK to just return null, since caller will ignore return value + // anyway if aRv is a failure. + return nullptr; + } + + RefPtr<WorkletFetchHandler> handler = + new WorkletFetchHandler(aWorklet, spec, promise); + fetchPromise->AppendNativeHandler(handler); + + aWorklet->AddImportFetchHandler(spec, handler); + return promise.forget(); + } + + virtual void ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) override { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aValue.isObject()) { + RejectPromises(NS_ERROR_FAILURE); + return; + } + + RefPtr<Response> response; + nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPromises(NS_ERROR_FAILURE); + return; + } + + if (!response->Ok()) { + RejectPromises(NS_ERROR_DOM_NETWORK_ERR); + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + response->GetBody(getter_AddRefs(inputStream)); + if (!inputStream) { + RejectPromises(NS_ERROR_DOM_NETWORK_ERR); + return; + } + + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPromises(rv); + return; + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPromises(rv); + return; + } + + rv = pump->AsyncRead(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPromises(rv); + return; + } + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(pump); + if (rr) { + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + rv = rr->RetargetDeliveryTo(sts); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the nsIInputStreamPump to a IO thread."); + } + } + } + + NS_IMETHOD + OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) override { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(aStatus)) { + RejectPromises(aStatus); + return NS_OK; + } + + JS::UniqueTwoByteChars scriptTextBuf; + size_t scriptTextLength; + nsresult rv = + ScriptLoader::ConvertToUTF16(nullptr, aString, aStringLen, u"UTF-8"_ns, + nullptr, scriptTextBuf, scriptTextLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPromises(rv); + return NS_OK; + } + + // Moving the ownership of the buffer + nsCOMPtr<nsIRunnable> runnable = new ExecutionRunnable( + this, mWorklet->mImpl, std::move(scriptTextBuf), scriptTextLength); + + if (NS_FAILED(mWorklet->mImpl->SendControlMessage(runnable.forget()))) { + RejectPromises(NS_ERROR_FAILURE); + return NS_OK; + } + + return NS_OK; + } + + virtual void RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) override { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromises(NS_ERROR_DOM_NETWORK_ERR); + } + + const nsCString& URL() const { return mURL; } + + void ExecutionFailed(nsresult aRv) { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromises(aRv); + } + + void ExecutionSucceeded() { + MOZ_ASSERT(NS_IsMainThread()); + ResolvePromises(); + } + + private: + WorkletFetchHandler(Worklet* aWorklet, const nsACString& aURL, + Promise* aPromise) + : mWorklet(aWorklet), mStatus(ePending), mErrorStatus(NS_OK), mURL(aURL) { + MOZ_ASSERT(aWorklet); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(NS_IsMainThread()); + + mPromises.AppendElement(aPromise); + } + + ~WorkletFetchHandler() = default; + + void AddPromise(Promise* aPromise) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(NS_IsMainThread()); + + switch (mStatus) { + case ePending: + mPromises.AppendElement(aPromise); + return; + + case eRejected: + MOZ_ASSERT(NS_FAILED(mErrorStatus)); + aPromise->MaybeReject(mErrorStatus); + return; + + case eResolved: + aPromise->MaybeResolveWithUndefined(); + return; + } + } + + void RejectPromises(nsresult aResult) { + MOZ_ASSERT(mStatus == ePending); + MOZ_ASSERT(NS_FAILED(aResult)); + MOZ_ASSERT(NS_IsMainThread()); + + for (uint32_t i = 0; i < mPromises.Length(); ++i) { + mPromises[i]->MaybeReject(aResult); + } + mPromises.Clear(); + + mStatus = eRejected; + mErrorStatus = aResult; + mWorklet = nullptr; + } + + void ResolvePromises() { + MOZ_ASSERT(mStatus == ePending); + MOZ_ASSERT(NS_IsMainThread()); + + for (uint32_t i = 0; i < mPromises.Length(); ++i) { + mPromises[i]->MaybeResolveWithUndefined(); + } + mPromises.Clear(); + + mStatus = eResolved; + mWorklet = nullptr; + } + + RefPtr<Worklet> mWorklet; + nsTArray<RefPtr<Promise>> mPromises; + + enum { ePending, eRejected, eResolved } mStatus; + + nsresult mErrorStatus; + + nsCString mURL; +}; + +NS_IMPL_ISUPPORTS(WorkletFetchHandler, nsIStreamLoaderObserver) + +NS_IMETHODIMP +ExecutionRunnable::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. + if (!NS_IsMainThread()) { + RunOnWorkletThread(); + return NS_DispatchToMainThread(this); + } + + RunOnMainThread(); + return NS_OK; +} + +bool ExecutionRunnable::ParseAndLinkModule( + JSContext* aCx, JS::MutableHandle<JSObject*> aModule) { + JS::CompileOptions compileOptions(aCx); + compileOptions.setIntroductionType("Worklet"); + compileOptions.setFileAndLine(mHandler->URL().get(), 1); + compileOptions.setIsRunOnce(true); + compileOptions.setNoScriptRval(true); + + JS::SourceText<char16_t> buffer; + if (!buffer.init(aCx, std::move(mScriptBuffer), mScriptLength)) { + return false; + } + JS::Rooted<JSObject*> module(aCx, + JS::CompileModule(aCx, compileOptions, buffer)); + if (!module) { + return false; + } + // Link() was previously named Instantiate(). + // https://github.com/tc39/ecma262/pull/1312 + // Any imports will fail here - bug 1572644. + if (!JS::ModuleInstantiate(aCx, module)) { + return false; + } + aModule.set(module); + return true; +} + +void ExecutionRunnable::RunOnWorkletThread() { + WorkletThread* workletThread = + static_cast<WorkletThread*>(NS_GetCurrentThread()); + workletThread->EnsureCycleCollectedJSContext(mParentRuntime); + + WorkletGlobalScope* globalScope = mWorkletImpl->GetGlobalScope(); + if (!globalScope) { + mResult = NS_ERROR_DOM_UNKNOWN_ERR; + return; + } + + AutoEntryScript aes(globalScope, "Worklet"); + JSContext* cx = aes.cx(); + + JS::Rooted<JSObject*> module(cx); + if (!ParseAndLinkModule(cx, &module)) { + mResult = NS_ERROR_DOM_ABORT_ERR; + return; + } + + // https://drafts.css-houdini.org/worklets/#fetch-and-invoke-a-worklet-script + // invokes + // https://html.spec.whatwg.org/multipage/webappapis.html#run-a-module-script + // without /rethrow errors/ and so unhandled exceptions do not cause the + // promise to be rejected. + JS::Rooted<JS::Value> ignored(cx); + JS::ModuleEvaluate(cx, module, &ignored); + + // All done. + mResult = NS_OK; +} + +void ExecutionRunnable::RunOnMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(mResult)) { + mHandler->ExecutionFailed(mResult); + return; + } + + mHandler->ExecutionSucceeded(); +} + +// --------------------------------------------------------------------------- +// Worklet + +NS_IMPL_CYCLE_COLLECTION_CLASS(Worklet) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Worklet) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwnedObject) + 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_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Worklet) + +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); +} + +already_AddRefed<Promise> Worklet::AddModule(JSContext* aCx, + const nsAString& aModuleURL, + const WorkletOptions& aOptions, + CallerType aCallerType, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + return WorkletFetchHandler::Fetch(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.Put(aURI, RefPtr{aHandler}); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/worklet/Worklet.h b/dom/worklet/Worklet.h new file mode 100644 index 0000000000..5d7627428f --- /dev/null +++ b/dom/worklet/Worklet.h @@ -0,0 +1,73 @@ +/* -*- 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; +struct WorkletOptions; +enum class CallerType : uint32_t; + +class Worklet final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_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; } + + 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; + + friend class WorkletFetchHandler; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Worklet_h diff --git a/dom/worklet/WorkletGlobalScope.cpp b/dom/worklet/WorkletGlobalScope.cpp new file mode 100644 index 0000000000..52372650c4 --- /dev/null +++ b/dom/worklet/WorkletGlobalScope.cpp @@ -0,0 +1,92 @@ +/* -*- 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/dom/WorkletGlobalScopeBinding.h" +#include "mozilla/dom/WorkletImpl.h" +#include "mozilla/dom/WorkletThread.h" +#include "mozilla/dom/Console.h" +#include "nsJSUtils.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkletGlobalScope) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkletGlobalScope) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole) + tmp->UnlinkObjectsInGlobal(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkletGlobalScope) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole) + tmp->TraverseObjectsInGlobal(cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(WorkletGlobalScope) + +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(const Maybe<nsID>& aAgentClusterId, + bool aSharedMemoryAllowed) + : mCreationTimeStamp(TimeStamp::Now()), + mAgentClusterId(aAgentClusterId), + mSharedMemoryAllowed(aSharedMemoryAllowed) {} + +WorkletGlobalScope::~WorkletGlobalScope() = default; + +JSObject* WorkletGlobalScope::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + MOZ_CRASH("We should never get here!"); + return nullptr; +} + +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::Dump(const Optional<nsAString>& aString) const { + WorkletThread::AssertIsOnWorkletThread(); + + if (!nsJSUtils::DumpEnabled()) { + return; + } + + if (!aString.WasPassed()) { + return; + } + + NS_ConvertUTF16toUTF8 str(aString.Value()); + +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get()); +#endif + + fputs(str.get(), stdout); + fflush(stdout); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/worklet/WorkletGlobalScope.h b/dom/worklet/WorkletGlobalScope.h new file mode 100644 index 0000000000..3f24415d42 --- /dev/null +++ b/dom/worklet/WorkletGlobalScope.h @@ -0,0 +1,89 @@ +/* -*- 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 mozilla { + +class ErrorResult; +class WorkletImpl; + +namespace dom { + +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_SCRIPT_HOLDER_CLASS(WorkletGlobalScope) + + WorkletGlobalScope(const Maybe<nsID>& aAgentClusterId, + bool aSharedMemoryAllowed); + + nsIGlobalObject* GetParentObject() const { return nullptr; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) = 0; + + JSObject* GetGlobalJSObject() override { return GetWrapper(); } + JSObject* GetGlobalJSObjectPreserveColor() const override { + return GetWrapperPreserveColor(); + } + + already_AddRefed<Console> GetConsole(JSContext* aCx, ErrorResult& aRv); + + virtual WorkletImpl* Impl() const = 0; + + void Dump(const Optional<nsAString>& aString) const; + + DOMHighResTimeStamp TimeStampToDOMHighRes(const TimeStamp& aTimeStamp) const { + MOZ_ASSERT(!aTimeStamp.IsNull()); + TimeDuration duration = aTimeStamp - mCreationTimeStamp; + return duration.ToMilliseconds(); + } + + Maybe<nsID> GetAgentClusterId() const override { return mAgentClusterId; } + + bool IsSharedMemoryAllowed() const override { return mSharedMemoryAllowed; } + + protected: + ~WorkletGlobalScope(); + ; + + private: + TimeStamp mCreationTimeStamp; + Maybe<nsID> mAgentClusterId; + RefPtr<Console> mConsole; + bool mSharedMemoryAllowed; +}; + +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..8e6a70289d --- /dev/null +++ b/dom/worklet/WorkletImpl.cpp @@ -0,0 +1,142 @@ +/* -*- 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/RegisterWorkletBindings.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkletBinding.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "nsGlobalWindowInner.h" + +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) { + Unused << NS_WARN_IF( + NS_FAILED(ipc::PrincipalToPrincipalInfo(mPrincipal, &mPrincipalInfo))); + + if (aWindow->GetDocGroup()) { + mAgentClusterId.emplace(aWindow->GetDocGroup()->AgentClusterId()); + } + + mSharedMemoryAllowed = + nsGlobalWindowInner::Cast(aWindow)->IsSharedMemoryAllowed(); +} + +WorkletImpl::~WorkletImpl() { + MOZ_ASSERT(!mGlobalScope); + MOZ_ASSERT(!mPrincipal || NS_IsMainThread()); +} + +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); + + 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; + } + mPrincipal = 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..94c962fa30 --- /dev/null +++ b/dom/worklet/WorkletImpl.h @@ -0,0 +1,112 @@ +/* -*- 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/Maybe.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/ipc/PBackgroundSharedTypes.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 { + 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); + + nsIPrincipal* Principal() const { + MOZ_ASSERT(NS_IsMainThread()); + return mPrincipal; + } + + void NotifyWorkletFinished(); + + virtual nsContentPolicyType ContentPolicyType() const = 0; + + // Execution thread only. + dom::WorkletGlobalScope* GetGlobalScope(); + + // Any thread. + + const WorkletLoadInfo& LoadInfo() const { return mWorkletLoadInfo; } + const OriginAttributes& OriginAttributesRef() const { + return mPrincipalInfo.get_NullPrincipalInfo().attrs(); + } + const ipc::PrincipalInfo& PrincipalInfo() const { return mPrincipalInfo; } + + const Maybe<nsID>& GetAgentClusterId() const { return mAgentClusterId; } + + bool IsSharedMemoryAllowed() const { return mSharedMemoryAllowed; } + + protected: + WorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal); + virtual ~WorkletImpl(); + + virtual already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() = 0; + + // Modified only in constructor. + ipc::PrincipalInfo mPrincipalInfo; + // Accessed on only worklet parent thread. + nsCOMPtr<nsIPrincipal> mPrincipal; + + const WorkletLoadInfo mWorkletLoadInfo; + + // Parent thread only. + RefPtr<dom::WorkletThread> mWorkletThread; + bool mTerminated; + + // Execution thread only. + RefPtr<dom::WorkletGlobalScope> mGlobalScope; + bool mFinishedOnExecutionThread; + + Maybe<nsID> mAgentClusterId; + + bool mSharedMemoryAllowed; +}; + +} // namespace mozilla + +#endif // mozilla_dom_worklet_WorkletImpl_h diff --git a/dom/worklet/WorkletPrincipals.cpp b/dom/worklet/WorkletPrincipals.cpp new file mode 100644 index 0000000000..f86ec11a9c --- /dev/null +++ b/dom/worklet/WorkletPrincipals.cpp @@ -0,0 +1,41 @@ +/* -*- 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 "WorkletPrincipals.h" + +#include "mozilla/dom/WorkletImpl.h" +#include "nsJSPrincipals.h" + +namespace mozilla { +namespace dom { + +WorkletPrincipals::WorkletPrincipals(WorkletImpl* aWorkletImpl) + : JSPrincipals(), mWorkletImpl(aWorkletImpl) { + setDebugToken(kJSPrincipalsDebugToken); +} + +WorkletPrincipals::~WorkletPrincipals() = default; + +bool WorkletPrincipals::write(JSContext* aCx, + JSStructuredCloneWriter* aWriter) { + // This is a serialization of the NullPrincipal corresponding to the worklet + // environment settings object for the WorkletGlobalScope. + // https://drafts.css-houdini.org/worklets/#set-up-a-worklet-environment-settings-object + return nsJSPrincipals::WritePrincipalInfo(aWriter, + mWorkletImpl->PrincipalInfo()); +} + +bool WorkletPrincipals::isSystemOrAddonPrincipal() { + // Per Bug 1578623 rev a83797ed249c - Worklets are always NullPrincipal + return false; +} + +void WorkletPrincipals::Destroy(JSPrincipals* aPrincipals) { + delete static_cast<WorkletPrincipals*>(aPrincipals); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/worklet/WorkletPrincipals.h b/dom/worklet/WorkletPrincipals.h new file mode 100644 index 0000000000..a5132c04cb --- /dev/null +++ b/dom/worklet/WorkletPrincipals.h @@ -0,0 +1,40 @@ +/* -*- 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 DOM_WORKLET_WORKLETPRINCIPALS_H_ +#define DOM_WORKLET_WORKLETPRINCIPALS_H_ + +#include "js/Principals.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { + +class WorkletImpl; + +namespace dom { + +struct MOZ_HEAP_CLASS WorkletPrincipals final : public JSPrincipals { + // A new WorkletPrincipals has refcount zero. + explicit WorkletPrincipals(WorkletImpl* aWorkletImpl); + + bool write(JSContext* aCx, JSStructuredCloneWriter* aWriter) override; + + bool isSystemOrAddonPrincipal() override; + + // Callback for JS_InitDestroyPrincipalsCallback() + static void Destroy(JSPrincipals* aPrincipals); + + static const uint32_t kJSPrincipalsDebugToken = 0x7e2df9f4; + + private: + ~WorkletPrincipals(); + RefPtr<WorkletImpl> mWorkletImpl; +}; + +} // namespace dom +} // namespace mozilla + +#endif // DOM_WORKLET_WORKLETPRINCIPALS_H_ diff --git a/dom/worklet/WorkletThread.cpp b/dom/worklet/WorkletThread.cpp new file mode 100644 index 0000000000..e59c7c7919 --- /dev/null +++ b/dom/worklet/WorkletThread.cpp @@ -0,0 +1,458 @@ +/* -*- 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 "mozilla/dom/AtomList.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "mozilla/dom/WorkletPrincipals.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/EventQueue.h" +#include "mozilla/ThreadEventQueue.h" +#include "js/Exception.h" + +namespace mozilla { +namespace 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::HandleObject aObj) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aObj); + MOZ_ASSERT(mozilla::dom::IsDOMObject(aObj)); + return mozilla::dom::TryPreserveWrapper(aObj); +} + +JSObject* Wrap(JSContext* aCx, JS::HandleObject aExisting, + JS::HandleObject 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() 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(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, WorkletPrincipals::Destroy); + 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(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, 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 + // WorkletThread is alive. + WorkletThread* workletThread = reinterpret_cast<WorkletThread*>(aClosure); + + nsresult rv = workletThread->DispatchRunnable(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); + })); + + return NS_SUCCEEDED(rv); +} + +void WorkletThread::EnsureCycleCollectedJSContext(JSRuntime* aParentRuntime) { + 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_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 WorkletThread 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, + (void*)this); + + JS_SetNativeStackQuota(context->Context(), + WORKLET_CONTEXT_NATIVE_STACK_LIMIT); + + if (!JS::InitSelfHostedCode(context->Context())) { + // 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 dom +} // namespace mozilla diff --git a/dom/worklet/WorkletThread.h b/dom/worklet/WorkletThread.h new file mode 100644 index 0000000000..37324cd57e --- /dev/null +++ b/dom/worklet/WorkletThread.h @@ -0,0 +1,75 @@ +/* -*- 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 mozilla { +namespace 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. + void EnsureCycleCollectedJSContext(JSRuntime* aParentRuntime); + 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 dom +} // namespace mozilla + +#endif // mozilla_dom_worklet_WorkletThread_h diff --git a/dom/worklet/moz.build b/dom/worklet/moz.build new file mode 100644 index 0000000000..706921ff53 --- /dev/null +++ b/dom/worklet/moz.build @@ -0,0 +1,34 @@ +# -*- 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.h", + "WorkletGlobalScope.h", + "WorkletImpl.h", + "WorkletPrincipals.h", + "WorkletThread.h", +] + +UNIFIED_SOURCES += [ + "Worklet.cpp", + "WorkletGlobalScope.cpp", + "WorkletImpl.cpp", + "WorkletPrincipals.cpp", + "WorkletThread.cpp", +] + +LOCAL_INCLUDES += [ + "/js/xpconnect/src", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] + +FINAL_LIBRARY = "xul" diff --git a/dom/worklet/tests/.eslintrc.js b/dom/worklet/tests/.eslintrc.js new file mode 100644 index 0000000000..845ed3f013 --- /dev/null +++ b/dom/worklet/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/mochitest-test"], +}; diff --git a/dom/worklet/tests/common.js b/dom/worklet/tests/common.js new file mode 100644 index 0000000000..16e56e7dc5 --- /dev/null +++ b/dom/worklet/tests/common.js @@ -0,0 +1,23 @@ +window.onload = function() { + // We are the parent. Let's load the test. + if (parent == this || !location.search.includes("worklet_iframe")) { + SimpleTest.waitForExplicitFinish(); + + configureTest().then(() => { + 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/mochitest.ini b/dom/worklet/tests/mochitest.ini new file mode 100644 index 0000000000..91b87b17eb --- /dev/null +++ b/dom/worklet/tests/mochitest.ini @@ -0,0 +1,32 @@ +[DEFAULT] +scheme = https +support-files = + common.js + +[test_basic.html] +[test_console.html] +support-files=worklet_console.js +[test_import_with_cache.html] +skip-if = verify +support-files=server_import_with_cache.sjs +[test_dump.html] +support-files=worklet_dump.js +[test_audioWorklet_insecureContext.html] +scheme = http +[test_audioWorklet.html] +support-files=worklet_audioWorklet.js +[test_audioWorkletGlobalScopeRegisterProcessor.html] +support-files=worklet_test_audioWorkletGlobalScopeRegisterProcessor.js +[test_exception.html] +support-files=worklet_exception.js +[test_paintWorklet.html] +skip-if = release_or_beta +support-files=worklet_paintWorklet.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_options.html] +skip-if = release_or_beta # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled +support-files=worklet_audioWorklet_options.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..79a934dd3a --- /dev/null +++ b/dom/worklet/tests/server_import_with_cache.sjs @@ -0,0 +1,13 @@ +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/test_audioWorklet.html b/dom/worklet/tests/test_audioWorklet.html new file mode 100644 index 0000000000..88157f1d7c --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet.html @@ -0,0 +1,50 @@ +<!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() { + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event"); + } + + consoleListener.prototype = { + observe(aSubject, aTopic, aData) { + if (aTopic == "console-api-log-event") { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "So far so good") { + ok(true, "Message received \\o/"); + + SpecialPowers.removeObserver(this, "console-api-log-event"); + SimpleTest.finish(); + return; + } + } + } + } + + var cl = new consoleListener(); + + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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..428f3134eb --- /dev/null +++ b/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html @@ -0,0 +1,75 @@ +<!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; + + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event"); + } + + consoleListener.prototype = { + observe(aSubject, aTopic, aData) { + if (aTopic == "console-api-log-event") { + 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! + SpecialPowers.removeObserver(this, "console-api-log-event"); + SimpleTest.finish(); + return; + } + } + } + } + + var cl = new consoleListener(); + + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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..127cc8b924 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_WASM.html @@ -0,0 +1,85 @@ +<!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.audioworklet.enabled", true], + ["dom.worklet.enabled", true], + ["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_insecureContext.html b/dom/worklet/tests/test_audioWorklet_insecureContext.html new file mode 100644 index 0000000000..3cbb419ac3 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_insecureContext.html @@ -0,0 +1,29 @@ +<!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"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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..df7a8b5649 --- /dev/null +++ b/dom/worklet/tests/test_audioWorklet_options.html @@ -0,0 +1,81 @@ +<!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.audioworklet.enabled", true], + ["dom.worklet.enabled", true], + ["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..b13cadd6d1 --- /dev/null +++ b/dom/worklet/tests/test_basic.html @@ -0,0 +1,66 @@ +<!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"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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..bc27fa9bf5 --- /dev/null +++ b/dom/worklet/tests/test_console.html @@ -0,0 +1,51 @@ +<!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() { + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event"); + } + + consoleListener.prototype = { + observe(aSubject, aTopic, aData) { + if (aTopic == "console-api-log-event") { + 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()); + + SpecialPowers.removeObserver(this, "console-api-log-event"); + SimpleTest.finish(); + return; + } + } + } + } + + var cl = new consoleListener(); + + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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..f7885a1e21 --- /dev/null +++ b/dom/worklet/tests/test_dump.html @@ -0,0 +1,30 @@ +<!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"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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_exception.html b/dom/worklet/tests/test_exception.html new file mode 100644 index 0000000000..8124813d20 --- /dev/null +++ b/dom/worklet/tests/test_exception.html @@ -0,0 +1,47 @@ +<!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"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// This function is called into an iframe. +function runTestInIframe() { + // 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!"); + }) + + .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..de1744f9cc --- /dev/null +++ b/dom/worklet/tests/test_import_with_cache.html @@ -0,0 +1,49 @@ +<!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"> + +function configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +// 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..e477d975a7 --- /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() { + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event"); + } + + consoleListener.prototype = { + observe(aSubject, aTopic, aData) { + if (aTopic == "console-api-log-event") { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "So far so good") { + ok(true, "Message received \\o/"); + + SpecialPowers.removeObserver(this, "console-api-log-event"); + SimpleTest.finish(); + return; + } + } + } + } + + var cl = new consoleListener(); + + return SpecialPowers.pushPrefEnv( + {"set": [["dom.paintWorklet.enabled", true], + ["dom.worklet.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..8ea71af9a6 --- /dev/null +++ b/dom/worklet/tests/test_promise.html @@ -0,0 +1,57 @@ +<!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 configureTest() { + return SpecialPowers.pushPrefEnv( + {"set": [["dom.audioworklet.enabled", true], + ["dom.worklet.enabled", true]]}); +} + +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_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); +} |