Skip to content

fix(ext/node): reimplement setImmediate API#30328

Merged
bartlomieju merged 28 commits intodenoland:mainfrom
bartlomieju:set_immediate
Nov 28, 2025
Merged

fix(ext/node): reimplement setImmediate API#30328
bartlomieju merged 28 commits intodenoland:mainfrom
bartlomieju:set_immediate

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

@bartlomieju bartlomieju commented Aug 6, 2025

Closes #28016

tick.js:

import { setImmediate } from "node:timers";

let ticks = 0;
const startTime = performance.now();
const loop = () => {
  ticks++
  if (performance.now() - startTime > 1000) {
    console.log(ticks, "ticks per second");
    return;
  }
  setImmediate(loop);
}
loop();
# Node v24.8.0
$ node tick.js
81369 ticks per second

# Deno v3.0-rc.0
$ deno tick.js
825 ticks per second

# This PR
$ target/release/release tick.js
1271903 ticks per second

Running server:

Deno.serve((_req) => new Response("Hello world"));
# Deno v3.0.0-rc.0
$ deno -N server.ts &
$ wrk -d 10s --latency http://localhost:8000
Running 10s test @ http://localhost:8000
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    78.62us  151.14us   8.30ms   99.08%
    Req/Sec    62.76k     7.55k   68.62k    88.12%
  Latency Distribution
     50%   69.00us
     75%   81.00us
     90%   98.00us
     99%  208.00us
  1261160 requests in 10.10s, 180.41MB read
Requests/sec: 124869.81
Transfer/sec:     17.86MB

# this PR
$ target/release/deno -N server.ts &
wrk -d 10s --latency http://localhost:8000
Running 10s test @ http://localhost:8000
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    94.30us  422.58us  18.68ms   99.29%
    Req/Sec    61.56k     7.87k   68.58k    87.50%
  Latency Distribution
     50%   69.00us
     75%   82.00us
     90%  102.00us
     99%  320.00us
  1225432 requests in 10.01s, 175.30MB read
Requests/sec: 122478.35
Transfer/sec:     17.52MB

Comment thread t.js Outdated
@bartlomieju bartlomieju marked this pull request as draft August 6, 2025 07:47
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 18, 2025

Walkthrough

The PR adds a Node-like immediate system: introduces an Immediate class, ImmediateList linked-list queue, kRefed symbol, immediateQueue, and runImmediates in timers.mjs with op_immediate_* integrations. process.ts registers runImmediates via core.setImmediateCallback. timers.ts validates callbacks in setImmediate and tightens clearImmediate to mark destruction, update op counters, and remove entries from immediateQueue. ext/web/02_timers.js removes its setImmediate export. Cargo.toml bumps deno_core 0.370.0 → 0.371.0. Three timer tests were enabled.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant SetImm as setImmediate()
    participant Queue as immediateQueue (ImmediateList)
    participant Immediate as Immediate instance
    participant Bootstrap as Process Bootstrap
    participant Runner as runImmediates()
    participant Callback as User Callback
    participant Ops as op_immediate_* ops

    App->>SetImm: setImmediate(fn)
    SetImm->>SetImm: validateFunction(fn)
    SetImm->>Queue: new Immediate(fn) -> enqueue
    SetImm->>Ops: op_immediate_count(true)
    Immediate->>Ops: op_immediate_ref_count(true) [if refed]

    Bootstrap->>Bootstrap: core.setImmediateCallback(runImmediates)

    Note over Bootstrap: event loop tick
    Bootstrap->>Runner: runImmediates()
    Runner->>Queue: dequeue Immediate
    Runner->>Callback: invoke callback with argv
    Callback-->>Runner: return
    Runner->>Ops: op_immediate_count(false)
    Runner->>Ops: op_immediate_ref_count(false) [if was refed]
Loading

Estimated code review effort

3 (Moderate) — ~25 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: a reimplementation of the setImmediate API in the Node compatibility layer.
Description check ✅ Passed The description provides detailed context about the issue, benchmark results, and performance improvements achieved by the reimplementation.
Linked Issues check ✅ Passed The PR directly addresses issue #28016 by reimplementing setImmediate to fix performance problems, achieving dramatically improved ticks-per-second results.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing setImmediate performance: core logic rewrite in timers.mjs, integration in process.ts, validation in timers.ts, removal of duplicate API in web, and enabling related tests.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@bartlomieju bartlomieju marked this pull request as ready for review November 24, 2025 11:34
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ext/node/polyfills/process.ts (1)

920-970: Remove leftover console.log debugging from process event wiring

The new console.log calls when installing/removing uncaughtException listeners, when handling "error" events, and when installing the internal unhandledRejection callback will print on every such transition and error. That’s observable behavior, not present in Node, and will be very noisy in real applications and tests.

These should be removed or gated behind an internal debug logger/flag before merging:

  • Line 929: console.log("installed uncaughtException1");
  • Line 953: console.log("removed uncaughtException");
  • Line 969: console.log("process on error uncaughtException");
  • Line 996: console.log("installing internal listener");

Also applies to: 996-1010

🧹 Nitpick comments (2)
tests/node_compat/config.toml (1)

1190-1205: Nice immediate coverage; consider re-validating the zlib test that was disabled for broken setImmediate

Enabling the new test-timers-immediate-* cases here is good coverage for the new implementation. Given that parallel/test-zlib-destroy-pipe.js (around line 1402) is still disabled explicitly “because setImmediate is broken”, it’s worth re-running it with this PR and either re‑enabling it or updating the comment if a different issue remains.

ext/node/polyfills/timers.ts (1)

15-21: setImmediate / clearImmediate logic looks sound; consider minor API polish

The new implementation correctly:

  • Validates the callback (validateFunction(cb, "callback")).
  • Uses the new Immediate class for scheduling so async context and ref/unref semantics are handled in one place.
  • In clearImmediate, avoids double‑decrementing by early‑returning if _onImmediate is falsy or _destroyed is already set, and updates both op_immediate_count and op_immediate_ref_count before unlinking from immediateQueue.

Two small follow‑ups you may want to consider:

  1. Return type annotation

    setImmediate is annotated as returning Timeout but now returns an Immediate instance. If Timeout and Immediate aren’t intentionally aliased in the type layer, changing the return type here to Immediate would better document the runtime behavior and avoid surprising TS consumers.

  2. Defensive clearImmediate argument handling

    clearImmediate(immediate: Immediate) currently assumes a valid Immediate object. If you want closer parity with Node’s “no‑op on invalid handle” behavior, you could add a quick guard:

    export function clearImmediate(immediate: Immediate | null | undefined) {
      if (!immediate || !immediate._onImmediate || immediate._destroyed) {
        return;
      }
      // existing body…
    }

    That would tolerate stray calls like clearImmediate(undefined) without throwing.

Also applies to: 160-184

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 355d899 and 89bb1f0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml (2 hunks)
  • ext/node/polyfills/internal/timers.mjs (3 hunks)
  • ext/node/polyfills/process.ts (6 hunks)
  • ext/node/polyfills/timers.ts (2 hunks)
  • ext/web/02_timers.js (0 hunks)
  • tests/node_compat/config.toml (1 hunks)
💤 Files with no reviewable changes (1)
  • ext/web/02_timers.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-20T09:12:20.189Z
Learnt from: marvinhagemeister
Repo: denoland/deno PR: 31358
File: ext/node/polyfills/process.ts:759-766
Timestamp: 2025-11-20T09:12:20.189Z
Learning: In ext/node/polyfills/process.ts, the process.sourceMapsEnabled property should be defined with enumerable: true to match Node.js behavior (as seen in Node.js source: lib/internal/bootstrap/node.js).

Applied to files:

  • ext/node/polyfills/process.ts
🧬 Code graph analysis (3)
ext/node/polyfills/internal/timers.mjs (2)
ext/web/02_timers.js (1)
  • core (12-15)
ext/node/polyfills/_next_tick.ts (1)
  • runNextTicks (94-106)
ext/node/polyfills/timers.ts (2)
ext/node/polyfills/internal/validators.mjs (1)
  • validateFunction (289-295)
ext/node/polyfills/internal/timers.mjs (7)
  • cb (73-80)
  • Immediate (314-373)
  • immediate (230-230)
  • kRefed (46-46)
  • kRefed (46-46)
  • immediateQueue (220-220)
  • immediateQueue (220-220)
ext/node/polyfills/process.ts (1)
ext/node/polyfills/internal/timers.mjs (1)
  • runImmediates (226-312)
🪛 Biome (2.1.2)
ext/node/polyfills/internal/timers.mjs

[error] 17-17: Do not shadow the global "Symbol" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: test debug macos-x86_64
  • GitHub Check: test release linux-x86_64
  • GitHub Check: lint debug windows-x86_64
  • GitHub Check: bench release linux-x86_64
  • GitHub Check: test debug macos-aarch64
  • GitHub Check: test debug windows-x86_64
  • GitHub Check: test debug linux-x86_64
  • GitHub Check: test debug linux-aarch64
  • GitHub Check: lint debug linux-x86_64
  • GitHub Check: lint debug macos-x86_64
  • GitHub Check: build libs
🔇 Additional comments (1)
ext/node/polyfills/process.ts (1)

73-73: Wiring runImmediates into the core callback looks correct

Hooking runImmediates via core.setImmediateCallback(runImmediates) alongside setNextTickCallback / setMacrotaskCallback matches the new Immediate queue design and should give Node‑like setImmediate scheduling semantics.

Also applies to: 1058-1061

Comment thread Cargo.toml Outdated
Comment thread ext/node/polyfills/internal/timers.mjs
Comment thread ext/node/polyfills/internal/timers.mjs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/node_compat/config.toml (1)

1203-1227: All 21 referenced test files are missing from the repository.

The script confirms that none of the timer test files added to config.toml at lines 1203-1227 exist in the codebase. These entries cannot remain in the configuration file without the corresponding test files present. Either the test files need to be added to the repository or their entries removed from config.toml.

🧹 Nitpick comments (1)
ext/node/polyfills/timers.ts (1)

160-166: Align setImmediate return type with Immediate handle

The implementation now returns an Immediate instance but the signature still advertises Timeout. Given clearImmediate takes an Immediate, it would be clearer and more type-consistent to have setImmediate return Immediate (or loosen clearImmediate’s parameter type) so clearImmediate(setImmediate(...)) is cleanly typed.

Consider changing the signature along these lines (if it matches the Immediate/Timeout relationship in internal/timers.mjs):

-export function setImmediate(
-  cb: (...args: unknown[]) => void,
-  ...args: unknown[]
-): Timeout {
+export function setImmediate(
+  cb: (...args: unknown[]) => void,
+  ...args: unknown[]
+): Immediate {

The added validateFunction(cb, "callback") looks good and matches the other timer APIs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d616ad5 and 5289ba6.

📒 Files selected for processing (2)
  • ext/node/polyfills/timers.ts (2 hunks)
  • tests/node_compat/config.toml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: test debug macos-aarch64
  • GitHub Check: test debug linux-aarch64
  • GitHub Check: test debug linux-x86_64
  • GitHub Check: test debug windows-x86_64
  • GitHub Check: test release linux-x86_64
  • GitHub Check: test debug macos-x86_64
  • GitHub Check: lint debug windows-x86_64
  • GitHub Check: lint debug macos-x86_64
  • GitHub Check: build libs
  • GitHub Check: lint debug linux-x86_64
🔇 Additional comments (2)
tests/node_compat/config.toml (1)

1197-1201: Two setImmediate tests appropriately deferred with clear TODOs.

The disabled tests have documented reasons: one requires code rearrangement and the other depends on proper node:domain error handling. These can be addressed in follow-up work.

ext/node/polyfills/timers.ts (1)

15-21: clearImmediate guard and counter logic look correct; ensure op balance with constructor

The new clearImmediate correctly no-ops for null/undefined or already-destroyed handles, flips _destroyed, clears ref state via kRefed, and removes the item from immediateQueue while decrementing op_immediate_count (and op_immediate_ref_count only when still refed). This should make clearImmediate idempotent and safe.

Please double-check that:

  • Each Immediate creation/run path increments op_immediate_count(true) and op_immediate_ref_count(true) in ways that are exactly balanced by this clearImmediate logic (and the run path), and
  • immediateQueue.remove(immediate) is safe to call only for queue members (no double-remove on already-run handles, which you seem to guard via _onImmediate / _destroyed but is worth confirming against the Immediate implementation).

Also applies to: 168-184

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28f4f5f and bb11f43.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • Cargo.toml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: test debug windows-x86_64
  • GitHub Check: test debug linux-x86_64
  • GitHub Check: test release linux-aarch64
  • GitHub Check: test debug macos-aarch64
  • GitHub Check: test debug macos-x86_64
  • GitHub Check: lint debug macos-x86_64
  • GitHub Check: lint debug windows-x86_64
  • GitHub Check: lint debug linux-x86_64
  • GitHub Check: build libs

Comment thread Cargo.toml Outdated
@bartlomieju bartlomieju requested a review from dsherret November 27, 2025 22:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
ext/node/polyfills/internal/timers.mjs (2)

18-18: Symbol shadowing lint error persists.

Static analysis still flags Symbol on line 18 as shadowing the global. The previous review suggested renaming to PrimordialSymbol. If the fix was applied in commit d616ad5, this lint configuration may need updating, or the fix may not have stuck.


266-269: Crash when prevImmediate is undefined.

On the first loop iteration, prevImmediate is undefined. If immediate._destroyed is true (e.g., when resuming from outstandingQueue after an exception handler called clearImmediate), this will throw TypeError: Cannot read property '_idleNext' of undefined.

Apply this fix:

     if (immediate._destroyed) {
-      outstandingQueue.head = immediate = prevImmediate._idleNext;
+      const next = prevImmediate ? prevImmediate._idleNext : immediate._idleNext;
+      outstandingQueue.head = immediate = next;
       continue;
     }
🧹 Nitpick comments (1)
ext/node/polyfills/internal/timers.mjs (1)

202-202: Remove debug comment.

Leftover debug code should be removed.

-    // console.log("append", this.tail);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1b86c7 and 2230ebc.

📒 Files selected for processing (1)
  • ext/node/polyfills/internal/timers.mjs (4 hunks)
🧰 Additional context used
🪛 Biome (2.1.2)
ext/node/polyfills/internal/timers.mjs

[error] 18-18: Do not shadow the global "Symbol" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: test debug linux-x86_64
  • GitHub Check: test debug macos-aarch64
  • GitHub Check: test debug linux-aarch64
  • GitHub Check: test release linux-x86_64
  • GitHub Check: test debug macos-x86_64
  • GitHub Check: test debug windows-x86_64
  • GitHub Check: lint debug windows-x86_64
  • GitHub Check: lint debug macos-x86_64
  • GitHub Check: build libs
  • GitHub Check: lint debug linux-x86_64
🔇 Additional comments (1)
ext/node/polyfills/internal/timers.mjs (1)

320-377: Immediate class implementation looks solid.

Async context capture, linked list integration, and ref counting with op counters are all handled correctly. The strict === false/=== true checks in ref()/unref() combined with setting kRefed to null in runImmediates (line 277) prevents double-counting on processed Immediates.

Copy link
Copy Markdown
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

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.

setImmediate function does not work as expected

2 participants