Skip to content

fix(scripting/v8): use node's setImmediate on the server - #4402

Open
OneEarJoe5 wants to merge 1 commit into
citizenfx:masterfrom
OneEarJoe5:fix/server-js-native-timers
Open

OneEarJoe5 wants to merge 1 commit into
citizenfx:masterfrom
OneEarJoe5:fix/server-js-native-timers

Conversation

@OneEarJoe5

Copy link
Copy Markdown

Goal of this PR

Server-side JS that awaits DB queries (or anything else that yields with setImmediate) waits a full server frame per step. timer.js overrides setImmediate with setTimeout(cb, 0), and those timers only run in the resource tick, so ~50 ms each. The I/O itself is fast, the node runtime polls each resource's uv loop every 1 ms (UvLoopTimer), only the timer is slow. That's the 50 ms in #4186: postgres-js only writes a query in a setImmediate (src/connection.js, write batching), so every query pays a frame.

How is this PR achieving the goal

When timer.js runs inside the node runtime (process.versions.node is set) it keeps node's own setImmediate instead of the tick-based fallback.

  • The callback still runs inside runWithBoundaryStart, errors still go through printError('timer', ...) and the server keeps going.
  • These callbacks now run outside the resource tick, so while the profiler records they get their own scope setImmediate (<resource>). Otherwise their time would just disappear from profiler recordings.
  • The returned handle keeps the shape of our other timer handles (id, refresh(), converts to a number), so code that stores it as a number or key keeps working. clearImmediate and clearTimeout work on both kinds.
  • setTimeout, setInterval, setTick and the client side (no process there) are untouched, so only setImmediate gets faster.

I kept it to setImmediate on purpose. setTimeout timing is tick based on both sides and scripts may rely on that, so that felt like a bigger change than needed for this.

This PR applies to the following area(s)

Server, ScRT: JS

Successfully tested on

Platforms: Windows (server), Linux (server, WSL2 Ubuntu 24.04)

Windows: server built from current master (5412ed8). Linux: the official 38621 artifact, which is built from that same commit, with only timer.js swapped. Same test resources on both, before and after this change.

The case from the issue with the real postgres-js 3.4.5 against PostgreSQL 16 (same loop as in #4186):

postgres-js Windows before Windows after Linux before Linux after
ping 2–5 (SELECT 1) 50–51 ms 0.6–2 ms 49–50 ms 1.3–1.6 ms
20 more sequential queries 49–52 ms each (first one 102) 0.9–2 ms 49–52 ms (first one 102) 1.3–3 ms
50 queries in parallel 452 ms 80 ms 452 ms 84 ms

Everything else, Windows (Linux gave the same results for every row it ran, see below):

Case before after
await new Promise(r => setImmediate(r)) ~50 ms 1–2 ms
postgres-js write pattern (query bytes written in a setImmediate like connection.js does, reply over local TCP) ~50 ms 2–4 ms
args, FIFO order, nested setImmediate ok same
clearImmediate / clearTimeout on an immediate, clearing null/unknown/already fired ok same
error thrown in the callback printed, next immediates run same, stack starts at the script line
async callback that rejects node's unhandled rejection warning, server keeps going same
natives and emit inside the callback ok same
non-function callback TypeError same
handle as number / string / object key, id, refresh() ok same
hasRef() after unref() always true false, then true again after ref() (node's real value, the uv loop is polled anyway so it makes no other difference)
refresh() on a pending immediate still fired on the next tick (~50 ms), no extra delay no-op, fires right away (<1 ms)
6 rounds of 250k immediates (half of them cleared), heap after a forced GC each round ~5 MB, stays flat same
profiler record with 5 ms of work per immediate counted under tick (<resource>) (177 ms) counted under setImmediate (<resource>) (166–185 ms), scopes stay balanced, also when the callback throws
setTimeout(0), setInterval, setTick ~50 ms / tick based unchanged
10000 immediates 52–110 ms 23–55 ms
resource restarted from outside while immediates keep getting queued - restarts fine
txAdmin 8.1.2 (monitor) starting, panel requests for a minute ok, 9 s to ready, ~1–2 ms per request same, 8 s, ~1–2 ms
txAdmin starting and watching the test server for ~4 min (monitor resource also running inside the server) no heartbeat problems, no restarts same

Client built from the same master, same test as a client script with this exact commit: there's no process on the client, so setImmediate still takes one frame like before, and clearing, args and errors behave the same.

On Linux I ran the same resource for everything except txAdmin and oxmysql: timing, order/args, clearing, errors, async rejections, natives, handle shape, hasRef/refresh, setTimeout/setInterval/setTick, 10000 immediates, restart from outside, the profiler (tick (jstiming) 181 ms before, setImmediate (jstiming) 157 ms after) and the heap test (~5 MB flat both ways). Same results as on Windows.

Something I noticed that this PR does not change: awaiting a callback from another JS resource (e.g. exports.oxmysql.query(..., cb) from a JS resource) still takes ~50 ms, with or without this change. Looks like microtasks aren't drained after a ref call into the node runtime, so the continuation waits for the next tick. That's a separate C++ change, happy to look at it in its own PR if wanted.

One thing I ran into while testing, unrelated to this PR: a JS resource that restarts itself (ExecuteCommand('restart <self>')) from inside a timer callback crashes the server. Happens on master with setTimeout and with setImmediate, with or without this change. I'll open a separate issue for that.

Checklist

  • Code compiles and has been tested successfully.
  • Code explains itself well and/or is documented.
  • My commit message explains what the changes do and what they are for.
  • No extra compilation warnings are added by these changes.

Fixes issues

fixes #4186

timer.js replaces setImmediate with a tick-based timer, so on the server
every setImmediate waits for the next server frame (~50 ms). The node
runtime already polls the resource's uv loop every millisecond, so use
node's own setImmediate there. Callbacks still run through
runWithBoundaryStart, errors still go through printError and they get
their own profiler scope since they now run outside the resource tick.
The handle keeps the id/refresh/number shape of our timer handles and
clearImmediate and clearTimeout clear both kinds. setTimeout,
setInterval, setTick and the client are unchanged.

Signed-off-by: OneEarJoe5 <212275319+OneEarJoe5@users.noreply.github.com>
@github-actions github-actions Bot added ScRT: JS Issues/PRs related to the JavaScript scripting runtime triage Needs a preliminary assessment to determine the urgency and required action labels Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ScRT: JS Issues/PRs related to the JavaScript scripting runtime triage Needs a preliminary assessment to determine the urgency and required action

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FXServer server-side JS awaits resume on ~50 ms scheduler cadence

1 participant