From da0155c439815d21431f85c6b8d0c91a80b23763 Mon Sep 17 00:00:00 2001 From: Gene Lian Date: Mon, 24 Feb 2014 21:56:54 +0800 Subject: [PATCH] Bug 949325 - C++ wrapper to support DataStore API on the worker (part 2-1, provide a proxy to resolve/reject Promise on workers). r=baku,nsm,khuey --- dom/promise/Promise.cpp | 200 ++++++++++++++++++++++++++++- dom/promise/Promise.h | 2 + dom/promise/PromiseCallback.cpp | 4 +- dom/promise/PromiseNativeHandler.h | 4 +- dom/promise/PromiseWorkerProxy.h | 106 +++++++++++++++ dom/promise/moz.build | 3 +- 6 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 dom/promise/PromiseWorkerProxy.h diff --git a/dom/promise/Promise.cpp b/dom/promise/Promise.cpp index ec883c1d143d..ce4c03d83f2a 100644 --- a/dom/promise/Promise.cpp +++ b/dom/promise/Promise.cpp @@ -13,6 +13,7 @@ #include "mozilla/Preferences.h" #include "PromiseCallback.h" #include "PromiseNativeHandler.h" +#include "PromiseWorkerProxy.h" #include "nsContentUtils.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" @@ -694,13 +695,13 @@ public: } void - ResolvedCallback(JS::Handle aValue) + ResolvedCallback(JSContext* aCx, JS::Handle aValue) { mCountdownHolder->SetValue(mIndex, aValue); } void - RejectedCallback(JS::Handle aValue) + RejectedCallback(JSContext* aCx, JS::Handle aValue) { // Should never be attached to Promise as a reject handler. MOZ_ASSERT(false, "AllResolveHandler should never be attached to a Promise's reject handler!"); @@ -1125,5 +1126,200 @@ PromiseReportRejectFeature::Notify(JSContext* aCx, workers::Status aStatus) return true; } +// A WorkerRunnable to resolve/reject the Promise on the worker thread. + +class PromiseWorkerProxyRunnable : public workers::WorkerRunnable +{ +public: + PromiseWorkerProxyRunnable(PromiseWorkerProxy* aPromiseWorkerProxy, + JSStructuredCloneCallbacks* aCallbacks, + JSAutoStructuredCloneBuffer&& aBuffer, + PromiseWorkerProxy::RunCallbackFunc aFunc) + : WorkerRunnable(aPromiseWorkerProxy->GetWorkerPrivate(), + WorkerThreadUnchangedBusyCount) + , mPromiseWorkerProxy(aPromiseWorkerProxy) + , mCallbacks(aCallbacks) + , mBuffer(Move(aBuffer)) + , mFunc(aFunc) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromiseWorkerProxy); + } + + virtual bool + WorkerRun(JSContext* aCx, workers::WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); + + MOZ_ASSERT(mPromiseWorkerProxy); + nsRefPtr workerPromise = mPromiseWorkerProxy->GetWorkerPromise(); + MOZ_ASSERT(workerPromise); + + // Here we convert the buffer to a JS::Value. + JS::Rooted value(aCx); + if (!mBuffer.read(aCx, &value, mCallbacks, mPromiseWorkerProxy)) { + JS_ClearPendingException(aCx); + return false; + } + + // TODO Bug 975246 - nsRefPtr should support operator |nsRefPtr->*funcType|. + (workerPromise.get()->*mFunc)(aCx, + value, + Promise::PromiseTaskSync::SyncTask); + + // Release the Promise because it has been resolved/rejected for sure. + mPromiseWorkerProxy->CleanUp(aCx); + return true; + } + +protected: + ~PromiseWorkerProxyRunnable() {} + +private: + nsRefPtr mPromiseWorkerProxy; + JSStructuredCloneCallbacks* mCallbacks; + JSAutoStructuredCloneBuffer mBuffer; + + // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. + PromiseWorkerProxy::RunCallbackFunc mFunc; +}; + +PromiseWorkerProxy::PromiseWorkerProxy(WorkerPrivate* aWorkerPrivate, + Promise* aWorkerPromise, + JSStructuredCloneCallbacks* aCallbacks) + : mWorkerPrivate(aWorkerPrivate) + , mWorkerPromise(aWorkerPromise) + , mCleanedUp(false) + , mCallbacks(aCallbacks) + , mCleanUpLock("cleanUpLock") +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPromise); + + // We do this to make sure the worker thread won't shut down before the + // promise is resolved/rejected on the worker thread. + if (!mWorkerPrivate->AddFeature(mWorkerPrivate->GetJSContext(), this)) { + MOZ_ASSERT(false, "cannot add the worker feature!"); + return; + } +} + +PromiseWorkerProxy::~PromiseWorkerProxy() +{ + MOZ_ASSERT(mCleanedUp); + MOZ_ASSERT(!mWorkerPromise); +} + +WorkerPrivate* +PromiseWorkerProxy::GetWorkerPrivate() const +{ + // It's ok to race on |mCleanedUp|, because it will never cause us to fire + // the assertion when we should not. + MOZ_ASSERT(!mCleanedUp); + + return mWorkerPrivate; +} + +Promise* +PromiseWorkerProxy::GetWorkerPromise() const +{ + return mWorkerPromise; +} + +void +PromiseWorkerProxy::StoreISupports(nsISupports* aSupports) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsMainThreadPtrHandle supports = + new nsMainThreadPtrHolder(aSupports); + mSupportsArray.AppendElement(supports); +} + +void +PromiseWorkerProxy::RunCallback(JSContext* aCx, + JS::Handle aValue, + RunCallbackFunc aFunc) +{ + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mCleanUpLock); + // If the worker thread's been cancelled we don't need to resolve the Promise. + if (mCleanedUp) { + return; + } + + // The |aValue| is written into the buffer. Note that we also pass |this| + // into the structured-clone write in order to set its |mSupportsArray| to + // keep objects alive until the structured-clone read/write is done. + JSAutoStructuredCloneBuffer buffer; + if (!buffer.write(aCx, aValue, mCallbacks, this)) { + JS_ClearPendingException(aCx); + MOZ_ASSERT(false, "cannot write the JSAutoStructuredCloneBuffer!"); + } + + nsRefPtr runnable = + new PromiseWorkerProxyRunnable(this, + mCallbacks, + Move(buffer), + aFunc); + + runnable->Dispatch(aCx); +} + +void +PromiseWorkerProxy::ResolvedCallback(JSContext* aCx, + JS::Handle aValue) +{ + RunCallback(aCx, aValue, &Promise::ResolveInternal); +} + +void +PromiseWorkerProxy::RejectedCallback(JSContext* aCx, + JS::Handle aValue) +{ + RunCallback(aCx, aValue, &Promise::RejectInternal); +} + +bool +PromiseWorkerProxy::Notify(JSContext* aCx, Status aStatus) +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->GetJSContext() == aCx); + + if (aStatus >= Canceling) { + CleanUp(aCx); + } + + return true; +} + +void +PromiseWorkerProxy::CleanUp(JSContext* aCx) +{ + MutexAutoLock lock(mCleanUpLock); + + // |mWorkerPrivate| might not be safe to use anymore if we have already + // cleaned up and RemoveFeature(), so we need to check |mCleanedUp| first. + if (mCleanedUp) { + return; + } + + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->GetJSContext() == aCx); + + // Release the Promise and remove the PromiseWorkerProxy from the features of + // the worker thread since the Promise has been resolved/rejected or the + // worker thread has been cancelled. + mWorkerPromise = nullptr; + mWorkerPrivate->RemoveFeature(aCx, this); + mCleanedUp = true; +} + } // namespace dom } // namespace mozilla diff --git a/dom/promise/Promise.h b/dom/promise/Promise.h index a22853d58b98..fd372958ae79 100644 --- a/dom/promise/Promise.h +++ b/dom/promise/Promise.h @@ -55,6 +55,8 @@ class Promise MOZ_FINAL : public nsISupports, friend class PromiseResolverTask; friend class PromiseTask; friend class PromiseReportRejectFeature; + friend class PromiseWorkerProxy; + friend class PromiseWorkerProxyRunnable; friend class RejectPromiseCallback; friend class ResolvePromiseCallback; friend class WorkerPromiseResolverTask; diff --git a/dom/promise/PromiseCallback.cpp b/dom/promise/PromiseCallback.cpp index eeecc70ab846..62d1871ed69f 100644 --- a/dom/promise/PromiseCallback.cpp +++ b/dom/promise/PromiseCallback.cpp @@ -338,12 +338,12 @@ NativePromiseCallback::Call(JSContext* aCx, JS::Handle aValue) { if (mState == Promise::Resolved) { - mHandler->ResolvedCallback(aValue); + mHandler->ResolvedCallback(aCx, aValue); return; } if (mState == Promise::Rejected) { - mHandler->RejectedCallback(aValue); + mHandler->RejectedCallback(aCx, aValue); return; } diff --git a/dom/promise/PromiseNativeHandler.h b/dom/promise/PromiseNativeHandler.h index a9f8c505d5d5..bfef812e8fe2 100644 --- a/dom/promise/PromiseNativeHandler.h +++ b/dom/promise/PromiseNativeHandler.h @@ -27,10 +27,10 @@ public: { } virtual void - ResolvedCallback(JS::Handle aValue) = 0; + ResolvedCallback(JSContext* aCx, JS::Handle aValue) = 0; virtual void - RejectedCallback(JS::Handle aValue) = 0; + RejectedCallback(JSContext* aCx, JS::Handle aValue) = 0; }; } // namespace dom diff --git a/dom/promise/PromiseWorkerProxy.h b/dom/promise/PromiseWorkerProxy.h new file mode 100644 index 000000000000..102635d74fad --- /dev/null +++ b/dom/promise/PromiseWorkerProxy.h @@ -0,0 +1,106 @@ +/* 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_PromiseWorkerProxy_h +#define mozilla_dom_PromiseWorkerProxy_h + +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/workers/bindings/WorkerFeature.h" +#include "nsProxyRelease.h" + +namespace mozilla { +namespace dom { + +class Promise; + +namespace workers { +class WorkerPrivate; +} + +// A proxy to catch the resolved/rejected Promise's result from the main thread +// and resolve/reject that on the worker thread eventually. +// +// How to use: +// +// 1. Create a Promise on the worker thread and return it to the content +// script: +// +// nsRefPtr promise = new Promise(workerPrivate->GlobalScope()); +// // Pass |promise| around to the WorkerMainThreadRunnable +// return promise.forget(); +// +// 2. In your WorkerMainThreadRunnable's ctor, create a PromiseWorkerProxy +// which holds a nsRefPtr to the Promise created at #1. +// +// 3. In your WorkerMainThreadRunnable::MainThreadRun(), obtain a Promise on +// the main thread and call its AppendNativeHandler(PromiseNativeHandler*) +// to bind the PromiseWorkerProxy created at #2. +// +// 4. Then the Promise results returned by ResolvedCallback/RejectedCallback +// will be dispatched as a WorkerRunnable to the worker thread to +// resolve/reject the Promise created at #1. + +class PromiseWorkerProxy : public PromiseNativeHandler, + public workers::WorkerFeature +{ + friend class PromiseWorkerProxyRunnable; + + // This overrides the non-threadsafe refcounting in PromiseNativeHandler. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PromiseWorkerProxy) + +public: + PromiseWorkerProxy(workers::WorkerPrivate* aWorkerPrivate, + Promise* aWorkerPromise, + JSStructuredCloneCallbacks* aCallbacks = nullptr); + + workers::WorkerPrivate* GetWorkerPrivate() const; + + Promise* GetWorkerPromise() const; + + void StoreISupports(nsISupports* aSupports); + + void CleanUp(JSContext* aCx); + +protected: + virtual void ResolvedCallback(JSContext* aCx, + JS::Handle aValue) MOZ_OVERRIDE; + + virtual void RejectedCallback(JSContext* aCx, + JS::Handle aValue) MOZ_OVERRIDE; + + virtual bool Notify(JSContext* aCx, workers::Status aStatus) MOZ_OVERRIDE; + +private: + virtual ~PromiseWorkerProxy(); + + // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. + typedef void (Promise::*RunCallbackFunc)(JSContext*, + JS::Handle, + Promise::PromiseTaskSync); + + void RunCallback(JSContext* aCx, + JS::Handle aValue, + RunCallbackFunc aFunc); + + workers::WorkerPrivate* mWorkerPrivate; + + // This lives on the worker thread. + nsRefPtr mWorkerPromise; + + bool mCleanedUp; // To specify if the cleanUp() has been done. + + JSStructuredCloneCallbacks* mCallbacks; + + // Aimed to keep objects alive when doing the structured-clone read/write, + // which can be added by calling StoreISupports() on the main thread. + nsTArray> mSupportsArray; + + // Ensure the worker and the main thread won't race to access |mCleanedUp|. + Mutex mCleanUpLock; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PromiseWorkerProxy_h diff --git a/dom/promise/moz.build b/dom/promise/moz.build index 3b59c3779228..ccf91fced4f6 100644 --- a/dom/promise/moz.build +++ b/dom/promise/moz.build @@ -8,7 +8,8 @@ TEST_DIRS += ['tests'] EXPORTS.mozilla.dom += [ 'Promise.h', - 'PromiseNativeHandler.h' + 'PromiseNativeHandler.h', + 'PromiseWorkerProxy.h', ] SOURCES += [