From 4b099ea2fd191908a06cbf04fdaa7b1954fec30d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 12 Aug 2026 09:15:54 -0500 Subject: [PATCH] fix(runtime): share uptime anchor across threads --- packages/runtime/src/scr_lib.c | 33 +++++++++++++++++-- tests/harness/library-multi.test.ts | 10 +++--- tests/library-mode/thread-instances/lib.ts | 10 ++++++ tests/library-mode/thread-instances/probe.c | 8 ++++- .../thread-instances/profile_t.json | 4 ++- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 5f4fb5a0c..baa3f6c83 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -1103,8 +1103,27 @@ void scr_process_exit(double code) { * attribute monotonic stamp — the binary's own start, which is what * "the current Node.js process" means for a compiled program). */ #ifdef _WIN32 +#if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES) +/* The uptime anchor belongs to the host PROCESS, not to a library + * instance. InitOnce keeps the lazy Windows spelling race-free when + * several thread instances ask for the clock concurrently. */ +static INIT_ONCE scr_uptime_once = INIT_ONCE_STATIC_INIT; +static double scr_uptime_t0_ms; +static BOOL CALLBACK scr_uptime_anchor_once(PINIT_ONCE once, PVOID param, + PVOID *ctx) { + (void)once; + (void)param; + (void)ctx; + scr_uptime_t0_ms = (double)GetTickCount64(); + return TRUE; +} +static void scr_uptime_anchor_init(void) { + InitOnceExecuteOnce(&scr_uptime_once, scr_uptime_anchor_once, NULL, NULL); +} +#else static SCR_TL double scr_uptime_t0_ms; static void scr_uptime_anchor_init(void) { scr_uptime_t0_ms = (double)GetTickCount64(); } +#endif static double scr_uptime_now_ms(void) { return (double)GetTickCount64(); } #else #include @@ -1112,7 +1131,11 @@ static double scr_uptime_now_ms(void) { return (double)GetTickCount64(); } #ifdef __APPLE__ #include #endif -static SCR_TL double scr_uptime_t0_ms; +/* A load-time, process-wide anchor: the constructor runs on only the + * initial thread, while thread-instanced archives are entered from worker + * threads whose TLS slots would otherwise stay zero. It is immutable once + * main starts, so sharing it adds no cross-instance mutable state. */ +static double scr_uptime_t0_ms; static double scr_uptime_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); @@ -1124,7 +1147,9 @@ __attribute__((constructor)) static void scr_uptime_anchor_init(void) { #endif double scr_process_uptime(void) { -#ifdef _WIN32 +#if defined(_WIN32) && defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES) + scr_uptime_anchor_init(); +#elif defined(_WIN32) if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init(); #endif return (scr_uptime_now_ms() - scr_uptime_t0_ms) / 1000.0; @@ -1134,7 +1159,9 @@ double scr_process_uptime(void) { * start (Node's timeOrigin anchor), fractional — the same monotonic * clock and anchor as uptime, in Node's performance.now units. */ double scr_perf_now(void) { -#ifdef _WIN32 +#if defined(_WIN32) && defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES) + scr_uptime_anchor_init(); +#elif defined(_WIN32) if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init(); #endif return scr_uptime_now_ms() - scr_uptime_t0_ms; diff --git a/tests/harness/library-multi.test.ts b/tests/harness/library-multi.test.ts index 552fbe45d..cb6d65569 100644 --- a/tests/harness/library-multi.test.ts +++ b/tests/harness/library-multi.test.ts @@ -446,11 +446,11 @@ function buildThreadProbe( return bin; } -const THREADED_EXPECTED = `t0: bump x100 -> 101, calls_seen 100, sums_ok=1, trap fell through 0 +const THREADED_EXPECTED = `t0: bump x100 -> 101, calls_seen 100, sums_ok=1, clocks_ok=1, trap fell through 0 t0 sink: calls=1 ctx_ok=1 fields=3 code=[SC4014] symbol=[mt_boom] addr_nonzero=1 -t1: bump x150 -> 151, calls_seen 150, sums_ok=1, post_ok=1 -t2: bump x200 -> 201, calls_seen 200, sums_ok=1, post_ok=1 -t3: bump x250 -> 251, calls_seen 250, sums_ok=1, post_ok=1 +t1: bump x150 -> 151, calls_seen 150, sums_ok=1, clocks_ok=1, post_ok=1 +t2: bump x200 -> 201, calls_seen 200, sums_ok=1, clocks_ok=1, post_ok=1 +t3: bump x250 -> 251, calls_seen 250, sums_ok=1, clocks_ok=1, post_ok=1 survivor sinks: 0 0 0 `; @@ -510,7 +510,7 @@ localizationTest("M7: thread-instanced and runtime-localized archives compose in : []; expect([...defined].sort()).toEqual( [ - "mt_boom", "mt_bump", "mt_calls_seen", "mt_collect", "mt_init", "mt_set_panic_sink", "mt_sum_to", + "mt_boom", "mt_bump", "mt_calls_seen", "mt_collect", "mt_init", "mt_perf_now", "mt_set_panic_sink", "mt_sum_to", "mt_uptime", ...toolchainDefinitions, ].sort(), ); diff --git a/tests/library-mode/thread-instances/lib.ts b/tests/library-mode/thread-instances/lib.ts index 35017ef86..17601d867 100644 --- a/tests/library-mode/thread-instances/lib.ts +++ b/tests/library-mode/thread-instances/lib.ts @@ -4,6 +4,8 @@ // (array index OOB — the runtime's own range trap) that must reach only the // calling thread's sink. No top-level output: four instances init // concurrently in the probe. +import { performance } from "node:perf_hooks"; + let calls = 0; export function bump(x: number): number { @@ -27,3 +29,11 @@ export function boom(i: number): number { const xs = [1, 2, 3]; return xs[i]!; } + +export function uptime(): number { + return process.uptime(); +} + +export function perfNow(): number { + return performance.now(); +} diff --git a/tests/library-mode/thread-instances/probe.c b/tests/library-mode/thread-instances/probe.c index 24fefc79d..781671664 100644 --- a/tests/library-mode/thread-instances/probe.c +++ b/tests/library-mode/thread-instances/probe.c @@ -23,6 +23,8 @@ extern double mt_bump(double x); extern double mt_calls_seen(void); extern double mt_sum_to(double n); extern double mt_boom(double i); +extern double mt_uptime(void); +extern double mt_perf_now(void); #define NTHREADS 4 @@ -80,6 +82,7 @@ typedef struct { double last_bump; double calls_seen; int sums_ok; + int clocks_ok; int trap_fell_through; /* thread 0 only */ int post_ok; /* threads 1..3 only */ } Worker; @@ -121,6 +124,9 @@ static void *worker(void *arg) { Worker *w = arg; mt_set_panic_sink(sink, &w->sink); mt_init(); /* concurrent across all four threads: each init is instance-local */ + double uptime = mt_uptime(); + double perf_now = mt_perf_now(); + w->clocks_ok = uptime >= 0 && uptime < 60 && perf_now >= 0 && perf_now < 60000; w->sums_ok = 1; double last = 0; for (int i = 0; i < w->iters; i++) { @@ -163,7 +169,7 @@ int main(void) { for (int i = 0; i < NTHREADS; i++) pthread_join(threads[i], NULL); for (int i = 0; i < NTHREADS; i++) { Worker *w = &workers[i]; - printf("t%d: bump x%d -> %.0f, calls_seen %.0f, sums_ok=%d", i, w->iters, w->last_bump, w->calls_seen, w->sums_ok); + printf("t%d: bump x%d -> %.0f, calls_seen %.0f, sums_ok=%d, clocks_ok=%d", i, w->iters, w->last_bump, w->calls_seen, w->sums_ok, w->clocks_ok); if (i == 0) { printf(", trap fell through %d\n", w->trap_fell_through); printf("t0 sink: calls=%d ctx_ok=%d fields=%d code=[%s] symbol=[%s] addr_nonzero=%d\n", diff --git a/tests/library-mode/thread-instances/profile_t.json b/tests/library-mode/thread-instances/profile_t.json index d8fe42320..031700718 100644 --- a/tests/library-mode/thread-instances/profile_t.json +++ b/tests/library-mode/thread-instances/profile_t.json @@ -16,6 +16,8 @@ { "export": "bump", "symbol": "mt_bump", "params": ["f64"], "returns": "f64" }, { "export": "callsSeen", "symbol": "mt_calls_seen", "params": [], "returns": "f64" }, { "export": "sumTo", "symbol": "mt_sum_to", "params": ["f64"], "returns": "f64" }, - { "export": "boom", "symbol": "mt_boom", "params": ["f64"], "returns": "f64" } + { "export": "boom", "symbol": "mt_boom", "params": ["f64"], "returns": "f64" }, + { "export": "uptime", "symbol": "mt_uptime", "params": [], "returns": "f64" }, + { "export": "perfNow", "symbol": "mt_perf_now", "params": [], "returns": "f64" } ] }