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 毫秒等值,但它们遇到无法完全精确时的处理方式不同。
- wall clock 的 不安全当前时间
-
总是尽可能贴近用户直观时间。由于计算机有时会变慢、变快或丢失时间,wall clock 有时需调整,这意味着不安全当前时间可能减少,因此不可靠于性能测量或事件顺序记录。Web 平台与 [ECMA-262] 的 time 共享同一 wall clock。
- monotonic clock 的 不安全当前时间
-
永远不会减少,因此不会被系统时钟调整影响。单调时钟仅存在于一次用户代理(user agent)运行中,无法用于比较不同运行中的事件。
由于单调时钟无法调整为用户时间,应将其用于测量而非用户可见时间。与用户通信任何时间值时应使用 wall clock。
用户代理可在浏览器重启、进入隔离浏览(如隐私模式)或创建无法与现有 settings object 通信的环境设置对象时,为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 的 当前壁钟时间 计算如下:
若使用自 壁钟 的 时刻,需确保设计兼容用户前移或后移时钟的情形。
壁钟的 时刻 在 JavaScript 中可这样表示:
传入 Unix 纪元
到该时刻的毫秒数,作为 Date
构造函数参数;或传入自 Unix 纪元
起的纳秒数给
Temporal.Instant
构造器。[Temporal]
避免在不同计算机间发送类似的表示方法,否则可能暴露用户的时钟偏差,这是一种追踪向量。 更推荐采用类似单调时钟时刻方式,即发送两时刻间的时长。
3.1. 示例
4. 时间原点
Unix 纪元 是 壁钟上 对应 1970 年 1 月 1 日 00:00:00 UTC 的 时刻。
任何可互相通信的 环境设置对象组, 拥有一个 Unix 纪元的估算单调时间, 它是 单调时钟上的一个 时刻, 其值通过以下步骤初始化:
- 令 wall time 为 壁钟 的 不安全当前时间。
- 令 monotonic time 为 单调时钟的 不安全当前时间。
- 令 epoch time =
monotonic time - (wall time - Unix 纪元) - 将 Unix 纪元的估算单调时间 初始化为 以 epoch time 作为参数调用 粗化时间 的结果。
性能测量报告从相关 环境设置对象 初始化早期某 时刻 到当前的 时长。 此 时刻 存储于该 settings object 的 时间原点中。
若要 获取时间原点时间戳,给定 全局对象 global,返回 时长, 按如下步骤:
-
令 timeOrigin 为 global 的 相关设置对象的 时间原点。
在
Window上下文,该值表示 导航开始时刻。 在Worker和ServiceWorker内容中,该值表示 worker 启动时刻。 [SERVICE-WORKERS] - 返回 时长, 从 Unix 纪元的估算单调时间 到 timeOrigin。
获取时间原点时间戳 返回的值大致等于 global 的 Unix 纪元 之后其 时间原点 的时刻。该值与在时间原点执行 Date.now() 可能不同, 因为前者相对于不受系统/用户时钟调整、时钟漂移等影响的 单调时钟 记录。
- 令 coarse time 为调用 粗化时间 ,以 time 和 global 的 相关设置对象的 跨源隔离能力 为参数。
- 返回 相对高精度粗化时间,参数是 coarse time 和 global。
当前高精度时间, 给定 全局对象 current global,须返回 以 不安全共享当前时间 和 current global 为参数调用 相对高精度时间 的结果。
粗化共享当前时间 给定可选布尔 crossOriginIsolatedCapability(默认为 false),须返回以 不安全共享当前时间 和 crossOriginIsolatedCapability 调用 粗化时间 的结果。
不安全共享当前时间 应返回 单调时钟的不安全当前时间。
5. DOMHighResTimeStamp 类型别名
DOMHighResTimeStamp
类型用于存储以毫秒为单位的时长。根据上下文,它可以表示自某个基准时刻(如时刻)、时间原点或Unix 纪元后持续的时长对应的时刻。
typedef double ;DOMHighResTimeStamp
DOMHighResTimeStamp
应当能以足够精度(毫秒)表示时间,既能进行测量,又能防止计时攻击——详见 § 9.1 时钟分辨率。
DOMHighResTimeStamp
是 double,
因此它只能以有限分辨率表示基于 epoch 的时间——即 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 属性必须返回 获取时间原点时间戳 (以 相关全局对象 为参数)的时长的毫秒数。
访问
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
类型的最小分辨率应当足够粗糙以防御攻击。
必要时,用户代理应在 粗化时间处理模型中将 time resolution 设为更高,以减少因体系结构/软件约束或其它考虑给隐私与安全带来的风险。
为减轻上述攻击,用户代理可使用任何其认为必要的技术。这些技术的部署可能依赖于浏览器体系结构、用户设备、内容类型及其跨源读取数据的能力等各种因素。
可能的缓解手段包括:
- 降低分辨率。
- 增加抖动。
- 滥用检测及/或 API 调用节流。
要彻底防止此类定时侧信道攻击实际上几乎不可能:要么所有操作都在和任何敏感信息取值无关的固定时间内完成,要么应用必须完全隔离计时原语(时钟、定时器、计数器等)。这两者都因实现复杂度及对应用/浏览器性能与响应性的损害而不现实。
9.2. 时钟漂移
本规范还定义了能提供时间原点零时刻亚毫秒分辨率的 API,它要求并暴露给应用一个单调时钟,并需覆盖所有浏览器上下文。单调时钟不必与物理时间一致,但推荐与[ECMA-262] 的time定义进行校准,以免增加新的指纹信息——例如应用可轻松获得此时间,若暴露新的逻辑时钟将带来新信息。
即便如此,单调时钟仍可能暴露更高分辨率的时钟漂移。应用可在同一上下文多次记录日时与单调时间值(Date.now() 与 now()),
观察两者的漂移——譬如因自动或用户时钟调校导致的漂移。有了 timeOrigin
属性,攻击者还可将 时间原点
(由单调时钟报告)
与 时间原点 的当前 day-time 估算(即 `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 对本规范的贡献。