Skip to content

Commit 66fc5ba

Browse files
committed
性能ref追踪
1 parent ca50ba6 commit 66fc5ba

12 files changed

Lines changed: 1576 additions & 56 deletions

File tree

packages/core/@types/index.d.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,65 @@ export interface WebviewConfig {
275275
apiImplementations?: object
276276
}
277277

278+
export interface RnRenderProfileMeta {
279+
type: 'page' | 'component'
280+
moduleId: string
281+
componentPath: string
282+
resource: string
283+
}
284+
285+
export interface RnRenderCause {
286+
kind: 'mount' | 'reactive' | 'force-update' | 'unknown'
287+
source?: 'props' | 'data' | 'setup' | 'store' | 'inject' | 'unknown'
288+
operation?: 'set' | 'add' | 'delete' | 'array-mutation' | 'trigger-ref'
289+
key?: string
290+
method?: string
291+
/** 同一批渲染中相同原因被触发的次数。 */
292+
count: number
293+
}
294+
295+
export interface RnRenderProfileEvent extends RnRenderProfileMeta {
296+
instanceId: string
297+
pageId: string
298+
phase: 'mount' | 'update' | 'nested-update'
299+
/** 已提交 render 在 React 中花费的 CPU 时间,包含该组件的已渲染子树。 */
300+
actualDuration: number
301+
baseDuration: number
302+
startTime: number
303+
commitTime: number
304+
/** Mpx 组件包装层开始本次 render 的单调时钟时间。 */
305+
renderStartedAt: number
306+
/** Mpx 组件包装层本次 render 的同步执行时间。 */
307+
wrapperDuration: number
308+
/** 本次 render 中执行 Mpx 模板函数所花费的同步时间。 */
309+
templateDuration: number
310+
templateExecuted: boolean
311+
updateScheduledAt: number | null
312+
/** 首次响应式调度到 React commit 回调的耗时,使用框架单调时钟计算。 */
313+
updateToCommitDuration: number | null
314+
coalescedUpdateCount: number
315+
/** 本次实际执行并提交的 Mpx 模板渲染原因。 */
316+
causes: Array<RnRenderCause>
317+
changedPropKeys: Array<string>
318+
}
319+
320+
export interface RnRenderProfilerConfig {
321+
enabled?: boolean
322+
shouldProfile?: (meta: RnRenderProfileMeta) => boolean
323+
onRender?: (event: RnRenderProfileEvent) => void
324+
}
325+
278326
/**
279327
* 输出为 ReactNative 时使用的特殊配置,用于与容器进行功能桥接
280328
*/
281329
export interface RnConfig {
330+
/**
331+
* RN React 渲染性能采集,仅建议在调试包中开启。
332+
* 需要在对应页面或组件执行注册前完成配置,运行时切换不会影响已注册组件。
333+
* actualDuration 表示 React render/reconciliation CPU 时间,不包含原生布局、绘制及上屏时间。
334+
*/
335+
renderProfiler?: RnRenderProfilerConfig
336+
282337
/**
283338
* 当导航状态发生变化时触发,例如页面跳转、返回等。
284339
*
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { ReactiveEffect } from '../../src/observer/effect'
2+
import { reactive, set, del } from '../../src/observer/reactive'
3+
4+
const createTrackedEffect = (fn) => {
5+
const scheduler = jest.fn()
6+
const onTrigger = jest.fn()
7+
const effect = new ReactiveEffect(fn, scheduler)
8+
effect.onTrigger = onTrigger
9+
effect.run()
10+
return { effect, scheduler, onTrigger }
11+
}
12+
13+
describe('reactive trigger metadata', () => {
14+
it('reports set, add and delete operations with only their key', () => {
15+
const state = reactive({
16+
value: 1,
17+
nested: {}
18+
})
19+
const { scheduler, onTrigger } = createTrackedEffect(() => {
20+
return [state.value, state.nested]
21+
})
22+
23+
state.value = 2
24+
set(state.nested, 'added', true)
25+
del(state.nested, 'added')
26+
27+
expect(onTrigger.mock.calls).toEqual([
28+
[{ type: 'set', key: 'value' }],
29+
[{ type: 'add', key: 'added' }],
30+
[{ type: 'delete', key: 'added' }]
31+
])
32+
expect(scheduler).toHaveBeenCalledTimes(3)
33+
})
34+
35+
it.each([
36+
['push', [1]],
37+
['pop', []],
38+
['shift', []],
39+
['unshift', [0]],
40+
['splice', [0, 1, 2]],
41+
['sort', []],
42+
['reverse', []]
43+
])('reports the %s array mutation without retaining arguments', (method, args) => {
44+
const state = reactive({ list: [1] })
45+
const { scheduler, onTrigger } = createTrackedEffect(() => state.list)
46+
47+
state.list[method](...args)
48+
49+
expect(onTrigger).toHaveBeenCalledWith({
50+
type: 'array-mutation',
51+
method
52+
})
53+
expect(onTrigger.mock.calls[0][0]).not.toHaveProperty('args')
54+
expect(scheduler).toHaveBeenCalledTimes(1)
55+
})
56+
57+
it('does not report unchanged assignments', () => {
58+
const state = reactive({ value: 1 })
59+
const { scheduler, onTrigger } = createTrackedEffect(() => state.value)
60+
61+
state.value = 1
62+
63+
expect(onTrigger).not.toHaveBeenCalled()
64+
expect(scheduler).not.toHaveBeenCalled()
65+
})
66+
67+
it('adds an explicit source without retaining the reactive target', () => {
68+
const state = reactive({
69+
value: 1,
70+
nested: { visible: false },
71+
list: []
72+
}, 'data')
73+
const { onTrigger } = createTrackedEffect(() => {
74+
return [state.value, state.nested.visible, state.list]
75+
})
76+
77+
state.value = 2
78+
state.nested.visible = true
79+
state.list.push({ id: 1 })
80+
81+
expect(onTrigger.mock.calls).toEqual([
82+
[{ source: 'data', type: 'set', key: 'value' }],
83+
[{ source: 'data', type: 'set', key: 'visible' }],
84+
[{ source: 'data', type: 'array-mutation', method: 'push' }]
85+
])
86+
onTrigger.mock.calls.forEach(([info]) => {
87+
expect(info).not.toHaveProperty('target')
88+
expect(info).not.toHaveProperty('newValue')
89+
expect(info).not.toHaveProperty('oldValue')
90+
})
91+
})
92+
93+
it('does not traverse non-reactive objects while initializing a source', () => {
94+
const readNested = jest.fn()
95+
const external = Object.defineProperty(new Date(), 'nested', {
96+
enumerable: true,
97+
get: readNested
98+
})
99+
reactive({ external }, 'data')
100+
101+
expect(readNested).not.toHaveBeenCalled()
102+
})
103+
104+
it('only calls onTrigger when the effect is actually scheduled', () => {
105+
const state = reactive({ value: 0 })
106+
const scheduler = jest.fn()
107+
const onTrigger = jest.fn()
108+
let shouldMutate = true
109+
const effect = new ReactiveEffect(() => {
110+
if (shouldMutate) {
111+
shouldMutate = false
112+
state.value++
113+
}
114+
return state.value
115+
}, scheduler)
116+
effect.onTrigger = onTrigger
117+
118+
effect.run()
119+
expect(onTrigger).not.toHaveBeenCalled()
120+
expect(scheduler).not.toHaveBeenCalled()
121+
122+
effect.pause()
123+
state.value++
124+
expect(onTrigger).not.toHaveBeenCalled()
125+
expect(scheduler).not.toHaveBeenCalled()
126+
127+
effect.resume()
128+
expect(onTrigger).not.toHaveBeenCalled()
129+
expect(scheduler).toHaveBeenCalledTimes(1)
130+
})
131+
})

0 commit comments

Comments
 (0)