Skip to content

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

Closed

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)

Activity

  1. proggeramlug commented on Apr 17, 2026

    @proggeramlug
    ContributorAuthor

    Verified on v0.5.91: Date.now() now advances correctly. Same 100M-iter repro:

    Runtime delta
    Perry 0.5.91 32ms (was 0ms on 0.5.88)
    Bun 53ms
    Node 57ms

    Perry is actually faster than V8/JSC on the pure arithmetic loop now. Thanks for the quick fix.

    Separately, while re-running the @perry/postgres driver benchmarks to get accurate per-query numbers, I've hit a new blocker that looks like a Promise-resolution / async-continuation bug. Filing it as a separate issue — handshake messages arrive and are processed (traces show AuthenticationOk → 15×ParameterStatus → BackendKeyData → ReadyForQuery), but the startupGate.resolve(conn) call never wakes the await connect(...) site in the main async function. Reproduces on 0.5.87, 0.5.88, 0.5.89, and 0.5.91, so it predates the asm-sideeffect fix.

  2. added a commit that references this issue on Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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