summaryrefslogtreecommitdiffstats
path: root/dom/worklet
diff options
context:
space:
mode:
Diffstat (limited to 'dom/worklet')
-rw-r--r--dom/worklet/Worklet.cpp492
-rw-r--r--dom/worklet/Worklet.h73
-rw-r--r--dom/worklet/WorkletGlobalScope.cpp92
-rw-r--r--dom/worklet/WorkletGlobalScope.h89
-rw-r--r--dom/worklet/WorkletImpl.cpp142
-rw-r--r--dom/worklet/WorkletImpl.h112
-rw-r--r--dom/worklet/WorkletPrincipals.cpp41
-rw-r--r--dom/worklet/WorkletPrincipals.h40
-rw-r--r--dom/worklet/WorkletThread.cpp458
-rw-r--r--dom/worklet/WorkletThread.h75
-rw-r--r--dom/worklet/moz.build34
-rw-r--r--dom/worklet/tests/.eslintrc.js5
-rw-r--r--dom/worklet/tests/common.js23
-rw-r--r--dom/worklet/tests/mochitest.ini32
-rw-r--r--dom/worklet/tests/server_import_with_cache.sjs13
-rw-r--r--dom/worklet/tests/test_audioWorklet.html50
-rw-r--r--dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html75
-rw-r--r--dom/worklet/tests/test_audioWorklet_WASM.html85
-rw-r--r--dom/worklet/tests/test_audioWorklet_insecureContext.html29
-rw-r--r--dom/worklet/tests/test_audioWorklet_options.html81
-rw-r--r--dom/worklet/tests/test_basic.html66
-rw-r--r--dom/worklet/tests/test_console.html51
-rw-r--r--dom/worklet/tests/test_dump.html30
-rw-r--r--dom/worklet/tests/test_exception.html47
-rw-r--r--dom/worklet/tests/test_import_with_cache.html49
-rw-r--r--dom/worklet/tests/test_paintWorklet.html46
-rw-r--r--dom/worklet/tests/test_promise.html57
-rw-r--r--dom/worklet/tests/worklet_audioWorklet.js16
-rw-r--r--dom/worklet/tests/worklet_audioWorklet_WASM.js16
-rw-r--r--dom/worklet/tests/worklet_audioWorklet_options.js12
-rw-r--r--dom/worklet/tests/worklet_console.js1
-rw-r--r--dom/worklet/tests/worklet_dump.js1
-rw-r--r--dom/worklet/tests/worklet_exception.js1
-rw-r--r--dom/worklet/tests/worklet_paintWorklet.js5
-rw-r--r--dom/worklet/tests/worklet_promise.js22
-rw-r--r--dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js384
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);
+}