1. 引言
本节为非规范性内容。
ECMAScript 语言规范 [ECMA-262] 定义了
Date 对象,
它表示自 1970 年 1 月 1 日 UTC 起的毫秒数型时间值。
对于大部分用途,这样的时间定义已足够,因为这些值可表示自 1970 年 1 月 1 日 UTC 起约 285,616 年内任意时间点的毫秒精度。
实际上,这些时间定义会受到时钟漂移和系统时钟调整的影响。时间值不一定总是单调递增,后续数值可能降低,或停滞不变。
例如,以下脚本计算的 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. 应用场景
本节为非规范性内容。本规范定义了几种不同能力:它通过稳定、单调的时钟提供时间戳、支持上下文间比较,并支持亚毫秒分辨率(如实现允许的话)。
性能测量需要稳定的单调时钟,因为无关的时钟漂移会扭曲测量结果,甚至使其失效。例如,要精确测量导航到 Document、获取资源或脚本执行的耗时,需要具备亚毫秒分辨率的单调递增时钟。
在不同上下文间比较时间戳也非常重要,例如 Worker 与主线程之间协作,或对这些工作进行分析以获取统一事件时间线视图时。
对亚毫秒定时器的需求主要包括以下场景:
- 能够以亚毫秒间隔调度任务。这对于主线程尤为重要,否则任务会影响需要高频且规律的帧渲染,从而造成用户可见的卡顿。
- 计算基于脚本的动画帧率时,开发者需亚毫秒分辨率来判断动画能否 60 FPS 绘制。若无亚毫秒分辨率,只能区分 58.8 FPS(1000ms/17)与 62.5 FPS(1000ms/16)。
- 原地收集 JS 性能(如使用 User-Timing)时,开发者可能希望采集函数的亚毫秒定时数据,以便及早发现性能回退。
- 当希望让音频与动画某关键帧对齐、或确保音频与动画完全同步时,开发者需精确测量用时。
1.2. 示例
本节为非规范性内容。
开发者可能希望构建整个应用的时间线,包括 Worker
或 SharedWorker
上下文产生的事件,这些上下文拥有不同的 时间原点。
要在同一个时间线展示这些事件,应用可借助
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. 时钟
时钟追踪时间的流逝,可报告 算法执行时的不安全当前时间。 时钟种类很多。Web 平台上的所有时钟都试图保证 1 毫秒时钟与真实世界 1 毫秒等值,但它们遇到无法完全精确时的处理方式不同。
由于 单调时钟 无法调整为用户的时间概念,应当用于测量用途而非用户可见时间。任何与用户交流的时间都应当使用挂钟时间。
用户代理在浏览器重启、启动一个隔离的浏览会话(如隐身或类似模式)或创建无法与任何现有设置对象通信的 环境设置对象时,可以选择新的 Unix
纪元的单调时间估算值。因此,开发者不应将共享时间戳作为跨所有过去、现在和未来上下文均保持单调特性的绝对时间;实际上,单调特性只适用于能够通过现有消息机制互相通信的上下文,例如 postMessage(message, options)、BroadcastChannel等。
在某些场景下(如标签页被置于后台),用户代理可能会选择减少该上下文中的定时器和周期回调的运行频率,甚至全部冻结。任何这类节流都不应影响单调时钟所返回时间的分辨率或精度。
2.2. 时刻与时长
每个 时钟 的 不安全当前时间 返回一个 不安全时刻。时间粗化会将这些 不安全时刻 转换成 粗化时刻 或简称 时刻。由不同时钟产生的 不安全时刻 和 时刻 是不可比较的。
时刻和 不安全时刻 表示时间点,也就是说它们不能直接存储为数字。实现通常会把 时刻 表示为距离某个其他固定时间点的 持续时间,但规范应直接处理 时刻本身。
持续时间 表示从一个 时刻到同一 时钟上的另一个时刻的距离。两个端点都不能是 不安全时刻,以确保 持续时间及其差值能够缓解[§ 9.1 时钟分辨率]中的问题。 持续时间以毫秒、秒等单位度量。由于所有 时钟都试图以相同的速度计数,持续时间没有关联的 时钟,并且如果某个 持续时间由一个时钟上的两个 时刻计算得到,则可以加到另一个时钟上的某个 时刻,以得到该第二个 时钟上的另一个 时刻。
从 a 到 b 的持续时间 是通过以下算法得到的:
持续时间可被隐式当作DOMHighResTimeStamp
使用。
隐式转换持续时间为时间戳时,
给定一个
持续时间 d,返回
d 的毫秒数。
3. 规范作者工具
若要在单个页面内(单个
环境设置对象的上下文中)进行时间测量,使用
settingsObject 的 当前相对时间戳,
定义为 从
settingsObject 的 时间原点到 settingsObject 的 当前单调时间 的
持续时间。此值可通过
将持续时间隐式转换为 DOMHighResTimeStamp
直接暴露给 JavaScript。
若要在单个 UA 执行过程中测量时间,但是某个 环境设置对象的 时间原点不适合作为比较的基础时, 可通过 时刻创建, 使用 环境设置对象的 当前单调时间。 一个 环境设置对象 settingsObject 的 当前单调时间 由以下步骤得到:
- 令 unsafeMonotonicTime 为 单调时钟 的 不安全当前时间。
- 返回调用 粗化时间,参数为 unsafeMonotonicTime 以及 settingsObject 的 跨源隔离能力。
时刻 来自 单调时钟 无法在 JavaScript 或 HTTP 中被直接表示。 相反,应公开两个此类 持续时间 之间的 时刻。
为了在多个 UA 执行之间测量时间,创建 时刻 ,使用 当前粗化墙钟时间 或(如果在 跨源隔离上下文 中需要更高精度) 使用 环境设置对象 的 当前墙钟时间。 当前粗化墙钟时间 是调用 粗化时间 并以 墙钟 的 不安全的当前时间 作为参数所得的结果。
环境设置对象 settingsObject 的 当前挂钟时间 由以下步骤得到:
当使用来自墙钟的时刻时,请确保你的 设计考虑到用户将其时钟向前或向后调整的情况。
来自墙钟的时刻,
可以通过将该时刻距离
Unix 纪元
的毫秒数,传入 Date
构造函数的方式在 JavaScript 中表示,或者将
该时刻距离
Unix 纪元的纳秒数,
传入 Temporal.Instant
构造函数也可。[Temporal]
避免在计算机之间传递类似的表示,因为这样会暴露用户的时钟偏差, 这是一种跟踪向量。 应采用类似单调时钟 时刻 的方式,传递两个时刻之间的持续时间。
3.1. 示例
4. 时间原点
Unix 纪元(epoch) 是 时刻,对应于 墙钟上的 1970 年 1 月 1 日 00:00:00 UTC。
每一组可能以任何方式通信的 环境设置对象,都具有一个 Unix 纪元的估算单调时间(estimated monotonic time of the Unix epoch),即对应于 时刻 的 单调时钟上的一个瞬间, 其值通过以下步骤初始化:
- 令 wall time 为 墙钟 的 不安全当前时间。
- 令 monotonic time 为 单调时钟 的 不安全当前时间。
- 令 epoch time 为
monotonic time - (wall time - Unix 纪元) - 将 Unix 纪元的估算单调时间 初始化为调用 coarsen time 并以 epoch time 作为参数的结果。
性能度量会报告一个 持续时间,该持续时间从相关 时刻 的初始化早期 开始,属于某个 环境设置对象。 该 时刻 被存储在该设置对象的 time origin 中。
若要 获取 time origin 时间戳,给定一个 全局对象 (global object) global,按照下列步骤返回一个 持续时间:
-
令 timeOrigin 为 global 的 相关设置对象(relevant settings object) 的 time origin。
在
Window上下文中,此值表示 导航已开始的时间。在Worker和ServiceWorker内容中,此值表示 worker 被启动的时间。[SERVICE-WORKERS] - 返回 持续时间,从 Unix 纪元的估算单调时间 到 timeOrigin。
get time origin timestamp 返回的值大致等于 global 的 Unix 纪元以来time origin发生的时间。它可能与在 time origin 时调用 Date.now() 返回的值不同,因为前者是基于不受系统及用户时钟调整、时钟偏移等影响的 单调时钟记录的。
- 令 coarse time 为调用 coarsen time,参数为 time 和 global 的 相关设置对象的 cross-origin isolated capability 的结果。
- 返回 relative high resolution coarse time,参数为 coarse time 和 global。
current high resolution time(当前高精度时间),给定一个 全局对象 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(不安全的共享当前时间) 必须返回 单调时钟 的 不安全当前时间。
5. DOMHighResTimeStamp 类型别名
DOMHighResTimeStamp
类型用于存储以毫秒为单位的持续时间。
根据其上下文,它还可以表示某个时刻,
即该持续时间
加在某个基准时刻(如time origin 或 Unix 纪元)后的时间点。
typedef double ;DOMHighResTimeStamp
DOMHighResTimeStamp
应当以毫秒为单位表示时间,精度应足够保证可测量性,同时防范计时攻击 —
参见§ 9.1 时钟分辨率获取更多注意事项。
DOMHighResTimeStamp
是一个 double,
所以它只能以有限分辨率表示相对于纪元的时间——即从Unix
纪元
到某个时刻的毫秒数。
对于 2023 年的时刻,其分辨率约为 0.2 微秒。
6. EpochTimeStamp 类型别名
typedef unsigned long long ;EpochTimeStamp
EpochTimeStamp
表示从 Unix 纪元 到 挂钟时间上某个 时刻的整数毫秒数,
不包含闰秒。使用该类型的规范会定义毫秒数的具体解释方式。
7. Performance 接口
[Exposed =(Window ,Worker )]interface :Performance EventTarget {DOMHighResTimeStamp ();now readonly attribute DOMHighResTimeStamp ; [timeOrigin Default ]object (); };toJSON
7.1.
now() 方法
now() 方法必须返回以毫秒为单位的, 当前高分辨率时间 (以 this 的 相关全局对象 为参数), 得到的 持续时间。
当在同一个 时间原点的
Performance
对象上调用 now()
方法时,返回的时间值必须使用同一个
单调时钟。
若任意两个按时间顺序记录的、
来自 now()
方法的时间值具有同一 时间原点,
那么这两个时间值之差绝不能为负值。
7.2.
timeOrigin 属性
timeOrigin 属性必须返回 获取时间原点时间戳为 相关全局对象(即 this)返回的 持续时间的毫秒数。
当获取
Performance.timeOrigin
时,必须使用同一
单调时钟
——即所有 时间原点共享的时钟,其参考点为 [ECMA-262] 的
time 定义。详见 [§ 9 安全注意事项]。
7.3.
toJSON() 方法
调用 toJSON() 时,应执行 [WEBIDL] 的 默认 toJSON 步骤。
8.
WindowOrWorkerGlobalScope mixin 的扩展
8.1.
performance 属性
performance
属性定义在接口 mixin
WindowOrWorkerGlobalScope
上,可从全局对象
访问与性能有关的属性与方法。
partial interface mixin WindowOrWorkerGlobalScope { [Replaceable ]readonly attribute Performance ; };performance
9. 安全性考虑
本节为非规范性内容。9.1. 时钟分辨率
许多应用在测量和调度时都需要精确计时。例如,协调页面上的动画、声音等活动需要高分辨率时间来提升用户体验。测量同样能帮助开发者追踪关键代码组件的性能,发现性能回退等。
但同样的精确计时能力也有被攻击者用于恶意目的的风险,攻击者可能借此推测、推断本不应可见或可访问的数据。例如,缓存攻击、指纹统计分析与微架构攻击都涉及利用浏览器或应用各类高分辨率计时信息来分辨用户子集、标识具体用户、揭示同进程的无关数据 —— 见 [CACHE-ATTACKS] 及 [SPECTRE]。
本规范定义的 API 可提供亚毫秒分辨率时间,比分辨率仅有毫秒的 EpochTimeStamp
更精确。但即便无该 API,攻击者也可通过反复执行与统计分析获得高分辨率估算值。
为了防止该新 API 显著提升此类攻击的精度与速度,
DOMHighResTimeStamp
类型的最小分辨率应当足够粗糙以防御攻击。
在必要时,用户代理应将更高精度值设为 时间分辨率,用于 粗化时间 的处理模型, 以应对架构或软件限制等带来的隐私和安全问题,或其他需要关注的事项。
为减轻上述攻击,用户代理可使用任何其认为必要的技术。这些技术的部署可能依赖于浏览器体系结构、用户设备、内容类型及其跨源读取数据的能力等各种因素。
可能的缓解手段包括:
- 降低分辨率。
- 增加抖动。
- 滥用检测及/或 API 调用节流。
要彻底防止此类定时侧信道攻击实际上几乎不可能:要么所有操作都在和任何敏感信息取值无关的固定时间内完成,要么应用必须完全隔离计时原语(时钟、定时器、计数器等)。这两者都因实现复杂度及对应用/浏览器性能与响应性的损害而不现实。
9.2. 时钟漂移
本规范还定义了一个 API,能提供时间原点零时点的亚毫秒级时间分辨率,并要求且向应用公开 单调时钟,且必须在所有浏览器上下文间共享。 单调时钟无需与物理时间绑定, 但推荐依据 [ECMA-262] 的 time 定义进行设置, 以避免暴露关于用户的新的指纹熵——例如该时间已可被应用轻易获得, 若公开新的逻辑时钟则会泄露新的信息。
不过,即使采用上述机制,单调时钟仍可能提供额外的
时钟漂移分辨率。
目前,应用可在同一上下文中多次记录当天时间和单调时间值(通过 Date.now() 和
now()),
并观测它们之间的漂移——例如因自动或用户时钟调整。
通过 timeOrigin
属性,攻击者还能将 时间原点(由
单调时钟报告)
与 时间原点的当前当天时间(即
`performance.timeOrigin` 与 `Date.now() - performance.now()` 的差值)进行比较,
还可以在更长时间周期内观测这些时钟间的漂移。
实际上,应用可以跨多次导航观测到同样的时钟漂移: 应用可在每个上下文记录逻辑时间,并借助客户端或服务器时间同步机制推断用户时钟的变化。 类似地,底层机制如 TCP 时间戳,也可向服务器揭示同样的高分辨率信息,无需多次访问。 因此,该 API 提供的信息不应暴露有关用户的重大或此前不可获得的熵信息。
10. 隐私性考虑
本节为非规范性内容。
当前针对 Document
的时间原点定义会暴露
跨源重定向带来的总耗时(直到请求到达文档源为止)。
这暴露了跨源信息,
但如何在不对性能指标造成重大影响的前提下缓解,尚无定论。
相关讨论见 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 对本规范的贡献。