diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index cbbe881f97..223a3027ba 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "ae934a592466ef1e67c1", + "artifactVersion": "cb524f2eb043e40eb9f4", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 1391cf23b7..1553483063 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -746,9 +746,9 @@ "apiEntries": 24, "static": { "supported": 0, - "partial": 10, + "partial": 11, "refused": 0, - "not-implemented": 14, + "not-implemented": 13, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -61038,9 +61038,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index e73d033de3..e16c10848d 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 42, - "verify-gap": 2418, - "audit-partial": 746, + "verify-gap": 2417, + "audit-partial": 747, "classify": 2, "implement": 80 }, @@ -66294,12 +66294,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:os", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.os.availableParallelism", + "tests": [ + "tests/corpus/4598-os-available-parallelism.ts" + ] }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index faa5855d9b..4a4faed267 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -819,9 +819,9 @@ "apiEntries": 24, "static": { "supported": 0, - "partial": 10, + "partial": 11, "refused": 0, - "not-implemented": 14, + "not-implemented": 13, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -83033,7 +83033,8 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/1480-os-network-interfaces.ts", - "tests/corpus/2917-builtin-callable-widths.ts" + "tests/corpus/2917-builtin-callable-widths.ts", + "tests/corpus/4598-os-available-parallelism.ts" ] }, "dynamic": { @@ -83279,8 +83280,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:os" + "status": "partial", + "evidence": "surface-manifest:node-builtin.os.availableParallelism", + "tests": [ + "tests/corpus/4598-os-available-parallelism.ts" + ] }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 80cdeb2f98..3f5ecefff8 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -80,6 +80,7 @@ "node-builtin.module": ["tests/corpus/1598-cjs-builtin-require/main.js"], "node-builtin.net": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"], "node-builtin.os": ["tests/corpus/1480-os-network-interfaces.ts", "tests/corpus/2917-builtin-callable-widths.ts"], + "node-builtin.os.availableParallelism": ["tests/corpus/4598-os-available-parallelism.ts"], "node-builtin.path": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2915-path-function-values.ts", "tests/corpus/2916-path-function-values-cjs.ts", "tests/corpus/2917-builtin-callable-widths.ts", "tests/corpus/2918-builtin-callable-widths-cjs.ts"], "node-builtin.perf_hooks": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"], diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 7e4590be56..b3933ef5e6 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -1688,6 +1688,7 @@ declare module "os" { export function type(): string; /* Total system memory in bytes (sysctl hw.memsize / sysconf). */ export function totalmem(): number; + export function availableParallelism(): number; /* The passwd-entry snapshot (uv_os_get_passwd): shell is `string | * null` to match @types/node (POSIX always answers the string arm); * homedir is pw_dir — NOT os.homedir()'s $HOME-first cascade. The type diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 4d367c1eb1..07df1406f7 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -151,6 +151,7 @@ export const LIB_FN_SYMS: Record = { "os.homedir": "scr_os_homedir", "os.type": "scr_os_type", "os.totalmem": "scr_os_totalmem", + "os.availableParallelism": "scr_os_available_parallelism", "os.release": "scr_os_release", "os.userName": "scr_os_user_name", "os.userShell": "scr_os_user_shell", diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8080b0fed8..91ef6c4b5e 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1311,6 +1311,14 @@ export const BUILTIN_MODULE_FNS: Record< type: { fn: "os.type", params: [], result: STRING, valueParams: [] }, // Total physical memory in bytes — same predating-pair story. totalmem: { fn: "os.totalmem", params: [], result: F64, valueParams: [] }, + // uv_available_parallelism: the CPUs this process may use (affinity, + // then cgroup quota on Linux), at least 1. + availableParallelism: { + fn: "os.availableParallelism", + params: [], + result: F64, + valueParams: [], + }, // Entirely special-cased (lowerOsNetworkInterfacesCall): the result is // the call site's mapped Dict shape, verified // structurally there — this entry only routes the dispatch. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9e367aacf5..245157851c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -432,6 +432,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "os.type": { argTypes: [], result: STRING }, /** os.totalmem(): total physical memory in bytes. Never throws. */ "os.totalmem": { argTypes: [], result: F64 }, + /** os.availableParallelism(): CPUs the process may use (libuv's answer), >= 1. Never throws. */ + "os.availableParallelism": { argTypes: [], result: F64 }, /** umask(2): arg < 0 reads without setting (umask has no read-only form * — set 0, restore); otherwise sets and answers the PREVIOUS mask. * Never throws. */ diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 00a1908f5c..531a171974 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1334,6 +1334,12 @@ "status": "static", "note": "constant value read" }, + { + "id": "node-builtin.os.availableParallelism", + "kind": "node-builtin", + "name": "os.availableParallelism", + "status": "static" + }, { "id": "node-builtin.os.homedir", "kind": "node-builtin", diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 06376bc1b2..35d3bc0b43 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -11409,12 +11409,6 @@ ], "diags": [] }, - "/tests/corpus/typed-array-nan-coded-reads.ts": { - "order": [ - "/tests/corpus/typed-array-nan-coded-reads.ts" - ], - "diags": [] - }, "/tests/corpus/typed-array-unknown.ts": { "order": [ "/tests/corpus/typed-array-unknown.ts" @@ -14536,6 +14530,12 @@ ], "diags": [] }, + "/tests/corpus/native-worker-arena-reuse.ts": { + "order": [ + "/tests/corpus/native-worker-arena-reuse.ts" + ], + "diags": [] + }, "/tests/corpus/native-worker-buffer-consumers.ts": { "order": [ "/tests/corpus/native-worker-buffer-consumers.ts" @@ -15743,83 +15743,83 @@ ], "diags": [] }, - "/tests/corpus/4598-live-array-ref-element-access.ts": { + "/tests/corpus/4596-tdz-nullable-forward-capture.ts": { "order": [ - "/tests/corpus/4598-live-array-ref-element-access.ts" + "/tests/corpus/4596-tdz-nullable-forward-capture.ts" ], "diags": [] }, - "/tests/corpus/native-worker-arena-reuse.ts": { + "/tests/corpus/4598-map-set-clear-reuse.ts": { "order": [ - "/tests/corpus/native-worker-arena-reuse.ts" + "/tests/corpus/4598-map-set-clear-reuse.ts" ], "diags": [] }, - "/tests/corpus/4596-tdz-nullable-forward-capture.ts": { + "/tests/corpus/missing-read-lambda-results.ts": { "order": [ - "/tests/corpus/4596-tdz-nullable-forward-capture.ts" + "/tests/corpus/missing-read-lambda-results.ts" ], "diags": [] }, - "/tests/corpus/4620-optional-chain-borrowed-receivers.ts": { + "/tests/corpus/missing-read-narrowed-storage.ts": { "order": [ - "/tests/corpus/4620-optional-chain-borrowed-receivers.ts" + "/tests/corpus/missing-read-narrowed-storage.ts" ], "diags": [] }, - "/tests/corpus/4621-edge-preserving-callees/main.ts": { + "/tests/corpus/missing-read-number-conversions.ts": { "order": [ - "/tests/corpus/4621-edge-preserving-callees/compare.ts", - "/tests/corpus/4621-edge-preserving-callees/items.ts", - "/tests/corpus/4621-edge-preserving-callees/main.ts" + "/tests/corpus/missing-read-number-conversions.ts" ], "diags": [] }, - "/tests/corpus/native-worker-arena-reuse.ts": { + "/tests/corpus/typed-array-nan-coded-reads.ts": { "order": [ - "/tests/corpus/native-worker-arena-reuse.ts" + "/tests/corpus/typed-array-nan-coded-reads.ts" ], "diags": [] }, - "/tests/corpus/4598-map-set-clear-reuse.ts": { + "/tests/corpus/typed-array-present-reads.ts": { "order": [ - "/tests/corpus/4598-map-set-clear-reuse.ts" + "/tests/corpus/typed-array-present-reads.ts" ], "diags": [] }, - "/tests/corpus/missing-read-lambda-results.ts": { + "/tests/corpus/4601-cycle-holders.ts": { "order": [ - "/tests/corpus/missing-read-lambda-results.ts" + "/tests/corpus/4601-cycle-holders.ts" ], "diags": [] }, - "/tests/corpus/missing-read-narrowed-storage.ts": { + "/tests/corpus/4602-constructor-escape-order.ts": { "order": [ - "/tests/corpus/missing-read-narrowed-storage.ts" + "/tests/corpus/4602-constructor-escape-order.ts" ], "diags": [] }, - "/tests/corpus/missing-read-number-conversions.ts": { + "/tests/corpus/4598-os-available-parallelism.ts": { "order": [ - "/tests/corpus/missing-read-number-conversions.ts" + "/tests/corpus/4598-os-available-parallelism.ts" ], "diags": [] }, - "/tests/corpus/typed-array-present-reads.ts": { + "/tests/corpus/4598-live-array-ref-element-access.ts": { "order": [ - "/tests/corpus/typed-array-present-reads.ts" + "/tests/corpus/4598-live-array-ref-element-access.ts" ], "diags": [] }, - "/tests/corpus/4601-cycle-holders.ts": { + "/tests/corpus/4620-optional-chain-borrowed-receivers.ts": { "order": [ - "/tests/corpus/4601-cycle-holders.ts" + "/tests/corpus/4620-optional-chain-borrowed-receivers.ts" ], "diags": [] }, - "/tests/corpus/4602-constructor-escape-order.ts": { + "/tests/corpus/4621-edge-preserving-callees/main.ts": { "order": [ - "/tests/corpus/4602-constructor-escape-order.ts" + "/tests/corpus/4621-edge-preserving-callees/compare.ts", + "/tests/corpus/4621-edge-preserving-callees/items.ts", + "/tests/corpus/4621-edge-preserving-callees/main.ts" ], "diags": [] }, diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index e259f6f7e1..14ba35d213 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -123,6 +123,11 @@ static ScrStr *scr_fs_win_utf8(const WCHAR *text, size_t length) { #elif defined(__linux__) #include #endif +#if defined(__linux__) +#include /* sched_getaffinity, CPU_COUNT (os.availableParallelism) */ +#elif defined(__APPLE__) +#include /* sysctlbyname (os.availableParallelism) */ +#endif #define scr_sys_mkdir(p, m) mkdir((p), (m)) @@ -1376,6 +1381,13 @@ ScrStr *scr_os_type(void) { return scr_str_new("Windows_NT", 10); } +double scr_os_available_parallelism(void) { + /* uv_available_parallelism on Windows: GetSystemInfo's processor count. */ + SYSTEM_INFO info; + GetSystemInfo(&info); + return info.dwNumberOfProcessors < 1 ? 1 : (double)info.dwNumberOfProcessors; +} + double scr_os_totalmem(void) { MEMORYSTATUSEX ms; memset(&ms, 0, sizeof ms); @@ -1413,6 +1425,8 @@ ScrStr *scr_os_user_homedir(void) { return scr_os_homedir(); } ScrStr *scr_os_release(void) { return scr_str_new("", 0); } ScrStr *scr_os_type(void) { return scr_str_new("WASI", 4); } double scr_os_totalmem(void) { return 0; } +/* One thread of execution, as the dynamic island's os answers. */ +double scr_os_available_parallelism(void) { return 1; } /* The guest temp namespace is stable across hosts. `scriptc run` preopens * the host's /tmp at this path; other WASI hosts can provide the same * capability without leaking a host-specific TMPDIR into the module. */ @@ -1486,6 +1500,98 @@ ScrStr *scr_os_type(void) { return scr_str_new(u.sysname, strlen(u.sysname)); } +#ifdef __linux__ +/* libuv's uv__slurp: one read of at most cap - 1 bytes of path into buf + * (NUL-terminated); false when the file cannot be opened or read. A single + * read, like libuv, so a long /proc/self/cgroup is cut where Node cuts it. */ +static bool scr_os_slurp(const char *path, char *buf, size_t cap) { + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) return false; + ssize_t n; + do + n = read(fd, buf, cap - 1); + while (n < 0 && errno == EINTR); + close(fd); + if (n < 0) return false; + buf[n] = '\0'; + return true; +} + +/* libuv's uv__get_constrained_cpu (1.51): the CPU quota of the process's + * cgroup in whole CPUs. For cgroup v2 it walks cpu.max from the process's + * cgroup up to the root; like libuv, the last level with a numeric limit + * (the outermost) decides. Returns false when there is no answer. */ +static bool scr_os_cgroup_cpu_quota(long long *quota) { + char cgroup[1024]; + if (!scr_os_slurp("/proc/self/cgroup", cgroup, sizeof cgroup)) return false; + if (strncmp(cgroup, "0::/", 4) == 0) { + static const char mount[] = "/sys/fs/cgroup"; + const char *trimmed = cgroup + 4; + int size = (int)strcspn(trimmed, "\n"); + char path[256], file[256], buf[1024], value[16]; + snprintf(path, sizeof path, "%s/%.*s/cgroup.controllers", mount, size, trimmed); + if (!scr_os_slurp(path, buf, sizeof buf)) return false; + snprintf(path, sizeof path, "%s/%.*s", mount, size, trimmed); + while (strncmp(path, mount, sizeof mount - 1) == 0) { + long long limit, period; + snprintf(file, sizeof file, "%s/cpu.max", path); + if (scr_os_slurp(file, value, sizeof value) && strncmp(value, "max", 3) != 0 && + sscanf(value, "%lld %lld", &limit, &period) == 2 && period != 0) + *quota = limit / period; + char *slash = strrchr(path, '/'); + if (slash == NULL || strcmp(path, mount) == 0) break; + *slash = '\0'; + } + return true; + } + char *cpu = strstr(cgroup, ":cpu,"); + if (cpu == NULL) return false; + cpu += sizeof ":cpu," - 1; + int size = (int)strcspn(cpu, "\n"); + char path[256], buf[64]; + long long per_period, period; + snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/cpu.cfs_quota_us", size, cpu); + if (!scr_os_slurp(path, buf, sizeof buf) || sscanf(buf, "%lld", &per_period) != 1) return false; + snprintf(path, sizeof path, "/sys/fs/cgroup/%.*s/cpu.cfs_period_us", size, cpu); + if (!scr_os_slurp(path, buf, sizeof buf) || sscanf(buf, "%lld", &period) != 1 || period == 0) + return false; + *quota = per_period / period; + return true; +} +#endif + +double scr_os_available_parallelism(void) { + /* uv_available_parallelism (libuv 1.51, Node's os.availableParallelism): + * the CPUs this process may run on (Linux: its affinity mask, then any + * smaller cgroup CPU quota; Darwin: hw.activecpu, hw.logicalcpu, hw.ncpu), + * else the online processors; at least 1. */ + long rc = -1; +#if defined(__linux__) + cpu_set_t set; + memset(&set, 0, sizeof set); + if (sched_getaffinity(0, sizeof set, &set) == 0) rc = CPU_COUNT(&set); +#elif defined(__APPLE__) + static const char *const mib[] = {"hw.activecpu", "hw.logicalcpu", "hw.ncpu"}; + for (size_t i = 0; i < sizeof mib / sizeof mib[0]; i++) { + int nprocs = 0; + size_t len = sizeof nprocs; + if (sysctlbyname(mib[i], &nprocs, &len, NULL, 0) == 0 && len == sizeof nprocs && nprocs > 0) { + rc = nprocs; + break; + } + } +#endif + if (rc < 0) rc = sysconf(_SC_NPROCESSORS_ONLN); +#if defined(__linux__) + /* A quota below one CPU truncates to 0 and is ignored, not clamped to 1: + * libuv (1.51 through 1.52) applies only `quota > 0 && quota < rc`, so + * Node then answers the affinity count. */ + long long quota = 0; + if (scr_os_cgroup_cpu_quota("a) && quota > 0 && quota < rc) rc = (long)quota; +#endif + return rc < 1 ? 1 : (double)rc; +} + double scr_os_totalmem(void) { /* Total physical memory in bytes (sysconf pages × page size — Darwin * and Linux both answer _SC_PHYS_PAGES). */ diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 913f6e82dd..0649814e3e 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4085,6 +4085,7 @@ ScrStr *scr_os_tmpdir(void); ScrStr *scr_os_release(void); /* uname(2) release — +1 fresh */ ScrStr *scr_os_type(void); /* uname(2) sysname — +1 fresh */ double scr_os_totalmem(void); /* total physical memory, bytes */ +double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */ /* os.userInfo()'s field trio (pw_name / pw_shell / pw_dir — the passwd * home, not the $HOME cascade). +1 fresh; abort on lookup failure. */ ScrStr *scr_os_user_name(void); diff --git a/tests/corpus/4598-os-available-parallelism.ts b/tests/corpus/4598-os-available-parallelism.ts new file mode 100644 index 0000000000..31a89a24c5 --- /dev/null +++ b/tests/corpus/4598-os-available-parallelism.ts @@ -0,0 +1,13 @@ +// os.availableParallelism() — differential against the host's own Node: +// libuv's uv_available_parallelism (the CPU affinity mask, then a smaller +// cgroup CPU quota on Linux; hw.activecpu on Darwin), so both processes +// see the same count. +import * as os from "node:os"; +import { availableParallelism } from "node:os"; + +const n = os.availableParallelism(); +console.log(typeof n, Number.isInteger(n), n >= 1); +console.log(n === availableParallelism()); +console.log(n); +// A thread count derived from it, like a parallel tool's default. +console.log(Math.max(1, Math.min(n, 64)) === n || n > 64);