Bug 1289318 - Part 3: Merge Promise fulfillment and rejection reaction lists into a single list. r?efaust draft
authorTill Schneidereit <till@tillschneidereit.net>
Wed, 03 Aug 2016 19:37:23 +0200
changeset 400696 94d70296f22a8b1e3e1b707ce1a004d137d20a56
parent 400695 e6681331ef9d56cba16468697bc837dabdb22439
child 400697 6429d48ca8d8547f7a2ecb737723c5d304a8e779
push id26245
push userbmo:till@tillschneidereit.net
push dateMon, 15 Aug 2016 14:02:13 +0000
reviewersefaust
bugs1289318
milestone51.0a1
Bug 1289318 - Part 3: Merge Promise fulfillment and rejection reaction lists into a single list. r?efaust This saves a slot on Promise instances, an Array allocation, and a rejection record per dependent promise. While the first doesn't in itself save anything (going from 12 to 11 slots doesn't do anything), the second saves 96 bytes per Promise, and the third 64 bytes per dependent Promise. MozReview-Commit-ID: BglU9tx89rD
js/src/builtin/Promise.cpp
js/src/builtin/Promise.js
js/src/builtin/SelfHostingDefines.h
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -109,27 +109,22 @@ PromiseObject::create(JSContext* cx, Han
 
         promise = NewObjectWithClassProto<PromiseObject>(cx, usedProto);
         if (!promise)
             return nullptr;
 
         // Step 4.
         promise->setFixedSlot(PROMISE_STATE_SLOT, Int32Value(PROMISE_STATE_PENDING));
 
-        // Step 5.
+        // Steps 5-6.
+        // We only have a single list of reaction records.
         RootedArrayObject reactions(cx, NewDenseEmptyArray(cx));
         if (!reactions)
             return nullptr;
-        promise->setFixedSlot(PROMISE_FULFILL_REACTIONS_SLOT, ObjectValue(*reactions));
-
-        // Step 6.
-        reactions = NewDenseEmptyArray(cx);
-        if (!reactions)
-            return nullptr;
-        promise->setFixedSlot(PROMISE_REJECT_REACTIONS_SLOT, ObjectValue(*reactions));
+        promise->setFixedSlot(PROMISE_REACTIONS_SLOT, ObjectValue(*reactions));
 
         // Step 7.
         promise->setFixedSlot(PROMISE_IS_HANDLED_SLOT,
                               Int32Value(PROMISE_IS_HANDLED_STATE_UNHANDLED));
 
         // Store an allocation stack so we can later figure out what the
         // control flow was for some unexpected results. Frightfully expensive,
         // but oh well.
@@ -232,38 +227,31 @@ PromiseObject::getID()
 }
 
 /**
  * Returns all promises that directly depend on this one. That means those
  * created by calling `then` on this promise, or the promise returned by
  * `Promise.all(iterable)` or `Promise.race(iterable)`, with this promise
  * being a member of the passed-in `iterable`.
  *
- *
- * For the then() case, we have both resolve and reject callbacks that know
- * what the next promise is.
- *
- * For the race() case, likewise.
- *
- * For the all() case, our reject callback knows what the next promise is, but
- * our resolve callback doesn't.
- *
- * So we walk over our _reject_ callbacks and ask each of them what promise
- * its dependent promise is.
+ * Per spec, we should have separate lists of reaction records for the
+ * fulfill and reject cases. As an optimization, we have only one of those,
+ * containing the required data for both cases. So we just walk that list
+ * and extract the dependent promises from all reaction records.
  */
 bool
 PromiseObject::dependentPromises(JSContext* cx, MutableHandle<GCVector<Value>> values)
 {
-    RootedValue rejectReactionsVal(cx, getReservedSlot(PROMISE_REJECT_REACTIONS_SLOT));
-    RootedObject rejectReactions(cx, rejectReactionsVal.toObjectOrNull());
-    if (!rejectReactions)
+    RootedValue reactionsVal(cx, getReservedSlot(PROMISE_REACTIONS_SLOT));
+    RootedObject reactions(cx, reactionsVal.toObjectOrNull());
+    if (!reactions)
         return true;
 
     AutoIdVector keys(cx);
-    if (!GetPropertyKeys(cx, rejectReactions, JSITER_OWNONLY, &keys))
+    if (!GetPropertyKeys(cx, reactions, JSITER_OWNONLY, &keys))
         return false;
 
     if (keys.length() == 0)
         return true;
 
     if (!values.growBy(keys.length()))
         return false;
 
@@ -274,17 +262,17 @@ PromiseObject::dependentPromises(JSConte
     //   reject:  [the `reject` callback content code provided],
     //   handler: [the internal handler that fulfills/rejects the promise]
     // }
     //
     // In the following loop we collect the `capabilities.promise` values for
     // each reaction.
     for (size_t i = 0; i < keys.length(); i++) {
         MutableHandleValue val = values[i];
-        if (!GetProperty(cx, rejectReactions, rejectReactions, keys[i], val))
+        if (!GetProperty(cx, reactions, reactions, keys[i], val))
             return false;
         RootedObject reaction(cx, &val.toObject());
         if (!GetProperty(cx, reaction, reaction, cx->runtime()->commonNames->promise, val))
             return false;
     }
 
     return true;
 }
--- a/js/src/builtin/Promise.js
+++ b/js/src/builtin/Promise.js
@@ -1,20 +1,22 @@
 /* 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/. */
 
 // ES6, 25.4.1.2.
 // This object is used to verify that an object is a PromiseReaction record.
 var PromiseReactionRecordProto = {__proto__: null};
-function PromiseReactionRecord(promise, resolve, reject, handler, incumbentGlobal) {
+function PromiseReactionRecord(promise, resolve, reject, fulfillHandler, rejectHandler,
+                               incumbentGlobal) {
     this.promise = promise;
     this.resolve = resolve;
     this.reject = reject;
-    this.handler = handler;
+    this.fulfillHandler = fulfillHandler;
+    this.rejectHandler = rejectHandler;
     this.incumbentGlobal = incumbentGlobal;
 }
 
 MakeConstructible(PromiseReactionRecord, PromiseReactionRecordProto);
 
 // ES6, 25.4.1.3.
 function CreateResolvingFunctions(promise) {
     // The callbacks created here can deal with Promises wrapped in cross-
@@ -120,57 +122,61 @@ function CreateResolvingFunctions(promis
 
     // Return an array instead of an object with resolve/reject properties
     // to make value extraction from C++ easier.
     return [resolve, reject];
 }
 
 // ES6, 25.4.1.4.
 function FulfillPromise(promise, value) {
-    return ResolvePromise(promise, value, PROMISE_FULFILL_REACTIONS_SLOT, PROMISE_STATE_FULFILLED);
+    return ResolvePromise(promise, value, PROMISE_STATE_FULFILLED);
 }
 function FulfillUnwrappedPromise(value) {
-    return ResolvePromise(this, value, PROMISE_FULFILL_REACTIONS_SLOT, PROMISE_STATE_FULFILLED);
+    return ResolvePromise(this, value, PROMISE_STATE_FULFILLED);
 }
 
 // Commoned-out implementation of 25.4.1.4. and 25.4.1.7.
 // ES2016 February 12 draft.
-function ResolvePromise(promise, valueOrReason, reactionsSlot, state) {
+function ResolvePromise(promise, valueOrReason, state) {
     // Step 1.
     assert(GetPromiseState(promise) === PROMISE_STATE_PENDING,
            "Can't resolve non-pending promise");
-    assert(state >= PROMISE_STATE_PENDING && state <= PROMISE_STATE_REJECTED,
-           `Invalid Promise state <${state}>`);
+    assert(state >= PROMISE_STATE_FULFILLED && state <= PROMISE_STATE_REJECTED,
+           `Invalid Promise resolution state <${state}>`);
 
     // Step 2.
-    var reactions = UnsafeGetObjectFromReservedSlot(promise, reactionsSlot);
+    // We only have one list of reactions for both resolution types. So
+    // instead of getting the right list of reactions, we determine the
+    // resolution type to retrieve the right information from the
+    // reaction records.
+    var reactions = UnsafeGetObjectFromReservedSlot(promise, PROMISE_REACTIONS_SLOT);
+    let jobType = state === PROMISE_STATE_FULFILLED
+                  ? PROMISE_JOB_TYPE_FULFILL
+                  : PROMISE_JOB_TYPE_REJECT;
 
     // Step 3.
     UnsafeSetReservedSlot(promise, PROMISE_RESULT_SLOT, valueOrReason);
 
-    // Step 4.
-    UnsafeSetReservedSlot(promise, PROMISE_FULFILL_REACTIONS_SLOT, null);
-
-    // Step 5.
-    UnsafeSetReservedSlot(promise, PROMISE_REJECT_REACTIONS_SLOT, null);
+    // Steps 4-5.
+    UnsafeSetReservedSlot(promise, PROMISE_REACTIONS_SLOT, null);
 
     // Step 6.
     UnsafeSetReservedSlot(promise, PROMISE_STATE_SLOT, state);
 
     // Also null out the resolve/reject functions so they can be GC'd.
     UnsafeSetReservedSlot(promise, PROMISE_RESOLVE_FUNCTION_SLOT, null);
     UnsafeSetReservedSlot(promise, PROMISE_REJECT_FUNCTION_SLOT, null);
 
     // Now that everything else is done, do the things the debugger needs.
     // Step 7 of RejectPromise implemented in the debugger intrinsic.
     _dbg_onPromiseSettled(promise);
 
     // Step 7 of FulfillPromise.
     // Step 8 of RejectPromise.
-    return TriggerPromiseReactions(reactions, valueOrReason);
+    return TriggerPromiseReactions(reactions, jobType, valueOrReason);
 }
 
 // Used to verify that an object is a PromiseCapability record.
 var PromiseCapabilityRecordProto = {__proto__: null};
 
 // ES6, 25.4.1.5.
 // Creates PromiseCapability records, see 25.4.1.1.
 function NewPromiseCapability(C) {
@@ -214,32 +220,36 @@ function NewPromiseCapability(C) {
         reject
     };
 }
 
 // ES6, 25.4.1.6. is implemented as an intrinsic in SelfHosting.cpp.
 
 // ES2016, February 12 draft, 25.4.1.7.
 function RejectPromise(promise, reason) {
-    return ResolvePromise(promise, reason, PROMISE_REJECT_REACTIONS_SLOT, PROMISE_STATE_REJECTED);
+    return ResolvePromise(promise, reason, PROMISE_STATE_REJECTED);
 }
 
 // ES6, 25.4.1.8.
-function TriggerPromiseReactions(reactions, argument) {
+function TriggerPromiseReactions(reactions, jobType, argument) {
     // Step 1.
     for (var i = 0, len = reactions.length; i < len; i++)
-        EnqueuePromiseReactionJob(reactions[i], argument);
+        EnqueuePromiseReactionJob(reactions[i], jobType, argument);
     // Step 2 (implicit).
 }
 
 // ES2016, February 12 draft 25.4.1.9, implemented in SelfHosting.cpp.
 
 // ES6, 25.4.2.1.
-function EnqueuePromiseReactionJob(reaction, argument) {
-    _EnqueuePromiseReactionJob(reaction.handler,
+function EnqueuePromiseReactionJob(reaction, jobType, argument) {
+    // Reaction records contain handlers for both fulfillment and rejection.
+    // The `jobType` parameter allows us to retrieves the right one.
+    assert(jobType === PROMISE_JOB_TYPE_FULFILL || jobType === PROMISE_JOB_TYPE_REJECT,
+           "Invalid job type");
+    _EnqueuePromiseReactionJob(reaction[jobType],
                                argument,
                                reaction.resolve,
                                reaction.reject,
                                reaction.promise,
                                reaction.incumbentGlobal || null);
 }
 
 // ES6, 25.4.2.2. (Implemented in C++).
@@ -606,61 +616,48 @@ function BlockOnPromise(promise, blocked
     // overridden by content and could leak it, or because a constructor
     // other than the original value of |Promise| was used to create it).
     // To have both that object and |blockedPromise| show up as dependent
     // promises in the debugger, add a dummy reaction to the list of reject
     // reactions that contains |blockedPromise|, but otherwise does nothing.
     // If the object isn't a maybe-wrapped instance of |Promise|, we ignore
     // it. All this does is lose some small amount of debug information
     // in scenarios that are highly unlikely to occur in useful code.
-    if (IsPromise(promise)) {
-        return callFunction(AddPromiseReaction, promise, PROMISE_REJECT_REACTIONS_SLOT,
-                            blockedPromise);
-    }
+    if (IsPromise(promise))
+        return callFunction(AddDependentPromise, promise, blockedPromise);
 
-    if (IsWrappedPromise(promise)) {
-        callFunction(CallPromiseMethodIfWrapped, promise, PROMISE_REJECT_REACTIONS_SLOT,
-                     blockedPromise, "AddPromiseReaction");
-    }
+    if (IsWrappedPromise(promise))
+        callFunction(CallPromiseMethodIfWrapped, promise, blockedPromise, "AddDependentPromise");
 }
 
 /**
- * Invoked with a Promise as the receiver, AddPromiseReaction adds an entry to
- * the reactions list in `slot`, using the other parameters as values for that
- * reaction.
+ * Invoked with a Promise as the receiver, AddDependentPromise adds an entry
+ * to the reactions list.
  *
- * If any of the callback functions aren't specified, they're set to
- * NullFunction. Doing that here is useful in case the call is performed on an
- * unwrapped Promise. Passing in NullFunctions would cause useless compartment
- * switches.
+ * This is only used to make dependent promises visible in the devtools, so no
+ * callbacks are provided. To make handling that case easier elsewhere,
+ * they're all set to NullFunction.
  *
  * The reason for the target Promise to be passed as the receiver is so the
  * same function can be used for wrapped and unwrapped Promise objects.
  */
-function AddPromiseReaction(slot, dependentPromise, onResolve, onReject, handler) {
-    assert(IsPromise(this), "AddPromiseReaction expects an unwrapped Promise as the receiver");
-    assert(slot === PROMISE_FULFILL_REACTIONS_SLOT || slot === PROMISE_REJECT_REACTIONS_SLOT,
-           "Invalid slot");
+function AddDependentPromise(dependentPromise) {
+    assert(IsPromise(this), "AddDependentPromise expects an unwrapped Promise as the receiver");
 
-    if (!onResolve)
-        onResolve = NullFunction;
-    if (!onReject)
-        onReject = NullFunction;
-    if (!handler)
-        handler = NullFunction;
+    let reaction = new PromiseReactionRecord(dependentPromise, NullFunction, NullFunction,
+                                             NullFunction, NullFunction, null);
 
-    let reactions = UnsafeGetReservedSlot(this, slot);
+    let reactions = UnsafeGetReservedSlot(this, PROMISE_REACTIONS_SLOT);
 
     // The reactions slot might've been reset because the Promise was resolved.
     if (!reactions) {
         assert(GetPromiseState(this) !== PROMISE_STATE_PENDING,
                "Pending promises must have reactions lists.");
         return;
     }
-    let reaction = new PromiseReactionRecord(promise, reject, resolve, handler, null);
     _DefineDataProperty(reactions, reactions.length, reaction);
 }
 
 // ES6, 25.4.4.4.
 function Promise_static_reject(r) {
     // Step 1.
     let C = this;
 
@@ -882,51 +879,43 @@ function PerformPromiseThen(promise, onF
     if (!IsCallable(onFulfilled))
         onFulfilled = PROMISE_HANDLER_IDENTITY;
 
     // Step 4.
     if (!IsCallable(onRejected))
         onRejected = PROMISE_HANDLER_THROWER;
 
     let incumbentGlobal = _GetObjectFromIncumbentGlobal();
-    // Step 5.
-    let fulfillReaction = new PromiseReactionRecord(resultCapability.promise,
-                                                    resultCapability.resolve,
-                                                    resultCapability.reject,
-                                                    onFulfilled,
-                                                    incumbentGlobal);
-
-    // Step 6.
-    let rejectReaction = new PromiseReactionRecord(resultCapability.promise,
-                                                   resultCapability.resolve,
-                                                   resultCapability.reject,
-                                                   onRejected,
-                                                   incumbentGlobal);
+    // Steps 5,6.
+    // Instead of creating separate reaction records for fulfillment and
+    // rejection, we create a combined record. All places we use the record
+    // can handle that.
+    let reaction = new PromiseReactionRecord(resultCapability.promise,
+                                             resultCapability.resolve,
+                                             resultCapability.reject,
+                                             onFulfilled,
+                                             onRejected,
+                                             incumbentGlobal);
 
     // Step 7.
     let state = GetPromiseState(promise);
     if (state === PROMISE_STATE_PENDING) {
-        // Step 7.a.
-        let fulfillReactions = UnsafeGetObjectFromReservedSlot(promise,
-                                                               PROMISE_FULFILL_REACTIONS_SLOT);
-        _DefineDataProperty(fulfillReactions, fulfillReactions.length, fulfillReaction);
-
-        // Step 7.b.
-        let rejectReactions = UnsafeGetObjectFromReservedSlot(promise,
-                                                              PROMISE_REJECT_REACTIONS_SLOT);
-        _DefineDataProperty(rejectReactions, rejectReactions.length, rejectReaction);
+        // Steps 7.a,b.
+        // We only have a single list for fulfill and reject reactions.
+        let reactions = UnsafeGetObjectFromReservedSlot(promise, PROMISE_REACTIONS_SLOT);
+        _DefineDataProperty(reactions, reactions.length, reaction);
     }
 
     // Step 8.
     else if (state === PROMISE_STATE_FULFILLED) {
         // Step 8.a.
         let value = UnsafeGetReservedSlot(promise, PROMISE_RESULT_SLOT);
 
         // Step 8.b.
-        EnqueuePromiseReactionJob(fulfillReaction, value);
+        EnqueuePromiseReactionJob(reaction, PROMISE_JOB_TYPE_FULFILL, value);
     }
 
     // Step 9.
     else {
         // Step 9.a.
         assert(state === PROMISE_STATE_REJECTED, "Invalid Promise state " + state);
 
         // Step 9.b.
@@ -935,17 +924,17 @@ function PerformPromiseThen(promise, onF
         // Step 9.c.
         if (UnsafeGetInt32FromReservedSlot(promise, PROMISE_IS_HANDLED_SLOT) !==
             PROMISE_IS_HANDLED_STATE_HANDLED)
         {
             HostPromiseRejectionTracker(promise, PROMISE_REJECTION_TRACKER_OPERATION_HANDLE);
         }
 
         // Step 9.d.
-        EnqueuePromiseReactionJob(rejectReaction, reason);
+        EnqueuePromiseReactionJob(reaction, PROMISE_JOB_TYPE_REJECT, reason);
     }
 
     // Step 10.
     UnsafeSetReservedSlot(promise, PROMISE_IS_HANDLED_SLOT, PROMISE_IS_HANDLED_STATE_HANDLED);
 
     // Step 11.
     return resultCapability.promise;
 }
--- a/js/src/builtin/SelfHostingDefines.h
+++ b/js/src/builtin/SelfHostingDefines.h
@@ -54,41 +54,43 @@
 #define ITERATOR_SLOT_NEXT_METHOD 2
 
 #define ITEM_KIND_KEY 0
 #define ITEM_KIND_VALUE 1
 #define ITEM_KIND_KEY_AND_VALUE 2
 
 #define PROMISE_STATE_SLOT             0
 #define PROMISE_RESULT_SLOT            1
-#define PROMISE_FULFILL_REACTIONS_SLOT 2
-#define PROMISE_REJECT_REACTIONS_SLOT  3
-#define PROMISE_RESOLVE_FUNCTION_SLOT  4
-#define PROMISE_REJECT_FUNCTION_SLOT   5
-#define PROMISE_ALLOCATION_SITE_SLOT   6
-#define PROMISE_RESOLUTION_SITE_SLOT   7
-#define PROMISE_ALLOCATION_TIME_SLOT   8
-#define PROMISE_RESOLUTION_TIME_SLOT   9
-#define PROMISE_ID_SLOT               10
-#define PROMISE_IS_HANDLED_SLOT       11
+#define PROMISE_REACTIONS_SLOT         2
+#define PROMISE_RESOLVE_FUNCTION_SLOT  3
+#define PROMISE_REJECT_FUNCTION_SLOT   4
+#define PROMISE_ALLOCATION_SITE_SLOT   5
+#define PROMISE_RESOLUTION_SITE_SLOT   6
+#define PROMISE_ALLOCATION_TIME_SLOT   7
+#define PROMISE_RESOLUTION_TIME_SLOT   8
+#define PROMISE_ID_SLOT                9
+#define PROMISE_IS_HANDLED_SLOT       10
 
 #define PROMISE_STATE_PENDING   0
 #define PROMISE_STATE_FULFILLED 1
 #define PROMISE_STATE_REJECTED  2
 
 #define PROMISE_IS_HANDLED_STATE_HANDLED   0
 #define PROMISE_IS_HANDLED_STATE_UNHANDLED 1
 #define PROMISE_IS_HANDLED_STATE_REPORTED  2
 
 #define PROMISE_HANDLER_IDENTITY 0
 #define PROMISE_HANDLER_THROWER  1
 
 #define PROMISE_REJECTION_TRACKER_OPERATION_REJECT false
 #define PROMISE_REJECTION_TRACKER_OPERATION_HANDLE true
 
+#define PROMISE_JOB_TYPE_FULFILL "fulfillHandler"
+#define PROMISE_JOB_TYPE_REJECT  "rejectHandler"
+
 // NB: keep these in sync with the copy in jsfriendapi.h.
 #define JSITER_OWNONLY    0x8   /* iterate over obj's own properties only */
 #define JSITER_HIDDEN     0x10  /* also enumerate non-enumerable properties */
 #define JSITER_SYMBOLS    0x20  /* also include symbol property keys */
 #define JSITER_SYMBOLSONLY 0x40 /* exclude string property keys */
 
 
 #define REGEXP_FLAGS_SLOT 2