Bug 965970 - Add support to compile asm.js code at install/update time r=luke,bholley,marco
authorFabrice Desré <fabrice@mozilla.com>
Thu, 17 Apr 2014 22:03:03 -0700
changeset 179684 51fd619edd5a32ecc6cdd0e431468dba881285e3
parent 179683 7b9d094bf6628ec6ea0b15bca7004dd18f067c17
child 179685 7eb486b7accbfbdfb9e9b38676d9cd7eba69f1e3
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersluke, bholley, marco
bugs965970
milestone31.0a1
Bug 965970 - Add support to compile asm.js code at install/update time r=luke,bholley,marco
content/base/src/nsScriptLoader.cpp
dom/apps/src/ScriptPreloader.jsm
dom/apps/src/Webapps.jsm
dom/apps/src/moz.build
js/src/jsapi.cpp
js/xpconnect/idl/mozIJSSubScriptLoader.idl
js/xpconnect/loader/mozJSSubScriptLoader.cpp
--- a/content/base/src/nsScriptLoader.cpp
+++ b/content/base/src/nsScriptLoader.cpp
@@ -48,16 +48,17 @@
 #include "nsCrossSiteListenerProxy.h"
 #include "nsSandboxFlags.h"
 #include "nsContentTypeParser.h"
 #include "nsINetworkSeer.h"
 #include "mozilla/dom/EncodingUtils.h"
 
 #include "mozilla/CORSMode.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/unused.h"
 
 #ifdef PR_LOGGING
 static PRLogModuleInfo* gCspPRLog;
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
@@ -809,21 +810,20 @@ NotifyOffThreadScriptLoadCompletedRunnab
   }
 
   return rv;
 }
 
 static void
 OffThreadScriptLoaderCallback(void *aToken, void *aCallbackData)
 {
-  NotifyOffThreadScriptLoadCompletedRunnable* aRunnable =
-    static_cast<NotifyOffThreadScriptLoadCompletedRunnable*>(aCallbackData);
+  nsRefPtr<NotifyOffThreadScriptLoadCompletedRunnable> aRunnable =
+    dont_AddRef(static_cast<NotifyOffThreadScriptLoadCompletedRunnable*>(aCallbackData));
   aRunnable->SetToken(aToken);
   NS_DispatchToMainThread(aRunnable);
-  NS_RELEASE(aRunnable);
 }
 
 nsresult
 nsScriptLoader::AttemptAsyncScriptParse(nsScriptLoadRequest* aRequest)
 {
   if (!aRequest->mElement->GetScriptAsync() || aRequest->mIsInline) {
     return NS_ERROR_FAILURE;
   }
@@ -843,31 +843,29 @@ nsScriptLoader::AttemptAsyncScriptParse(
 
   JS::CompileOptions options(cx);
   FillCompileOptionsForRequest(aRequest, global, &options);
 
   if (!JS::CanCompileOffThread(cx, options, aRequest->mScriptText.Length())) {
     return NS_ERROR_FAILURE;
   }
 
-  NotifyOffThreadScriptLoadCompletedRunnable* runnable =
+  nsRefPtr<NotifyOffThreadScriptLoadCompletedRunnable> runnable =
     new NotifyOffThreadScriptLoadCompletedRunnable(aRequest, this);
 
-  // This reference will be consumed by OffThreadScriptLoaderCallback.
-  NS_ADDREF(runnable);
-
   if (!JS::CompileOffThread(cx, options,
                             aRequest->mScriptText.get(), aRequest->mScriptText.Length(),
                             OffThreadScriptLoaderCallback,
                             static_cast<void*>(runnable))) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   mDocument->BlockOnload();
 
+  unused << runnable.forget();
   return NS_OK;
 }
 
 nsresult
 nsScriptLoader::ProcessRequest(nsScriptLoadRequest* aRequest, void **aOffThreadToken)
 {
   NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
                "Processing requests when running scripts is unsafe.");
new file mode 100644
--- /dev/null
+++ b/dom/apps/src/ScriptPreloader.jsm
@@ -0,0 +1,76 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+this.EXPORTED_SYMBOLS = ["ScriptPreloader"];
+
+function debug(aMsg) {
+  //dump("--*-- ScriptPreloader: " + aMsg + "\n");
+}
+
+this.ScriptPreloader = {
+#ifdef MOZ_B2G
+  _enabled: true,
+#else
+  _enabled: false,
+#endif
+
+  preload: function(aApp, aManifest) {
+    debug("Preloading " + aApp.origin);
+    let deferred = Promise.defer();
+
+    if (!this._enabled) {
+      deferred.resolve();
+      return deferred.promise;
+    }
+
+    if (aManifest.precompile &&
+        Array.isArray(aManifest.precompile) &&
+        aManifest.precompile.length > 0) {
+      let origin = Services.io.newURI(aApp.origin, null, null);
+      let toLoad = aManifest.precompile.length;
+      let principal =
+        Services.scriptSecurityManager
+                .getAppCodebasePrincipal(origin, aApp.localId, false);
+
+      aManifest.precompile.forEach((aPath) => {
+        let uri = Services.io.newURI(aPath, null, origin);
+        debug("Script to compile: " + uri.spec);
+        try {
+          Services.scriptloader.precompileScript(uri, principal,
+            (aSubject, aTopic, aData) => {
+              let uri = aSubject.QueryInterface(Ci.nsIURI);
+              debug("Done compiling " + uri.spec);
+
+              toLoad--;
+              if (toLoad == 0) {
+                deferred.resolve();
+              }
+            });
+        } catch (e) {
+          // Resolve the promise if precompileScript throws.
+          deferred.resolve();
+        }
+      });
+    } else {
+      // The precompile field is not an array, let the developer know.
+      // We don't want to have to enable debug for that to show up.
+      if (aManifest.precompile) {
+        Cu.reportError("ASM.JS compilation failed: the 'precompile' manifest " +
+                       "property should be an array of script uris.\n");
+      }
+      deferred.resolve();
+    }
+
+    return deferred.promise;
+  }
+}
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -56,16 +56,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/SystemMessagePermissionsChecker.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
   "resource://gre/modules/WebappOSUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ScriptPreloader",
+                                  "resource://gre/modules/ScriptPreloader.jsm");
+
 #ifdef MOZ_WIDGET_GONK
 XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
   Cu.import("resource://gre/modules/systemlibs.js");
   return libcutils;
 });
 #endif
 
 function debug(aMsg) {
@@ -1484,17 +1487,19 @@ this.DOMApplicationRegistry = {
           for (let prop in app.staged) {
             app[prop] = app.staged[prop];
           }
           delete app.staged;
         }
 
         delete app.retryingDownload;
 
-        this._saveApps().then(() => {
+        // Update the asm.js scripts we need to compile.
+        ScriptPreloader.preload(app, aData)
+          .then(() => this._saveApps()).then(() => {
           // Update the handlers and permissions for this app.
           this.updateAppHandlers(aOldManifest, aData, app);
 
           AppsUtils.loadJSONAsync(staged.path).then((aUpdateManifest) => {
             let appObject = AppsUtils.cloneAppObject(app);
             appObject.updateManifest = aUpdateManifest;
             this.notifyUpdateHandlers(appObject, aData, appFile.path);
           });
@@ -2590,23 +2595,29 @@ onInstallSuccessAck: function onInstallS
       this.updateDataStore(this.webapps[aId].localId, aNewApp.origin,
                            aNewApp.manifestURL, aManifest, aNewApp.appStatus);
 
       this.broadcastMessage("Webapps:UpdateState", {
         app: app,
         manifest: aManifest,
         manifestURL: aNewApp.manifestURL
       });
-      this.broadcastMessage("Webapps:FireEvent", {
-        eventType: ["downloadsuccess", "downloadapplied"],
-        manifestURL: aNewApp.manifestURL
-      });
-      if (aInstallSuccessCallback) {
-        aInstallSuccessCallback(aManifest, zipFile.path);
-      }
+
+      // Check if we have asm.js code to preload for this application.
+      ScriptPreloader.preload(aNewApp, aManifest)
+                     .then(() => {
+          this.broadcastMessage("Webapps:FireEvent", {
+            eventType: ["downloadsuccess", "downloadapplied"],
+            manifestURL: aNewApp.manifestURL
+          });
+          if (aInstallSuccessCallback) {
+            aInstallSuccessCallback(aManifest, zipFile.path);
+          }
+        }
+      );
     });
   },
 
   _nextLocalId: function() {
     let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
 
     while (this.getManifestURLByLocalId(id)) {
       id++;
--- a/dom/apps/src/moz.build
+++ b/dom/apps/src/moz.build
@@ -31,16 +31,17 @@ EXTRA_JS_MODULES += [
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
     'StoreTrustAnchor.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'AppsUtils.jsm',
     'OperatorApps.jsm',
+    'ScriptPreloader.jsm',
     'Webapps.jsm',
 ]
 
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'gklayout'
 
 LOCAL_INCLUDES += [
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4336,16 +4336,18 @@ JS::ReadOnlyCompileOptions::copyPODOptio
     forEval = rhs.forEval;
     noScriptRval = rhs.noScriptRval;
     selfHostingMode = rhs.selfHostingMode;
     canLazilyParse = rhs.canLazilyParse;
     strictOption = rhs.strictOption;
     extraWarningsOption = rhs.extraWarningsOption;
     werrorOption = rhs.werrorOption;
     asmJSOption = rhs.asmJSOption;
+    forceAsync = rhs.forceAsync;
+    installedFile = rhs.installedFile;
     sourcePolicy = rhs.sourcePolicy;
     introductionType = rhs.introductionType;
     introductionLineno = rhs.introductionLineno;
     introductionOffset = rhs.introductionOffset;
     hasIntroductionInfo = rhs.hasIntroductionInfo;
 }
 
 JSPrincipals *
--- a/js/xpconnect/idl/mozIJSSubScriptLoader.idl
+++ b/js/xpconnect/idl/mozIJSSubScriptLoader.idl
@@ -1,17 +1,21 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  *
  * 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 "nsISupports.idl"
 
-[scriptable, uuid(b21f1579-d994-4e99-a85d-a685140f3ec1)]
+interface nsIURI;
+interface nsIPrincipal;
+interface nsIObserver;
+
+[scriptable, uuid(19533e7b-f321-4ef1-bc59-6e812dc2a733)]
 interface mozIJSSubScriptLoader : nsISupports
 {
     /**
      * This method should only be called from JS!
      * In JS, the signature looks like:
      * rv loadSubScript (url [, obj] [, charset]);
      * @param url the url of the sub-script, it MUST be either a file:,
      *            resource:, or chrome: url, and MUST be local.
@@ -34,9 +38,27 @@ interface mozIJSSubScriptLoader : nsISup
      * @param optionsObject an object with parameters. Valid parameters are:
      *                      - charset: specifying the character encoding of the file (default: ASCII)
      *                      - target:  an object to evaluate onto (default: global object of the caller)
      *                      - ignoreCache: if set to true, will bypass the cache for reading the file.
      * @retval rv the value returned by the sub-script
      */
     [implicit_jscontext]
     jsval loadSubScriptWithOptions(in AString url, in jsval options);
+
+    /*
+     * Compiles a JS script off the main thread and calls back the
+     * observer once it's done.
+     * The script will be cached in temporary or persistent storage depending
+     * on the principal used.
+     * We fire the notification callback in all cases - there is no fatal
+     * error there.
+     * @param uri       the uri of the script to load.
+     * @param principal the principal from which we get the app id if any.
+     * @param observer  this observer will be called once the script has
+     *                  been precompiled. The notification topic will be
+     *                  'script-precompiled' and the subject the uri of the
+     *                  script as a nsIURI.
+     */
+    void precompileScript(in nsIURI uri,
+                          in nsIPrincipal principal,
+                          in nsIObserver observer);
 };
--- a/js/xpconnect/loader/mozJSSubScriptLoader.cpp
+++ b/js/xpconnect/loader/mozJSSubScriptLoader.cpp
@@ -13,30 +13,34 @@
 #include "nsIIOService.h"
 #include "nsIChannel.h"
 #include "nsIInputStream.h"
 #include "nsNetCID.h"
 #include "nsNetUtil.h"
 #include "nsIFileURL.h"
 #include "nsScriptLoader.h"
 #include "nsIScriptSecurityManager.h"
+#include "nsThreadUtils.h"
 
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "js/OldDebugAPI.h"
 #include "nsJSPrincipals.h"
 #include "xpcpublic.h" // For xpc::SystemErrorReporter
 #include "xpcprivate.h" // For xpc::OptionsBase
+#include "jswrapper.h"
 
 #include "mozilla/scache/StartupCache.h"
 #include "mozilla/scache/StartupCacheUtils.h"
+#include "mozilla/unused.h"
 
 using namespace mozilla::scache;
 using namespace JS;
 using namespace xpc;
+using namespace mozilla;
 
 class MOZ_STACK_CLASS LoadSubScriptOptions : public OptionsBase {
 public:
     LoadSubScriptOptions(JSContext *cx = xpc_GetSafeJSContext(),
                          JSObject *options = nullptr)
         : OptionsBase(cx, options)
         , target(cx)
         , charset(NullString())
@@ -360,8 +364,216 @@ mozJSSubScriptLoader::DoLoadSubScriptWit
     }
 
     if (cache && ok && writeScript) {
         WriteCachedScript(cache, cachePath, cx, mSystemPrincipal, script);
     }
 
     return NS_OK;
 }
+
+/**
+  * Let us compile scripts from a URI off the main thread.
+  */
+
+class ScriptPrecompiler : public nsIStreamLoaderObserver
+{
+public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSISTREAMLOADEROBSERVER
+
+    ScriptPrecompiler(nsIObserver* aObserver,
+                      nsIPrincipal* aPrincipal,
+                      nsIChannel* aChannel)
+        : mObserver(aObserver)
+        , mPrincipal(aPrincipal)
+        , mChannel(aChannel)
+    {}
+
+    virtual ~ScriptPrecompiler()
+    {}
+
+    static void OffThreadCallback(void *aToken, void *aData);
+
+    /* Sends the "done" notification back. Main thread only. */
+    void SendObserverNotification();
+
+private:
+    nsRefPtr<nsIObserver> mObserver;
+    nsRefPtr<nsIPrincipal> mPrincipal;
+    nsRefPtr<nsIChannel> mChannel;
+    nsString mScript;
+};
+
+NS_IMPL_ISUPPORTS1(ScriptPrecompiler, nsIStreamLoaderObserver);
+
+class NotifyPrecompilationCompleteRunnable : public nsRunnable
+{
+public:
+    NS_DECL_NSIRUNNABLE
+
+    NotifyPrecompilationCompleteRunnable(ScriptPrecompiler* aPrecompiler)
+        : mPrecompiler(aPrecompiler)
+        , mToken(nullptr)
+    {}
+
+    void SetToken(void* aToken) {
+        MOZ_ASSERT(aToken && !mToken);
+        mToken = aToken;
+    }
+
+protected:
+    nsRefPtr<ScriptPrecompiler> mPrecompiler;
+    void* mToken;
+};
+
+/* RAII helper class to send observer notifications */
+class AutoSendObserverNotification {
+public:
+    AutoSendObserverNotification(ScriptPrecompiler* aPrecompiler)
+        : mPrecompiler(aPrecompiler)
+    {}
+
+    ~AutoSendObserverNotification() {
+        if (mPrecompiler) {
+            mPrecompiler->SendObserverNotification();
+        }
+    }
+
+    void Disarm() {
+        mPrecompiler = nullptr;
+    }
+
+private:
+    ScriptPrecompiler* mPrecompiler;
+};
+
+NS_IMETHODIMP
+NotifyPrecompilationCompleteRunnable::Run(void)
+{
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mPrecompiler);
+
+    AutoSendObserverNotification notifier(mPrecompiler);
+
+    if (mToken) {
+        JSRuntime *rt = XPCJSRuntime::Get()->Runtime();
+        NS_ENSURE_TRUE(rt, NS_ERROR_FAILURE);
+        JS::FinishOffThreadScript(nullptr, rt, mToken);
+    }
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptPrecompiler::OnStreamComplete(nsIStreamLoader* aLoader,
+                                    nsISupports* aContext,
+                                    nsresult aStatus,
+                                    uint32_t aLength,
+                                    const uint8_t* aString)
+{
+    AutoSendObserverNotification notifier(this);
+
+    // Just notify that we are done with this load.
+    NS_ENSURE_SUCCESS(aStatus, NS_OK);
+
+    // Convert data to jschar* and prepare to call CompileOffThread.
+    nsAutoString hintCharset;
+    nsresult rv =
+        nsScriptLoader::ConvertToUTF16(mChannel, aString, aLength,
+                                       hintCharset, nullptr, mScript);
+
+    NS_ENSURE_SUCCESS(rv, NS_OK);
+
+    // Our goal is to cache persistently the compiled script and to avoid quota
+    // checks. Since the caching mechanism decide the persistence type based on
+    // the principal, we create a new global with the app's principal.
+    // We then enter its compartment to compile with its principal.
+    AutoSafeJSContext cx;
+    RootedValue v(cx);
+    SandboxOptions sandboxOptions;
+    sandboxOptions.sandboxName.AssignASCII("asm.js precompilation");
+    sandboxOptions.invisibleToDebugger = true;
+    rv = CreateSandboxObject(cx, &v, mPrincipal, sandboxOptions);
+    NS_ENSURE_SUCCESS(rv, NS_OK);
+
+    JSAutoCompartment ac(cx, js::UncheckedUnwrap(&v.toObject()));
+
+    JS::CompileOptions options(cx, JSVERSION_DEFAULT);
+    options.setSourcePolicy(CompileOptions::NO_SOURCE);
+    options.forceAsync = true;
+    options.compileAndGo = true;
+    options.installedFile = true;
+
+    nsCOMPtr<nsIURI> uri;
+    mChannel->GetURI(getter_AddRefs(uri));
+    nsAutoCString spec;
+    uri->GetSpec(spec);
+    options.setFile(spec.get());
+
+    if (!JS::CanCompileOffThread(cx, options, mScript.Length())) {
+        NS_WARNING("Can't compile script off thread!");
+        return NS_OK;
+    }
+
+    nsRefPtr<NotifyPrecompilationCompleteRunnable> runnable =
+        new NotifyPrecompilationCompleteRunnable(this);
+
+    if (!JS::CompileOffThread(cx, options,
+                              mScript.get(), mScript.Length(),
+                              OffThreadCallback,
+                              static_cast<void*>(runnable))) {
+        NS_WARNING("Failed to compile script off thread!");
+        return NS_OK;
+    }
+
+    unused << runnable.forget();
+    notifier.Disarm();
+
+    return NS_OK;
+}
+
+/* static */
+void
+ScriptPrecompiler::OffThreadCallback(void* aToken, void* aData)
+{
+    nsRefPtr<NotifyPrecompilationCompleteRunnable> runnable =
+        dont_AddRef(static_cast<NotifyPrecompilationCompleteRunnable*>(aData));
+    runnable->SetToken(aToken);
+
+    NS_DispatchToMainThread(runnable);
+}
+
+void
+ScriptPrecompiler::SendObserverNotification()
+{
+    MOZ_ASSERT(mChannel && mObserver);
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsCOMPtr<nsIURI> uri;
+    mChannel->GetURI(getter_AddRefs(uri));
+    mObserver->Observe(uri, "script-precompiled", nullptr);
+}
+
+NS_IMETHODIMP
+mozJSSubScriptLoader::PrecompileScript(nsIURI* aURI,
+                                       nsIPrincipal* aPrincipal,
+                                       nsIObserver *aObserver)
+{
+    nsCOMPtr<nsIChannel> channel;
+    nsresult rv = NS_NewChannel(getter_AddRefs(channel),
+                                aURI, nullptr, nullptr, nullptr,
+                                nsIRequest::LOAD_NORMAL, nullptr);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsRefPtr<ScriptPrecompiler> loadObserver =
+        new ScriptPrecompiler(aObserver, aPrincipal, channel);
+
+    nsCOMPtr<nsIStreamLoader> loader;
+    rv = NS_NewStreamLoader(getter_AddRefs(loader), loadObserver);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIStreamListener> listener = loader.get();
+    rv = channel->AsyncOpen(listener, nullptr);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+}