1. 引言
本节是非规范性的。ECMAScript 语言规范 [ECMA-262] 将 Date 对象定义为自 1970 年 1 月 1 日 UTC 起的毫秒数时间值。对于大多数用途,这一定义足以满足需求,因为这些值在大约 285,616 年的范围内(自 1970 年 1 月 1 日 UTC 起)提供毫秒精度的时间点。
在实际使用中,这些时间定义会受到时钟漂移以及系统时钟调整的影响。时间值并不总是单调递增的,后续值可能下降或保持不变。
例如,下面的脚本可能会记录计算得到的 duration 为正数、负数或零。
var mark_start= Date. now(); doTask(); // Some task var duration= Date. now() - mark_start;
对于某些任务,这一定义的时间可能不够,因为它
- 没有稳定的单调时钟,结果会受到系统时钟漂移的影响。
- 不提供亚毫秒级的时间分辨率。
本规范不建议更改 Date.now() [ECMA-262] 的行为,因为它在确定当前日历时间值方面非常有用且使用历史悠久。DOMHighResTimeStamp 类型、Performance.now() 方法以及 Performance.timeOrigin 属性能够通过提供单调递增且具亚毫秒分辨率的时间值来解决上述问题。
提供亚毫秒分辨率不是本规范的强制性要求。实现可出于隐私和安全考虑限制所暴露的计时器分辨率,甚至不提供亚毫秒计时器。依赖亚毫秒分辨率的使用场景在此情况下可能无法满足。
1.1. 使用场景
本节是非规范性的。本规范定义了几种不同的能力:它提供基于稳定单调时钟的时间戳,可跨上下文进行比较,并可能具备亚毫秒分辨率。
在性能测量中需要稳定的单调时钟,因为不相关的时钟漂移会扭曲测量结果,使其毫无意义。例如,要准确测量导航到文档、资源获取或脚本执行的耗时,就需要一个具亚毫秒分辨率的单调递增时钟。
在不同上下文之间比较时间戳是必不可少的,例如在 Worker 与主线程之间同步工作,或对这类工作进行测量以创建统一的事件时间线视图。
最后,亚毫秒计时器的需求围绕以下使用场景
- 能够在亚毫秒间隔调度工作。对主线程尤为重要,因为工作可能干扰帧渲染,而帧渲染需在短且规律的间隔内进行,以避免用户可见的卡顿。
- 在计算基于脚本的动画帧率时,开发者需要亚毫秒分辨率才能判断动画是否以 60 FPS 渲染。如果没有亚毫秒分辨率,开发者只能判断动画以 58.8 FPS(1000 ms / 16)或 62.5 FPS(1000 ms / 17)渲染。
- 在收集真实环境中的 JS 代码测量(例如使用 User‑Timing)时,开发者可能希望获取函数的亚毫秒计时,以便及早发现回归。
- 在尝试将音频定位到动画的特定时间点或确保音频与动画完美同步时,开发者需要精确测量已过去的时间。
1.2. 示例
本节是非规范性的。开发者可能希望为整个应用构建时间线,包括来自 Worker 或 SharedWorker 的事件,这些 worker 具有不同的 时间原点。要在同一时间线上显示这些事件,应用可以利用 Performance.timeOrigin 属性将 DOMHighResTimeStamp 转换为统一的时间基准。
// ---- worker.js ----------------------------- // Shared worker script onconnect= function ( e) { var port= e. ports[ 0 ]; port. onmessage= function ( e) { // Time execution in worker var task_start= performance. now(); result= runSomeWorkerTask(); var task_end= performance. now(); } // Send results and epoch-relative timestamps to another context port. postMessage({ 'task' : 'Some worker task' , 'start_time' : task_start+ performance. timeOrigin, 'end_time' : task_end+ performance. timeOrigin, 'result' : result}); } // ---- application.js ------------------------ // Timing tasks in the document var task_start= performance. now(); runSomeApplicationTask(); var task_end= performance. now(); // developer provided method to upload runtime performance data reportEventToAnalytics({ 'task' : 'Some document task' , 'start_time' : task_start, 'duration' : task_end- task_start}); // Translating worker timestamps into document's time origin var worker= new SharedWorker( 'worker.js' ); worker. port. onmessage= function ( event) { var msg= event. data; // translate epoch-relative timestamps into document's time origin msg. start_time= msg. start_time- performance. timeOrigin; msg. end_time= msg. end_time- performance. timeOrigin; reportEventToAnalytics( msg); }
2. 时间概念
2.1. 时钟
时钟(clock)用于跟踪时间的流逝,并能够报告算法步骤执行时的 不安全的当前时间。时钟种类繁多。Web 平台上的所有时钟都尝试实现每真实毫秒计数 1 毫秒的时钟时间,但它们在无法完全精确时的处理方式各不相同。
- The wall clock’s unsafe current time
-
始终尽可能接近用户对时间的感知。由于计算机有时会运行慢、快或丢失时间,它的 wall clock 有时需要进行校准,这意味着 unsafe current time 可能会减小,从而导致它对性能测量或事件顺序记录不可靠。Web 平台共享了一个与 [ECMA-262] time 相同的 wall clock。
- The monotonic clock’s unsafe current time
-
永不减小,因此不会受到系统时钟调整的影响。monotonic clock 仅在单个 user agent 执行期间存在,因此无法用于比较可能在不同执行期间发生的事件。
由于 monotonic clock 不能被调整以匹配用户的时间感知,它应当仅用于测量,而非用户可见的时间。任何面向用户的时间通信都应使用 wall clock。
当浏览器重新启动、启动隐身或类似的隔离浏览会话,或创建无法与任何现有设置对象通信的 environment settings object 时,用户代理可以为 Unix 纪元的估计单调时间 选取一个新值。由此,开发者不应将共享时间戳作为跨所有过去、现在与未来上下文的绝对时间使用;实际情况是,单调属性仅在能够通过提供的消息机制(例如 postMessage(message, options)、BroadcastChannel 等)相互通信的上下文之间保持。
在某些情形(例如标签被置入后台)下,用户代理可能会选择对该上下文中的计时器和周期性回调进行限流,甚至完全冻结。这类限流不应影响单调时钟返回的时间的分辨率或准确性。
2.2. 时刻和持续时间
每个 clock 的 unsafe current time 返回一个 不安全时刻。Coarsen time 将这些 不安全时刻 转换为 粗化时刻(或简称 时刻)。不同时钟得到的 不安全时刻 与 时刻 之间不可比较。
时刻 与 不安全时刻 表示时间点,这意味着它们不能直接作为数字存储。实现通常会将一个 时刻 表示为相对于其他固定时间点的 持续时间,但规范应当直接使用 时刻 本身。
持续时间(duration)是同一 clock 上两个 时刻 之间的间隔。两端都不能是 不安全时刻,从而保证 duration 与 duration 差值能够缓解 § 9.1 时钟分辨率 中的关注点。Durations 使用毫秒、秒等单位衡量。由于所有 clocks 都尝试以相同速率计数,durations 并不关联特定的 clock,并且由同一时钟上的两个 时刻 计算得到的 duration 可以加到另一时钟上的 时刻,从而得到该第二时钟上的另一个 时刻。
duration from a 到 b 的算法如下:
Durations 可隐式用作 DOMHighResTimeStamp。要 隐式将持续时间转换为时间戳,给定一个 duration d,返回 d 中的毫秒数。
3. 规范作者的工具
在单个页面(即单个 environment settings object)内测量时间时,可使用 settingsObject 的 current relative timestamp,定义为 duration from settingsObject 的 time origin 到 settingsObject 的 current monotonic time。该值可通过 duration 的 隐式转换 为 DOMHighResTimeStamp 并直接暴露给 JavaScript。
在单次 UA 执行期间且 environment settings object 的 time origin 不是合适的比较基准时,可使用 时刻,这些时刻基于 environment settings object 的 current monotonic time。environment settings object settingsObject 的 current monotonic time 可通过以下步骤获得:
- 令 unsafeMonotonicTime 为 monotonic clock 的 unsafe current time。
- 返回调用 coarsen time 并传入 unsafeMonotonicTime 与 settingsObject 的 cross‑origin isolated capability 的结果。
时刻 来自 monotonic clock 不能直接在 JavaScript 或 HTTP 中表示。相反,公开两个此类 时刻 之间的 duration。
在跨多个 UA 执行期间测量时间时,可使用 时刻,这些时刻基于 当前粗化墙上时间,或者(如果在 cross‑origin‑isolated 上下文 中需要更高精度)基于 environment settings object 的 current wall time。current coarsened wall time 为调用 coarsen time 并传入 wall clock 的 unsafe current time 的结果。
environment settings object settingsObject 的 current wall time 通过以下步骤获得:
- 令 unsafeWallTime 为 wall clock 的 unsafe current time。
- 返回调用 coarsen time 并传入 unsafeWallTime 与 settingsObject 的 cross‑origin isolated capability 的结果。
在使用来自 wall clock 的 时刻 时,请确保你的设计能够处理用户向前或向后调校系统时钟的情况。
时刻 来自 wall clock 可通过将自 Unix epoch 起的毫秒数传入 Date 构造函数,或将自 Unix epoch 起的纳秒数传入 Temporal.Instant 构造函数。[Temporal]
避免在计算机之间发送相似的时间表示,因为这样会泄露用户的时钟漂移,这是一种 tracking vector。相反,请使用类似于 monotonic clock 的 时刻,在两 时刻 之间发送一个 duration。
3.1. 示例
DOM 事件发生的时间可通过以下方式报告:
- 将 event 的
timeStamp属性初始化为 this 所属的 relevant settings object 的 current relative timestamp。
错误报告的年龄可以使用以下方式计算:
- 将 report 的 generation time 初始化为 settings 的 current monotonic time。
随后
- 令 data 为包含以下键/值对的映射:
- age
- 该数值为 report 的 generation time 与 context 的 relevant settings object 的 current monotonic time 之间的毫秒数,四舍五入至最近的整数。
- ...
多天归因报告的过期处理方式如下:
- 令 source 为一个新的归因源结构,其项目如下:
- ...
- source time
- context 的 current wall time
- expiry
-
从
value["expiry"]解析持续时间字符串
若干天后
- 如果 context 的 current wall time 小于 source 的 source time 加上 source 的 expiry,则发送报告。
4. 时间原点
Unix 纪元(Unix epoch)是 时刻,对应于墙钟(wall clock)的 1970 年 1 月 1 日 00:00:00 UTC。
任何可能相互通信的一组 environment settings objects 都拥有一个 Unix 纪元的估计单调时间,即位于 monotonic clock 上的 时刻,其数值通过以下步骤初始化:
- 令 wall time 为 wall clock 的 unsafe current time。
- 令 monotonic time 为 monotonic clock 的 unsafe current time。
- 令 epoch time 为
monotonic time - (wall time - Unix epoch)。 - 将 estimated monotonic time of the Unix epoch 初始化为调用 coarsen time 并传入 epoch time 的结果。
性能测量会报告一个自相关 environment settings object 初始化早期的 时刻 的 duration。该 时刻 存储在该设置对象的 time origin 中。
要 获取时间原点时间戳,给定一个 global object global,执行以下步骤,返回一个 duration。
-
令 timeOrigin 为 global 的 relevant settings object 的 time origin。
在
Window上下文中,此值代表 导航开始 的时间。 在Worker与ServiceWorker中,此值代表 worker 启动 的时间。[SERVICE-WORKERS] - 返回从 Unix 纪元的估计单调时间 到 timeOrigin 的 duration。
由 获取时间原点时间戳 返回的值大致等同于 Unix epoch 之后 global 的 time origin 发生的时间。它可能与在该时间原点执行的 Date.now() 的返回值不同,因为前者是相对于一个不受系统和用户时钟调整、时钟漂移等影响的 monotonic clock 记录的。
- 令 time resolution 为 100 微秒,或更高的 implementation‑defined 值。
- 如果 crossOriginIsolatedCapability 为 true,则将 time resolution 设为 5 微秒,或更高的 implementation‑defined 值。
- 以 implementation‑defined 的方式对 timestamp 进行粗化并可能抖动,使其分辨率不超过 time resolution。
- 返回 timestamp 作为 moment。
- 令 coarse time 为调用 coarsen time,参数为 time 与 global 的relevant settings object的cross-origin isolated capability 所得到的结果。
- 返回 relative high resolution coarse time 对于 coarse time 和 global。
当前高分辨率时间(current high resolution time)在给定global object current global 时,必须返回relative high resolution time 对应unsafe shared current time 与 current global 的结果。
粗化的共享当前时间(coarsened shared current time)在给定可选布尔值 crossOriginIsolatedCapability(默认 false)时,必须返回调用coarsen time,参数为unsafe shared current time 与 crossOriginIsolatedCapability 的结果。
不安全的共享当前时间(unsafe shared current time)必须返回unsafe current time(不安全当前时间)对应的monotonic clock。
5. DOMHighResTimeStamp 类型定义
DOMHighResTimeStamp 类型用于以毫秒为单位存储duration。根据使用场景,它可能表示moment,即此duration在某个基准moment之后的时间,例如time origin或Unix epoch。
typedef double ;DOMHighResTimeStamp
DOMHighResTimeStamp 应该表示毫秒级时间,精度足以进行测量,同时防止计时攻击——有关更多考量,请参见§ 9.1 时钟分辨率。
DOMHighResTimeStamp 是一个double,因此只能表示相对于纪元的时间——即从Unix epoch到moment的毫秒数——以有限的分辨率表示。对于 2023 年的时间点,这一分辨率约为 0.2 微秒。
6. EpochTimeStamp 类型定义
typedef unsigned long long ;EpochTimeStamp
EpochTimeStamp 表示从Unix epoch到wall clock上某个moment的整数毫秒数,且不计闰秒。使用此类型的规范必须说明这些毫秒数的解释方式。
7. Performance 接口
[Exposed =(Window ,Worker )]interface :Performance EventTarget {DOMHighResTimeStamp ();now readonly attribute DOMHighResTimeStamp ; [timeOrigin Default ]object (); };toJSON
7.1. now() 方法
now() 方法必须返回current high resolution time 的毫秒数,该时间由this 的relevant global object(duration)决定。
对同一time origin 的 now() 调用返回的时间值必须使用相同的monotonic clock。如果两个时间值拥有相同的time origin,则任意两个按时间顺序记录的返回值之间的差值绝不能为负。
7.2. timeOrigin 属性
timeOrigin 属性必须返回duration 的毫秒数,此 get time origin timestamp 为relevant global object(this)所返回的结果。
获取 Performance.timeOrigin 时返回的时间值必须使用与monotonic clock 相同的时钟,而该时钟在所有time origin之间共享,其参照点是[ECMA-262] 中time 的定义——参见§ 9 安全考虑。
7.3. toJSON() 方法
当调用 toJSON() 时,执行[WEBIDL] 中的default toJSON steps。
8. 对 WindowOrWorkerGlobalScope mixin 的扩展
8.1. performance 属性
performance 属性位于接口 mixin WindowOrWorkerGlobalScope 上,允许从global object 访问性能相关的属性和方法。
partial interface mixin WindowOrWorkerGlobalScope { [Replaceable ]readonly attribute Performance ; };performance
9. 安全考虑
本节是非规范性的。9.1. 时钟分辨率
对准确计时信息的访问——无论用于测量还是调度——是许多应用的常见需求。例如,页面上的动画、音频以及其他活动的同步需要高分辨率计时,以提供良好的用户体验。同样,测量能够帮助开发者跟踪关键代码的性能、检测回退等。
然而,相同的准确计时信息有时也可能被攻击者利用,用于推测和推断本来无法看到或访问的数据。例如,缓存攻击、统计指纹识别以及微架构攻击都是隐私和安全的威胁,恶意站点可能利用各种浏览器或应用发起的高分辨率计时数据来区分用户子集、识别特定用户或泄露同进程的其他用户数据——参见[CACHE-ATTACKS] 与[SPECTRE] 以获取更多背景信息。
本规范定义的 API 提供亚毫秒级时间分辨率,比之前的EpochTimeStamp 所提供的毫秒分辨率更为精确。然而,即使没有此新 API,攻击者仍可通过重复执行和统计分析获得高分辨率估计。
为了确保新 API 不会显著提升此类攻击的精度或速度,DOMHighResTimeStamp 类型的最小分辨率应足够不精确,以防止攻击。
在必要时,用户代理应在coarsen time 的处理模型中为 time resolution 设置更高的分辨率值,以应对因架构或软件限制导致的隐私与安全问题,或其他考量。
为缓解此类攻击,用户代理可以部署其认为必要的任何技术。这些技术的部署可能因浏览器架构、用户设备、内容及其读取跨源数据的能力或其他实际因素而异。
这些技术可能包括:
- 降低分辨率。
- 增加抖动。
- 滥用检测和/或 API 调用限流。
彻底消除此类计时侧信道攻击在实践中基本不可能:要么所有操作必须在不随任何机密信息的取值而变化的时间内完成,要么应用必须与所有时间相关原语(时钟、计时器、计数器等)隔离。由于对浏览器和应用开发者带来的复杂性以及对性能和响应性的负面影响,这两种方案都不切实际。
9.2. 时钟漂移
本规范同样定义了一个 API,提供零时原点的亚毫秒级时间分辨率,这需要向应用公开monotonic clock,且该时钟必须在所有浏览器上下文之间共享。单调时钟不需要绑定到物理时间,但建议依据[ECMA-262] 对time 的定义进行设定,以避免向用户泄露新的指纹熵——例如,应用已经可以轻易获取此时间,而公开新的逻辑时钟则会提供额外信息。
然而,即使使用上述机制,monotonic clock 仍可能提供附加的时钟漂移分辨率。如今,应用可以在同一上下文的多个点上记录时间(通过 Date.now() 和 now())并观察它们之间的漂移——例如,由于自动或用户手动的时钟校正。借助 timeOrigin 属性,攻击者还可以将time origin(由monotonic clock 报告)的值与当前的时间估计(即 performance.timeOrigin 与 Date.now() - performance.now() 的差值)进行比较,并可能在更长时间跨度内观察到这两种时钟之间的漂移。
实际情况中,同一应用在多次导航之间也能观察到相同的时钟漂移:应用可以在每个上下文中记录逻辑时间,并使用客户端或服务器端的时间同步机制来推断用户时钟的变化。同样,底层机制如 TCP 时间戳也可能向服务器泄露相同的高分辨率信息,而无需多次访问。因此,此 API 提供的信息不应泄露任何显著或先前不可得的用户熵。
10. 隐私考虑
本节是非规范性的。当前对Document的time origin 的定义会暴露跨源重定向在请求到达文档源之前所消耗的总时间。这会泄露跨源信息,但尚未确定如何在不严重破坏性能度量的前提下进行缓解。
有关讨论请参见 Navigation Timing Issue 160。
11. 致谢
感谢 Arvind Jain、Angelos D. Keromytis、Boris Zbarsky、Jason Weber、Karen Anderson、Nat Duca、Philippe Le Hegaret、Ryosuke Niwa、Simha Sethumadhavan、Todd Reifsteck、Tony Gentilcore、Vasileios P. Kemerlis、Yoav Weiss 与 Yossef Oren 对本工作所作的贡献。