N-API `uv_timer_start()` does not wake the event loop at the timer deadline
A native timer started through Deno’s N-API/libuv compatibility layer does not fire near its requested deadline unless some other activity wakes the event loop.
## Reproduction
The reproduction script starts a one-shot `uv_timer_t` with a 1000 ms timeout. It supports two modes:
- By default, it creates no deliberate JavaScript timer or other wake source.
- With `--js-timer`, it also starts an independent 5000 ms JavaScript timer.
<details>
<summary>repro.cjs</summary>
```js
const addon = require("./timer_addon.node");
const requestedMs = 1000;
const jsTimerMs = 5000;
const runtime = globalThis.Deno
? `Deno ${Deno.version.deno}`
: `Node.js ${process.version}`;
const jsStartedNs = process.hrtime.bigint();
const elapsedMs = () => Number(process.hrtime.bigint() - jsStartedNs) / 1e6;
console.log(`${runtime}: starting native ${requestedMs}ms uv timer`);
addon.startTimer(requestedMs, (nativeElapsedMs) => {
console.log(
`${runtime}: requested=${requestedMs}ms native=${nativeElapsedMs.toFixed(3)}ms js=${elapsedMs().toFixed(3)}ms`,
);
});
if (process.argv.includes("--js-timer")) {
console.log(`${runtime}: also starting JavaScript ${jsTimerMs}ms timer`);
setTimeout(() => {
console.log(
`${runtime}: JavaScript timer fired after ${elapsedMs().toFixed(3)}ms`,
);
}, jsTimerMs);
}
```
</details>
<details>
<summary>timer_addon.c</summary>
```c
#include <node_api.h>
#include <uv.h>
#include <stdint.h>
#include <stdlib.h>
typedef struct {
napi_env env;
napi_ref callback;
uv_timer_t timer;
uint64_t started_ns;
} timer_state;
static void close_timer(uv_handle_t* handle) {
timer_state* state = handle->data;
napi_delete_reference(state->env, state->callback);
free(state);
}
static void on_timer(uv_timer_t* timer) {
timer_state* state = timer->data;
double elapsed_ms =
(double) (uv_hrtime() - state->started_ns) / 1000000.0;
napi_handle_scope scope;
napi_value callback;
napi_value global;
napi_value elapsed;
napi_value result;
napi_open_handle_scope(state->env, &scope);
napi_get_reference_value(state->env, state->callback, &callback);
napi_get_global(state->env, &global);
napi_create_double(state->env, elapsed_ms, &elapsed);
napi_call_function(
state->env, global, callback, 1, &elapsed, &result);
napi_close_handle_scope(state->env, scope);
uv_close((uv_handle_t*) timer, close_timer);
}
static napi_value start_timer(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_value undefined;
napi_valuetype callback_type;
uint32_t delay_ms;
uv_loop_t* loop;
timer_state* state;
napi_get_cb_info(env, info, &argc, args, NULL, NULL);
if (argc != 2 ||
napi_get_value_uint32(env, args[0], &delay_ms) != napi_ok) {
napi_throw_type_error(
env, NULL, "delayMs must be an unsigned 32-bit integer");
return NULL;
}
napi_typeof(env, args[1], &callback_type);
if (callback_type != napi_function) {
napi_throw_type_error(env, NULL, "callback must be a function");
return NULL;
}
if (napi_get_uv_event_loop(env, &loop) != napi_ok || loop == NULL) {
napi_throw_error(env, NULL, "napi_get_uv_event_loop failed");
return NULL;
}
state = calloc(1, sizeof(*state));
if (state == NULL) {
napi_throw_error(env, NULL, "allocation failed");
return NULL;
}
state->env = env;
if (napi_create_reference(env, args[1], 1, &state->callback) != napi_ok) {
free(state);
napi_throw_error(env, NULL, "napi_create_reference failed");
return NULL;
}
if (uv_timer_init(loop, &state->timer) != 0) {
napi_delete_reference(env, state->callback);
free(state);
napi_throw_error(env, NULL, "uv_timer_init failed");
return NULL;
}
state->timer.data = state;
state->started_ns = uv_hrtime();
if (uv_timer_start(&state->timer, on_timer, delay_ms, 0) != 0) {
uv_close((uv_handle_t*) &state->timer, close_timer);
napi_throw_error(env, NULL, "uv_timer_start failed");
return NULL;
}
napi_get_undefined(env, &undefined);
return undefined;
}
NAPI_MODULE_INIT() {
napi_value function;
napi_create_function(
env, "startTimer", NAPI_AUTO_LENGTH, start_timer, NULL, &function);
napi_set_named_property(env, exports, "startTimer", function);
return exports;
}
```
</details>
Build the addon (on Linux):
```sh
cc -std=c11 -D_GNU_SOURCE -Wall -Wextra -Werror -fPIC -shared \
-I/usr/include/node \
-o timer_addon.node timer_addon.c
```
### Deno (latest build from `main`)
Deno does not invoke the callback near the native timer’s deadline, and it arrives after approximately 30 seconds:
```text
$ deno run --allow-ffi --allow-read repro.cjs
Deno 2.9.4: starting native 1000ms uv timer
Deno 2.9.4: requested=1000ms native=30103.301ms js=30104.285ms
```
When the independent 5000 ms JavaScript timer is present, Deno invokes the overdue native callback at approximately the JavaScript timer’s deadline instead of at the native timer’s 1000 ms deadline:
```text
$ deno run --allow-ffi --allow-read repro.cjs --js-timer
Deno 2.9.4: starting native 1000ms uv timer
Deno 2.9.4: also starting JavaScript 5000ms timer
Deno 2.9.4: requested=1000ms native=5002.709ms js=5003.533ms
Deno 2.9.4: JavaScript timer fired after 5005.007ms
```
### Node.js 26.5.1 (Expected behavior)
Node.js invokes the native callback after approximately 1 second, with or without the JavaScript timer:
```text
$ node repro.cjs
Node.js v26.5.1: starting native 1000ms uv timer
Node.js v26.5.1: requested=1000ms native=983.636ms js=986.681ms
```
```text
$ node repro.cjs --js-timer
Node.js v26.5.1: starting native 1000ms uv timer
Node.js v26.5.1: also starting JavaScript 5000ms timer
Node.js v26.5.1: requested=1000ms native=982.284ms js=990.195ms
Node.js v26.5.1: JavaScript timer fired after 5009.878ms
```
关闭于 10 天前 0 条评论