Skip to content

Date.now() does not advance during tight CPU-bound loops #74

@proggeramlug

Description

@proggeramlug

Summary

On Perry 0.5.88, Date.now() returns an identical value before and after a 100M-iteration CPU-bound loop. Node and Bun advance correctly on the same machine and same script.

This breaks any micro-benchmark or per-operation timing that samples Date.now() in a tight loop, and means driver/bench users can't measure hot-path throughput from inside a Perry-compiled program.

Repro

// perry-date-now-repro.ts
const t0 = Date.now();
let sum = 0;
for (let i = 0; i < 100_000_000; i++) {
    sum += 1;
}
const t1 = Date.now();

console.log('iterations: 100,000,000');
console.log('sum (sanity): ' + sum);
console.log('Date.now() delta: ' + (t1 - t0) + 'ms');
console.log('raw t0: ' + t0);
console.log('raw t1: ' + t1);

Observed

Runtime delta raw t0 raw t1
Perry 0.5.88 0ms 1776450625708 1776450625708
Bun 53ms 1776450626201 1776450626254
Node 57ms 1776450626732 1776450626789

t0 and t1 are literally the same integer on Perry — so it's not a subtraction quirk, Date.now() itself is returning a cached/stale value inside the loop.

Expected

Date.now() returns current wall-clock milliseconds on each call, advancing as real time passes — as it does on Bun / Node / V8 / JSC.

Impact

  • Breaks micro-benchmarks: 1000 SELECT-1 queries against Postgres reported as total=1ms avg=1.0µs on Perry, while Node reports ~63µs/query and Bun ~45µs/query for the same driver code.
  • Makes any measure() helper that does const t0 = Date.now(); work(); const elapsed = Date.now() - t0; return 0 for any work under the sampling quantum.
  • Masks real per-query cost in the @perry/postgres benchmark suite — every sample quantizes to 0.5ms or 0.0ms, which we initially mistook for a real performance floor.

Guess at root cause

Looks like Date.now() is being CSE'd / hoisted out of the loop, or the runtime is reading a cached wall-clock value that is only refreshed on I/O / event-loop yield. A volatile-equivalent fence around the syscall (or simply not folding calls at the JIT/LLVM level) should fix it. Worth confirming whether the Rust-side implementation caches the result per tick.

Environment

  • Perry 0.5.88
  • macOS (Darwin 25.4.0)
  • Comparison runtimes: Bun latest, Node 22 (--experimental-strip-types)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions