diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 3ef371a..2683dea 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -5360,11 +5360,16 @@ globalThis.XMLSerializer = class XMLSerializer { }; globalThis.performance = globalThis.performance || { now: (function() { - var _lastMs = -1, _sub = 0; + // Monotonically non-decreasing: return the wall-clock offset, but never a + // value below the last one. The previous version added an uncapped per-call + // sub-millisecond bump that could exceed 1.0, so when the millisecond then + // ticked over and the bump reset to 0 the result went backwards. + var _last = 0; return function() { var ms = Date.now() - (globalThis.performance.timeOrigin || 0); - if (ms !== _lastMs) { _lastMs = ms; _sub = 0; } else { _sub += 0.1; } - return ms + _sub; + if (ms > _last) _last = ms; + else _last += 0.001; // same/earlier ms: advance by a tiny epsilon + return _last; }; })(), mark(){}, measure(){}, diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index fdfeb4d..5dca703 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1317,6 +1317,19 @@ mod tests { rt } + #[test] + fn performance_now_is_monotonic_under_bursty_calls() { + let mut rt = setup_runtime("
"); + // Hammer performance.now() so many calls land in the same millisecond and + // the wall clock rolls over repeatedly; the value must never go backwards. + let violations = rt + .evaluate( + "(function(){var prev=-Infinity, bad=0; for(var i=0;i<500000;i++){var t=performance.now(); if(t