ITADN

N-API `uv_timer_start()` does not wake the event loop at the timer deadline

#36454Closedyibe 创建于 24 天前
Y
yibecommented
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 条评论