Bug 1290021 - Implement a prototype version of Houdini "Worklets Level 1" spec - part 4 - cache for the imports, r=smaug
authorAndrea Marchesini <amarchesini@mozilla.com>
Sun, 06 Nov 2016 09:55:20 +0100
changeset 321314 1946c5517376329c66b51e7ec943e841f7a5e348
parent 321313 3769e657d10451ef2da58a52b9c3858de0e56fd8
child 321315 c77883513897e595255f14289a9f76c66b0167d1
push id83550
push useramarchesini@mozilla.com
push dateSun, 06 Nov 2016 08:56:23 +0000
treeherdermozilla-inbound@f99b99cc076d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1290021
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1290021 - Implement a prototype version of Houdini "Worklets Level 1" spec - part 4 - cache for the imports, r=smaug
dom/worklet/Worklet.cpp
dom/worklet/Worklet.h
dom/worklet/tests/file_basic.html
dom/worklet/tests/file_import_with_cache.html
dom/worklet/tests/mochitest.ini
dom/worklet/tests/server_import_with_cache.sjs
dom/worklet/tests/test_import_with_cache.html
--- a/dom/worklet/Worklet.cpp
+++ b/dom/worklet/Worklet.cpp
@@ -16,98 +16,136 @@
 #include "nsIThreadRetargetableRequest.h"
 #include "nsNetUtil.h"
 #include "nsScriptLoader.h"
 #include "xpcprivate.h"
 
 namespace mozilla {
 namespace dom {
 
-namespace {
+// ---------------------------------------------------------------------------
+// WorkletFetchHandler
 
 class WorkletFetchHandler : public PromiseNativeHandler
                           , public nsIStreamLoaderObserver
 {
 public:
   NS_DECL_ISUPPORTS
 
   static already_AddRefed<Promise>
   Fetch(Worklet* aWorklet, const nsAString& aModuleURL, ErrorResult& aRv)
   {
+    MOZ_ASSERT(aWorklet);
+
     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<nsIDocument> doc;
+    doc = window->GetExtantDoc();
+    if (!doc) {
+      promise->MaybeReject(NS_ERROR_FAILURE);
+      return promise.forget();
+    }
+
+    nsCOMPtr<nsIURI> baseURI = doc->GetBaseURI();
+    nsCOMPtr<nsIURI> resolvedURI;
+    nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), aModuleURL, nullptr, baseURI);
+    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 request;
     request.SetAsUSVString().Rebind(aModuleURL.Data(), aModuleURL.Length());
 
     RequestInit init;
 
     RefPtr<Promise> fetchPromise = FetchRequest(global, request, init, aRv);
     if (NS_WARN_IF(aRv.Failed())) {
       promise->MaybeReject(aRv);
       return promise.forget();
     }
 
     RefPtr<WorkletFetchHandler> handler =
       new WorkletFetchHandler(aWorklet, aModuleURL, promise);
     fetchPromise->AppendNativeHandler(handler);
 
+    aWorklet->AddImportFetchHandler(spec, handler);
     return promise.forget();
   }
 
   virtual void
   ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
   {
     if (!aValue.isObject()) {
-      mPromise->MaybeReject(NS_ERROR_FAILURE);
+      RejectPromises(NS_ERROR_FAILURE);
       return;
     }
 
     RefPtr<Response> response;
     nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response);
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      mPromise->MaybeReject(rv);
+      RejectPromises(NS_ERROR_FAILURE);
       return;
     }
 
     if (!response->Ok()) {
-      mPromise->MaybeReject(NS_ERROR_DOM_NETWORK_ERR);
+      RejectPromises(NS_ERROR_DOM_NETWORK_ERR);
       return;
     }
 
     nsCOMPtr<nsIInputStream> inputStream;
     response->GetBody(getter_AddRefs(inputStream));
     if (!inputStream) {
-      mPromise->MaybeReject(NS_ERROR_DOM_NETWORK_ERR);
+      RejectPromises(NS_ERROR_DOM_NETWORK_ERR);
       return;
     }
 
     nsCOMPtr<nsIInputStreamPump> pump;
     rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream);
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      mPromise->MaybeReject(rv);
+      RejectPromises(rv);
       return;
     }
 
     nsCOMPtr<nsIStreamLoader> loader;
     rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      mPromise->MaybeReject(rv);
+      RejectPromises(rv);
       return;
     }
 
     rv = pump->AsyncRead(loader, nullptr);
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      mPromise->MaybeReject(rv);
+      RejectPromises(rv);
       return;
     }
 
     nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(pump);
     if (rr) {
       nsCOMPtr<nsIEventTarget> sts =
         do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
       rv = rr->RetargetDeliveryTo(sts);
@@ -120,28 +158,28 @@ public:
   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)) {
-      mPromise->MaybeReject(aStatus);
+      RejectPromises(aStatus);
       return NS_OK;
     }
 
     char16_t* scriptTextBuf;
     size_t scriptTextLength;
     nsresult rv =
       nsScriptLoader::ConvertToUTF16(nullptr, aString, aStringLen,
                                      NS_LITERAL_STRING("UTF-8"), nullptr,
                                      scriptTextBuf, scriptTextLength);
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      mPromise->MaybeReject(rv);
+      RejectPromises(rv);
       return NS_OK;
     }
 
     // Moving the ownership of the buffer
     JS::SourceBufferHolder buffer(scriptTextBuf, scriptTextLength,
                                   JS::SourceBufferHolder::GiveOwnership);
 
     AutoJSAPI jsapi;
@@ -167,51 +205,117 @@ public:
     // We only need the setNoScriptRval bit when compiling off-thread here,
     // since otherwise nsJSUtils::EvaluateString will set it up for us.
     compileOptions.setNoScriptRval(true);
 
     JS::Rooted<JS::Value> unused(cx);
     if (!JS::Evaluate(cx, compileOptions, buffer, &unused)) {
       ErrorResult error;
       error.StealExceptionFromJSContext(cx);
-      mPromise->MaybeReject(error);
+      RejectPromises(error.StealNSResult());
       return NS_OK;
     }
 
     // All done.
-    mPromise->MaybeResolveWithUndefined();
+    ResolvePromises();
     return NS_OK;
   }
 
   virtual void
   RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
   {
-    mPromise->MaybeReject(aCx, aValue);
+    RejectPromises(NS_ERROR_DOM_NETWORK_ERR);
   }
 
 private:
   WorkletFetchHandler(Worklet* aWorklet, const nsAString& aURL,
                       Promise* aPromise)
     : mWorklet(aWorklet)
-    , mPromise(aPromise)
+    , mStatus(ePending)
+    , mErrorStatus(NS_OK)
     , mURL(aURL)
-  {}
+  {
+    MOZ_ASSERT(aWorklet);
+    MOZ_ASSERT(aPromise);
+
+    mPromises.AppendElement(aPromise);
+  }
 
   ~WorkletFetchHandler()
   {}
 
+  void
+  AddPromise(Promise* aPromise)
+  {
+    MOZ_ASSERT(aPromise);
+
+    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));
+
+    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);
+
+    for (uint32_t i = 0; i < mPromises.Length(); ++i) {
+      mPromises[i]->MaybeResolveWithUndefined();
+    }
+    mPromises.Clear();
+
+    mStatus = eResolved;
+    mWorklet = nullptr;
+  }
+
   RefPtr<Worklet> mWorklet;
-  RefPtr<Promise> mPromise;
+  nsTArray<RefPtr<Promise>> mPromises;
+
+  enum {
+    ePending,
+    eRejected,
+    eResolved
+  } mStatus;
+
+  nsresult mErrorStatus;
 
   nsString mURL;
 };
 
 NS_IMPL_ISUPPORTS(WorkletFetchHandler, nsIStreamLoaderObserver)
 
-} // anonymous namespace
+// ---------------------------------------------------------------------------
+// Worklet
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Worklet, mWindow, mScope)
 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)
@@ -258,10 +362,26 @@ Worklet::GetOrCreateGlobalScope(JSContex
     }
 
     JS_FireOnNewGlobalObject(aCx, global);
   }
 
   return mScope;
 }
 
+WorkletFetchHandler*
+Worklet::GetImportFetchHandler(const nsACString& aURI)
+{
+  return mImportHandlers.GetWeak(aURI);
+}
+
+void
+Worklet::AddImportFetchHandler(const nsACString& aURI,
+                               WorkletFetchHandler* aHandler)
+{
+  MOZ_ASSERT(aHandler);
+  MOZ_ASSERT(!mImportHandlers.GetWeak(aURI));
+
+  mImportHandlers.Put(aURI, aHandler);
+}
+
 } // dom namespace
 } // mozilla namespace
--- a/dom/worklet/Worklet.h
+++ b/dom/worklet/Worklet.h
@@ -4,27 +4,29 @@
  * 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 "mozilla/ErrorResult.h"
+#include "nsRefPtrHashtable.h"
 #include "nsWrapperCache.h"
 #include "nsCOMPtr.h"
 
 class nsPIDOMWindowInner;
 class nsIPrincipal;
 
 namespace mozilla {
 namespace dom {
 
 class Promise;
 class WorkletGlobalScope;
+class WorkletFetchHandler;
 
 class Worklet final : public nsISupports
                     , public nsWrapperCache
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Worklet)
 
@@ -42,18 +44,27 @@ public:
   Import(const nsAString& aModuleURL, ErrorResult& aRv);
 
   WorkletGlobalScope*
   GetOrCreateGlobalScope(JSContext* aCx);
 
 private:
   ~Worklet();
 
+  WorkletFetchHandler*
+  GetImportFetchHandler(const nsACString& aURI);
+
+  void
+  AddImportFetchHandler(const nsACString& aURI, WorkletFetchHandler* aHandler);
+
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
   nsCOMPtr<nsIPrincipal> mPrincipal;
 
   RefPtr<WorkletGlobalScope> mScope;
+  nsRefPtrHashtable<nsCStringHashKey, WorkletFetchHandler> mImportHandlers;
+
+  friend class WorkletFetchHandler;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_Worklet_h
--- a/dom/worklet/tests/file_basic.html
+++ b/dom/worklet/tests/file_basic.html
@@ -8,26 +8,50 @@
 
 <script type="application/javascript">
 
 setupTest();
 
 var worklet = window.createWorklet();
 ok(!!worklet, "We have a Worklet");
 
+// First loading
 worklet.import("common.js")
 .then(() => {
   ok(true, "Import should load a resource.");
 })
+
+// Second loading - same file
+.then(() => {
+  return worklet.import("common.js")
+})
+.then(() => {
+  ok(true, "Import should load a resource.");
+})
+
+// 3rd loading - a network error
 .then(() => {
   return worklet.import("404.js");
 })
 .then(() => {
   ok(false, "The loading should fail.");
 }, () => {
   ok(true, "The loading should fail.");
-}).then(() => {
+})
+
+// 4th loading - a network error
+.then(() => {
+  return worklet.import("404.js");
+})
+.then(() => {
+  ok(false, "The loading should fail.");
+}, () => {
+  ok(true, "The loading should fail.");
+})
+
+// done
+.then(() => {
   SimpleTest.finish();
 });
 
 </script>
 </body>
 </html>
copy from dom/worklet/tests/file_basic.html
copy to dom/worklet/tests/file_import_with_cache.html
--- a/dom/worklet/tests/file_basic.html
+++ b/dom/worklet/tests/file_import_with_cache.html
@@ -8,26 +8,36 @@
 
 <script type="application/javascript">
 
 setupTest();
 
 var worklet = window.createWorklet();
 ok(!!worklet, "We have a Worklet");
 
-worklet.import("common.js")
-.then(() => {
-  ok(true, "Import should load a resource.");
-})
-.then(() => {
-  return worklet.import("404.js");
-})
-.then(() => {
-  ok(false, "The loading should fail.");
-}, () => {
-  ok(true, "The loading should fail.");
-}).then(() => {
-  SimpleTest.finish();
-});
+function loading() {
+  worklet.import("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>
--- a/dom/worklet/tests/mochitest.ini
+++ b/dom/worklet/tests/mochitest.ini
@@ -1,8 +1,10 @@
 [DEFAULT]
 support-files =
   common.js
 
 [test_basic.html]
 support-files=file_basic.html
 [test_console.html]
 support-files=file_console.html worklet_console.js
+[test_import_with_cache.html]
+support-files=file_import_with_cache.html server_import_with_cache.sjs
new file mode 100644
--- /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");
+}
copy from dom/worklet/tests/test_basic.html
copy to dom/worklet/tests/test_import_with_cache.html
--- a/dom/worklet/tests/test_basic.html
+++ b/dom/worklet/tests/test_import_with_cache.html
@@ -9,13 +9,13 @@
 <body>
 
 <script type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SpecialPowers.pushPrefEnv(
   {"set": [["dom.worklet.testing.enabled", true],
            ["dom.worklet.enabled", true]]},
-  function() { loadTest("file_basic.html"); });
+  function() { loadTest("file_import_with_cache.html"); });
 
 </script>
 </body>
 </html>