Skip to content

feat(runtime): #789 — real async_hooks createHook lifecycle + asyncId tracking - #865

Merged
TheHypnoo merged 2 commits into
mainfrom
feat/issue-789-async-hooks-lifecycle
May 16, 2026
Merged

feat(runtime): #789 — real async_hooks createHook lifecycle + asyncId tracking#865
TheHypnoo merged 2 commits into
mainfrom
feat/issue-789-async-hooks-lifecycle

Conversation

@TheHypnoo

Copy link
Copy Markdown
Member

Summary

Closes #789. Implements the observability half of node:async_hookscreateHook callbacks, real executionAsyncId() / triggerAsyncId(), and the full AsyncResource surface — on top of the AsyncLocalStorage context propagation that landed in #788.

  • Process-wide hook registry with registration-order iteration and a zero-hook fast path (single relaxed atomic load + branch).
  • Monotonic asyncId allocator + per-thread execution/trigger id stack.
  • init / before / after / destroy / promiseResolve emitted at Promise construction & settlement, timer scheduling & firing, AsyncResource construction, runInAsyncScope, and emitDestroy.
  • Eager destroy for settled Promises, fired timers, and explicit emitDestroy(); GC-driven destroy for dropped/unsettled Promises via a new finalizer queue in perry-runtime/src/gc.rs.
  • AsyncResource#bind(fn) synthesizes a native closure trampoline that replays through runInAsyncScope, so bound callbacks fire before / after with the bound resource's async id active.
  • V8 fallback (perry-jsruntime/src/modules.rs) mirrors the same surface for code running through the embedded runtime.
  • Drops the compile-time [perry] note: shim warning for node:async_hooks — it's no longer accurate once both AsyncLocalStorage: real async-context tracking across await / microtasks / timers #788 and this issue land.

Test plan

  • cargo build --release -p perry-runtime -p perry-stdlib -p perry
  • test-files/test_parity_async_hooks.ts byte-for-byte parity vs node --experimental-strip-types
  • Ad-hoc AsyncResource#bind exercise — bound callback observes the bound resource's executionAsyncId() and fires before / after
  • CI parity sweep

TheHypnoo added 2 commits May 16, 2026 15:50
… tracking

- New crates/perry-runtime/src/async_hooks.rs: process-wide hook registry,
  monotonic asyncId allocator, per-thread execution/trigger id stack, eager
  destroy queue, GC-driven destroy for dropped/unsettled Promises. Zero-hook
  hot path is a single relaxed atomic load + branch.
- Emission sites wired in promise.rs (init/promiseResolve/destroy), timer.rs
  (init/before/after/destroy for setTimeout/setImmediate), and async_hooks
  itself for AsyncResource construction / runInAsyncScope / emitDestroy.
- AsyncResource#bind synthesizes a real native closure trampoline (captures =
  handle + callback, registered as rest-arity 0) that replays the call
  through runInAsyncScope so before/after fire with the bound resource's id.
- V8 fallback module (perry-jsruntime/src/modules.rs) implements the same
  surface in JS so the embedded runtime stays in sync.
- gc.rs gains a finalizer queue + async_hooks root scanner so leaked
  Promises still fire destroy on collection.
- Drops the compile-time `[perry] note:` shim warning for node:async_hooks
  now that #788 and this issue both land.

Verified byte-for-byte against `node --experimental-strip-types` for
test-files/test_parity_async_hooks.ts.
@TheHypnoo
TheHypnoo merged commit d90165c into main May 16, 2026
9 checks passed
@TheHypnoo
TheHypnoo deleted the feat/issue-789-async-hooks-lifecycle branch May 16, 2026 14:32
proggeramlug added a commit that referenced this pull request May 25, 2026
…guard exports (#789, #788, #1423)

The async-context core landed earlier (#852 ALS propagation, #865 async_hooks
foundation). This finishes the remaining gaps and makes it usable in release:

#789 — async_hooks lifecycle on promise/await:
  - emit_init boxed the `type` string with POINTER_TAG, so the init hook's
    `type` argument read back as `[object Object]` instead of `"PROMISE"`.
    Box it with STRING_TAG (new box_string helper).
  - the await continuation (Task::AsyncStep) didn't fire before/after or set
    the execution id, so executionAsyncId() stayed 0 across `await` and the
    after hook never fired. Bracket the step call with
    async_hooks::before/after using the result promise's async_id (captured
    as a plain value before the callback per #1663; no-op when async_hooks
    are inactive), mirroring the Task::Promise arm.

#1764 follow-up — #1762 added class-field / direct-call guard helpers in
  typed_feedback/guards.rs with no #[used] anchors, so the auto-optimize
  thin-LTO + strip build dead-stripped them, failing the link for any
  class-field program (incl. the async harness). Add typed fn-pointer
  retention anchors for the four new guards.

Verified: executionAsyncId now survives await, init reports "PROMISE", and
before/after both fire (byte-for-byte vs node). All four async tests pass
under both PERRY_NO_AUTO_OPTIMIZE and auto-optimize; removed their stale
known_failures entries.

Closes #789, #788, #1423.
proggeramlug added a commit that referenced this pull request May 25, 2026
…guard exports (#789, #788, #1423) (#1769)

The async-context core landed earlier (#852 ALS propagation, #865 async_hooks
foundation). This finishes the remaining gaps and makes it usable in release:

#789 — async_hooks lifecycle on promise/await:
  - emit_init boxed the `type` string with POINTER_TAG, so the init hook's
    `type` argument read back as `[object Object]` instead of `"PROMISE"`.
    Box it with STRING_TAG (new box_string helper).
  - the await continuation (Task::AsyncStep) didn't fire before/after or set
    the execution id, so executionAsyncId() stayed 0 across `await` and the
    after hook never fired. Bracket the step call with
    async_hooks::before/after using the result promise's async_id (captured
    as a plain value before the callback per #1663; no-op when async_hooks
    are inactive), mirroring the Task::Promise arm.

#1764 follow-up — #1762 added class-field / direct-call guard helpers in
  typed_feedback/guards.rs with no #[used] anchors, so the auto-optimize
  thin-LTO + strip build dead-stripped them, failing the link for any
  class-field program (incl. the async harness). Add typed fn-pointer
  retention anchors for the four new guards.

Verified: executionAsyncId now survives await, init reports "PROMISE", and
before/after both fire (byte-for-byte vs node). All four async tests pass
under both PERRY_NO_AUTO_OPTIMIZE and auto-optimize; removed their stale
known_failures entries.

Closes #789, #788, #1423.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

async_hooks: real createHook lifecycle + asyncId tracking

1 participant