diff --git a/jest.config.js b/jest.config.js index aeda0802..622dd1c9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,7 +18,8 @@ module.exports = { 'packages/*/src/**/*.ts', '!packages/runtime-test/src/utils/**', '!packages/template-explorer/**', - '!packages/size-check/**' + '!packages/size-check/**', + '!packages/runtime-core/src/profiling.ts' ], watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index bf5aaeeb..6bed9170 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -41,6 +41,7 @@ import { currentRenderingInstance, markAttrsAccessed } from './componentRenderUtils' +import { startMeasure, endMeasure } from './profiling' export type Data = { [key: string]: unknown } @@ -108,6 +109,7 @@ export type RenderFunction = { } export interface ComponentInternalInstance { + uid: number type: Component parent: ComponentInternalInstance | null appContext: AppContext @@ -176,6 +178,8 @@ export interface ComponentInternalInstance { const emptyAppContext = createAppContext() +let uid = 0 + export function createComponentInstance( vnode: VNode, parent: ComponentInternalInstance | null, @@ -185,6 +189,7 @@ export function createComponentInstance( const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext const instance: ComponentInternalInstance = { + uid: uid++, vnode, parent, appContext, @@ -383,7 +388,7 @@ function setupStatefulComponent( handleSetupResult(instance, setupResult, parentSuspense, isSSR) } } else { - finishComponentSetup(instance, parentSuspense, isSSR) + finishComponentSetup(instance, isSSR) } } @@ -413,7 +418,7 @@ export function handleSetupResult( }` ) } - finishComponentSetup(instance, parentSuspense, isSSR) + finishComponentSetup(instance, isSSR) } type CompileFunction = ( @@ -430,7 +435,6 @@ export function registerRuntimeCompiler(_compile: any) { function finishComponentSetup( instance: ComponentInternalInstance, - parentSuspense: SuspenseBoundary | null, isSSR: boolean ) { const Component = instance.type as ComponentOptions @@ -442,9 +446,15 @@ function finishComponentSetup( } } else if (!instance.render) { if (compile && Component.template && !Component.render) { + if (__DEV__) { + startMeasure(instance, `compile`) + } Component.render = compile(Component.template, { isCustomElement: instance.appContext.config.isCustomElement || NO }) + if (__DEV__ && instance.appContext.config.performance) { + endMeasure(instance, `compile`) + } // mark the function as runtime compiled ;(Component.render as RenderFunction)._rc = true } @@ -529,3 +539,23 @@ export function recordInstanceBoundEffect(effect: ReactiveEffect) { ;(currentInstance.effects || (currentInstance.effects = [])).push(effect) } } + +const classifyRE = /(?:^|[-_])(\w)/g +const classify = (str: string): string => + str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') + +export function formatComponentName( + Component: Component, + file?: string +): string { + let name = isFunction(Component) + ? Component.displayName || Component.name + : Component.name + if (!name && file) { + const match = file.match(/([^/\\]+)\.vue$/) + if (match) { + name = match[1] + } + } + return name ? classify(name) : 'Anonymous' +} diff --git a/packages/runtime-core/src/profiling.ts b/packages/runtime-core/src/profiling.ts new file mode 100644 index 00000000..d841cd84 --- /dev/null +++ b/packages/runtime-core/src/profiling.ts @@ -0,0 +1,42 @@ +import { ComponentInternalInstance, formatComponentName } from './component' + +let supported: boolean +let perf: any + +export function startMeasure( + instance: ComponentInternalInstance, + type: string +) { + if (!instance.appContext) debugger + if (instance.appContext.config.performance && isSupported()) { + perf.mark(`vue-${type}-${instance.uid}`) + } +} + +export function endMeasure(instance: ComponentInternalInstance, type: string) { + if (instance.appContext.config.performance && isSupported()) { + const startTag = `vue-${type}-${instance.uid}` + const endTag = startTag + `:end` + perf.mark(endTag) + perf.measure( + `<${formatComponentName(instance.type)}> ${type}`, + startTag, + endTag + ) + perf.clearMarks(startTag) + perf.clearMarks(endTag) + } +} + +function isSupported() { + if (supported !== undefined) { + return supported + } + if (typeof window !== 'undefined' && window.performance) { + supported = true + perf = window.performance + } else { + supported = false + } + return supported +} diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d774b473..6b571358 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -68,6 +68,7 @@ import { } from './errorHandling' import { createHydrationFunctions, RootHydrateFunction } from './hydration' import { invokeDirectiveHook } from './directives' +import { startMeasure, endMeasure } from './profiling' const __HMR__ = __BUNDLER__ && __DEV__ @@ -1031,6 +1032,7 @@ function baseCreateRenderer( if (__DEV__) { pushWarningContext(initialVNode) + startMeasure(instance, `mount`) } // inject renderer internals for keepAlive @@ -1041,7 +1043,13 @@ function baseCreateRenderer( } // resolve props and slots for setup context + if (__DEV__) { + startMeasure(instance, `init`) + } setupComponent(instance, parentSuspense) + if (__DEV__) { + endMeasure(instance, `init`) + } // setup() is async. This component relies on async logic to be resolved // before proceeding @@ -1072,6 +1080,7 @@ function baseCreateRenderer( if (__DEV__) { popWarningContext() + endMeasure(instance, `mount`) } } @@ -1089,7 +1098,13 @@ function baseCreateRenderer( let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode const { bm, m, a, parent } = instance + if (__DEV__) { + startMeasure(instance, `render`) + } const subTree = (instance.subTree = renderComponentRoot(instance)) + if (__DEV__) { + endMeasure(instance, `render`) + } // beforeMount hook if (bm) { invokeHooks(bm) @@ -1099,6 +1114,9 @@ function baseCreateRenderer( invokeVNodeHook(vnodeHook, parent, initialVNode) } if (el && hydrateNode) { + if (__DEV__) { + startMeasure(instance, `hydrate`) + } // vnode has adopted host node - perform hydration instead of mount. hydrateNode( initialVNode.el as Node, @@ -1106,7 +1124,13 @@ function baseCreateRenderer( instance, parentSuspense ) + if (__DEV__) { + endMeasure(instance, `hydrate`) + } } else { + if (__DEV__) { + startMeasure(instance, `patch`) + } patch( null, subTree, @@ -1116,6 +1140,9 @@ function baseCreateRenderer( parentSuspense, isSVG ) + if (__DEV__) { + endMeasure(instance, `patch`) + } initialVNode.el = subTree.el } // mounted hook @@ -1151,7 +1178,13 @@ function baseCreateRenderer( } else { next = vnode } + if (__DEV__) { + startMeasure(instance, `render`) + } const nextTree = renderComponentRoot(instance) + if (__DEV__) { + endMeasure(instance, `render`) + } const prevTree = instance.subTree instance.subTree = nextTree next.el = vnode.el @@ -1168,6 +1201,9 @@ function baseCreateRenderer( if (instance.refs !== EMPTY_OBJ) { instance.refs = {} } + if (__DEV__) { + startMeasure(instance, `patch`) + } patch( prevTree, nextTree, @@ -1179,6 +1215,9 @@ function baseCreateRenderer( parentSuspense, isSVG ) + if (__DEV__) { + endMeasure(instance, `patch`) + } next.el = nextTree.el if (next === null) { // self-triggered update. In case of HOC, update parent component diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index ef6168d2..c31ecfed 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -1,5 +1,10 @@ import { VNode } from './vnode' -import { Data, ComponentInternalInstance, Component } from './component' +import { + Data, + ComponentInternalInstance, + Component, + formatComponentName +} from './component' import { isString, isFunction } from '@vue/shared' import { toRaw, isRef, pauseTracking, resetTracking } from '@vue/reactivity' import { callWithErrorHandling, ErrorCodes } from './errorHandling' @@ -43,7 +48,10 @@ export function warn(msg: string, ...args: any[]) { msg + args.join(''), instance && instance.proxy, trace - .map(({ vnode }) => `at <${formatComponentName(vnode)}>`) + .map( + ({ vnode }) => + `at <${formatComponentName(vnode.type as Component)}>` + ) .join('\n'), trace ] @@ -111,24 +119,6 @@ function formatTraceEntry({ vnode, recurseCount }: TraceEntry): any[] { : [open + close, rootLabel] } -const classifyRE = /(?:^|[-_])(\w)/g -const classify = (str: string): string => - str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') - -function formatComponentName(vnode: ComponentVNode, file?: string): string { - const Component = vnode.type as Component - let name = isFunction(Component) - ? Component.displayName || Component.name - : Component.name - if (!name && file) { - const match = file.match(/([^/\\]+)\.vue$/) - if (match) { - name = match[1] - } - } - return name ? classify(name) : 'Anonymous' -} - function formatProps(props: Data): any[] { const res: any[] = [] const keys = Object.keys(props)