From 9f3b16bf4c0a471a4f195a92e789c8761d3f0922 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Fri, 24 Jul 2015 07:01:00 -0400 Subject: [PATCH] Bug 1148593 - Create async stack in callback objects. r=bz, r=fitzgen --HG-- extra : rebase_source : f9b507d8f005dbca6f40f510ca41a0cbb03aebf9 --- .../browser_canvas-frontend-call-stack-01.js | 6 +- .../browser_canvas-frontend-call-stack-02.js | 6 +- .../browser_canvas-frontend-call-stack-03.js | 12 +- .../browser_timelineMarkers-frame-05.js | 2 +- dom/base/test/mochitest.ini | 2 + .../test/test_async_setTimeout_stack.html | 60 ++++++++++ ...async_setTimeout_stack_across_globals.html | 60 ++++++++++ dom/bindings/CallbackObject.cpp | 11 ++ dom/bindings/CallbackObject.h | 34 +++++- dom/bindings/test/mochitest.ini | 1 + dom/bindings/test/test_async_stacks.html | 108 ++++++++++++++++++ ..._exception_options_from_jsimplemented.html | 56 +++++---- ...promise_rejections_from_jsimplemented.html | 44 ++++--- js/src/builtin/TestingFunctions.cpp | 3 +- js/src/jsapi.cpp | 8 +- js/src/jsapi.h | 13 ++- js/src/vm/Runtime.cpp | 1 + js/src/vm/Runtime.h | 6 + js/src/vm/SavedStacks.cpp | 15 ++- js/src/vm/Stack-inl.h | 3 + js/src/vm/Stack.h | 8 ++ js/xpconnect/src/XPCComponents.cpp | 3 +- toolkit/modules/Promise-backend.js | 14 ++- 23 files changed, 421 insertions(+), 55 deletions(-) create mode 100644 dom/base/test/test_async_setTimeout_stack.html create mode 100644 dom/base/test/test_async_setTimeout_stack_across_globals.html create mode 100644 dom/bindings/test/test_async_stacks.html diff --git a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js index 92ca163e5733..21b7d4e8ed6f 100644 --- a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js +++ b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js @@ -28,8 +28,10 @@ function* ifTestingSupported() { isnot($(".call-item-stack", callItem.target), null, "There should be a stack container available now for the draw call."); - is($all(".call-item-stack-fn", callItem.target).length, 4, - "There should be 4 functions on the stack for the draw call."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should be at least 4 functions on the stack for the draw call."); ok($all(".call-item-stack-fn-name", callItem.target)[0].getAttribute("value") .includes("C()"), diff --git a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js index bc3be3e169b3..491aa695eb00 100644 --- a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js +++ b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js @@ -29,8 +29,10 @@ function* ifTestingSupported() { isnot($(".call-item-stack", callItem.target), null, "There should be a stack container available now for the draw call."); - is($all(".call-item-stack-fn", callItem.target).length, 4, - "There should be 4 functions on the stack for the draw call."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should be at least 4 functions on the stack for the draw call."); let jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target)); diff --git a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js index 796e329123ce..24780c566102 100644 --- a/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js +++ b/browser/devtools/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js @@ -40,8 +40,10 @@ function* ifTestingSupported() { "There should be a stack container available now for the draw call."); is($(".call-item-stack", callItem.target).hidden, false, "The stack container should now be visible."); - is($all(".call-item-stack-fn", callItem.target).length, 4, - "There should be 4 functions on the stack for the draw call."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should be at least 4 functions on the stack for the draw call."); EventUtils.sendMouseEvent({ type: "dblclick" }, contents, window); @@ -53,8 +55,10 @@ function* ifTestingSupported() { "There should still be a stack container available for the draw call."); is($(".call-item-stack", callItem.target).hidden, true, "The stack container should now be hidden."); - is($all(".call-item-stack-fn", callItem.target).length, 4, - "There should still be 4 functions on the stack for the draw call."); + // We may have more than 4 functions, depending on whether async + // stacks are available. + ok($all(".call-item-stack-fn", callItem.target).length >= 4, + "There should still be at least 4 functions on the stack for the draw call."); yield teardown(panel); finish(); diff --git a/docshell/test/browser/browser_timelineMarkers-frame-05.js b/docshell/test/browser/browser_timelineMarkers-frame-05.js index bca5d882a9f2..2f8bd05f6611 100644 --- a/docshell/test/browser/browser_timelineMarkers-frame-05.js +++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js @@ -94,7 +94,7 @@ if (Services.prefs.getBoolPref("javascript.options.asyncstack")) { let frame = markers[0].endStack; ok(frame.parent.asyncParent !== null, "Parent frame has async parent"); - is(frame.parent.asyncParent.asyncCause, "Promise", + is(frame.parent.asyncParent.asyncCause, "promise callback", "Async parent has correct cause"); is(frame.parent.asyncParent.functionDisplayName, "makePromise", "Async parent has correct function name"); diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index e7862fb30332..9473eb0f82ec 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -248,6 +248,8 @@ support-files = [test_anonymousContent_insert.html] [test_anonymousContent_manipulate_content.html] [test_appname_override.html] +[test_async_setTimeout_stack.html] +[test_async_setTimeout_stack_across_globals.html] [test_audioWindowUtils.html] [test_audioNotification.html] skip-if = buildapp == 'mulet' diff --git a/dom/base/test/test_async_setTimeout_stack.html b/dom/base/test/test_async_setTimeout_stack.html new file mode 100644 index 000000000000..fb3c74b29116 --- /dev/null +++ b/dom/base/test/test_async_setTimeout_stack.html @@ -0,0 +1,60 @@ + + + + + + Test for Bug 1142577 - Async stacks for setTimeout + + + + + Mozilla Bug 1142577 +

+  
+
+
diff --git a/dom/base/test/test_async_setTimeout_stack_across_globals.html b/dom/base/test/test_async_setTimeout_stack_across_globals.html
new file mode 100644
index 000000000000..5b44072d6f31
--- /dev/null
+++ b/dom/base/test/test_async_setTimeout_stack_across_globals.html
@@ -0,0 +1,60 @@
+
+
+
+
+  
+  Test for Bug 1142577 - Async stacks for setTimeout
+  
+  
+
+
+  Mozilla Bug 1142577
+  

+  
+  
+
+
diff --git a/dom/bindings/CallbackObject.cpp b/dom/bindings/CallbackObject.cpp
index b1e2b62cf19b..e119d99a71bc 100644
--- a/dom/bindings/CallbackObject.cpp
+++ b/dom/bindings/CallbackObject.cpp
@@ -43,6 +43,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CallbackObject)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CallbackObject)
   NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCreationStack)
   NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mIncumbentJSGlobal)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
@@ -169,6 +170,16 @@ CallbackObject::CallSetup::CallSetup(CallbackObject* aCallback,
     }
   }
 
+  mAsyncStack.emplace(cx, aCallback->GetCreationStack());
+  if (*mAsyncStack) {
+    mAsyncCause.emplace(cx, JS_NewStringCopyZ(cx, aExecutionReason));
+    if (*mAsyncCause) {
+      mAsyncStackSetter.emplace(cx, *mAsyncStack, *mAsyncCause);
+    } else {
+      JS_ClearPendingException(cx);
+    }
+  }
+
   // Enter the compartment of our callback, so we can actually work with it.
   //
   // Note that if the callback is a wrapper, this will not be the same
diff --git a/dom/bindings/CallbackObject.h b/dom/bindings/CallbackObject.h
index 5ba68fdf13c6..ff1bdcbf039f 100644
--- a/dom/bindings/CallbackObject.h
+++ b/dom/bindings/CallbackObject.h
@@ -30,6 +30,7 @@
 #include "nsWrapperCache.h"
 #include "nsJSEnvironment.h"
 #include "xpcpublic.h"
+#include "jsapi.h"
 
 namespace mozilla {
 namespace dom {
@@ -56,7 +57,15 @@ public:
   explicit CallbackObject(JSContext* aCx, JS::Handle aCallback,
                           nsIGlobalObject *aIncumbentGlobal)
   {
-    Init(aCallback, aIncumbentGlobal);
+    if (aCx && JS::RuntimeOptionsRef(aCx).asyncStack()) {
+      JS::RootedObject stack(aCx);
+      if (!JS::CaptureCurrentStack(aCx, &stack)) {
+        JS_ClearPendingException(aCx);
+      }
+      Init(aCallback, stack, aIncumbentGlobal);
+    } else {
+      Init(aCallback, nullptr, aIncumbentGlobal);
+    }
   }
 
   JS::Handle Callback() const
@@ -65,6 +74,15 @@ public:
     return CallbackPreserveColor();
   }
 
+  JSObject* GetCreationStack() const
+  {
+    JSObject* result = mCreationStack;
+    if (result) {
+      JS::ExposeObjectToActiveJS(result);
+    }
+    return result;
+  }
+
   /*
    * This getter does not change the color of the JSObject meaning that the
    * object returned is not guaranteed to be kept alive past the next CC.
@@ -112,7 +130,8 @@ protected:
 
   explicit CallbackObject(CallbackObject* aCallbackObject)
   {
-    Init(aCallbackObject->mCallback, aCallbackObject->mIncumbentGlobal);
+    Init(aCallbackObject->mCallback, aCallbackObject->mCreationStack,
+         aCallbackObject->mIncumbentGlobal);
   }
 
   bool operator==(const CallbackObject& aOther) const
@@ -125,12 +144,14 @@ protected:
   }
 
 private:
-  inline void Init(JSObject* aCallback, nsIGlobalObject* aIncumbentGlobal)
+  inline void Init(JSObject* aCallback, JSObject* aCreationStack,
+                   nsIGlobalObject* aIncumbentGlobal)
   {
     MOZ_ASSERT(aCallback && !mCallback);
     // Set script objects before we hold, on the off chance that a GC could
     // somehow happen in there... (which would be pretty odd, granted).
     mCallback = aCallback;
+    mCreationStack = aCreationStack;
     if (aIncumbentGlobal) {
       mIncumbentGlobal = aIncumbentGlobal;
       mIncumbentJSGlobal = aIncumbentGlobal->GetGlobalJSObject();
@@ -147,12 +168,14 @@ protected:
     MOZ_ASSERT_IF(mIncumbentJSGlobal, mCallback);
     if (mCallback) {
       mCallback = nullptr;
+      mCreationStack = nullptr;
       mIncumbentJSGlobal = nullptr;
       mozilla::DropJSObjects(this);
     }
   }
 
   JS::Heap mCallback;
+  JS::Heap mCreationStack;
   // Ideally, we'd just hold a reference to the nsIGlobalObject, since that's
   // what we need to pass to AutoIncumbentScript. Unfortunately, that doesn't
   // hold the actual JS global alive. So we maintain an additional pointer to
@@ -213,6 +236,11 @@ protected:
     // always within a request during its lifetime.
     Maybe > mRootedCallable;
 
+    // Members which are used to set the async stack.
+    Maybe> mAsyncStack;
+    Maybe> mAsyncCause;
+    Maybe mAsyncStackSetter;
+
     // Can't construct a JSAutoCompartment without a JSContext either.  Also,
     // Put mAc after mAutoEntryScript so that we exit the compartment before
     // we pop the JSContext. Though in practice we'll often manually order
diff --git a/dom/bindings/test/mochitest.ini b/dom/bindings/test/mochitest.ini
index 557a3c914e55..3b82263d0fb7 100644
--- a/dom/bindings/test/mochitest.ini
+++ b/dom/bindings/test/mochitest.ini
@@ -8,6 +8,7 @@ support-files =
   file_proxies_via_xray.html
   forOf_iframe.html
 
+[test_async_stacks.html]
 [test_ByteString.html]
 [test_InstanceOf.html]
 [test_bug560072.html]
diff --git a/dom/bindings/test/test_async_stacks.html b/dom/bindings/test/test_async_stacks.html
new file mode 100644
index 000000000000..8b655a14d70c
--- /dev/null
+++ b/dom/bindings/test/test_async_stacks.html
@@ -0,0 +1,108 @@
+
+
+
+
+  
+  Test for Bug 1148593
+  
+  
+  
+
+
+Mozilla Bug 1148593
+

+ +
+
+ + diff --git a/dom/bindings/test/test_exception_options_from_jsimplemented.html b/dom/bindings/test/test_exception_options_from_jsimplemented.html index 61ff8bc98b8a..32bf6ca31c85 100644 --- a/dom/bindings/test/test_exception_options_from_jsimplemented.html +++ b/dom/bindings/test/test_exception_options_from_jsimplemented.html @@ -15,6 +15,17 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 SimpleTest.waitForExplicitFinish(); function doTest() { + var file = location.href; + var asyncFrame; + /* Async parent frames from pushPrefEnv don't show up in e10s. */ + var isE10S = !SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser"); + if (!isE10S && SpecialPowers.getBoolPref("javascript.options.asyncstack")) { + asyncFrame = `Async*@${file}:153:1 +`; + } else { + asyncFrame = ""; + } + var t = new TestInterfaceJS(); try { t.testThrowError(); @@ -25,12 +36,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 is(e.name, "Error", "Should not have an interesting name here"); is(e.message, "We are an Error", "Should have the right message"); is(e.stack, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:20:7\n", + `doTest@${file}:31:7 +${asyncFrame}`, "Exception stack should still only show our code"); is(e.fileName, - "http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html", + file, "Should have the right file name"); - is(e.lineNumber, 20, "Should have the right line number"); + is(e.lineNumber, 31, "Should have the right line number"); is(e.columnNumber, 7, "Should have the right column number"); } @@ -45,12 +57,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 is(e.code, DOMException.NOT_SUPPORTED_ERR, "Should have the right 'code'"); is(e.stack, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:38:7\n", + `doTest@${file}:50:7 +${asyncFrame}`, "Exception stack should still only show our code"); is(e.filename, - "http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html", + file, "Should still have the right file name"); - is(e.lineNumber, 38, "Should still have the right line number"); + is(e.lineNumber, 50, "Should still have the right line number"); todo_isnot(e.columnNumber, 0, "No column number support for DOMException yet"); } @@ -65,12 +78,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 is(e.message, "We are a TypeError", "Should also have the right message (2)"); is(e.stack, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:59:7\n", + `doTest@${file}:72:7 +${asyncFrame}`, "Exception stack for TypeError should only show our code"); is(e.fileName, - "http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html", + file, "Should still have the right file name for TypeError"); - is(e.lineNumber, 59, "Should still have the right line number for TypeError"); + is(e.lineNumber, 72, "Should still have the right line number for TypeError"); is(e.columnNumber, 7, "Should have the right column number for TypeError"); } @@ -84,14 +98,14 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 is(e.message, "missing argument 0 when calling function Array.indexOf", "Should also have the right message (3)"); is(e.stack, - "doTest/<@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:78:45\n" + - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:78:7\n" - , + `doTest/<@${file}:92:45 +doTest@${file}:92:7 +${asyncFrame}`, "Exception stack for TypeError should only show our code (3)"); is(e.fileName, - "http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html", + file, "Should still have the right file name for TypeError (3)"); - is(e.lineNumber, 78, "Should still have the right line number for TypeError (3)"); + is(e.lineNumber, 92, "Should still have the right line number for TypeError (3)"); is(e.columnNumber, 45, "Should have the right column number for TypeError (3)"); } @@ -104,12 +118,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 is(e.name, "NS_ERROR_UNEXPECTED", "Name should be sanitized (4)"); is(e.message, "", "Message should be sanitized (5)"); is(e.stack, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:99:7\n", + `doTest@${file}:113:7 +${asyncFrame}`, "Exception stack for sanitized exception should only show our code (4)"); is(e.filename, - "http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html", + file, "Should still have the right file name for sanitized exception (4)"); - is(e.lineNumber, 99, "Should still have the right line number for sanitized exception (4)"); + is(e.lineNumber, 113, "Should still have the right line number for sanitized exception (4)"); todo_isnot(e.columnNumber, 0, "Should have the right column number for sanitized exception (4)"); } @@ -122,12 +137,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 is(e.name, "NS_ERROR_UNEXPECTED", "Name should be sanitized (5)"); is(e.message, "", "Message should be sanitized (5)"); is(e.stack, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html:117:7\n", + `doTest@${file}:132:7 +${asyncFrame}`, "Exception stack for sanitized exception should only show our code (5)"); is(e.filename, - "http://mochi.test:8888/tests/dom/bindings/test/test_exception_options_from_jsimplemented.html", + file, "Should still have the right file name for sanitized exception (5)"); - is(e.lineNumber, 117, "Should still have the right line number for sanitized exception (5)"); + is(e.lineNumber, 132, "Should still have the right line number for sanitized exception (5)"); todo_isnot(e.columnNumber, 0, "Should have the right column number for sanitized exception (5)"); } diff --git a/dom/bindings/test/test_promise_rejections_from_jsimplemented.html b/dom/bindings/test/test_promise_rejections_from_jsimplemented.html index d109d5ec8993..6c9b5f22b2a2 100644 --- a/dom/bindings/test/test_promise_rejections_from_jsimplemented.html +++ b/dom/bindings/test/test_promise_rejections_from_jsimplemented.html @@ -37,23 +37,32 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 function doTest() { var t = new TestInterfaceJS(); + /* Async parent frames from pushPrefEnv don't show up in e10s. */ + var isE10S = !SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser"); var asyncStack = SpecialPowers.getBoolPref("javascript.options.asyncstack"); - var ourFile = "http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html"; + var ourFile = location.href; + var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:121:1 +` : ""; Promise.all([ t.testPromiseWithThrowingChromePromiseInit().then( ensurePromiseFail.bind(null, 1), - checkExn.bind(null, 44, "NS_ERROR_UNEXPECTED", "", undefined, + checkExn.bind(null, 48, "NS_ERROR_UNEXPECTED", "", undefined, ourFile, 1, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:44:7\n")), + `doTest@${ourFile}:48:7 +` + + parentFrame)), t.testPromiseWithThrowingContentPromiseInit(function() { thereIsNoSuchContentFunction1(); }).then( ensurePromiseFail.bind(null, 2), - checkExn.bind(null, 50, "ReferenceError", + checkExn.bind(null, 56, "ReferenceError", "thereIsNoSuchContentFunction1 is not defined", undefined, ourFile, 2, - "doTest/<@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:50:11\ndoTest@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:49:7\n")), + `doTest/<@${ourFile}:56:11 +doTest@${ourFile}:55:7 +` + + parentFrame)), t.testPromiseWithThrowingChromeThenFunction().then( ensurePromiseFail.bind(null, 3), checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 3, "")), @@ -61,10 +70,14 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 thereIsNoSuchContentFunction2(); }).then( ensurePromiseFail.bind(null, 4), - checkExn.bind(null, 61, "ReferenceError", + checkExn.bind(null, 70, "ReferenceError", "thereIsNoSuchContentFunction2 is not defined", undefined, ourFile, 4, - "doTest/<@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:61:11\n" + (asyncStack ? "Async*doTest@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:60:7\n" : ""))), + `doTest/<@${ourFile}:70:11 +` + + (asyncStack ? `Async*doTest@${ourFile}:69:7 +` : "") + + parentFrame)), t.testPromiseWithThrowingChromeThenable().then( ensurePromiseFail.bind(null, 5), checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 5, "")), @@ -72,22 +85,27 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592 then: function() { thereIsNoSuchContentFunction3(); } }).then( ensurePromiseFail.bind(null, 6), - checkExn.bind(null, 72, "ReferenceError", + checkExn.bind(null, 85, "ReferenceError", "thereIsNoSuchContentFunction3 is not defined", undefined, ourFile, 6, - "doTest/<.then@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:72:32\n")), + `doTest/<.then@${ourFile}:85:32 +`)), t.testPromiseWithDOMExceptionThrowingPromiseInit().then( ensurePromiseFail.bind(null, 7), - checkExn.bind(null, 79, "NotFoundError", + checkExn.bind(null, 93, "NotFoundError", "We are a second DOMException", DOMException.NOT_FOUND_ERR, ourFile, 7, - "doTest@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:79:7\n")), + `doTest@${ourFile}:93:7 +` + + parentFrame)), t.testPromiseWithDOMExceptionThrowingThenFunction().then( ensurePromiseFail.bind(null, 8), - checkExn.bind(null, asyncStack ? 85 : 0, "NetworkError", + checkExn.bind(null, asyncStack ? 101 : 0, "NetworkError", "We are a third DOMException", DOMException.NETWORK_ERR, asyncStack ? ourFile : "", 8, - asyncStack ? "Async*doTest@http://mochi.test:8888/tests/dom/bindings/test/test_promise_rejections_from_jsimplemented.html:85:7\n" : "")), + (asyncStack ? `Async*doTest@${ourFile}:101:7 +` + + parentFrame : ""))), t.testPromiseWithDOMExceptionThrowingThenable().then( ensurePromiseFail.bind(null, 9), checkExn.bind(null, 0, "TypeMismatchError", diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index a0e4bb33f542..868b077ee27f 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -943,7 +943,8 @@ CallFunctionWithAsyncStack(JSContext* cx, unsigned argc, Value* vp) RootedObject stack(cx, &args[1].toObject()); RootedString asyncCause(cx, args[2].toString()); - JS::AutoSetAsyncStackForNewCalls sas(cx, stack, asyncCause); + JS::AutoSetAsyncStackForNewCalls sas(cx, stack, asyncCause, + JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::EXPLICIT); return Call(cx, UndefinedHandleValue, function, JS::HandleValueArray::empty(), args.rval()); } diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 946ba63cc99e..3c6327b22c7c 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -4730,10 +4730,12 @@ JS_RestoreFrameChain(JSContext* cx) } JS::AutoSetAsyncStackForNewCalls::AutoSetAsyncStackForNewCalls( - JSContext* cx, HandleObject stack, HandleString asyncCause) + JSContext* cx, HandleObject stack, HandleString asyncCause, + JS::AutoSetAsyncStackForNewCalls::AsyncCallKind kind) : cx(cx), oldAsyncStack(cx, cx->runtime()->asyncStackForNewActivations), - oldAsyncCause(cx, cx->runtime()->asyncCauseForNewActivations) + oldAsyncCause(cx, cx->runtime()->asyncCauseForNewActivations), + oldAsyncCallIsExplicit(cx->runtime()->asyncCallIsExplicit) { CHECK_REQUEST(cx); @@ -4748,6 +4750,7 @@ JS::AutoSetAsyncStackForNewCalls::AutoSetAsyncStackForNewCalls( cx->runtime()->asyncStackForNewActivations = asyncStack; cx->runtime()->asyncCauseForNewActivations = asyncCause; + cx->runtime()->asyncCallIsExplicit = kind == AsyncCallKind::EXPLICIT; } JS::AutoSetAsyncStackForNewCalls::~AutoSetAsyncStackForNewCalls() @@ -4755,6 +4758,7 @@ JS::AutoSetAsyncStackForNewCalls::~AutoSetAsyncStackForNewCalls() cx->runtime()->asyncCauseForNewActivations = oldAsyncCause; cx->runtime()->asyncStackForNewActivations = oldAsyncStack ? &oldAsyncStack->as() : nullptr; + cx->runtime()->asyncCallIsExplicit = oldAsyncCallIsExplicit; } /************************************************************************/ diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 7b13c55438a9..ee9b01bf838e 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -4027,14 +4027,25 @@ class MOZ_STACK_CLASS JS_PUBLIC_API(AutoSetAsyncStackForNewCalls) JSContext* cx; RootedObject oldAsyncStack; RootedString oldAsyncCause; + bool oldAsyncCallIsExplicit; public: + enum class AsyncCallKind { + // The ordinary kind of call, where we may apply an async + // parent if there is no ordinary parent. + IMPLICIT, + // An explicit async parent, e.g., callFunctionWithAsyncStack, + // where we always want to override any ordinary parent. + EXPLICIT + }; + // The stack parameter cannot be null by design, because it would be // ambiguous whether that would clear any scheduled async stack and make the // normal stack reappear in the new call, or just keep the async stack // already scheduled for the new call, if any. AutoSetAsyncStackForNewCalls(JSContext* cx, HandleObject stack, - HandleString asyncCause); + HandleString asyncCause, + AsyncCallKind kind = AsyncCallKind::IMPLICIT); ~AutoSetAsyncStackForNewCalls(); }; diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 15e7c7b93019..f8309f05483b 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -128,6 +128,7 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime) asmJSActivationStack_(nullptr), asyncStackForNewActivations(nullptr), asyncCauseForNewActivations(nullptr), + asyncCallIsExplicit(false), entryMonitor(nullptr), parentRuntime(parentRuntime), interrupt_(false), diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 0e72361fd48e..d4a6dde87f66 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -697,6 +697,12 @@ struct JSRuntime : public JS::shadow::Runtime, */ JSString* asyncCauseForNewActivations; + /* + * True if the async call was explicitly requested, e.g. via + * callFunctionWithAsyncStack. + */ + bool asyncCallIsExplicit; + /* If non-null, report JavaScript entry points to this monitor. */ JS::dbg::AutoEntryMonitor* entryMonitor; diff --git a/js/src/vm/SavedStacks.cpp b/js/src/vm/SavedStacks.cpp index ca1d714768b4..481f92fae75b 100644 --- a/js/src/vm/SavedStacks.cpp +++ b/js/src/vm/SavedStacks.cpp @@ -912,6 +912,17 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram while (!iter.done()) { Activation& activation = *iter.activation(); + if (asyncActivation && asyncActivation != &activation) { + // We found an async stack in the previous activation, and we + // walked past the oldest frame of that activation, we're done. + // However, we only want to use the async parent if it was + // explicitly requested; if we got here otherwise, we have + // a direct parent, which we prefer. + if (asyncActivation->asyncCallIsExplicit()) + break; + asyncActivation = nullptr; + } + if (!asyncActivation) { asyncStack = activation.asyncStack(); if (asyncStack) { @@ -923,10 +934,6 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram asyncCause = activation.asyncCause(); asyncActivation = &activation; } - } else if (asyncActivation != &activation) { - // We found an async stack in the previous activation, and we - // walked past the oldest frame of that activation, we're done. - break; } AutoLocationValueRooter location(cx); diff --git a/js/src/vm/Stack-inl.h b/js/src/vm/Stack-inl.h index 254eff6687a9..93691f519979 100644 --- a/js/src/vm/Stack-inl.h +++ b/js/src/vm/Stack-inl.h @@ -868,11 +868,13 @@ Activation::Activation(JSContext* cx, Kind kind) hideScriptedCallerCount_(0), asyncStack_(cx, cx->runtime_->asyncStackForNewActivations), asyncCause_(cx, cx->runtime_->asyncCauseForNewActivations), + asyncCallIsExplicit_(cx->runtime_->asyncCallIsExplicit), entryMonitor_(cx->runtime_->entryMonitor), kind_(kind) { cx->runtime_->asyncStackForNewActivations = nullptr; cx->runtime_->asyncCauseForNewActivations = nullptr; + cx->runtime_->asyncCallIsExplicit = false; cx->runtime_->entryMonitor = nullptr; cx->runtime_->activation_ = this; } @@ -886,6 +888,7 @@ Activation::~Activation() cx_->runtime_->entryMonitor = entryMonitor_; cx_->runtime_->asyncCauseForNewActivations = asyncCause_; cx_->runtime_->asyncStackForNewActivations = asyncStack_; + cx_->runtime_->asyncCallIsExplicit = asyncCallIsExplicit_; } bool diff --git a/js/src/vm/Stack.h b/js/src/vm/Stack.h index ffda53d372e6..8f26aedcf37c 100644 --- a/js/src/vm/Stack.h +++ b/js/src/vm/Stack.h @@ -1130,6 +1130,10 @@ class Activation // Value of asyncCause to be attached to asyncStack_. RootedString asyncCause_; + // True if the async call was explicitly requested, e.g. via + // callFunctionWithAsyncStack. + bool asyncCallIsExplicit_; + // The entry point monitor that was set on cx_->runtime() when this // Activation was created. Subclasses should report their entry frame's // function or script here. @@ -1215,6 +1219,10 @@ class Activation return asyncCause_; } + bool asyncCallIsExplicit() const { + return asyncCallIsExplicit_; + } + private: Activation(const Activation& other) = delete; void operator=(const Activation& other) = delete; diff --git a/js/xpconnect/src/XPCComponents.cpp b/js/xpconnect/src/XPCComponents.cpp index 3513381ad3f8..c91a73101fb2 100644 --- a/js/xpconnect/src/XPCComponents.cpp +++ b/js/xpconnect/src/XPCComponents.cpp @@ -2824,7 +2824,8 @@ nsXPCComponents_Utils::CallFunctionWithAsyncStack(HandleValue function, if (!asyncCauseString) return NS_ERROR_OUT_OF_MEMORY; - JS::AutoSetAsyncStackForNewCalls sas(cx, asyncStackObj, asyncCauseString); + JS::AutoSetAsyncStackForNewCalls sas(cx, asyncStackObj, asyncCauseString, + JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::EXPLICIT); if (!JS_CallFunctionValue(cx, nullptr, function, JS::HandleValueArray::empty(), retval)) diff --git a/toolkit/modules/Promise-backend.js b/toolkit/modules/Promise-backend.js index bf2105384f84..fb5c958ce2da 100644 --- a/toolkit/modules/Promise-backend.js +++ b/toolkit/modules/Promise-backend.js @@ -43,6 +43,10 @@ let Cu = this.require ? require("chrome").Cu : Components.utils; let Cc = this.require ? require("chrome").Cc : Components.classes; let Ci = this.require ? require("chrome").Ci : Components.interfaces; +// If we can access Components, then we use it to capture an async +// parent stack trace; see scheduleWalkerLoop. However, as it might +// not be available (see above), users of this must check it first. +let Components_ = this.require ? require("chrome").components : Components; // If Cu is defined, use it to lazily define the FinalizationWitnessService. if (Cu) { @@ -737,7 +741,15 @@ this.PromiseWalker = { // If Cu is defined, this file is loaded on the main thread. Otherwise, it // is loaded on the worker thread. if (Cu) { - DOMPromise.resolve().then(() => this.walkerLoop()); + let stack = Components_ ? Components_.stack : null; + if (stack) { + DOMPromise.resolve().then(() => { + Cu.callFunctionWithAsyncStack(this.walkerLoop.bind(this), stack, + "Promise") + }); + } else { + DOMPromise.resolve().then(() => this.walkerLoop()); + } } else { setImmediate(this.walkerLoop); }