diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index d1a92c6246..34c721f722 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -154,6 +154,172 @@ NewPromiseAllDataHolder(JSContext* cx, HandleObject resultPromise, HandleValue v return dataHolder; } +namespace { +// Generator used by PromiseObject::getID. +mozilla::Atomic gIDGenerator(0); +} // namespace + +static MOZ_ALWAYS_INLINE bool +ShouldCaptureDebugInfo(JSContext* cx) +{ + return cx->options().asyncStack() || cx->compartment()->isDebuggee(); +} + +class PromiseDebugInfo : public NativeObject +{ + private: + enum Slots { + Slot_AllocationSite, + Slot_ResolutionSite, + Slot_AllocationTime, + Slot_ResolutionTime, + Slot_Id, + SlotCount + }; + + public: + static const Class class_; + static PromiseDebugInfo* create(JSContext* cx, Handle promise) { + Rooted debugInfo(cx, NewObjectWithClassProto(cx)); + if (!debugInfo) + return nullptr; + + RootedObject stack(cx); + if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) + return nullptr; + debugInfo->setFixedSlot(Slot_AllocationSite, ObjectOrNullValue(stack)); + debugInfo->setFixedSlot(Slot_ResolutionSite, NullValue()); + debugInfo->setFixedSlot(Slot_AllocationTime, DoubleValue(MillisecondsSinceStartup())); + debugInfo->setFixedSlot(Slot_ResolutionTime, NumberValue(0)); + promise->setFixedSlot(PromiseSlot_DebugInfo, ObjectValue(*debugInfo)); + + return debugInfo; + } + + static PromiseDebugInfo* FromPromise(PromiseObject* promise) { + Value val = promise->getFixedSlot(PromiseSlot_DebugInfo); + if (val.isObject()) + return &val.toObject().as(); + return nullptr; + } + + /** + * Returns the given PromiseObject's process-unique ID. + * The ID is lazily assigned when first queried, and then either stored + * in the DebugInfo slot if no debug info was recorded for this Promise, + * or in the Id slot of the DebugInfo object. + */ + static uint64_t id(PromiseObject* promise) { + Value idVal(promise->getFixedSlot(PromiseSlot_DebugInfo)); + if (idVal.isUndefined()) { + idVal.setDouble(++gIDGenerator); + promise->setFixedSlot(PromiseSlot_DebugInfo, idVal); + } else if (idVal.isObject()) { + PromiseDebugInfo* debugInfo = FromPromise(promise); + idVal = debugInfo->getFixedSlot(Slot_Id); + if (idVal.isUndefined()) { + idVal.setDouble(++gIDGenerator); + debugInfo->setFixedSlot(Slot_Id, idVal); + } + } + return uint64_t(idVal.toNumber()); + } + + double allocationTime() { return getFixedSlot(Slot_AllocationTime).toNumber(); } + double resolutionTime() { return getFixedSlot(Slot_ResolutionTime).toNumber(); } + JSObject* allocationSite() { return getFixedSlot(Slot_AllocationSite).toObjectOrNull(); } + JSObject* resolutionSite() { return getFixedSlot(Slot_ResolutionSite).toObjectOrNull(); } + + static void setResolutionInfo(JSContext* cx, Handle promise) { + if (!ShouldCaptureDebugInfo(cx)) + return; + + // If async stacks weren't enabled and the Promise's global wasn't a + // debuggee when the Promise was created, we won't have a debugInfo + // object. We still want to capture the resolution stack, so we + // create the object now and change it's slots' values around a bit. + Rooted debugInfo(cx, FromPromise(promise)); + if (!debugInfo) { + RootedValue idVal(cx, promise->getFixedSlot(PromiseSlot_DebugInfo)); + debugInfo = create(cx, promise); + if (!debugInfo) { + cx->clearPendingException(); + return; + } + + // The current stack was stored in the AllocationSite slot, move + // it to ResolutionSite as that's what it really is. + debugInfo->setFixedSlot(Slot_ResolutionSite, + debugInfo->getFixedSlot(Slot_AllocationSite)); + debugInfo->setFixedSlot(Slot_AllocationSite, NullValue()); + + // There's no good default for a missing AllocationTime, so + // instead of resetting that, ensure that it's the same as + // ResolutionTime, so that the diff shows as 0, which isn't great, + // but bearable. + debugInfo->setFixedSlot(Slot_ResolutionTime, + debugInfo->getFixedSlot(Slot_AllocationTime)); + + // The Promise's ID might've been queried earlier, in which case + // it's stored in the DebugInfo slot. We saved that earlier, so + // now we can store it in the right place (or leave it as + // undefined if it wasn't ever initialized.) + debugInfo->setFixedSlot(Slot_Id, idVal); + return; + } + + RootedObject stack(cx); + if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) { + cx->clearPendingException(); + return; + } + + debugInfo->setFixedSlot(Slot_ResolutionSite, ObjectOrNullValue(stack)); + debugInfo->setFixedSlot(Slot_ResolutionTime, DoubleValue(MillisecondsSinceStartup())); + } +}; + +const Class PromiseDebugInfo::class_ = { + "PromiseDebugInfo", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) +}; + +double +PromiseObject::allocationTime() +{ + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) + return debugInfo->allocationTime(); + return 0; +} + +double +PromiseObject::resolutionTime() +{ + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) + return debugInfo->resolutionTime(); + return 0; +} + +JSObject* +PromiseObject::allocationSite() +{ + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) + return debugInfo->allocationSite(); + return nullptr; +} + +JSObject* +PromiseObject::resolutionSite() +{ + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) + return debugInfo->resolutionSite(); + return nullptr; +} + /** * Wrapper for GetAndClearException that handles cases where no exception is * pending, but an error occurred. This can be the case if an OOM was @@ -1278,13 +1444,11 @@ CreatePromiseObjectInternal(JSContext* cx, HandleObject proto /* = nullptr */, // Store an allocation stack so we can later figure out what the // control flow was for some unexpected results. Frightfully expensive, // but oh well. - RootedObject stack(cx); - if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) { - if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) + if (ShouldCaptureDebugInfo(cx)) { + PromiseDebugInfo* debugInfo = PromiseDebugInfo::create(cx, promise); + if (!debugInfo) return nullptr; } - promise->setFixedSlot(PromiseSlot_AllocationSite, ObjectOrNullValue(stack)); - promise->setFixedSlot(PromiseSlot_AllocationTime, DoubleValue(MillisecondsSinceStartup())); // Let the Debugger know about this Promise. if (informDebugger) @@ -3384,10 +3548,11 @@ AddPromiseReaction(JSContext* cx, Handle promise, HandleValue on return AddPromiseReaction(cx, promise, reaction); } -namespace { -// Generator used by PromiseObject::getID. -mozilla::Atomic gIDGenerator(0); -} // namespace +uint64_t +PromiseObject::getID() +{ + return PromiseDebugInfo::id(this); +} double PromiseObject::lifetime() @@ -3395,17 +3560,6 @@ PromiseObject::lifetime() return MillisecondsSinceStartup() - allocationTime(); } -uint64_t -PromiseObject::getID() -{ - Value idVal(getFixedSlot(PromiseSlot_Id)); - if (idVal.isUndefined()) { - idVal.setDouble(++gIDGenerator); - setFixedSlot(PromiseSlot_Id, idVal); - } - return uint64_t(idVal.toNumber()); -} - /** * Returns all promises that directly depend on this one. That means those * created by calling `then` on this promise, or the promise returned by @@ -3515,15 +3669,7 @@ PromiseObject::reject(JSContext* cx, Handle promise, HandleValue /* static */ void PromiseObject::onSettled(JSContext* cx, Handle promise) { - RootedObject stack(cx); - if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) { - if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) { - cx->clearPendingException(); - return; - } - } - promise->setFixedSlot(PromiseSlot_ResolutionSite, ObjectOrNullValue(stack)); - promise->setFixedSlot(PromiseSlot_ResolutionTime, DoubleValue(MillisecondsSinceStartup())); + PromiseDebugInfo::setResolutionInfo(cx, promise); if (promise->state() == JS::PromiseState::Rejected && promise->isUnhandled()) cx->runtime()->addUnhandledRejectedPromise(cx, promise); diff --git a/js/src/builtin/Promise.h b/js/src/builtin/Promise.h index d8d178891a..a6d8c0831d 100644 --- a/js/src/builtin/Promise.h +++ b/js/src/builtin/Promise.h @@ -16,11 +16,7 @@ enum PromiseSlots { PromiseSlot_ReactionsOrResult, PromiseSlot_RejectFunction, PromiseSlot_AwaitGenerator = PromiseSlot_RejectFunction, - PromiseSlot_AllocationSite, - PromiseSlot_ResolutionSite, - PromiseSlot_AllocationTime, - PromiseSlot_ResolutionTime, - PromiseSlot_Id, + PromiseSlot_DebugInfo, PromiseSlots, }; @@ -74,14 +70,10 @@ class PromiseObject : public NativeObject static void onSettled(JSContext* cx, Handle promise); - double allocationTime() { return getFixedSlot(PromiseSlot_AllocationTime).toNumber(); } - double resolutionTime() { return getFixedSlot(PromiseSlot_ResolutionTime).toNumber(); } - JSObject* allocationSite() { - return getFixedSlot(PromiseSlot_AllocationSite).toObjectOrNull(); - } - JSObject* resolutionSite() { - return getFixedSlot(PromiseSlot_ResolutionSite).toObjectOrNull(); - } + double allocationTime(); + double resolutionTime(); + JSObject* allocationSite(); + JSObject* resolutionSite(); double lifetime(); double timeToResolution() { MOZ_ASSERT(state() != JS::PromiseState::Pending);