From 73fbd61bd016eebbf2776dc94c15a26bf39668d6 Mon Sep 17 00:00:00 2001 From: snek Date: Fri, 8 Nov 2024 23:20:24 +0100 Subject: [PATCH] fix: performance.timeOrigin (#26787) `performance.timeOrigin` was being set from when JS started executing, but `op_now` measures from an `std::time::Instant` stored in `OpState`, which is created at a completely different time. This caused `performance.timeOrigin` to be very incorrect. This PR corrects the origin and also cleans up some of the timer code. Compared to `Date.now()`, `performance`'s time origin is now consistently within 5us (0.005ms) of system time. ![image](https://github.com/user-attachments/assets/0a7be04a-4f6d-4816-bd25-38a2e6136926) --- cli/ops/bench.rs | 4 +-- ext/web/02_timers.js | 13 +------- ext/web/15_performance.js | 24 ++++++++++----- ext/web/lib.rs | 6 ++-- ext/web/timers.rs | 65 ++++++++++++++++++++++++++++----------- runtime/js/99_main.js | 5 ++- 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/cli/ops/bench.rs b/cli/ops/bench.rs index 79a7ddef0e..1f4a4bd9b5 100644 --- a/cli/ops/bench.rs +++ b/cli/ops/bench.rs @@ -2,7 +2,6 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::time; use deno_core::error::generic_error; use deno_core::error::type_error; @@ -13,6 +12,7 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_runtime::deno_permissions::ChildPermissionsArg; use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::deno_web::StartTime; use tokio::sync::mpsc::UnboundedSender; use uuid::Uuid; @@ -148,7 +148,7 @@ fn op_dispatch_bench_event(state: &mut OpState, #[serde] event: BenchEvent) { #[op2(fast)] #[number] fn op_bench_now(state: &mut OpState) -> Result { - let ns = state.borrow::().elapsed().as_nanos(); + let ns = state.borrow::().elapsed().as_nanos(); let ns_u64 = u64::try_from(ns)?; Ok(ns_u64) } diff --git a/ext/web/02_timers.js b/ext/web/02_timers.js index 89acaca42b..6058febd59 100644 --- a/ext/web/02_timers.js +++ b/ext/web/02_timers.js @@ -1,12 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { core, primordials } from "ext:core/mod.js"; -import { op_defer, op_now } from "ext:core/ops"; +import { op_defer } from "ext:core/ops"; const { - Uint8Array, - Uint32Array, PromisePrototypeThen, - TypedArrayPrototypeGetBuffer, TypeError, indirectEval, ReflectApply, @@ -18,13 +15,6 @@ const { import * as webidl from "ext:deno_webidl/00_webidl.js"; -const hrU8 = new Uint8Array(8); -const hr = new Uint32Array(TypedArrayPrototypeGetBuffer(hrU8)); -function opNow() { - op_now(hrU8); - return (hr[0] * 1000 + hr[1] / 1e6); -} - // --------------------------------------------------------------------------- function checkThis(thisArg) { @@ -151,7 +141,6 @@ export { clearInterval, clearTimeout, defer, - opNow, refTimer, setImmediate, setInterval, diff --git a/ext/web/15_performance.js b/ext/web/15_performance.js index ea55572781..9e0e310a57 100644 --- a/ext/web/15_performance.js +++ b/ext/web/15_performance.js @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { primordials } from "ext:core/mod.js"; +import { op_now, op_time_origin } from "ext:core/ops"; const { ArrayPrototypeFilter, ArrayPrototypePush, @@ -10,19 +11,34 @@ const { Symbol, SymbolFor, TypeError, + TypedArrayPrototypeGetBuffer, + Uint8Array, + Uint32Array, } = primordials; import * as webidl from "ext:deno_webidl/00_webidl.js"; import { structuredClone } from "./02_structured_clone.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; import { EventTarget } from "./02_event.js"; -import { opNow } from "./02_timers.js"; import { DOMException } from "./01_dom_exception.js"; const illegalConstructorKey = Symbol("illegalConstructorKey"); let performanceEntries = []; let timeOrigin; +const hrU8 = new Uint8Array(8); +const hr = new Uint32Array(TypedArrayPrototypeGetBuffer(hrU8)); + +function setTimeOrigin() { + op_time_origin(hrU8); + timeOrigin = hr[0] * 1000 + hr[1] / 1e6; +} + +function now() { + op_now(hrU8); + return hr[0] * 1000 + hr[1] / 1e6; +} + webidl.converters["PerformanceMarkOptions"] = webidl .createDictionaryConverter( "PerformanceMarkOptions", @@ -90,10 +106,6 @@ webidl.converters["DOMString or PerformanceMeasureOptions"] = ( return webidl.converters.DOMString(V, prefix, context, opts); }; -function setTimeOrigin(origin) { - timeOrigin = origin; -} - function findMostRecent( name, type, @@ -135,8 +147,6 @@ function filterByNameType( ); } -const now = opNow; - const _name = Symbol("[[name]]"); const _entryType = Symbol("[[entryType]]"); const _startTime = Symbol("[[startTime]]"); diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 4935af5bd3..af0fc2c276 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -52,7 +52,8 @@ pub use crate::message_port::Transferable; use crate::timers::op_defer; use crate::timers::op_now; -use crate::timers::StartTime; +use crate::timers::op_time_origin; +pub use crate::timers::StartTime; pub use crate::timers::TimersPermission; deno_core::extension!(deno_web, @@ -84,6 +85,7 @@ deno_core::extension!(deno_web, compression::op_compression_write, compression::op_compression_finish, op_now

, + op_time_origin

, op_defer, stream_resource::op_readable_stream_resource_allocate, stream_resource::op_readable_stream_resource_allocate_sized, @@ -123,7 +125,7 @@ deno_core::extension!(deno_web, if let Some(location) = options.maybe_location { state.put(Location(location)); } - state.put(StartTime::now()); + state.put(StartTime::default()); } ); diff --git a/ext/web/timers.rs b/ext/web/timers.rs index a9ab7c97e4..06444ed34f 100644 --- a/ext/web/timers.rs +++ b/ext/web/timers.rs @@ -4,7 +4,10 @@ use deno_core::op2; use deno_core::OpState; +use std::time::Duration; use std::time::Instant; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; pub trait TimersPermission { fn allow_hrtime(&mut self) -> bool; @@ -17,21 +20,28 @@ impl TimersPermission for deno_permissions::PermissionsContainer { } } -pub type StartTime = Instant; +pub struct StartTime(Instant); -// Returns a milliseconds and nanoseconds subsec -// since the start time of the deno runtime. -// If the High precision flag is not set, the -// nanoseconds are rounded on 2ms. -#[op2(fast)] -pub fn op_now(state: &mut OpState, #[buffer] buf: &mut [u8]) +impl Default for StartTime { + fn default() -> Self { + Self(Instant::now()) + } +} + +impl std::ops::Deref for StartTime { + type Target = Instant; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +fn expose_time(state: &mut OpState, duration: Duration, out: &mut [u8]) where TP: TimersPermission + 'static, { - let start_time = state.borrow::(); - let elapsed = start_time.elapsed(); - let seconds = elapsed.as_secs(); - let mut subsec_nanos = elapsed.subsec_nanos(); + let seconds = duration.as_secs() as u32; + let mut subsec_nanos = duration.subsec_nanos(); // If the permission is not enabled // Round the nano result on 2 milliseconds @@ -40,14 +50,33 @@ where let reduced_time_precision = 2_000_000; // 2ms in nanoseconds subsec_nanos -= subsec_nanos % reduced_time_precision; } - if buf.len() < 8 { - return; + + if out.len() >= 8 { + out[0..4].copy_from_slice(&seconds.to_ne_bytes()); + out[4..8].copy_from_slice(&subsec_nanos.to_ne_bytes()); } - let buf: &mut [u32] = - // SAFETY: buffer is at least 8 bytes long. - unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as _, 2) }; - buf[0] = seconds as u32; - buf[1] = subsec_nanos; +} + +#[op2(fast)] +pub fn op_now(state: &mut OpState, #[buffer] buf: &mut [u8]) +where + TP: TimersPermission + 'static, +{ + let start_time = state.borrow::(); + let elapsed = start_time.elapsed(); + expose_time::(state, elapsed, buf); +} + +#[op2(fast)] +pub fn op_time_origin(state: &mut OpState, #[buffer] buf: &mut [u8]) +where + TP: TimersPermission + 'static, +{ + // https://w3c.github.io/hr-time/#dfn-estimated-monotonic-time-of-the-unix-epoch + let wall_time = SystemTime::now(); + let monotonic_time = state.borrow::().elapsed(); + let epoch = wall_time.duration_since(UNIX_EPOCH).unwrap() - monotonic_time; + expose_time::(state, epoch, buf); } #[allow(clippy::unused_async)] diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 4d391dcc3a..6ddaa1335e 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -27,7 +27,6 @@ const { ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeMap, - DateNow, Error, ErrorPrototype, FunctionPrototypeBind, @@ -642,7 +641,7 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) { removeImportedOps(); - performance.setTimeOrigin(DateNow()); + performance.setTimeOrigin(); globalThis_ = globalThis; // Remove bootstrapping data from the global scope @@ -858,7 +857,7 @@ function bootstrapWorkerRuntime( 7: nodeDebug, } = runtimeOptions; - performance.setTimeOrigin(DateNow()); + performance.setTimeOrigin(); globalThis_ = globalThis; // Remove bootstrapping data from the global scope