From 60fdfb99b9a61eb67b09802928f6eeb282eeaf71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 22 Aug 2026 04:04:26 +0200 Subject: [PATCH] perf(runtime): eliminate redundant asyncpipe microtask work --- changelog.d/8409-redundant-microtask-wakes.md | 10 ++ crates/perry-runtime/src/event_pump.rs | 56 ++++++++ .../perry-runtime/src/promise/assimilate.rs | 6 +- .../perry-runtime/src/promise/async_step.rs | 4 +- .../perry-runtime/src/promise/combinators.rs | 47 +++++++ .../perry-runtime/src/promise/microtasks.rs | 64 ++++++--- crates/perry-runtime/src/promise/mod.rs | 5 +- .../src/promise/spec_combinators.rs | 127 ++++++++++++------ crates/perry-runtime/src/promise/then.rs | 8 +- 9 files changed, 257 insertions(+), 70 deletions(-) create mode 100644 changelog.d/8409-redundant-microtask-wakes.md diff --git a/changelog.d/8409-redundant-microtask-wakes.md b/changelog.d/8409-redundant-microtask-wakes.md new file mode 100644 index 0000000000..61e79b5524 --- /dev/null +++ b/changelog.d/8409-redundant-microtask-wakes.md @@ -0,0 +1,10 @@ +## Suppress redundant promise-drain wakeups + +Promise and `queueMicrotask` work queued by the main thread while it is already +draining microtasks no longer signals the event loop. Cross-thread producers, +timer callbacks, and rejection processing retain the normal wake path. + +`Promise.all` over plain native promises also uses compact settlement records +instead of allocating a closure and `AlreadyCalled` array for every element; +observable constructor, resolver, `then`, and lifecycle-hook paths still use +the spec dispatch. diff --git a/crates/perry-runtime/src/event_pump.rs b/crates/perry-runtime/src/event_pump.rs index 202d12da42..6495dd0208 100644 --- a/crates/perry-runtime/src/event_pump.rs +++ b/crates/perry-runtime/src/event_pump.rs @@ -231,6 +231,7 @@ static NOTIFIED: AtomicBool = AtomicBool::new(false); static WAITER_COUNT: AtomicI64 = AtomicI64::new(0); pub static PROFILE_NOTIFY_COUNT: AtomicI64 = AtomicI64::new(0); pub static PROFILE_NOTIFY_DURING_DRAIN_COUNT: AtomicI64 = AtomicI64::new(0); +pub static PROFILE_NOTIFY_DRAIN_SUPPRESSED_COUNT: AtomicI64 = AtomicI64::new(0); pub static PROFILE_WAIT_COUNT: AtomicI64 = AtomicI64::new(0); pub static PROFILE_WAIT_FAST_COUNT: AtomicI64 = AtomicI64::new(0); pub static PROFILE_WAIT_ZERO_COUNT: AtomicI64 = AtomicI64::new(0); @@ -367,6 +368,23 @@ pub extern "C" fn js_notify_main_thread() { PUMP.cvar.notify_one(); } +/// Notify after queueing promise/microtask work, unless this same thread is +/// already draining that queue to quiescence. +/// +/// Cross-thread producers see a thread-local drain depth of zero and retain +/// the full wake path. Timer and rejection phases are outside the job-drain +/// scope as well, because work they queue is consumed by a later pump turn. +#[inline(always)] +pub(crate) fn js_notify_promise_progress() { + if crate::promise::microtasks::microtask_job_drain_active() { + if crate::promise::mt_profile_enabled() { + PROFILE_NOTIFY_DRAIN_SUPPRESSED_COUNT.fetch_add(1, Ordering::Relaxed); + } + return; + } + js_notify_main_thread(); +} + // ============================================================================ // #1088 — Unified Event Loop FFI facade for host embedding. // @@ -673,6 +691,44 @@ mod tests { /// timer state — there is no per-thread injection point.) static SERIAL: StdMutex<()> = StdMutex::new(()); + /// A promise settled while promise jobs are already draining must not + /// leave a redundant event-loop wake behind. The active runner consumes + /// the propagated job before returning; outside that scope the same + /// helper must still publish a wake. + #[test] + fn promise_progress_notify_is_suppressed_during_job_drain() { + let _g = SERIAL.lock().unwrap(); + + NOTIFIED.store(false, Ordering::Release); + js_notify_promise_progress(); + assert!(NOTIFIED.swap(false, Ordering::Acquire)); + + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let mut observed_clean_drain = false; + for _ in 0..32 { + let promise = crate::promise::js_promise_new(); + crate::promise::js_promise_then(promise, std::ptr::null(), std::ptr::null()); + crate::promise::js_promise_resolve(promise, undefined); + + // Consume the required outside-drain notify, then run only the + // promise-job phase so unrelated global timers cannot wake us. + NOTIFIED.store(false, Ordering::Release); + crate::promise::microtasks::js_promise_run_microtasks_checkpoint(); + if !NOTIFIED.load(Ordering::Acquire) { + observed_clean_drain = true; + break; + } + // A parallel runtime test may use the process-global notifier; + // retry until we observe an uncontended drain. + NOTIFIED.store(false, Ordering::Release); + } + + assert!( + observed_clean_drain, + "promise propagation repeatedly left an event-loop wake behind" + ); + } + struct ForcedZeroBudgetGuard; impl ForcedZeroBudgetGuard { diff --git a/crates/perry-runtime/src/promise/assimilate.rs b/crates/perry-runtime/src/promise/assimilate.rs index 7b8b8d563d..6e1a1bbd72 100644 --- a/crates/perry-runtime/src/promise/assimilate.rs +++ b/crates/perry-runtime/src/promise/assimilate.rs @@ -151,7 +151,7 @@ pub(super) fn enqueue_thenable_job(promise: *mut Promise, thenable: f64, then_ac trigger_async_id: ids.trigger_async_id, }); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); } pub(crate) fn promise_resolve_assimilating(promise: *mut Promise, value: f64) { @@ -243,7 +243,7 @@ pub(super) fn enqueue_native_adoption_job(outer: *mut Promise, inner: *mut Promi trigger_async_id: ids.trigger_async_id, }); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); } /// Job body — the intrinsic-`then` invocation of the adoption job. For a @@ -280,7 +280,7 @@ extern "C" fn native_promise_adoption_job(closure: *const crate::closure::Closur std::ptr::null_mut(), )); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); } None => { super::then::js_promise_resolve_with_promise(outer, inner); diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 4b2655f5c1..5175cd8295 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -270,7 +270,7 @@ pub extern "C" fn js_promise_resolved_then( capture_context(), )); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); // Suppress the rejection-handler bookkeeping: it would only // matter if `value` were a Promise, which it isn't here. let _ = on_rejected; @@ -594,7 +594,7 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * trap.box_activation, )); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); next } diff --git a/crates/perry-runtime/src/promise/combinators.rs b/crates/perry-runtime/src/promise/combinators.rs index 7c5456e5d5..0c5dde2a30 100644 --- a/crates/perry-runtime/src/promise/combinators.rs +++ b/crates/perry-runtime/src/promise/combinators.rs @@ -60,6 +60,53 @@ pub(super) fn promise_all_settle(state: PromiseAllState, value: f64, is_fulfille } } +/// Attach the allocation-free `Promise.all` state used when every observable +/// combinator hook has been ruled out. Settled inputs become ordinary +/// microtasks; pending inputs park the state in the same GC-scanned keyed table +/// that `js_promise_resolve` drains on settlement. +pub(super) fn attach_promise_all_state(promise: *mut Promise, state: PromiseAllState) { + if promise.is_null() { + return; + } + mark_rejection_handled(promise); + let mut queued = false; + unsafe { + match (*promise).state { + PromiseState::Fulfilled => { + TASK_QUEUE.with(|q| { + q.borrow_mut().push_back(Task::PromiseAll( + state, + (*promise).value, + true, + context_for_promise(promise), + )); + }); + queued = true; + } + PromiseState::Rejected => { + TASK_QUEUE.with(|q| { + q.borrow_mut().push_back(Task::PromiseAll( + state, + (*promise).reason, + false, + context_for_promise(promise), + )); + }); + queued = true; + } + PromiseState::Pending => { + PROMISE_ALL_STATES.with(|states| { + states.borrow_mut().push(promise as usize, state); + }); + set_promise_callback_context(promise); + } + } + } + if queued { + crate::event_pump::js_notify_promise_progress(); + } +} + pub(super) fn scan_promise_all_states_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { PROMISE_ALL_STATES.with(|states| { let mut states = states.borrow_mut(); diff --git a/crates/perry-runtime/src/promise/microtasks.rs b/crates/perry-runtime/src/promise/microtasks.rs index b4ec58bd7b..2cc00ef346 100644 --- a/crates/perry-runtime/src/promise/microtasks.rs +++ b/crates/perry-runtime/src/promise/microtasks.rs @@ -4,6 +4,12 @@ use super::*; +#[derive(Clone, Copy)] +struct MicrotaskRunDepths { + pump: u32, + jobs: u32, +} + crate::perry_thread_local! { /// Promise currently being dispatched by the microtask runner after its /// task has been popped from TASK_QUEUE. While user callbacks run this is @@ -22,14 +28,21 @@ crate::perry_thread_local! { pub(super) static CURRENT_MICROTASK_NEXT: std::cell::Cell<*mut Promise> = const { std::cell::Cell::new(std::ptr::null_mut()) }; - /// Nesting depth for `js_promise_run_microtasks` on this thread. + /// Nesting depths for `js_promise_run_microtasks` on this thread. /// /// Await lowering can re-enter the microtask runner from inside a /// microtask or timer callback. Re-entrant drains may run promise jobs, /// but they must not recursively enter the timer queues: timers are /// macrotasks, and running them from a nested microtask checkpoint can /// build an unbounded stack of exception traps. - static MICROTASK_RUN_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; + /// `pump` covers the whole checkpoint. `jobs` covers only the part that is + /// about to drain (or is actively draining) promise/queueMicrotask jobs; + /// it deliberately ends before rejection processing and timer phases, as + /// work queued there needs to wake an immediately-following event-loop + /// wait. Keep both counters in this already-audited TLS holder. + static MICROTASK_RUN_DEPTH: std::cell::Cell = const { + std::cell::Cell::new(MicrotaskRunDepths { pump: 0, jobs: 0 }) + }; /// One-shot: the entry module is ESM and its evaluation checkpoint has /// not happened yet. Consumed by the first `run_microtasks` drain, which @@ -39,15 +52,22 @@ crate::perry_thread_local! { static ESM_EVAL_CHECKPOINT_PENDING: std::cell::Cell = const { std::cell::Cell::new(false) }; } -/// Whether this thread is already executing a microtask checkpoint. -/// -/// A same-thread enqueue during a checkpoint does not need to wake the event -/// loop: the runner drains the queue to quiescence before returning. Producers -/// on other threads observe their own depth (zero) and still take the normal -/// cross-thread wake path. +/// Whether this thread is already executing a microtask pump, including its +/// rejection and timer phases. Used by the event-path profiler. #[inline(always)] pub(crate) fn microtask_drain_active() -> bool { - MICROTASK_RUN_DEPTH.with(|depth| depth.get() != 0) + MICROTASK_RUN_DEPTH.with(|depth| depth.get().pump != 0) +} + +/// Whether a same-thread promise job enqueue will be consumed by the active +/// drain before it can return to the event loop. +/// +/// Producers on other threads observe their own depth (zero) and still take +/// the normal cross-thread wake path. The timer/rejection tail also observes +/// zero so work queued there wakes the next event-loop wait. +#[inline(always)] +pub(crate) fn microtask_job_drain_active() -> bool { + MICROTASK_RUN_DEPTH.with(|depth| depth.get().jobs != 0) } /// Called once from the compiled entry (before top-level statements) when the @@ -186,9 +206,12 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { bump(&MT_DRAIN_COUNT); let async_box_ref_depth = async_box_execution_ref_depth(); let reentrant = MICROTASK_RUN_DEPTH.with(|depth| { - let current = depth.get(); - depth.set(current.saturating_add(1)); - current > 0 + let mut current = depth.get(); + let reentrant = current.pump > 0; + current.pump = current.pump.saturating_add(1); + current.jobs = current.jobs.saturating_add(1); + depth.set(current); + reentrant }); let mut ran = 0; @@ -970,6 +993,15 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { } } + // Promise/queueMicrotask jobs are quiescent. Notifications after this + // point (rejection processing or timer callbacks) must remain observable: + // the generated event loop may wait before its next drain. + MICROTASK_RUN_DEPTH.with(|depth| { + let mut current = depth.get(); + current.jobs = current.jobs.saturating_sub(1); + depth.set(current); + }); + // #6077: the microtask checkpoint is over — the queue drained to empty. // This is where Node decides whether a rejection went unhandled // (`processTicksAndRejections` → `processPromiseRejections`), BEFORE the @@ -1019,7 +1051,7 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // compacting, O(survivors) young collection instead of the non-moving // alloc-point fallback. Gated (default off); additive. if crate::gc::gc_moving_safepoint_enabled() - && MICROTASK_RUN_DEPTH.with(|depth| depth.get()) == 1 + && MICROTASK_RUN_DEPTH.with(|depth| depth.get().pump) == 1 { crate::gc::gc_safepoint_moving_minor(); } @@ -1028,14 +1060,16 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // activation (principally direct runtime tests). Production async frames // publish at their own queued/running AsyncStep refcount reaching zero; // they do not wait for this global pump boundary. - if MICROTASK_RUN_DEPTH.with(|depth| depth.get()) == 1 + if MICROTASK_RUN_DEPTH.with(|depth| depth.get().pump) == 1 && TASK_QUEUE.with(|q| q.borrow().is_empty()) { crate::r#box::flush_released_boxes(); } MICROTASK_RUN_DEPTH.with(|depth| { - depth.set(depth.get().saturating_sub(1)); + let mut current = depth.get(); + current.pump = current.pump.saturating_sub(1); + depth.set(current); }); ran diff --git a/crates/perry-runtime/src/promise/mod.rs b/crates/perry-runtime/src/promise/mod.rs index 8598f01c06..8d10844efe 100644 --- a/crates/perry-runtime/src/promise/mod.rs +++ b/crates/perry-runtime/src/promise/mod.rs @@ -232,9 +232,10 @@ extern "C" fn mt_profile_atexit() { crate::timer::PROFILE_INTERVAL_TIMERS_FIRED.load(Ordering::Relaxed), ); eprintln!( - "[mt-profile] event_notify={{sent:{},during_drain:{}}} event_wait={{total:{},fast:{},zero:{},driver:{},condvar:{}}}", + "[mt-profile] event_notify={{sent:{},during_drain:{},drain_suppressed:{}}} event_wait={{total:{},fast:{},zero:{},driver:{},condvar:{}}}", crate::event_pump::PROFILE_NOTIFY_COUNT.load(Ordering::Relaxed), crate::event_pump::PROFILE_NOTIFY_DURING_DRAIN_COUNT.load(Ordering::Relaxed), + crate::event_pump::PROFILE_NOTIFY_DRAIN_SUPPRESSED_COUNT.load(Ordering::Relaxed), crate::event_pump::PROFILE_WAIT_COUNT.load(Ordering::Relaxed), crate::event_pump::PROFILE_WAIT_FAST_COUNT.load(Ordering::Relaxed), crate::event_pump::PROFILE_WAIT_ZERO_COUNT.load(Ordering::Relaxed), @@ -736,7 +737,7 @@ pub(crate) fn enqueue_queue_microtask(callback: i64) { trigger_async_id: ids.trigger_async_id, }); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); } #[derive(Default)] diff --git a/crates/perry-runtime/src/promise/spec_combinators.rs b/crates/perry-runtime/src/promise/spec_combinators.rs index be78c6431b..be6ba05c4b 100644 --- a/crates/perry-runtime/src/promise/spec_combinators.rs +++ b/crates/perry-runtime/src/promise/spec_combinators.rs @@ -574,12 +574,12 @@ fn build_settled_rejected(reason: f64) -> f64 { // Invoke(p, "then", «onFul, onRej») == js_promise_then(p, …) // (`native_call_method/primitive_methods.rs`'s intrinsic then arm) // -// with one deliberate difference: `js_promise_attach_handlers` replaces -// `js_promise_then`, i.e. the chained promise `Promise.prototype.then` would -// return is not allocated. `Promise.all` discards it, it is unreachable from -// user code, and its only externally visible effects are the -// `v8.promiseHooks` / `async_hooks` lifecycle callbacks — which the guard -// excludes by requiring both hook sets to be inactive. +// with one deliberate difference: the intrinsic-only arm attaches a compact +// `PromiseAllState` rather than allocating the resolve-element closure, its +// `[[AlreadyCalled]]` record, and the chained promise that +// `Promise.prototype.then` would return. A native Promise settles once, and +// `Promise.all` discards the chained promise, so none is observable after the +// guard excludes `v8.promiseHooks` / `async_hooks` lifecycle callbacks. // A test-only tally of how many elements actually took the fast arm. A test // that only asserts "nothing broke" cannot tell a working fast path from a @@ -684,17 +684,24 @@ fn perform( let elements_h = scope.root_nanbox_f64(boxed_ptr(elements)); let ctor_h = scope.root_nanbox_f64(c); let resolve_fn_h = scope.root_nanbox_f64(promise_resolve); + let cap_promise_h = scope.root_nanbox_f64(cap.promise); let cap_resolve_h = scope.root_nanbox_f64(cap.resolve); let cap_reject_h = scope.root_nanbox_f64(cap.reject); let count = unsafe { (*elements).length }; // Shared state: remaining-count (init 1, spec's remainingElementsCount). - let state = js_array_alloc(1); + // The intrinsic Promise.all state uses slot 1 as its rejection latch. + // Other combinators retain the single spec remaining-count slot. + let state_slots = if kind == CombinatorKind::All { 2 } else { 1 }; + let state = js_array_alloc(state_slots); unsafe { - (*state).length = 1; + (*state).length = state_slots; } js_array_set_f64(state, 0, 1.0); + if kind == CombinatorKind::All { + js_array_set_f64(state, 1, 0.0); + } // Rooted BEFORE the `values` allocation below, which can collect. let state_h = scope.root_nanbox_f64(boxed_ptr(state)); @@ -760,46 +767,46 @@ fn perform( match kind { CombinatorKind::All => { - let guard_h = iter.root_nanbox_f64(boxed_ptr(new_guard())); - let elem_h = iter.root_nanbox_f64(boxed_ptr(build_element_closure( - all_resolve_element_fn as *const u8, - unboxed_ptr(guard_h.get_nanbox_f64()), - i, - values_ptr(), - state_ptr(), - cap_resolve_h.get_nanbox_f64(), - cap_reject_h.get_nanbox_f64(), - ))); let state = state_ptr(); js_array_set_f64(state, 0, js_array_get_f64(state, 0) + 1.0); - // `attachable_native_promise` is re-tested here rather than - // above because `js_promise_resolved` may have RETURNED the - // element itself (promise identity), so the own-`then` / - // own-`constructor` expandos that would make `Invoke` observable - // belong to the resolved value, not to the raw element. - let attached = if element_fast - && !promise_lifecycle_observed() - && attachable_native_promise(next_promise_h.get_nanbox_f64()).is_some() - { - // Every address below is re-read from its handle here, AFTER - // the last thing that can allocate (`attachable_native_promise` - // interns the "then"/"constructor" keys), and nothing between - // these four lines allocates. - let p: *mut Promise = unboxed_ptr(next_promise_h.get_nanbox_f64()); - let on_fulfilled = as_closure_ptr(elem_h.get_nanbox_f64()); - let on_rejected = as_closure_ptr(cap_reject_h.get_nanbox_f64()); - match (on_fulfilled, on_rejected) { - (Some(f), Some(r)) => { - crate::promise::js_promise_attach_handlers(p, f, r); - true - } - _ => false, - } + + // `attachable_native_promise` is tested after + // `js_promise_resolved`: that call may return the element + // itself, so the expandos that make `Invoke` observable belong + // to the resolved promise rather than the raw input. + let direct = if element_fast && !promise_lifecycle_observed() { + attachable_native_promise(next_promise_h.get_nanbox_f64()) } else { - false + None }; - note_fast_arm_element(attached); - if !attached { + + if let Some(promise) = direct { + // Re-read every pointer from a live handle after the + // expandos probe above, the last operation that can + // allocate. The keyed table / Task queue becomes their GC + // root before this function does any more runtime work. + super::combinators::attach_promise_all_state( + promise, + super::combinators::PromiseAllState { + result_promise: unboxed_ptr(cap_promise_h.get_nanbox_f64()), + results_arr: values_ptr(), + state_arr: state_ptr(), + index: i, + }, + ); + note_fast_arm_element(true); + } else { + let guard_h = iter.root_nanbox_f64(boxed_ptr(new_guard())); + let elem_h = iter.root_nanbox_f64(boxed_ptr(build_element_closure( + all_resolve_element_fn as *const u8, + unboxed_ptr(guard_h.get_nanbox_f64()), + i, + values_ptr(), + state_ptr(), + cap_resolve_h.get_nanbox_f64(), + cap_reject_h.get_nanbox_f64(), + ))); + note_fast_arm_element(false); invoke_then( next_promise_h.get_nanbox_f64(), &[elem_h.get_nanbox_f64(), cap_reject_h.get_nanbox_f64()], @@ -1288,6 +1295,38 @@ mod fast_arm_tests { assert_eq!(taken(), 2); } + /// A single pending input may feed more than one intrinsic Promise.all. + /// The compact fast arm must append both states to the keyed table rather + /// than overwrite the first registration. + #[test] + fn fast_arm_tracks_shared_pending_inputs() { + reset(); + let shared = crate::promise::js_promise_new(); + let shared_value = boxed_ptr(shared); + let first = run_all(array_of(&[shared_value])); + let second = run_all(array_of(&[shared_value])); + + unsafe { + assert_eq!((*first).state, PromiseState::Pending); + assert_eq!((*second).state, PromiseState::Pending); + } + + crate::promise::js_promise_resolve(shared, 42.0); + js_promise_run_microtasks(); + + unsafe { + assert_eq!((*first).state, PromiseState::Fulfilled); + assert_eq!((*second).state, PromiseState::Fulfilled); + let first_values = crate::value::js_nanbox_get_pointer((*first).value) + as *const crate::array::ArrayHeader; + let second_values = crate::value::js_nanbox_get_pointer((*second).value) + as *const crate::array::ArrayHeader; + assert_eq!(js_array_get_f64(first_values, 0), 42.0); + assert_eq!(js_array_get_f64(second_values, 0), 42.0); + } + assert_eq!(taken(), 2); + } + extern "C" fn own_then_fn( closure: *const crate::closure::ClosureHeader, on_fulfilled: f64, diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index 804cc0add8..ba5c7fa6f0 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -241,7 +241,7 @@ pub extern "C" fn js_promise_resolve(promise: *mut Promise, value: f64) { // following `js_wait_for_event` sleep — otherwise it blocks for the // 1 s idle cap before the loop re-checks promise state. The notify // sets the flag so the immediately-following wait returns at once. - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); unsafe { crate::async_hooks::destroy_promise((*promise).async_id); } @@ -460,7 +460,7 @@ pub extern "C" fn js_promise_reject(promise: *mut Promise, reason: f64) { } } // Issue #84: see js_promise_resolve — same wake reasoning. - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); unsafe { crate::async_hooks::destroy_promise((*promise).async_id); } @@ -1742,7 +1742,7 @@ extern "C" fn finally_passthrough_fulfill( std::ptr::null_mut(), )); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); } f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -1768,7 +1768,7 @@ extern "C" fn finally_passthrough_reject( std::ptr::null_mut(), )); }); - crate::event_pump::js_notify_main_thread(); + crate::event_pump::js_notify_promise_progress(); } f64::from_bits(crate::value::TAG_UNDEFINED) }