Repository navigation
perf: use V8 native queueMicrotask instead of op - #35787
Merged
Merged
Conversation
V8 14.7+ ships a native `queueMicrotask` on the global object behind the `--enable-queue-microtask` flag. Enable it and have our `queueMicrotask` wrapper enqueue through the native builtin instead of the custom `op_queue_microtask` op, removing an op-boundary crossing on every call. The wrapper's `try/catch` -> `reportExceptionCallback` is preserved, so uncaught exceptions in microtask callbacks are still routed to Deno's error handling (not V8's default message listeners) and behavior is unchanged. V8 skips experimental globals while snapshotting, so the native function is only installed at runtime: grab it directly in a from-scratch runtime, and fall back to a lazy capture (temporarily unshadowing our own global property) when restoring from a snapshot. Closes #32968
…ener Instead of wrapping every queueMicrotask callback in a try/catch closure to route uncaught exceptions to reportExceptionCallback, register a V8 isolate message listener and dispatch from there. V8 reports a throwing queueMicrotask/enqueue_microtask callback via Runtime::kReportMessageFromMicrotask -> MessageHandler::ReportMessage, which invokes registered message listeners (and skips the default stderr print). This fires only for CallableTask microtask exceptions - promise rejections and TryCatch-caught exceptions don't reach it - so it's a precise replacement for the JS wrapper. The listener invokes the realm's JS report-exception callback (dispatching an error event etc.), falling back to dispatch_exception when none is set. It runs synchronously, right after the throwing microtask and before the next one, so ordering and async context are unchanged. The listener is registered on the runtime isolate only (not the snapshot isolate, whose serialized heap has no external reference for it). queueMicrotask now passes the callback straight to V8's native queueMicrotask with no per-call closure allocation. Uncaught microtask error stacks no longer include the internal ext:core/01_core.js frame. Bench (min per run, release, vs the wrapper approach): queueMicrotask x1000: ~11.7us -> ~9.3us (~20% faster) queueMicrotask single: ~77ns -> ~68ns (~12% faster) and ~2.6x over the op_queue_microtask baseline.
dispatch_handled_rejections invoked the "rejectionhandled" handler with a bare function.call and no TryCatch, unlike its siblings dispatch_rejections and drain_next_tick_and_macrotasks. A throwing handler left a pending exception on the isolate, which the newly added microtask message listener surfaced as an uncaught error (and which could also poison the next microtask checkpoint). Wrap the call in a TryCatch so a faulty rejectionhandled handler no longer leaks. This event only notifies that an earlier rejection was handled, so a throwing handler must not halt the event loop.
This reverts commit 3732baf.
…age listener" This reverts commit b645900.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #32968
V8 14.7+ ships a native
queueMicrotaskon the global object behind the--enable-queue-microtaskflag. This PR enables the flag and routesqueueMicrotaskthrough the native builtin instead of the customop_queue_microtaskop.op_queue_microtask(anofastop that just calledisolate.enqueue_microtask) is removed. OurqueueMicrotaskwrapperenqueues through V8's native builtin — no op-boundary crossing per call.
try/catchand routes uncaught exceptions throughreportExceptionCallback(the
errorevent /uncaughtException), so ordering and async context(
AsyncLocalStorage) are preserved.only installed at runtime. Internal extensions capture
queueMicrotaskfromthe primordials at snapshot time, so the wrapper is a stable reference for
them that lazily grabs the native builtin on first use.
Performance
Release build, Apple M-series, best-case (min) per run vs. the current
canary (op-based, same V8
14.9.207.2-rusty):op_queue_microtask)Net: ~2× faster on a full enqueue+drain roundtrip, ~3× lower per-call
enqueue cost.