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)
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
Observed
t0andt1are 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
total=1ms avg=1.0µson Perry, while Node reports ~63µs/query and Bun ~45µs/query for the same driver code.measure()helper that doesconst t0 = Date.now(); work(); const elapsed = Date.now() - t0;return 0 for any work under the sampling quantum.@perry/postgresbenchmark 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. Avolatile-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
--experimental-strip-types)