apm里的指标和事务

公司用到了APM对前端应用进行性能监控,为了了解其原理和数值指标,我对源码进行了一番研究。

APM的指标

APM JS客户端主要使用Navigation_timing_APIResource_Timing_API对界面加载性能和静态资源加载时间进行监控。

他们提供了一系列指标对页面的加载性能进行评估。

例如 Navigation_timing_API:

1
performance.getEntriesByType('navigation')[0]

输出:

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
{
unloadEventStart: 0
unloadEventEnd: 0
domInteractive: 2981.1850000114646
domContentLoadedEventStart: 3019.0850000071805
domContentLoadedEventEnd: 3019.6350000042003
domComplete: 78957.03499999945
loadEventStart: 78957.06000001519
loadEventEnd: 78957.12999999523
type: "navigate"
redirectCount: 0
initiatorType: "navigation"
nextHopProtocol: "h2"
workerStart: 0
redirectStart: 0
redirectEnd: 0
fetchStart: 4.27000000490807
domainLookupStart: 113.70499999611638
domainLookupEnd: 246.34000001242384
connectStart: 246.34000001242384
connectEnd: 337.3050000227522
secureConnectionStart: 271.5100000204984
requestStart: 337.5800000212621
responseStart: 1154.450000001816
responseEnd: 2945.3750000102445
transferSize: 22851
encodedBodySize: 22299
decodedBodySize: 219855
serverTiming: []
name: "https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API"
entryType: "navigation"
startTime: 0
duration: 78957.12999999523
}

image.png

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
 
重定向次数:
redirectCount

跳转耗时:
redirectEnd - redirectStart

APP CACHE 耗时:
Math.max(domainLookupStart - fetchStart, 0)

DNS 解析耗时:
domainLookupEnd - domainLookupStart

TCP 链接耗时:
connectEnd - connectStart

等待服务器响应耗时(注意是否存在cache):
responseStart - requestStart

内容加载耗时(注意是否存在cache):
responseEnd - responseStart

总体网络交互耗时,即开始跳转到服务器资源下载完成:
responseEnd - navigationStart

渲染处理:
(domComplete || domLoading) - domLoading

抛出 load 事件:
loadEventEnd - loadEventStart

总耗时:
(loadEventEnd || loadEventStart || domComplete || domLoading) - navigationStart

可交互:
domInteractive - navigationStart

请求响应耗时,即 T0,注意cache:
responseStart - navigationStart

首次出现内容,即 T1:
domLoading - navigationStart

内容加载完毕,即 T3:
loadEventEnd - navigationStart

APM的管理页面上对上述参数进行整理,page-load事务中包含以下指标:

timeToFirstByte

对应PerformanceTiming的responseStart

domInteractive

对应PerformanceTiming的domInteractive。指浏览器解析html文档的状态为interactive时的时间节点,此时主文档的解析器结束工作,即 Document.readyState 改变为 ‘interactive’,但是内嵌资源(比如图片、css、js等)还未加载完成。

domComplete

顾名思义。

firstContentfulPaint

用来指示一部分内容元素能够在可视窗口显示出来,需要的渲染时间。

1
2
3
4
5
6
const unloadDiff = timing.fetchStart - timing.navigationStart
const firstContentfulPaint = performance.getEntriesByType('paint').reduce((_fcp, entry) => {
if (entry.name === 'first-contentful-paint') {
return unloadDiff >= 0 ? entry.startTime - unloadDiff : entry.startTime
}
}, null)

lagestContentfulPaint

用来指示最大内容元素能够在可视窗口显示出来,需要的渲染时间。

1
2
3
4
5
6
7
8
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
const lastEntry = entries[entries.length - 1]

lcp = lastEntry.renderTime || lastEntry.loadTime
})

po.observe({type: 'largest-contentful-paint', buffered: true})

由此我们可以根据上述指标来给自己项目的性能优化进行定位。

APM的事务

APM的监控数据以事务(Transaction)为单位,每个事务下有多个时间跨度(span),同一时间只能有一个正在活跃的事务。如果在存在正在活跃的事务的情况下再开始另一个事务,事务之间会按照优先级进行合并。

事务的类型的优先级如下:page-load > route-change > user-interaction > http-request > custom > temporary。例如用户点击了页面,使当前事务为user-interaction,如果此时导致了页面路由切换,即开始了事务route-change,由于route-change的优先级比较大,当前事务就切换为route-change,原事务的监控数据会并入到当前事务。

通过transaction.startSpan(name, type, { blocking: true })添加的span会延长事务的结束时间,APM默认所有http请求都为blocking: true,所有事务都会因为http请求延长时间。

page-load

这类事务监控页面加载过程的性能数据。最短到domComplete就结束,但如果结束前有其他blocking的任务,则会等到所有任务完成后才结束。这就是为什么有些事务duration非常大,因为在domComplete之前添加了耗时的请求。

route-change

这类事务监控页面路由切换的性能数据。APM会拦截history.pushState来开启route-change事务。

注意,route-change的事务开始后并没有主动去检测结束状态,它不是像page-load那样在domComplete主动检测状态并结束,如果route-change过程中没有其他任务,则这个事务可能会一直处于活跃状态,直到加了一些任务并结束才会有机会结束这个事务,所以才会经常出现事务长达几十秒的情况。所以这里推荐使用@elastic/apm-rum-react或者@elastic/apm-rum-vue等特定的框架,这样才能有效反映route-change的耗时。

packages/rum-react/src/get-with-transaction.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const [transaction] = React.useState(() => {
const tr = apm.startTransaction(name, type, {
managed: true,
canReuse: true
})
callback(tr, props)
return tr
})

React.useEffect(() => {
afterFrame(() => transaction && transaction.detectFinish())
return () => {
transaction && transaction.detectFinish()
}
}, [])

packages/rum-vue/src/route-hooks.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
router.beforeEach((to, from, next) => {
const matched = to.matched || []
let path = to.path

if (matched.length > 0) {
path = matched[matched.length - 1].path || path
}
transaction = apm.startTransaction(path, 'route-change', {
managed: true,
canReuse: true
})
next()
})

router.afterEach(() => {
afterFrame(() => transaction && transaction.detectFinish())
})

router.onError(() => {
transaction && transaction.end()
})

或者可以手动detectFinish:

1
2
3
4
5
6
7
8
9
10
11
history.listen(() => {
const trans = apm.getCurrentTransaction();
if (trans) {
afterFrame(() => {
const trans = apm.getCurrentTransaction();
if (trans) {
(trans as any).detectFinish();
}
});
}
});

user-interaction

点击事件(eventType === 'click')会触发这个事务。复用间隔为100毫秒,即100毫秒后的点击事件会触发新的user-interaction事务。

http-request

任何http请求都会触发这个事务。当然,前提是没有更高优先级的事务。

custom

自定义事务类型。apm.startTransaction(name, 'custom')

temporary

临时的事务。当前没有活跃的事务,且用户通过apm.startSpan(name, type)的方式添加span时,会开启类型为temporary的事务。这类事务如果没有合并到优先级较高的事务,最终会被抛弃,不会上传到APM Server。