Bug 1364117 - Encode JS bytecode of scripts which are parsed on the main thread. r=mrbkap
authorNicolas B. Pierron <nicolas.b.pierron@mozilla.com>
Mon, 29 May 2017 16:01:37 +0000
changeset 409311 930b86eafc2c0063ac47e099ef3fea8ff9d30ea6
parent 409310 adc68cbe1295500e348dd3ff8e6737f7b77006b0
child 409312 9421074e0074875575103ca3697f5ba7cc912ba2
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs1364117
milestone55.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 1364117 - Encode JS bytecode of scripts which are parsed on the main thread. r=mrbkap
dom/base/nsJSUtils.cpp
dom/base/nsJSUtils.h
dom/base/test/mochitest.ini
dom/base/test/test_script_loader_js_cache.html
dom/script/ScriptLoader.cpp
--- a/dom/base/nsJSUtils.cpp
+++ b/dom/base/nsJSUtils.cpp
@@ -141,16 +141,17 @@ nsJSUtils::ExecutionContext::ExecutionCo
 #endif
     mCx(aCx)
   , mCompartment(aCx, aGlobal)
   , mRetValue(aCx)
   , mScopeChain(aCx)
   , mRv(NS_OK)
   , mSkip(false)
   , mCoerceToString(false)
+  , mEncodeBytecode(false)
 #ifdef DEBUG
   , mWantsReturnValue(false)
   , mExpectScopeChain(false)
 #endif
 {
   MOZ_ASSERT(aCx == nsContentUtils::GetCurrentJSContext());
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(nsContentUtils::IsInMicroTask());
@@ -199,69 +200,106 @@ nsJSUtils::ExecutionContext::JoinAndExec
   if (mSkip) {
     return mRv;
   }
 
   MOZ_ASSERT(!mWantsReturnValue);
   MOZ_ASSERT(!mExpectScopeChain);
   aScript.set(JS::FinishOffThreadScript(mCx, *aOffThreadToken));
   *aOffThreadToken = nullptr; // Mark the token as having been finished.
-  if (!aScript || !JS_ExecuteScript(mCx, mScopeChain, aScript)) {
+  if (!aScript) {
+    mSkip = true;
+    mRv = EvaluationExceptionToNSResult(mCx);
+    return mRv;
+  }
+
+  if (mEncodeBytecode && !StartIncrementalEncoding(mCx, aScript)) {
+    mSkip = true;
+    mRv = EvaluationExceptionToNSResult(mCx);
+    return mRv;
+  }
+
+  if (!JS_ExecuteScript(mCx, mScopeChain, aScript)) {
     mSkip = true;
     mRv = EvaluationExceptionToNSResult(mCx);
     return mRv;
   }
 
   return NS_OK;
 }
 
 nsresult
 nsJSUtils::ExecutionContext::CompileAndExec(JS::CompileOptions& aCompileOptions,
-                                            JS::SourceBufferHolder& aSrcBuf)
+                                            JS::SourceBufferHolder& aSrcBuf,
+                                            JS::MutableHandle<JSScript*> aScript)
 {
   if (mSkip) {
     return mRv;
   }
 
   MOZ_ASSERT_IF(aCompileOptions.versionSet,
                 aCompileOptions.version != JSVERSION_UNKNOWN);
   MOZ_ASSERT(aSrcBuf.get());
   MOZ_ASSERT(mRetValue.isUndefined());
 #ifdef DEBUG
   mWantsReturnValue = !aCompileOptions.noScriptRval;
 #endif
+
+  bool compiled = true;
+  if (mScopeChain.length() == 0) {
+    compiled = JS::Compile(mCx, aCompileOptions, aSrcBuf, aScript);
+  } else {
+    compiled = JS::CompileForNonSyntacticScope(mCx, aCompileOptions, aSrcBuf, aScript);
+  }
+
+  MOZ_ASSERT_IF(compiled, aScript);
+  if (!compiled) {
+    mSkip = true;
+    mRv = EvaluationExceptionToNSResult(mCx);
+    return mRv;
+  }
+
+  if (mEncodeBytecode && !StartIncrementalEncoding(mCx, aScript)) {
+    mSkip = true;
+    mRv = EvaluationExceptionToNSResult(mCx);
+    return mRv;
+  }
+
   MOZ_ASSERT(!mCoerceToString || mWantsReturnValue);
-  if (!JS::Evaluate(mCx, mScopeChain, aCompileOptions, aSrcBuf, &mRetValue)) {
+  if (!JS_ExecuteScript(mCx, mScopeChain, aScript, &mRetValue)) {
     mSkip = true;
     mRv = EvaluationExceptionToNSResult(mCx);
     return mRv;
   }
 
   return NS_OK;
 }
 
 nsresult
 nsJSUtils::ExecutionContext::CompileAndExec(JS::CompileOptions& aCompileOptions,
                                             const nsAString& aScript)
 {
+  MOZ_ASSERT(!mEncodeBytecode, "A JSScript is needed for calling FinishIncrementalEncoding");
   if (mSkip) {
     return mRv;
   }
 
   const nsPromiseFlatString& flatScript = PromiseFlatString(aScript);
   JS::SourceBufferHolder srcBuf(flatScript.get(), aScript.Length(),
                                 JS::SourceBufferHolder::NoOwnership);
-  return CompileAndExec(aCompileOptions, srcBuf);
+  JS::Rooted<JSScript*> script(mCx);
+  return CompileAndExec(aCompileOptions, srcBuf, &script);
 }
 
 nsresult
 nsJSUtils::ExecutionContext::DecodeAndExec(JS::CompileOptions& aCompileOptions,
                                            mozilla::Vector<uint8_t>& aBytecodeBuf,
                                            size_t aBytecodeIndex)
 {
+  MOZ_ASSERT(!mEncodeBytecode, "A JSScript is needed for calling FinishIncrementalEncoding");
   if (mSkip) {
     return mRv;
   }
 
   MOZ_ASSERT(!mWantsReturnValue);
   JS::Rooted<JSScript*> script(mCx);
   JS::TranscodeResult tr = JS::DecodeScript(mCx, aBytecodeBuf, &script, aBytecodeIndex);
   // These errors are external parameters which should be handled before the
@@ -301,44 +339,16 @@ nsJSUtils::ExecutionContext::DecodeJoinA
     mRv = EvaluationExceptionToNSResult(mCx);
     return mRv;
   }
 
   return NS_OK;
 }
 
 nsresult
-nsJSUtils::ExecutionContext::JoinEncodeAndExec(void **aOffThreadToken,
-                                               JS::MutableHandle<JSScript*> aScript)
-{
-  MOZ_ASSERT_IF(aOffThreadToken, !mWantsReturnValue);
-  aScript.set(JS::FinishOffThreadScript(mCx, *aOffThreadToken));
-  *aOffThreadToken = nullptr; // Mark the token as having been finished.
-  if (!aScript) {
-    mSkip = true;
-    mRv = EvaluationExceptionToNSResult(mCx);
-    return mRv;
-  }
-
-  if (!StartIncrementalEncoding(mCx, aScript)) {
-    mSkip = true;
-    mRv = EvaluationExceptionToNSResult(mCx);
-    return mRv;
-  }
-
-  if (!JS_ExecuteScript(mCx, mScopeChain, aScript)) {
-    mSkip = true;
-    mRv = EvaluationExceptionToNSResult(mCx);
-    return mRv;
-  }
-
-  return mRv;
-}
-
-nsresult
 nsJSUtils::ExecutionContext::ExtractReturnValue(JS::MutableHandle<JS::Value> aRetValue)
 {
   MOZ_ASSERT(aRetValue.isUndefined());
   if (mSkip) {
     // Repeat earlier result, as NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW are not
     // failures cases.
 #ifdef DEBUG
     mWantsReturnValue = false;
--- a/dom/base/nsJSUtils.h
+++ b/dom/base/nsJSUtils.h
@@ -90,16 +90,19 @@ public:
 
     // Used to skip upcoming phases in case of a failure.  In such case the
     // result is carried by mRv.
     bool mSkip;
 
     // Should the result be serialized before being returned.
     bool mCoerceToString;
 
+    // Encode the bytecode before it is being executed.
+    bool mEncodeBytecode;
+
 #ifdef DEBUG
     // Should we set the return value.
     bool mWantsReturnValue;
 
     bool mExpectScopeChain;
 #endif
 
    public:
@@ -119,16 +122,25 @@ public:
 
     // The returned value would be converted to a string if the
     // |aCoerceToString| is flag set.
     ExecutionContext& SetCoerceToString(bool aCoerceToString) {
       mCoerceToString = aCoerceToString;
       return *this;
     }
 
+    // When set, this flag records and encodes the bytecode as soon as it is
+    // being compiled, and before it is being executed. The bytecode can then be
+    // requested by using |JS::FinishIncrementalEncoding| with the mutable
+    // handle |aScript| argument of |CompileAndExec| or |JoinAndExec|.
+    ExecutionContext& SetEncodeBytecode(bool aEncodeBytecode) {
+      mEncodeBytecode = aEncodeBytecode;
+      return *this;
+    }
+
     // Set the scope chain in which the code should be executed.
     void SetScopeChain(const JS::AutoObjectVector& aScopeChain);
 
     // Copy the returned value in the mutable handle argument, in case of a
     // evaluation failure either during the execution or the conversion of the
     // result to a string, the nsresult would be set to the corresponding result
     // code, and the mutable handle argument would remain unchanged.
     //
@@ -144,36 +156,32 @@ public:
     // thread before starting the execution of the script.
     //
     // The compiled script would be returned in the |aScript| out-param.
     MOZ_MUST_USE nsresult JoinAndExec(void **aOffThreadToken,
                                       JS::MutableHandle<JSScript*> aScript);
 
     // Compile a script contained in a SourceBuffer, and execute it.
     nsresult CompileAndExec(JS::CompileOptions& aCompileOptions,
-                            JS::SourceBufferHolder& aSrcBuf);
+                            JS::SourceBufferHolder& aSrcBuf,
+                            JS::MutableHandle<JSScript*> aScript);
 
     // Compile a script contained in a string, and execute it.
     nsresult CompileAndExec(JS::CompileOptions& aCompileOptions,
                             const nsAString& aScript);
 
     // Decode a script contained in a buffer, and execute it.
     MOZ_MUST_USE nsresult DecodeAndExec(JS::CompileOptions& aCompileOptions,
                                         mozilla::Vector<uint8_t>& aBytecodeBuf,
                                         size_t aBytecodeIndex);
 
     // After getting a notification that an off-thread decoding terminated, this
     // function will get the result of the decoder by moving it to the main
     // thread before starting the execution of the script.
     MOZ_MUST_USE nsresult DecodeJoinAndExec(void **aOffThreadToken);
-
-    // Similar to JoinAndExec, except that in addition to fecthing the source,
-    // we register the fact that we plan to encode its bytecode later.
-    MOZ_MUST_USE nsresult JoinEncodeAndExec(void **aOffThreadToken,
-                                            JS::MutableHandle<JSScript*> aScript);
   };
 
   static nsresult CompileModule(JSContext* aCx,
                                 JS::SourceBufferHolder& aSrcBuf,
                                 JS::Handle<JSObject*> aEvaluationGlobal,
                                 JS::CompileOptions &aCompileOptions,
                                 JS::MutableHandle<JSObject*> aModule);
 
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -755,17 +755,16 @@ skip-if = toolkit == 'android'
 [test_script_loader_crossorigin_data_url.html]
 [test_script_loader_js_cache.html]
 support-files =
   file_js_cache.html
   file_js_cache_with_sri.html
   file_js_cache.js
   file_js_cache_save_after_load.html
   file_js_cache_save_after_load.js
-skip-if = (os == 'linux' || toolkit == 'android') # mochitest are executed on a single core
 [test_setInterval_uncatchable_exception.html]
 skip-if = debug == false
 [test_settimeout_extra_arguments.html]
 [test_settimeout_inner.html]
 [test_setTimeoutWith0.html]
 [test_setting_opener.html]
 [test_simplecontentpolicy.html]
 skip-if = e10s # Bug 1156489.
--- a/dom/base/test/test_script_loader_js_cache.html
+++ b/dom/base/test/test_script_loader_js_cache.html
@@ -13,32 +13,36 @@
   <script src="/resources/testharness.js"></script>
   <script src="/resources/testharnessreport.js"></script>
   <script type="application/javascript">
     // This is the state machine of the trace events produced by the
     // ScriptLoader. This state machine is used to give a name to each
     // code path, such that we can assert each code path with a single word.
     var scriptLoaderStateMachine = {
       "scriptloader_load_source": {
-        "scriptloader_encode_and_execute": {
-          "scriptloader_bytecode_saved": "bytecode_saved",
-          "scriptloader_bytecode_failed": "bytecode_failed"
-        },
-        "scriptloader_execute": "source_exec"
+        "scriptloader_execute": {
+          "scriptloader_encode": {
+            "scriptloader_bytecode_saved": "bytecode_saved",
+            "scriptloader_bytecode_failed": "bytecode_failed"
+          },
+          "scriptloader_no_encode": "source_exec"
+        }
       },
       "scriptloader_load_bytecode": {
         "scriptloader_fallback": {
           // Replicate the top-level state machine without
           // "scriptloader_load_bytecode" transition.
           "scriptloader_load_source": {
-            "scriptloader_encode_and_execute": {
-              "scriptloader_bytecode_saved": "fallback_bytecode_saved",
-              "scriptloader_bytecode_failed": "fallback_bytecode_failed"
-            },
-            "scriptloader_execute": "fallback_source_exec"
+            "scriptloader_execute": {
+              "scriptloader_encode": {
+                "scriptloader_bytecode_saved": "fallback_bytecode_saved",
+                "scriptloader_bytecode_failed": "fallback_bytecode_failed"
+              },
+              "scriptloader_no_encode": "fallback_source_exec"
+            }
           }
         },
         "scriptloader_execute": "bytecode_exec"
       }
     };
 
     function flushNeckoCache() {
       return new Promise (resolve => {
@@ -88,17 +92,18 @@
         }
       }
 
       var iwin = iframe.contentWindow;
       iwin.addEventListener("scriptloader_load_source", log_event);
       iwin.addEventListener("scriptloader_load_bytecode", log_event);
       iwin.addEventListener("scriptloader_generate_bytecode", log_event);
       iwin.addEventListener("scriptloader_execute", log_event);
-      iwin.addEventListener("scriptloader_encode_and_execute", log_event);
+      iwin.addEventListener("scriptloader_encode", log_event);
+      iwin.addEventListener("scriptloader_no_encode", log_event);
       iwin.addEventListener("scriptloader_bytecode_saved", log_event);
       iwin.addEventListener("scriptloader_bytecode_failed", log_event);
       iwin.addEventListener("scriptloader_fallback", log_event);
       iwin.addEventListener("ping", (evt) => {
         ping += 1;
         dump(`## Content event: ${evt.type} (=${ping})\n`);
       });
       iframe.src = url;
--- a/dom/script/ScriptLoader.cpp
+++ b/dom/script/ScriptLoader.cpp
@@ -1874,23 +1874,16 @@ ScriptLoader::FillCompileOptionsForReque
   MOZ_ASSERT(aRequest->mElement);
   if (NS_SUCCEEDED(nsContentUtils::WrapNative(cx, aRequest->mElement,
                                               &elementVal,
                                               /* aAllowWrapping = */ true))) {
     MOZ_ASSERT(elementVal.isObject());
     aOptions->setElement(&elementVal.toObject());
   }
 
-  // At the moment, the bytecode cache is only triggered if a script is large
-  // enough to be parsed out of the main thread.  Thus, for testing purposes, we
-  // force parsing any script out of the main thread.
-  if (IsEagerBytecodeCache()) {
-    aOptions->forceAsync = true;
-  }
-
   return NS_OK;
 }
 
 nsresult
 ScriptLoader::EvaluateScript(ScriptLoadRequest* aRequest)
 {
   MOZ_ASSERT(aRequest->IsReadyToRun());
 
@@ -1988,59 +1981,56 @@ ScriptLoader::EvaluateScript(ScriptLoadR
             rv = exec.DecodeAndExec(options, aRequest->mScriptBytecode,
                                     aRequest->mBytecodeOffset);
           }
           // We do not expect to be saving anything when we already have some
           // bytecode.
           MOZ_ASSERT(!aRequest->mCacheInfo);
         } else {
           MOZ_ASSERT(aRequest->IsSource());
-          if (aRequest->mOffThreadToken) {
-            // Off-main-thread parsing.
-            LOG(("ScriptLoadRequest (%p): Join (off-thread parsing) and Execute",
-                 aRequest));
-            {
-              nsJSUtils::ExecutionContext exec(aes.cx(), global);
-              JS::Rooted<JSScript*> script(aes.cx());
-              if (!aRequest->mCacheInfo) {
-                TRACE_FOR_TEST(aRequest->mElement, "scriptloader_execute");
-                rv = exec.JoinAndExec(&aRequest->mOffThreadToken, &script);
-                LOG(("ScriptLoadRequest (%p): Cannot cache anything (cacheInfo = nullptr)",
-                     aRequest));
-              } else {
-                TRACE_FOR_TEST(aRequest->mElement, "scriptloader_encode_and_execute");
-                MOZ_ASSERT(aRequest->mBytecodeOffset ==
-                           aRequest->mScriptBytecode.length());
-                rv = exec.JoinEncodeAndExec(&aRequest->mOffThreadToken,
-                                            &script);
-                // Queue the current script load request to later save the bytecode.
-                if (NS_SUCCEEDED(rv)) {
-                  aRequest->mScript = script;
-                  HoldJSObjects(aRequest);
-                  RegisterForBytecodeEncoding(aRequest);
-                } else {
-                  LOG(("ScriptLoadRequest (%p): Cannot cache anything (rv = %X, script = %p, cacheInfo = %p)",
-                       aRequest, unsigned(rv), script.get(), aRequest->mCacheInfo.get()));
-                  TRACE_FOR_TEST_NONE(aRequest->mElement, "scriptloader_bytecode_failed");
-                  aRequest->mCacheInfo = nullptr;
-                }
-              }
+          JS::Rooted<JSScript*> script(aes.cx());
+
+          bool encodeBytecode = false;
+          if (aRequest->mCacheInfo) {
+            MOZ_ASSERT(aRequest->mBytecodeOffset ==
+                       aRequest->mScriptBytecode.length());
+            encodeBytecode = IsEagerBytecodeCache(); // Heuristic!
+          }
+
+          {
+            nsJSUtils::ExecutionContext exec(aes.cx(), global);
+            exec.SetEncodeBytecode(encodeBytecode);
+            TRACE_FOR_TEST(aRequest->mElement, "scriptloader_execute");
+            if (aRequest->mOffThreadToken) {
+              // Off-main-thread parsing.
+              LOG(("ScriptLoadRequest (%p): Join (off-thread parsing) and Execute",
+                   aRequest));
+              rv = exec.JoinAndExec(&aRequest->mOffThreadToken, &script);
+            } else {
+              // Main thread parsing (inline and small scripts)
+              LOG(("ScriptLoadRequest (%p): Compile And Exec", aRequest));
+              nsAutoString inlineData;
+              SourceBufferHolder srcBuf = GetScriptSource(aRequest, inlineData);
+              rv = exec.CompileAndExec(options, srcBuf, &script);
             }
+          }
+
+          // Queue the current script load request to later save the bytecode.
+          if (NS_SUCCEEDED(rv) && encodeBytecode) {
+            aRequest->mScript = script;
+            HoldJSObjects(aRequest);
+            TRACE_FOR_TEST(aRequest->mElement, "scriptloader_encode");
+            RegisterForBytecodeEncoding(aRequest);
           } else {
-            // Main thread parsing (inline and small scripts)
-            LOG(("ScriptLoadRequest (%p): Compile And Exec", aRequest));
-            nsJSUtils::ExecutionContext exec(aes.cx(), global);
-            nsAutoString inlineData;
-            SourceBufferHolder srcBuf = GetScriptSource(aRequest, inlineData);
-            TRACE_FOR_TEST(aRequest->mElement, "scriptloader_execute");
-            rv = exec.CompileAndExec(options, srcBuf);
+            LOG(("ScriptLoadRequest (%p): Cannot cache anything (rv = %X, script = %p, cacheInfo = %p)",
+                 aRequest, unsigned(rv), script.get(), aRequest->mCacheInfo.get()));
+            TRACE_FOR_TEST_NONE(aRequest->mElement, "scriptloader_no_encode");
             aRequest->mCacheInfo = nullptr;
           }
         }
-
       }
     }
 
     // Even if we are not saving the bytecode of the current script, we have
     // to trigger the encoding of the bytecode, as the current script can
     // call functions of a script for which we are recording the bytecode.
     MaybeTriggerBytecodeEncoding();
   }