Bug 1331662 part 1 - Reimplement EvaluateString using the ExecutionContext class. r=bz
authorNicolas B. Pierron <nicolas.b.pierron@mozilla.com>
Wed, 22 Mar 2017 13:42:26 +0000
changeset 348959 b6a142776feedad9e7a00f66aab85310435c4348
parent 348958 d9f0bdf14469ffdb47987c7eab8a6dc2fc17d18c
child 348960 4e48165f8c8ef0388aebfc3356bcfcea4898896f
push id31540
push userkwierso@gmail.com
push dateThu, 23 Mar 2017 00:10:08 +0000
treeherdermozilla-central@7513b3f42058 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1331662
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 1331662 part 1 - Reimplement EvaluateString using the ExecutionContext class. r=bz
dom/base/nsJSUtils.cpp
dom/base/nsJSUtils.h
--- a/dom/base/nsJSUtils.cpp
+++ b/dom/base/nsJSUtils.cpp
@@ -118,16 +118,174 @@ nsJSUtils::CompileFunction(AutoJSAPI& js
   {
     return NS_ERROR_FAILURE;
   }
 
   *aFunctionObject = JS_GetFunctionObject(fun);
   return NS_OK;
 }
 
+static nsresult
+EvaluationExceptionToNSResult(JSContext* aCx)
+{
+  if (JS_IsExceptionPending(aCx)) {
+    return NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW;
+  }
+  return NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW_UNCATCHABLE;
+}
+
+nsJSUtils::ExecutionContext::ExecutionContext(JSContext* aCx,
+                                              JS::Handle<JSObject*> aGlobal)
+  : mSamplerRAII("nsJSUtils::ExecutionContext", /* PROFILER_LABEL */
+                 js::ProfileEntry::Category::JS, __LINE__)
+  , mCx(aCx)
+  , mCompartment(aCx, aGlobal)
+  , mRetValue(aCx)
+  , mScopeChain(aCx)
+  , mRv(NS_OK)
+  , mSkip(false)
+  , mCoerceToString(false)
+#ifdef DEBUG
+  , mWantsReturnValue(false)
+  , mExpectScopeChain(false)
+#endif
+{
+  MOZ_ASSERT(aCx == nsContentUtils::GetCurrentJSContext());
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(nsContentUtils::IsInMicroTask());
+  MOZ_ASSERT(mRetValue.isUndefined());
+
+  MOZ_ASSERT(js::GetGlobalForObjectCrossCompartment(aGlobal) == aGlobal);
+  if (MOZ_UNLIKELY(!xpc::Scriptability::Get(aGlobal).Allowed())) {
+    mSkip = true;
+    mRv = NS_OK;
+  }
+}
+
+void
+nsJSUtils::ExecutionContext::SetScopeChain(
+  const JS::AutoObjectVector& aScopeChain)
+{
+  if (mSkip) {
+    return;
+  }
+
+#ifdef DEBUG
+  mExpectScopeChain = true;
+#endif
+  // Now make sure to wrap the scope chain into the right compartment.
+  if (!mScopeChain.reserve(aScopeChain.length())) {
+    mSkip = true;
+    mRv = NS_ERROR_OUT_OF_MEMORY;
+    return;
+  }
+
+  for (size_t i = 0; i < aScopeChain.length(); ++i) {
+    JS::ExposeObjectToActiveJS(aScopeChain[i]);
+    mScopeChain.infallibleAppend(aScopeChain[i]);
+    if (!JS_WrapObject(mCx, mScopeChain[i])) {
+      mSkip = true;
+      mRv = NS_ERROR_OUT_OF_MEMORY;
+      return;
+    }
+  }
+}
+
+nsresult
+nsJSUtils::ExecutionContext::SyncAndExec(void **aOffThreadToken,
+                                         JS::MutableHandle<JSScript*> aScript)
+{
+  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)) {
+    mSkip = true;
+    mRv = EvaluationExceptionToNSResult(mCx);
+    return mRv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+nsJSUtils::ExecutionContext::CompileAndExec(JS::CompileOptions& aCompileOptions,
+                                            JS::SourceBufferHolder& aSrcBuf)
+{
+  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
+  MOZ_ASSERT(!mCoerceToString || mWantsReturnValue);
+  if (!JS::Evaluate(mCx, mScopeChain, aCompileOptions, aSrcBuf, &mRetValue)) {
+    mSkip = true;
+    mRv = EvaluationExceptionToNSResult(mCx);
+    return mRv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+nsJSUtils::ExecutionContext::CompileAndExec(JS::CompileOptions& aCompileOptions,
+                                            const nsAString& aScript)
+{
+  if (mSkip) {
+    return mRv;
+  }
+
+  const nsPromiseFlatString& flatScript = PromiseFlatString(aScript);
+  JS::SourceBufferHolder srcBuf(flatScript.get(), aScript.Length(),
+                                JS::SourceBufferHolder::NoOwnership);
+  return CompileAndExec(aCompileOptions, srcBuf);
+}
+
+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;
+#endif
+    return mRv;
+  }
+
+  MOZ_ASSERT(mWantsReturnValue);
+#ifdef DEBUG
+  mWantsReturnValue = false;
+#endif
+  if (mCoerceToString && !mRetValue.isUndefined()) {
+    JSString* str = JS::ToString(mCx, mRetValue);
+    if (!str) {
+      // ToString can be a function call, so an exception can be raised while
+      // executing the function.
+      mSkip = true;
+      return EvaluationExceptionToNSResult(mCx);
+    }
+    mRetValue.set(JS::StringValue(str));
+  }
+
+  aRetValue.set(mRetValue);
+  return NS_OK;
+}
+
 nsresult
 nsJSUtils::EvaluateString(JSContext* aCx,
                           const nsAString& aScript,
                           JS::Handle<JSObject*> aEvaluationGlobal,
                           JS::CompileOptions& aCompileOptions,
                           const EvaluateOptions& aEvaluateOptions,
                           JS::MutableHandle<JS::Value> aRetValue)
 {
@@ -142,102 +300,37 @@ nsresult
 nsJSUtils::EvaluateString(JSContext* aCx,
                           JS::SourceBufferHolder& aSrcBuf,
                           JS::Handle<JSObject*> aEvaluationGlobal,
                           JS::CompileOptions& aCompileOptions,
                           const EvaluateOptions& aEvaluateOptions,
                           JS::MutableHandle<JS::Value> aRetValue,
                           void **aOffThreadToken)
 {
-  PROFILER_LABEL("nsJSUtils", "EvaluateString",
-    js::ProfileEntry::Category::JS);
-
-  MOZ_ASSERT_IF(aCompileOptions.versionSet,
-                aCompileOptions.version != JSVERSION_UNKNOWN);
-  MOZ_ASSERT_IF(aEvaluateOptions.coerceToString, !aCompileOptions.noScriptRval);
-  MOZ_ASSERT(aCx == nsContentUtils::GetCurrentJSContext());
-  MOZ_ASSERT(aSrcBuf.get());
-  MOZ_ASSERT(js::GetGlobalForObjectCrossCompartment(aEvaluationGlobal) ==
-             aEvaluationGlobal);
-  MOZ_ASSERT_IF(aOffThreadToken, aCompileOptions.noScriptRval);
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(nsContentUtils::IsInMicroTask());
-
-  // Unfortunately, the JS engine actually compiles scripts with a return value
-  // in a different, less efficient way.  Furthermore, it can't JIT them in many
-  // cases.  So we need to be explicitly told whether the caller cares about the
-  // return value.  Callers can do this by calling the other overload of
-  // EvaluateString() which calls this function with
-  // aCompileOptions.noScriptRval set to true.
-  aRetValue.setUndefined();
+  ExecutionContext exec(aCx, aEvaluationGlobal);
+  exec.SetReturnValue(aCompileOptions)
+      .SetCoerceToString(aEvaluateOptions.coerceToString);
+  exec.SetScopeChain(aEvaluateOptions.scopeChain);
 
   nsresult rv = NS_OK;
-
-  NS_ENSURE_TRUE(xpc::Scriptability::Get(aEvaluationGlobal).Allowed(), NS_OK);
-
-  bool ok = true;
-  // Scope the JSAutoCompartment so that we can later wrap the return value
-  // into the caller's cx.
-  {
-    JSAutoCompartment ac(aCx, aEvaluationGlobal);
-
-    // Now make sure to wrap the scope chain into the right compartment.
-    JS::AutoObjectVector scopeChain(aCx);
-    if (!scopeChain.reserve(aEvaluateOptions.scopeChain.length())) {
-      return NS_ERROR_OUT_OF_MEMORY;
-    }
-
-    for (size_t i = 0; i < aEvaluateOptions.scopeChain.length(); ++i) {
-      JS::ExposeObjectToActiveJS(aEvaluateOptions.scopeChain[i]);
-      scopeChain.infallibleAppend(aEvaluateOptions.scopeChain[i]);
-      if (!JS_WrapObject(aCx, scopeChain[i])) {
-        ok = false;
-        break;
-      }
-    }
-
-    if (ok && aOffThreadToken) {
-      JS::Rooted<JSScript*>
-        script(aCx, JS::FinishOffThreadScript(aCx, *aOffThreadToken));
-      *aOffThreadToken = nullptr; // Mark the token as having been finished.
-      if (script) {
-        ok = JS_ExecuteScript(aCx, scopeChain, script);
-      } else {
-        ok = false;
-      }
-    } else if (ok) {
-      ok = JS::Evaluate(aCx, scopeChain, aCompileOptions, aSrcBuf, aRetValue);
-    }
-
-    if (ok && aEvaluateOptions.coerceToString && !aRetValue.isUndefined()) {
-      JS::Rooted<JS::Value> value(aCx, aRetValue);
-      JSString* str = JS::ToString(aCx, value);
-      ok = !!str;
-      aRetValue.set(ok ? JS::StringValue(str) : JS::UndefinedValue());
-    }
+  if (aOffThreadToken) {
+    JS::Rooted<JSScript*> script(aCx);
+    rv = exec.SyncAndExec(aOffThreadToken, &script);
+  } else {
+    rv = exec.CompileAndExec(aCompileOptions, aSrcBuf);
   }
 
-  if (!ok) {
-    if (JS_IsExceptionPending(aCx)) {
-      rv = NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW;
-    } else {
-      rv = NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW_UNCATCHABLE;
-    }
-
-    if (!aCompileOptions.noScriptRval) {
-      aRetValue.setUndefined();
-    }
+  if (NS_FAILED(rv)) {
+    return rv;
   }
 
-  // Wrap the return value into whatever compartment aCx was in.
-  if (ok && !aCompileOptions.noScriptRval) {
-    if (!JS_WrapValue(aCx, aRetValue)) {
-      return NS_ERROR_OUT_OF_MEMORY;
-    }
+  if (!aCompileOptions.noScriptRval) {
+    return exec.ExtractReturnValue(aRetValue);
   }
+
   return rv;
 }
 
 nsresult
 nsJSUtils::EvaluateString(JSContext* aCx,
                           JS::SourceBufferHolder& aSrcBuf,
                           JS::Handle<JSObject*> aEvaluationGlobal,
                           JS::CompileOptions& aCompileOptions,
--- a/dom/base/nsJSUtils.h
+++ b/dom/base/nsJSUtils.h
@@ -11,16 +11,17 @@
  * This is not a generated file. It contains common utility functions 
  * invoked from the JavaScript code generated from IDL interfaces.
  * The goal of the utility functions is to cut down on the size of
  * the generated code itself.
  */
 
 #include "mozilla/Assertions.h"
 
+#include "GeckoProfiler.h"
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "js/Conversions.h"
 #include "nsString.h"
 
 class nsIScriptContext;
 class nsIScriptGlobalObject;
 
@@ -59,16 +60,105 @@ public:
                                   JS::AutoObjectVector& aScopeChain,
                                   JS::CompileOptions& aOptions,
                                   const nsACString& aName,
                                   uint32_t aArgCount,
                                   const char** aArgArray,
                                   const nsAString& aBody,
                                   JSObject** aFunctionObject);
 
+
+  // ExecutionContext is used to switch compartment.
+  class MOZ_STACK_CLASS ExecutionContext {
+    // Register stack annotations for the Gecko profiler.
+    mozilla::SamplerStackFrameRAII mSamplerRAII;
+
+    JSContext* mCx;
+
+    // Handles switching to our global's compartment.
+    JSAutoCompartment mCompartment;
+
+    // Set to a valid handle if a return value is expected.
+    JS::Rooted<JS::Value> mRetValue;
+
+    // Scope chain in which the execution takes place.
+    JS::AutoObjectVector mScopeChain;
+
+    // returned value forwarded when we have to interupt the execution eagerly
+    // with mSkip.
+    nsresult mRv;
+
+    // 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;
+
+#ifdef DEBUG
+    // Should we set the return value.
+    bool mWantsReturnValue;
+
+    bool mExpectScopeChain;
+#endif
+
+   public:
+
+    // Enter compartment in which the code would be executed.  The JSContext
+    // must come from an AutoEntryScript that has had
+    // TakeOwnershipOfErrorReporting() called on it.
+    ExecutionContext(JSContext* aCx, JS::Handle<JSObject*> aGlobal);
+
+    ExecutionContext(const ExecutionContext&) = delete;
+    ExecutionContext(ExecutionContext&&) = delete;
+
+    ~ExecutionContext() {
+      // This flag is resetted, when the returned value is extracted.
+      MOZ_ASSERT(!mWantsReturnValue);
+    }
+
+    // The returned value would be converted to a string if the
+    // |aCoerceToString| is flag set.
+    ExecutionContext& SetCoerceToString(bool aCoerceToString) {
+      mCoerceToString = aCoerceToString;
+      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.
+    //
+    // The value returned in the mutable handle argument is part of the
+    // compartment given as argument to the ExecutionContext constructor. If the
+    // caller is in a different compartment, then the out-param value should be
+    // wrapped by calling |JS_WrapValue|.
+    MOZ_MUST_USE nsresult
+    ExtractReturnValue(JS::MutableHandle<JS::Value> aRetValue);
+
+    // After getting a notification that an off-thread compilation terminated,
+    // this function will synchronize the result by moving it to the main thread
+    // before starting the execution of the script.
+    //
+    // The compiled script would be returned in the |aScript| out-param.
+    MOZ_MUST_USE nsresult SyncAndExec(void **aOffThreadToken,
+                                      JS::MutableHandle<JSScript*> aScript);
+
+    // Compile a script contained in a SourceBuffer, and execute it.
+    nsresult CompileAndExec(JS::CompileOptions& aCompileOptions,
+                            JS::SourceBufferHolder& aSrcBuf);
+
+    // Compile a script contained in a string, and execute it.
+    nsresult CompileAndExec(JS::CompileOptions& aCompileOptions,
+                            const nsAString& aScript);
+  };
+
   struct MOZ_STACK_CLASS EvaluateOptions {
     bool coerceToString;
     JS::AutoObjectVector scopeChain;
 
     explicit EvaluateOptions(JSContext* cx)
       : coerceToString(false)
       , scopeChain(cx)
     {}