诗号:杀生为护生,斩业非斩人。

本方讲述的是 performance api. 本文所有内容都来自 w3c 标准,英语不好翻译的很蹩脚 ;(,如果不关心概念性东西可直接跳转到 Performance APIMDN API Doc

简介

ECMA-262 标准中定义了 Date 对象做为时间值,代表着从 1970-01-01(UTC) 开始的 毫秒数。从 1970-01-01 开始大约 285,616 年的时间内毫秒数在大部分情况下是足够用 了。DOMTimeStamp 有相同的定义[WEBIDl]。

在实际的实践和使用当中,这些时间的定义都受制于时钟偏移(clock skew)和系统时钟的调 整。时间的值并不总是单调递增的并且随后的值可能递增或保持原值。

比如,下面的代码中 duration 的值有可能是一个正数,负数或零:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function doSth() {
  let i = 0
  while (true) {
    if (++i>100) break
  }
}
var mark_start = Date.now()
doSth()
var duration = Date.now() - mark_start
console.log(duration)
0
undefined

在特定的情况下时间的定义明显不能够满足实际情况:

  1. 没有一个稳定的单调时钟,更何况还要依赖系统时钟偏移

  2. 没有提供亚毫秒级(sub-millisecond)的时间精度

Date.now() 就存在上面的问题,但是它依旧在大部分情况下可以使用。

只不过如果需要精确到亚毫秒级可能就不太合适,此时可以考虑使用 DOMHighResTimeStamp 类型, Performance.now()Performance 接口的属性 Performance.timeOrigin 去解决 这这些问题,因为它们提供了 亚毫秒级精度单调递增 的时间值 。

NOTE

亚毫秒级精度并非标准的一部分。实现方可以依据它们的隐私和安全性理由选择性的去暴露 对时间的精度的限制,以及是否要公开亚毫秒级时间。所以当用户使用依赖亚毫秒级时间的特 性的时候可能并不总能如愿以偿(不是标准,效果不一定好)。

可能使用情况(use-cases)

在有关性能度量的应用方面对于亚毫秒级精度是非常有必要的,例如:试图精准的测量 Document 操作消耗的时间,资源请求或脚本执行的时间的时候。

当在主线程和一个 Worker 之间同步工作的时候或者是为了创建一个统一的事件时间表去测 量这些工作时候,在不同的上下文之间比较时间戳是必不可少的。

最后,对于亚毫秒级时间的应用主要是围绕下面几种情况:

Ability to schedule work in sub-millisecond intervals. That is particularly important on the main thread, where work can interfere with frame rendering which needs to happen in short and regular intervals, to avoid user-visible jank.

  • 要求亚毫秒级的间隔中调试工作的能力。这在主线程中尤为重要,对于有些事情需要做到 直接干涉帧渲染,且需要在很短且有规律的时间间隙中不断触发还不能造成用户视图阻塞

    比如: react 中的 scheduler 对 Performance.now() 的应用,因为它要求在一帧渲染 时间内找出多余的空隙去执行 taskQueue 中的任务。

  • 当计算基于脚本动画的帧率的时候,开发者会需要亚毫秒级的精度来决定这个动画是不是 60FPS。在没有来毫秒级精度的情况下,开发者只能判断动画帧率是 58.8FPS(1000ms/16) 还是 62.6FPS(1000ms/17),也就是说要做到 60FPS 的动画必需要用 sub-millisecond。

  • JS 代码执行时间统计?

    When collecting in-the-wild measurements of JS code (e.g. using User-Timing), developers may be interested in gathering sub-milliseconds timing of their functions, to catch regressions early.

  • 在试图在指定时间点听音乐或为了确认音频和动画能完美同步的时候,开发者将需要精准 的测量已经播放的时间。

示例

开发者可能希望组织整个应用的时间轴的,包括来自拥有不同时间域WorkderSharedWorkder。为了能在同一时间轴上显示这些事件,应该可以借助 Performance.timeOrigin 属性来翻译 DOMHighResTimeStamps

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ---- 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);
}

Worker 在这不进行展开了,想了解更多可前往 JavaScript API - Worker标准文档 Worder

时间域(Time Origin)

时间域的值:

  1. 如果全局对象是 Window, time origin 必须等于:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    let timeOrigin
    if (previousDocument == null) {
      timeOrigin = context.firstCreated.time
    } else if (previousDocument && confirmDialogDisplayed) {
      when promptUnload { // 提示 unload 事件的时候
        timeOrigin = user.confirm.time
      }
    } else if (window.document.newest.loading) {
      if (navigation.responsible) { // 开始可以响应了
        timeOrigin = performance.now()
      }
    }
    
  2. 如果全局对象是 Workder 环境下的 WorkderGlobalScope 对象时, time origin 的 值是 workder 创建时的 official moment of creation

  3. 最后 timeOrigin = undefined

所以, timeOrigin 与具体的环境有关,WindowWorker

Window 环境下受到 unload 和 document 加载时间有关

Workder 是当前 workder 上下文创建的时间有关。

time origin 时间戳的获取步骤(伪码):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function getTimeOrigin() {
  let globalScope = typeof window !== undefined ? window : workderScope
  assert(globalScope !== undefined)

  // 代表高精度时间,共享的单调时钟值为 0
  let t1 = DOMHighResTimeStamp
  // 代表高精度时间,共享的单调时钟值为 0(在全局的 time origin 下)
  let t2 = DOMHighResTimeStamp
  let total = t1 + t2

  return CoarsenTime(
    total,
    globalScope.relevantSettingsObject.crossOriginIsolatedCapability
  )
}

关键词:

  1. DOMHighResTimeStamp DOM 中高精度的时间戳

  2. shared monotonic clock 共享单调时钟

  3. coarsen time

    一种时间算法,提供了一个 DOMHighResTimeStamp 时间戳和一个可选的布尔类型值 crossOriginIsolatedCapability 默认值 false

DOMHighResTimeStamp 类型定义

DOMHighResTimeStamp 用来存储一个 milliseconds 值,一个 time origin, shared monotonic clock 或两个 DOMHighResTimeStamps 之间时长的时间值

1
typedef double DOMHighResTimeStamp

Performance 接口

接口定义:

1
2
3
4
5
6
[Exposed=(Window,Worker)]
interface Performance : EventTarget {
    DOMHighResTimeStamp now();
    readonly attribute DOMHighResTimeStamp timeOrigin;
    [Default] object toJSON();
};

方法:

  1. performance.now(), 返回当前的高精度的时间

  2. performance.toJSON()

  3. performance.mark(name)

  4. performance.clearMarks()

  5. performance.measure(name, [startMark|undefined], endMark)

  6. performance.clearMeasures()

  7. performance.getEntries()

  8. performance.getEntriesByName(name)

  9. performance.getEntriesByType(type), type: 'mark' 或 'measure'

  10. performance.setResourceTimingBufferSize(maxSize) 设置 timing buffer 的大小, 如果满了会触发 resoucetimeingbufferfull 事件

  11. performance.clearResourceTimings()

属性:

  1. performance.timing, 当前性能数据对象

  2. performance.timeOrigin, 值如 Time Origin 伪码所示

事件:

  1. resoucetimeingbufferfull, timing buffer 满了之后触发的事件

相关构造函数:

  1. PerformanceEntry

  2. PerformanceResourceTiming

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var start_hrt = performance.now()
var start_date = Date.now()
setInterval(() => {
  var end_hrt = performance.now()
  var end_date = Date.now()
  log('...')
}, 2000)

// test event, performance 继承了 EventTarget
var didHandle = false
performance.addEventListener('testEvent', () => ( didHandle = true ), { once: true })
performnace.dispatchEvent(new Event('testEvent'))

performance.timing 中属性表:

namedesc
redirectEnd - redirectStart重定向耗时
domainLookupEnd - domainLookupStartDNS查询耗时
connectEnd - connectStartTCP链接耗时
connectEnd - connectStartHTTP请求耗时
responseEnd - responseStartHTTP请求耗时
domComplete - domInteractive解析dom树耗时
responseStart - navigationStart白屏时间
domContentLoadedEventEnd - navigationStartDOMready时间
loadEventEnd - navigationStartonload时间,也即是onload回调函数执行的时间。

MDN:Performance

完整测试源码(基于 Vue + ElementPlus) 测试:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
(function() {
  const E = ElementPlus
  const { reactive } = Vue
  const { ElMessage: Message, ElRow, ElCol, ElTooltip } = ElementPlus
  Vue.createApp({
    template: `
<p style="color:blue">每隔两秒取分别取一次 performance.now() 和 Date.now(): </p>
  <el-form :model="times" class="border">
  <el-form-item label="start performance:">{{times.startHrt}}</el-form-item>
  <el-form-item label="start date:">{{times.startDate}}</el-form-item>
  <el-form-item label="end performance:">{{times.endHrt}}</el-form-item>
  <el-form-item label="end date:">{{times.endDate}}</el-form-item>
  <el-form-item label="duration performance:">{{times.deltaHrt}}</el-form-item>
  <el-form-item label="duration date:">{{times.deltaDate}}</el-form-item>
  <el-form-item label="status" style="color:red">{{status ? '运行' : '暂停'}}中...</el-form-item>
  <el-form-item label="自定义事件">
    <el-input style="width:200px" placeholder="请输入自定义事件名" v-model="customEvent.data[0].label"/>
    <el-button type="primary" plain @click="add">添加事件</el-button>
    <el-button type="primary" plain @click="remove">移除事件</el-button>
    <el-button type="primary" plain @click="trigger">触发{{customEvent.name}}事件</el-button>
  </el-form-item>
</el-form>
<el-row>
<el-tree :data="customEvent.data"
          node-key="id"
          default-expand-all
          :render-content="renderTreeContent"/>
</el-row>
<el-button type="primary" @click="start" >开始</el-button>
<el-button type="primary" @click="stop">暂停</el-button>
<br/>
<div>
<el-card style="margin: 20px 0">
  <template #header>
    各属性所代表的含义:
  </template>
  <el-form :data="comments" label-width="220px">
    <el-form-item v-for="(value, prop) in comments" :label="prop">
    {{value}}
    </el-form-item>
  </el-form>
</el-card>
</div>
`,
    setup() {
      const times = reactive({
        startHrt: performance.now(),
        startDate: Date.now(),
        deltaHrt: 0,
        endHrt: 0,
        endDate: 0,
        deltaDate: 0,
      })
      const status = Vue.ref(true)
      const message = Vue.ref('')
      const customEvent = reactive({
        data: [{ id: 0, label: 'testEvent', children: [] }],
      })
      const eventName = Vue.computed(() => customEvent.data[0].label)

      let timer = null
      function start() {
        status.value = true
        timer = setInterval(() => {
          times.endHrt = performance.now()
          times.endDate = Date.now()
          times.deltaHrt = times.endHrt - times.startHrt
          times.deltaDate = times.endDate - times.startDate
        }, 2000)
      }
      function stop() {
        status.value = false
        clearInterval(timer)
      }

      const handler = (e) => {
        console.log(e.target.timing, '100');
        customEvent.data[0].children = jsonToTreeData(e.target)

      }
      const oldName = Vue.ref('')
      function add() {
        if (eventName.value) {
          if (oldName.value) {
            remove(oldName.value)
          }
          oldName.value = eventName.value
          performance.addEventListener(eventName.value, handler)
          Message({
            message: '添加事件 ' + eventName.value + ' 成功',
            type: 'success'
          })
        }
      }

      const comments = {
        navigationStart: '同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同。',
        unloadEventStart: '上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。',
        unloadEventEnd: '和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。',
        redirectStart: '第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。',
        redirectEnd: '最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0. ',
        fetchStart: '浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。',
        domainLookupStart: 'DNS 域名查询开始的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。',
        domainLookupEnd: 'DNS 域名查询完成的时间.如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等',
        connectStart: 'HTTP(TCP) 域名查询结束的时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。',
        connectEnd: 'HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。',
        requestStart: '返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。',
        responseStart: '返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。',
        responseEnd: '返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。',
        domLoading: '当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。',
        domInteractive: '当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。',
        domContentLoadedEventStart: '当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。',
        domContentLoadedEventEnd: '当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。',
        domComplete: '当前文档解析完成,即Document.readyState 变为 complete 且相对应的readystatechange 被触发时的时间戳',
        loadEventStart: 'load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。',
        loadEventEnd: '当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.',
      }
      function renderTreeContent(h, { node, data }) {
        const hasValue = data.value !== null
        return h(ElRow, {
          style: 'width: 500px'
        }, {
          default: () => [
            h(ElCol, { span: 12 }, {
              default: () => h(ElTooltip, {
                placement: 'top-start',
                effect: "dark",
                content: comments[node.label] || 'null'
              }, {
                default: () => h('span', {
                  style: 'color: #92278f'
                }, node.label + (hasValue ? ':' : ''))
              })
            }),
            hasValue ? h(ElCol, { span: 12 }, {
              default: () => h('span', {
                style: 'color:#25aae2'
              }, data.value)
            }) : null,
          ]
        })
      }

      function remove(name) {
        if (name || eventName.value) {
          performance.removeEventListener(name || eventName.value, handler)
          Message({
            message: '移除事件 ' + eventName.value + ' 成功',
            type: 'warning'
          })

        }
      }

      function trigger() {
        performance.dispatchEvent(new Event(eventName.value))
      }

      Vue.onMounted(() => {
        start()
        add()
        setTimeout(trigger, 100)
      })

      return {
        times, start, stop, status, customEvent,
        add, remove, trigger, message, renderTreeContent,
        comments, active: Vue.ref('form')
      }
    }
  }).use(ElementPlus).mount("#I9Wmow")
}())

总结

对于 performance 感觉还是对有些概念上的理解会有些因难, api 倒是不多也不复杂。

比如: 什么是单调时钟(monotonic clock)?什么是时钟偏移及其有关的限制(如系统时间 等)?什么是时间域(timeOrigin)? performance.timeOrigin 又是以什么为基准?还有在 Worker 中的应用?

其次是 API 上的应用,主要有:

  1. mark: performance.mark(name)performance.clearMarks() 配套

  2. measure: performance.measure(name, startMark, endMark)performance.clearMeasures() ,这个可以用来通过名字获取成对 mark 之间的 timing 数据。

  3. get: 一些 timing 结构获取函数,比如: getEntries() 取所有, getEntriesByName(name) 通过 PerformanceTiming{ name } 名称取记录, getEntriesByType(type) 根据 markmeasure 类型取记录。

对于 performance.measure(name, startMark, endMark) 的后两个参数,可以省略,也可 以只给 endMark

startMark: mark 标记起始标记,可以是 undefined 则表示是从 navigator 开始计时。

endMark: mark 标记结束标记,如果没有传则记录以 performance.now() 时间点结束。

所以,如果都不传,只有 measure name 则表示从 navigator 到 performance.now() 这中 间的所有 PerformanceTiming 记录都会收集。

Wow!

最后,应该能理解为何 React Scheduler 包中使用的是 performance.now() 来获取当前的 时间戳了,就因为它能精准到亚毫秒级,可以精准的在 frame 空隙执行 react 的 fiber 树渲染任务。