Bug 1317481 - Optimize away Generator/Promise handling for await in the topmost JS frame with already resolved/rejected Promise. r=anba,smaug
authorTooru Fujisawa <arai_a@mac.com>
Thu, 02 Aug 2018 16:11:57 +0900
changeset 484946 4c59bddb1d688cdb22523317833df797c1a20ccd
parent 484945 c459ea4efa8de9509f43a6b341a6ca86f0f708de
child 484947 7acb1055fd9888375466ab791bca316a7e45afa4
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersanba, smaug
bugs1317481
milestone63.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 1317481 - Optimize away Generator/Promise handling for await in the topmost JS frame with already resolved/rejected Promise. r=anba,smaug
dom/workers/RuntimeService.cpp
dom/worklet/WorkletThread.cpp
js/public/Value.h
js/src/builtin/Promise.cpp
js/src/builtin/Promise.h
js/src/frontend/BytecodeEmitter.cpp
js/src/jit/BaselineCompiler.cpp
js/src/jit/BaselineCompiler.h
js/src/jit/IonBuilder.cpp
js/src/jit/VMFunctions.cpp
js/src/jit/VMFunctions.h
js/src/jsapi.cpp
js/src/jsapi.h
js/src/vm/Interpreter.cpp
js/src/vm/JSContext.cpp
js/src/vm/JSContext.h
js/src/vm/Opcodes.h
xpcom/base/CycleCollectedJSContext.cpp
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -1134,16 +1134,17 @@ public:
       microTaskQueue = &GetMicroTaskQueue();
     } else {
       MOZ_ASSERT(IsWorkerDebuggerGlobal(global) ||
                  IsWorkerDebuggerSandbox(global));
 
       microTaskQueue = &GetDebuggerMicroTaskQueue();
     }
 
+    JS::JobQueueMayNotBeEmpty(cx);
     microTaskQueue->push(runnable.forget());
   }
 
   bool IsSystemCaller() const override
   {
     return mWorkerPrivate->UsesSystemPrincipal();
   }
 
--- a/dom/worklet/WorkletThread.cpp
+++ b/dom/worklet/WorkletThread.cpp
@@ -166,30 +166,31 @@ public:
     return NS_OK;
   }
 
   void
   DispatchToMicroTask(already_AddRefed<MicroTaskRunnable> aRunnable) override
   {
     RefPtr<MicroTaskRunnable> runnable(aRunnable);
 
-#ifdef DEBUG
     MOZ_ASSERT(!NS_IsMainThread());
     MOZ_ASSERT(runnable);
 
     WorkletThread* workletThread = WorkletThread::Get();
     MOZ_ASSERT(workletThread);
 
     JSContext* cx = workletThread->GetJSContext();
     MOZ_ASSERT(cx);
 
+#ifdef DEBUG
     JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
     MOZ_ASSERT(global);
 #endif
 
+    JS::JobQueueMayNotBeEmpty(cx);
     GetMicroTaskQueue().push(runnable.forget());
   }
 
   WorkletThread* GetWorkletThread() const
   {
     return mWorkletThread;
   }
 
--- a/js/public/Value.h
+++ b/js/public/Value.h
@@ -217,16 +217,19 @@ enum JSWhyMagic
     JS_OPTIMIZED_OUT,
 
     /** uninitialized lexical bindings that produce ReferenceError on touch. */
     JS_UNINITIALIZED_LEXICAL,
 
     /** standard constructors are not created for off-thread parsing. */
     JS_OFF_THREAD_CONSTRUCTOR,
 
+    /** used in jit::TrySkipAwait */
+    JS_CANNOT_SKIP_AWAIT,
+
     /** for local use */
     JS_GENERIC_MAGIC,
 
     JS_WHY_MAGIC_COUNT
 };
 
 namespace js {
 static inline JS::Value PoisonedObjectValue(uintptr_t poison);
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -16,16 +16,18 @@
 
 #include "gc/Heap.h"
 #include "js/Debug.h"
 #include "vm/AsyncFunction.h"
 #include "vm/AsyncIteration.h"
 #include "vm/Debugger.h"
 #include "vm/Iteration.h"
 #include "vm/JSContext.h"
+#include "vm/JSObject.h"
+#include "vm/SelfHosting.h"
 
 #include "vm/Compartment-inl.h"
 #include "vm/Debugger-inl.h"
 #include "vm/JSObject-inl.h"
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
@@ -4385,16 +4387,107 @@ js::PromiseLookup::isDefaultInstance(JSC
     // Promise and Promise.prototype must be in their default states.
     if (!ensureInitialized(cx, reinitialize))
         return false;
 
     // The object uses the default properties from Promise.prototype.
     return hasDefaultProtoAndNoShadowedProperties(cx, promise);
 }
 
+// We can skip `await` with an already resolved value only if the current frame
+// is the topmost JS frame and the current job is the last job in the job queue.
+// This guarantees that any new job enqueued in the current turn will be
+// executed immediately after the current job.
+//
+// Currently we only support skipping jobs when the async function is resumed
+// at least once.
+static MOZ_MUST_USE bool
+IsTopMostAsyncFunctionCall(JSContext* cx)
+{
+    FrameIter iter(cx);
+
+    // The current frame should be the async function.
+    if (iter.done())
+        return false;
+    if (!iter.calleeTemplate())
+        return false;
+    MOZ_ASSERT(iter.calleeTemplate()->isAsync());
+
+    ++iter;
+
+    // The parent frame should be the `next` function of the generator that is
+    // internally called in AsyncFunctionResume.
+    if (iter.done())
+        return false;
+    if (!iter.calleeTemplate())
+        return false;
+
+    if (!IsSelfHostedFunctionWithName(iter.calleeTemplate(), cx->names().GeneratorNext))
+        return false;
+
+    ++iter;
+
+    // There should be no more frames.
+    if (iter.done())
+        return true;
+
+    return false;
+}
+
+MOZ_MUST_USE bool
+js::TrySkipAwait(JSContext* cx, HandleValue val, bool* canSkip, MutableHandleValue resolved)
+{
+    if (!cx->canSkipEnqueuingJobs) {
+        *canSkip = false;
+        return true;
+    }
+
+    if (!IsTopMostAsyncFunctionCall(cx)) {
+        *canSkip = false;
+        return true;
+    }
+
+    // Primitive values cannot be 'thenables', so we can trivially skip the
+    // await operation.
+    if (!val.isObject()) {
+        resolved.set(val);
+        *canSkip = true;
+        return true;
+    }
+
+    JSObject* obj = &val.toObject();
+    if (!obj->is<PromiseObject>()) {
+        *canSkip = false;
+        return true;
+    }
+
+    PromiseObject* promise = &obj->as<PromiseObject>();
+
+    if (promise->state() == JS::PromiseState::Pending) {
+        *canSkip = false;
+        return true;
+    }
+
+    PromiseLookup& promiseLookup = cx->realm()->promiseLookup;
+    if (!promiseLookup.isDefaultInstance(cx, promise)) {
+        *canSkip = false;
+        return true;
+    }
+
+    if (promise->state() == JS::PromiseState::Rejected) {
+        // We don't optimize rejected Promises for now.
+        *canSkip = false;
+        return true;
+    }
+
+    resolved.set(promise->value());
+    *canSkip = true;
+    return true;
+}
+
 OffThreadPromiseTask::OffThreadPromiseTask(JSContext* cx, Handle<PromiseObject*> promise)
   : runtime_(cx->runtime()),
     promise_(cx, promise),
     registered_(false)
 {
     MOZ_ASSERT(runtime_ == promise_->zone()->runtimeFromMainThread());
     MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
     MOZ_ASSERT(cx->runtime()->offThreadPromiseState.ref().initialized());
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -154,16 +154,22 @@ MOZ_MUST_USE bool
 AsyncFunctionReturned(JSContext* cx, Handle<PromiseObject*> resultPromise, HandleValue value);
 
 MOZ_MUST_USE bool
 AsyncFunctionThrown(JSContext* cx, Handle<PromiseObject*> resultPromise);
 
 MOZ_MUST_USE bool
 AsyncFunctionAwait(JSContext* cx, Handle<PromiseObject*> resultPromise, HandleValue value);
 
+// If the await operation can be skipped and the resolution value for `val` can
+// be acquired, stored the resolved value to `resolved` and `true` to
+// `*canSkip`.  Otherwise, stores `false` to `*canSkip`.
+MOZ_MUST_USE bool
+TrySkipAwait(JSContext* cx, HandleValue val, bool* canSkip, MutableHandleValue resolved);
+
 class AsyncGeneratorObject;
 
 MOZ_MUST_USE bool
 AsyncGeneratorAwait(JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj, HandleValue value);
 
 MOZ_MUST_USE bool
 AsyncGeneratorResolve(JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
                       HandleValue value, bool done);
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -6063,20 +6063,36 @@ BytecodeEmitter::emitAwaitInInnermostSco
     if (!emitTree(pn->pn_kid))
         return false;
     return emitAwaitInInnermostScope();
 }
 
 bool
 BytecodeEmitter::emitAwaitInScope(EmitterScope& currentScope)
 {
+    if (!emit1(JSOP_TRYSKIPAWAIT))              // VALUE_OR_RESOLVED CANSKIP
+        return false;
+
+    if (!emit1(JSOP_NOT))                       // VALUE_OR_RESOLVED !CANSKIP
+        return false;
+
+    InternalIfEmitter ifCanSkip(this);
+    if (!ifCanSkip.emitThen())                  // VALUE_OR_RESOLVED
+        return false;
+
     if (!emitGetDotGeneratorInScope(currentScope))
-        return false;
-    if (!emitYieldOp(JSOP_AWAIT))
-        return false;
+        return false;                           // VALUE GENERATOR
+    if (!emitYieldOp(JSOP_AWAIT))               // RESOLVED
+        return false;
+
+    if (!ifCanSkip.emitEnd())
+        return false;
+
+    MOZ_ASSERT(ifCanSkip.popped() == 0);
+
     return true;
 }
 
 bool
 BytecodeEmitter::emitYieldStar(ParseNode* iter)
 {
     MOZ_ASSERT(sc->isFunctionBox());
     MOZ_ASSERT(sc->asFunctionBox()->isGenerator());
--- a/js/src/jit/BaselineCompiler.cpp
+++ b/js/src/jit/BaselineCompiler.cpp
@@ -1027,17 +1027,16 @@ BaselineCompiler::emitBody()
         switch (op) {
           // ===== NOT Yet Implemented =====
           case JSOP_FORCEINTERPRETER:
             // Intentionally not implemented.
           case JSOP_SETINTRINSIC:
             // Run-once opcode during self-hosting initialization.
           case JSOP_UNUSED126:
           case JSOP_UNUSED206:
-          case JSOP_UNUSED223:
           case JSOP_LIMIT:
             // === !! WARNING WARNING WARNING !! ===
             // Do you really want to sacrifice performance by not implementing
             // this operation in the BaselineCompiler?
             JitSpew(JitSpew_BaselineAbort, "Unhandled op: %s", CodeName[op]);
             return Method_CantCompile;
 
 #define EMIT_OP(OP)                            \
@@ -4117,16 +4116,48 @@ BaselineCompiler::emit_JSOP_TOASYNCITER(
         return false;
 
     masm.tagValue(JSVAL_TYPE_OBJECT, ReturnReg, R0);
     frame.popn(2);
     frame.push(R0);
     return true;
 }
 
+typedef bool (*TrySkipAwaitFn)(JSContext*, HandleValue, MutableHandleValue);
+static const VMFunction TrySkipAwaitInfo = FunctionInfo<TrySkipAwaitFn>(jit::TrySkipAwait, "TrySkipAwait");
+
+bool
+BaselineCompiler::emit_JSOP_TRYSKIPAWAIT()
+{
+    frame.syncStack(0);
+    masm.loadValue(frame.addressOfStackValue(frame.peek(-1)), R0);
+
+    prepareVMCall();
+    pushArg(R0);
+
+    if (!callVM(TrySkipAwaitInfo))
+        return false;
+
+    Label cannotSkip, done;
+    masm.branchTestMagicValue(Assembler::Equal, R0, JS_CANNOT_SKIP_AWAIT, &cannotSkip);
+    masm.moveValue(BooleanValue(true), R1);
+    masm.jump(&done);
+
+    masm.bind(&cannotSkip);
+    masm.loadValue(frame.addressOfStackValue(frame.peek(-1)), R0);
+    masm.moveValue(BooleanValue(false), R1);
+
+    masm.bind(&done);
+
+    frame.pop();
+    frame.push(R0);
+    frame.push(R1);
+    return true;
+}
+
 typedef bool (*ThrowObjectCoercibleFn)(JSContext*, HandleValue);
 static const VMFunction ThrowObjectCoercibleInfo =
     FunctionInfo<ThrowObjectCoercibleFn>(ThrowObjectCoercible, "ThrowObjectCoercible");
 
 bool
 BaselineCompiler::emit_JSOP_CHECKOBJCOERCIBLE()
 {
     frame.syncStack(0);
--- a/js/src/jit/BaselineCompiler.h
+++ b/js/src/jit/BaselineCompiler.h
@@ -212,16 +212,17 @@ namespace jit {
     _(JSOP_MOREITER)           \
     _(JSOP_ISNOITER)           \
     _(JSOP_ENDITER)            \
     _(JSOP_ISGENCLOSING)       \
     _(JSOP_GENERATOR)          \
     _(JSOP_INITIALYIELD)       \
     _(JSOP_YIELD)              \
     _(JSOP_AWAIT)              \
+    _(JSOP_TRYSKIPAWAIT)       \
     _(JSOP_DEBUGAFTERYIELD)    \
     _(JSOP_FINALYIELDRVAL)     \
     _(JSOP_RESUME)             \
     _(JSOP_CALLEE)             \
     _(JSOP_SUPERBASE)          \
     _(JSOP_SUPERFUN)           \
     _(JSOP_GETRVAL)            \
     _(JSOP_SETRVAL)            \
--- a/js/src/jit/IonBuilder.cpp
+++ b/js/src/jit/IonBuilder.cpp
@@ -2448,16 +2448,17 @@ IonBuilder::inspectOpcode(JSOp op)
       case JSOP_THROWING:
       case JSOP_ISGENCLOSING:
       case JSOP_INITIALYIELD:
       case JSOP_YIELD:
       case JSOP_FINALYIELDRVAL:
       case JSOP_RESUME:
       case JSOP_DEBUGAFTERYIELD:
       case JSOP_AWAIT:
+      case JSOP_TRYSKIPAWAIT:
       case JSOP_GENERATOR:
 
       // Misc
       case JSOP_DELNAME:
       case JSOP_FINALLY:
       case JSOP_GETRVAL:
       case JSOP_GOSUB:
       case JSOP_RETSUB:
@@ -2469,17 +2470,16 @@ IonBuilder::inspectOpcode(JSOp op)
         break;
 
       case JSOP_FORCEINTERPRETER:
         // Intentionally not implemented.
         break;
 
       case JSOP_UNUSED126:
       case JSOP_UNUSED206:
-      case JSOP_UNUSED223:
       case JSOP_LIMIT:
         break;
     }
 
     // Track a simpler message, since the actionable abort message is a
     // static string, and the internal opcode name isn't an actionable
     // thing anyways.
     trackActionableAbort("Unsupported bytecode");
--- a/js/src/jit/VMFunctions.cpp
+++ b/js/src/jit/VMFunctions.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * 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/. */
 
 #include "jit/VMFunctions.h"
 
+#include "builtin/Promise.h"
 #include "builtin/TypedObject.h"
 #include "frontend/BytecodeCompiler.h"
 #include "jit/arm/Simulator-arm.h"
 #include "jit/BaselineIC.h"
 #include "jit/JitFrames.h"
 #include "jit/JitRealm.h"
 #include "jit/mips32/Simulator-mips32.h"
 #include "jit/mips64/Simulator-mips64.h"
@@ -1946,10 +1947,23 @@ DoConcatStringObject(JSContext* cx, Hand
     return true;
 }
 
 typedef bool (*DoConcatStringObjectFn)(JSContext*, HandleValue, HandleValue,
                                        MutableHandleValue);
 const VMFunction DoConcatStringObjectInfo =
     FunctionInfo<DoConcatStringObjectFn>(DoConcatStringObject, "DoConcatStringObject", TailCall, PopValues(2));
 
+MOZ_MUST_USE bool
+TrySkipAwait(JSContext* cx, HandleValue val, MutableHandleValue resolved)
+{
+    bool canSkip;
+    if (!TrySkipAwait(cx, val, &canSkip, resolved))
+        return false;
+
+    if (!canSkip)
+        resolved.setMagic(JS_CANNOT_SKIP_AWAIT);
+
+    return true;
+}
+
 } // namespace jit
 } // namespace js
--- a/js/src/jit/VMFunctions.h
+++ b/js/src/jit/VMFunctions.h
@@ -952,16 +952,23 @@ void
 CloseIteratorFromIon(JSContext* cx, JSObject* obj);
 
 extern const VMFunction SetObjectElementInfo;
 
 bool
 DoConcatStringObject(JSContext* cx, HandleValue lhs, HandleValue rhs,
                      MutableHandleValue res);
 
+// Wrapper for js::TrySkipAwait.
+// If the await operation can be skipped and the resolution value for `val` can
+// be acquired, stored the resolved value to `resolved`.  Otherwise, stores
+// the JS_CANNOT_SKIP_AWAIT magic value to `resolved`.
+MOZ_MUST_USE bool
+TrySkipAwait(JSContext* cx, HandleValue val, MutableHandleValue resolved);
+
 // This is the tailcall version of DoConcatStringObject
 extern const VMFunction DoConcatStringObjectInfo;
 
 extern const VMFunction StringsEqualInfo;
 extern const VMFunction StringsNotEqualInfo;
 
 } // namespace jit
 } // namespace js
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4990,16 +4990,28 @@ JS::SetEnqueuePromiseJobCallback(JSConte
 extern JS_PUBLIC_API(void)
 JS::SetPromiseRejectionTrackerCallback(JSContext* cx, JSPromiseRejectionTrackerCallback callback,
                                        void* data /* = nullptr */)
 {
     cx->promiseRejectionTrackerCallback = callback;
     cx->promiseRejectionTrackerCallbackData = data;
 }
 
+extern JS_PUBLIC_API(void)
+JS::JobQueueIsEmpty(JSContext* cx)
+{
+    cx->canSkipEnqueuingJobs = true;
+}
+
+extern JS_PUBLIC_API(void)
+JS::JobQueueMayNotBeEmpty(JSContext* cx)
+{
+    cx->canSkipEnqueuingJobs = false;
+}
+
 JS_PUBLIC_API(JSObject*)
 JS::NewPromiseObject(JSContext* cx, HandleObject executor, HandleObject proto /* = nullptr */)
 {
     MOZ_ASSERT(!cx->zone()->isAtomsZone());
     AssertHeapIsIdle();
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, executor, proto);
 
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -4056,16 +4056,44 @@ SetEnqueuePromiseJobCallback(JSContext* 
  * a rejection handler, and when a Promise that was previously rejected
  * without a handler gets a handler attached.
  */
 extern JS_PUBLIC_API(void)
 SetPromiseRejectionTrackerCallback(JSContext* cx, JSPromiseRejectionTrackerCallback callback,
                                    void* data = nullptr);
 
 /**
+ * Inform the runtime that the job queue is empty and the embedding is going to
+ * execute its last promise job. The runtime may now choose to skip creating
+ * promise jobs for asynchronous execution and instead continue execution
+ * synchronously. More specifically, this optimization is used to skip the
+ * standard job queuing behavior for `await` operations in async functions.
+ *
+ * This function may be called before executing the last job in the job queue.
+ * When it was called, JobQueueMayNotBeEmpty must be called in order to restore
+ * the default job queuing behavior before the embedding enqueues its next job
+ * into the job queue.
+ */
+extern JS_PUBLIC_API(void)
+JobQueueIsEmpty(JSContext* cx);
+
+/**
+ * Inform the runtime that job queue is no longer empty. The runtime can now no
+ * longer skip creating promise jobs for asynchronous execution, because
+ * pending jobs in the job queue must be executed first to preserve the FIFO
+ * (first in - first out) property of the queue. This effectively undoes
+ * JobQueueIsEmpty and re-enables the standard job queuing behavior.
+ *
+ * This function must be called whenever enqueuing a job to the job queue when
+ * JobQueueIsEmpty was called previously.
+ */
+extern JS_PUBLIC_API(void)
+JobQueueMayNotBeEmpty(JSContext* cx);
+
+/**
  * Returns a new instance of the Promise builtin class in the current
  * compartment, with the right slot layout.
  *
  * The `executor` can be a `nullptr`. In that case, the only way to resolve or
  * reject the returned promise is via the `JS::ResolvePromise` and
  * `JS::RejectPromise` JSAPI functions.
  *
  * If a `proto` is passed, that gets set as the instance's [[Prototype]]
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -19,16 +19,17 @@
 #include <string.h>
 
 #include "jslibmath.h"
 #include "jsnum.h"
 
 #include "builtin/Array.h"
 #include "builtin/Eval.h"
 #include "builtin/ModuleObject.h"
+#include "builtin/Promise.h"
 #include "builtin/String.h"
 #include "jit/AtomicOperations.h"
 #include "jit/BaselineJIT.h"
 #include "jit/Ion.h"
 #include "jit/IonAnalysis.h"
 #include "jit/Jit.h"
 #include "util/StringBuffer.h"
 #include "vm/AsyncFunction.h"
@@ -2142,17 +2143,16 @@ CASE(EnableInterruptsPseudoOpcode)
 }
 
 /* Various 1-byte no-ops. */
 CASE(JSOP_NOP)
 CASE(JSOP_NOP_DESTRUCTURING)
 CASE(JSOP_TRY_DESTRUCTURING_ITERCLOSE)
 CASE(JSOP_UNUSED126)
 CASE(JSOP_UNUSED206)
-CASE(JSOP_UNUSED223)
 CASE(JSOP_CONDSWITCH)
 {
     MOZ_ASSERT(CodeSpec[*REGS.pc].length == 1);
     ADVANCE_AND_DISPATCH(1);
 }
 
 CASE(JSOP_TRY)
 CASE(JSOP_JUMPTARGET)
@@ -3823,16 +3823,34 @@ CASE(JSOP_TOASYNCITER)
     if (!asyncIter)
         goto error;
 
     REGS.sp--;
     REGS.sp[-1].setObject(*asyncIter);
 }
 END_CASE(JSOP_TOASYNCITER)
 
+CASE(JSOP_TRYSKIPAWAIT)
+{
+    ReservedRooted<Value> val(&rootValue0, REGS.sp[-1]);
+    ReservedRooted<Value> resolved(&rootValue1);
+    bool canSkip;
+
+    if (!TrySkipAwait(cx, val, &canSkip, &resolved))
+        goto error;
+
+    if (canSkip) {
+        REGS.sp[-1] = resolved;
+        PUSH_BOOLEAN(true);
+    } else {
+        PUSH_BOOLEAN(false);
+    }
+}
+END_CASE(JSOP_TRYSKIPAWAIT)
+
 CASE(JSOP_SETFUNNAME)
 {
     MOZ_ASSERT(REGS.stackDepth() >= 2);
     FunctionPrefixKind prefixKind = FunctionPrefixKind(GET_UINT8(REGS.pc));
     ReservedRooted<Value> name(&rootValue0, REGS.sp[-1]);
     ReservedRooted<JSFunction*> fun(&rootFunction0, &REGS.sp[-2].toObject().as<JSFunction>());
     if (!SetFunctionNameIfNoOwnName(cx, fun, name, prefixKind))
         goto error;
--- a/js/src/vm/JSContext.cpp
+++ b/js/src/vm/JSContext.cpp
@@ -1040,16 +1040,17 @@ JSContext::recoverFromOutOfMemory()
 }
 
 static bool
 InternalEnqueuePromiseJobCallback(JSContext* cx, JS::HandleObject job,
                                   JS::HandleObject allocationSite,
                                   JS::HandleObject incumbentGlobal, void* data)
 {
     MOZ_ASSERT(job);
+    JS::JobQueueMayNotBeEmpty(cx);
     if (!cx->jobQueue->append(job)) {
         ReportOutOfMemory(cx);
         return false;
     }
     return true;
 }
 
 namespace {
@@ -1093,16 +1094,17 @@ js::UseInternalJobQueues(JSContext* cx)
 
     return true;
 }
 
 JS_FRIEND_API(bool)
 js::EnqueueJob(JSContext* cx, JS::HandleObject job)
 {
     MOZ_ASSERT(cx->jobQueue);
+    JS::JobQueueMayNotBeEmpty(cx);
     if (!cx->jobQueue->append(job)) {
         ReportOutOfMemory(cx);
         return false;
     }
 
     return true;
 }
 
@@ -1148,16 +1150,22 @@ js::RunJobs(JSContext* cx)
             // It's possible that queue draining was interrupted prematurely,
             // leaving the queue partly processed. In that case, slots for
             // already-executed entries will contain nullptrs, which we should
             // just skip.
             if (!job)
                 continue;
 
             cx->jobQueue->get()[i] = nullptr;
+
+            // If the next job is the last job in the job queue, allow
+            // skipping the standard job queuing behavior.
+            if (i == cx->jobQueue->length() - 1)
+                JS::JobQueueIsEmpty(cx);
+
             AutoRealm ar(cx, job);
             {
                 if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval)) {
                     // Nothing we can do about uncatchable exceptions.
                     if (!cx->isExceptionPending())
                         continue;
                     RootedValue exn(cx);
                     if (cx->getPendingException(&exn)) {
@@ -1291,16 +1299,17 @@ JSContext::JSContext(JSRuntime* runtime,
     jitStackLimit(UINTPTR_MAX),
     jitStackLimitNoInterrupt(UINTPTR_MAX),
     getIncumbentGlobalCallback(nullptr),
     enqueuePromiseJobCallback(nullptr),
     enqueuePromiseJobCallbackData(nullptr),
     jobQueue(nullptr),
     drainingJobQueue(false),
     stopDrainingJobQueue(false),
+    canSkipEnqueuingJobs(false),
     promiseRejectionTrackerCallback(nullptr),
     promiseRejectionTrackerCallbackData(nullptr)
 {
     MOZ_ASSERT(static_cast<JS::RootingContext*>(this) ==
                JS::RootingContext::get(this));
 
     MOZ_ASSERT(!TlsContext.get());
     TlsContext.set(this);
--- a/js/src/vm/JSContext.h
+++ b/js/src/vm/JSContext.h
@@ -914,16 +914,17 @@ struct JSContext : public JS::RootingCon
     js::ThreadData<void*> enqueuePromiseJobCallbackData;
 
     // Queue of pending jobs as described in ES2016 section 8.4.
     // Only used if internal job queue handling was activated using
     // `js::UseInternalJobQueues`.
     js::ThreadData<JS::PersistentRooted<js::JobQueue>*> jobQueue;
     js::ThreadData<bool> drainingJobQueue;
     js::ThreadData<bool> stopDrainingJobQueue;
+    js::ThreadData<bool> canSkipEnqueuingJobs;
 
     js::ThreadData<JSPromiseRejectionTrackerCallback> promiseRejectionTrackerCallback;
     js::ThreadData<void*> promiseRejectionTrackerCallbackData;
 
     JSObject* getIncumbentGlobal(JSContext* cx);
     bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job, js::HandleObject promise,
                            js::HandleObject incumbentGlobal);
     void addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
--- a/js/src/vm/Opcodes.h
+++ b/js/src/vm/Opcodes.h
@@ -2263,17 +2263,28 @@ 1234567890123456789012345678901234567890
     /*
      * NOP opcode to hint to IonBuilder that the value on top of the stack is
      * the (likely string) key in a for-in loop.
      *   Category: Other
      *   Operands:
      *   Stack: val => val
      */ \
     macro(JSOP_ITERNEXT,      222, "iternext",   NULL,  1,  1,  1,  JOF_BYTE) \
-    macro(JSOP_UNUSED223,     223, "unused223",  NULL,  1,  0,  0,  JOF_BYTE) \
+    /*
+     * Pops the top of stack value as 'value', checks if the await for 'value'
+     * can be skipped.
+     * If the await operation can be skipped and the resolution value for
+     * 'value' can be acquired, pushes the resolution value and 'true' onto the
+     * stack.  Otherwise, pushes 'value' and 'false' on the stack.
+     *   Category: Statements
+     *   Type: Function
+     *   Operands:
+     *   Stack: value => value_or_resolved, canskip
+     */ \
+    macro(JSOP_TRYSKIPAWAIT,  223,"tryskipawait",  NULL,  1,  1,  2,  JOF_BYTE) \
     \
     /*
      * Creates rest parameter array for current function call, and pushes it
      * onto the stack.
      *   Category: Variables and Scopes
      *   Type: Arguments
      *   Operands:
      *   Stack: => rest
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -19,16 +19,17 @@
 #include "mozilla/DebuggerOnGCRunnable.h"
 #include "mozilla/dom/DOMJSClass.h"
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/ProfileTimelineMarkerBinding.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/PromiseDebugging.h"
 #include "mozilla/dom/ScriptSettings.h"
+#include "jsapi.h"
 #include "js/Debug.h"
 #include "js/GCAPI.h"
 #include "js/Utility.h"
 #include "nsContentUtils.h"
 #include "nsCycleCollectionNoteRootCallback.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsCycleCollector.h"
 #include "nsDOMJSUtils.h"
@@ -482,16 +483,17 @@ void
 CycleCollectedJSContext::DispatchToMicroTask(
     already_AddRefed<MicroTaskRunnable> aRunnable)
 {
   RefPtr<MicroTaskRunnable> runnable(aRunnable);
 
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(runnable);
 
+  JS::JobQueueMayNotBeEmpty(Context());
   mPendingMicroTaskRunnables.push(runnable.forget());
 }
 
 class AsyncMutationHandler final : public mozilla::Runnable
 {
 public:
   AsyncMutationHandler() : mozilla::Runnable("AsyncMutationHandler") {}
 
@@ -552,18 +554,23 @@ CycleCollectedJSContext::PerformMicroTas
       break;
     }
 
     if (runnable->Suppressed()) {
       // Microtasks in worker shall never be suppressed.
       // Otherwise, mPendingMicroTaskRunnables will be replaced later with
       // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly.
       MOZ_ASSERT(NS_IsMainThread());
+      JS::JobQueueMayNotBeEmpty(Context());
       suppressed.push(runnable);
     } else {
+      if (mPendingMicroTaskRunnables.empty() &&
+          mDebuggerMicroTaskQueue.empty() && suppressed.empty()) {
+        JS::JobQueueIsEmpty(Context());
+      }
       didProcess = true;
       runnable->Run(aso);
     }
   }
 
   // Put back the suppressed microtasks so that they will be run later.
   // Note, it is possible that we end up keeping these suppressed tasks around
   // for some time, but no longer than spinning the event loop nestedly
@@ -592,14 +599,18 @@ CycleCollectedJSContext::PerformDebugger
       break;
     }
 
     RefPtr<MicroTaskRunnable> runnable = microtaskQueue->front().forget();
     MOZ_ASSERT(runnable);
 
     // This function can re-enter, so we remove the element before calling.
     microtaskQueue->pop();
+
+    if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
+      JS::JobQueueIsEmpty(Context());
+    }
     runnable->Run(aso);
   }
 
   AfterProcessMicrotasks();
 }
 } // namespace mozilla