From 3ac951b5b514002ed9b82b599f97bf1e611fd960 Mon Sep 17 00:00:00 2001 From: XiaoDong Date: Sun, 16 Jun 2024 16:50:36 +0800 Subject: [PATCH] feat(runtime-vapor): implement app.config.performance (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(runtime-capor): add app.config.performance * refactor: move formatComponentName to component.ts * refactor: update import in warning.ts * fix * refactor * fix order --------- Co-authored-by: 三咲智子 Kevin Deng --- .../runtime-vapor/src/apiCreateVaporApp.ts | 2 + packages/runtime-vapor/src/apiRender.ts | 16 ++ packages/runtime-vapor/src/component.ts | 41 +++++ packages/runtime-vapor/src/devtools.ts | 160 ++++++++++++++++++ .../src/helpers/resolveAssets.ts | 2 +- packages/runtime-vapor/src/profiling.ts | 54 ++++++ packages/runtime-vapor/src/warning.ts | 43 +---- 7 files changed, 275 insertions(+), 43 deletions(-) create mode 100644 packages/runtime-vapor/src/devtools.ts create mode 100644 packages/runtime-vapor/src/profiling.ts diff --git a/packages/runtime-vapor/src/apiCreateVaporApp.ts b/packages/runtime-vapor/src/apiCreateVaporApp.ts index 9d318c902..e3ffef020 100644 --- a/packages/runtime-vapor/src/apiCreateVaporApp.ts +++ b/packages/runtime-vapor/src/apiCreateVaporApp.ts @@ -172,6 +172,7 @@ export function createAppContext(): AppContext { app: null as any, config: { isNativeTag: NO, + performance: false, errorHandler: undefined, warnHandler: undefined, globalProperties: {}, @@ -227,6 +228,7 @@ export interface AppConfig { // @private readonly isNativeTag: (tag: string) => boolean + performance: boolean errorHandler?: ( err: unknown, instance: ComponentInternalInstance | null, diff --git a/packages/runtime-vapor/src/apiRender.ts b/packages/runtime-vapor/src/apiRender.ts index fcc900b8e..a04e29833 100644 --- a/packages/runtime-vapor/src/apiRender.ts +++ b/packages/runtime-vapor/src/apiRender.ts @@ -18,6 +18,7 @@ import { import { isArray, isFunction, isObject } from '@vue/shared' import { fallThroughAttrs } from './componentAttrs' import { VaporErrorCodes, callWithErrorHandling } from './errorHandling' +import { endMeasure, startMeasure } from './profiling' export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``) @@ -32,6 +33,9 @@ export function setupComponent( instance: ComponentInternalInstance, singleRoot: boolean = false, ): void { + if (__DEV__) { + startMeasure(instance, `init`) + } const reset = setCurrentInstance(instance) instance.scope.run(() => { const { component, props } = instance @@ -93,6 +97,9 @@ export function setupComponent( return block }) reset() + if (__DEV__) { + endMeasure(instance, `init`) + } } export function render( @@ -115,6 +122,10 @@ function mountComponent( ) { instance.container = container + if (__DEV__) { + startMeasure(instance, 'mount') + } + // hook: beforeMount invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT, 'beforeMount') @@ -128,6 +139,11 @@ function mountComponent( instance => (instance.isMounted = true), true, ) + + if (__DEV__) { + endMeasure(instance, 'mount') + } + return instance } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 9a2517498..228c7b78a 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -427,3 +427,44 @@ function getSlotsProxy(instance: ComponentInternalInstance): Slots { })) ) } + +export function getComponentName( + Component: Component, + includeInferred = true, +): string | false | undefined { + return isFunction(Component) + ? Component.displayName || Component.name + : Component.name || (includeInferred && Component.__name) +} + +export function formatComponentName( + instance: ComponentInternalInstance | null, + Component: Component, + isRoot = false, +): string { + let name = getComponentName(Component) + if (!name && Component.__file) { + const match = Component.__file.match(/([^/\\]+)\.\w+$/) + if (match) { + name = match[1] + } + } + + if (!name && instance && instance.parent) { + // try to infer the name based on reverse resolution + const inferFromRegistry = (registry: Record | undefined) => { + for (const key in registry) { + if (registry[key] === Component) { + return key + } + } + } + name = inferFromRegistry(instance.appContext.components) + } + + return name ? classify(name) : isRoot ? `App` : `Anonymous` +} + +const classifyRE = /(?:^|[-_])(\w)/g +const classify = (str: string): string => + str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') diff --git a/packages/runtime-vapor/src/devtools.ts b/packages/runtime-vapor/src/devtools.ts new file mode 100644 index 000000000..4dff1bd99 --- /dev/null +++ b/packages/runtime-vapor/src/devtools.ts @@ -0,0 +1,160 @@ +/* eslint-disable no-restricted-globals */ +import type { App } from './apiCreateVaporApp' +import type { ComponentInternalInstance } from './component' + +interface AppRecord { + id: number + app: App + version: string + types: Record +} + +enum DevtoolsHooks { + APP_INIT = 'app:init', + APP_UNMOUNT = 'app:unmount', + COMPONENT_UPDATED = 'component:updated', + COMPONENT_ADDED = 'component:added', + COMPONENT_REMOVED = 'component:removed', + COMPONENT_EMIT = 'component:emit', + PERFORMANCE_START = 'perf:start', + PERFORMANCE_END = 'perf:end', +} + +export interface DevtoolsHook { + enabled?: boolean + emit: (event: string, ...payload: any[]) => void + on: (event: string, handler: Function) => void + once: (event: string, handler: Function) => void + off: (event: string, handler: Function) => void + appRecords: AppRecord[] + /** + * Added at https://github.com/vuejs/devtools/commit/f2ad51eea789006ab66942e5a27c0f0986a257f9 + * Returns whether the arg was buffered or not + */ + cleanupBuffer?: (matchArg: unknown) => boolean +} + +export let devtools: DevtoolsHook + +let buffer: { event: string; args: any[] }[] = [] + +let devtoolsNotInstalled = false + +function emit(event: string, ...args: any[]) { + if (devtools) { + devtools.emit(event, ...args) + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }) + } +} + +export function setDevtoolsHook(hook: DevtoolsHook, target: any) { + devtools = hook + if (devtools) { + devtools.enabled = true + buffer.forEach(({ event, args }) => devtools.emit(event, ...args)) + buffer = [] + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== 'undefined' && + // some envs mock window but not fully + window.HTMLElement && + // also exclude jsdom + // eslint-disable-next-line no-restricted-syntax + !window.navigator?.userAgent?.includes('jsdom') + ) { + const replay = (target.__VUE_DEVTOOLS_HOOK_REPLAY__ = + target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []) + replay.push((newHook: DevtoolsHook) => { + setDevtoolsHook(newHook, target) + }) + // clear buffer after 3s - the user probably doesn't have devtools installed + // at all, and keeping the buffer will cause memory leaks (#4738) + setTimeout(() => { + if (!devtools) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null + devtoolsNotInstalled = true + buffer = [] + } + }, 3000) + } else { + // non-browser env, assume not installed + devtoolsNotInstalled = true + buffer = [] + } +} + +export function devtoolsInitApp(app: App, version: string) { + emit(DevtoolsHooks.APP_INIT, app, version, {}) +} + +export function devtoolsUnmountApp(app: App) { + emit(DevtoolsHooks.APP_UNMOUNT, app) +} + +export const devtoolsComponentAdded = /*#__PURE__*/ createDevtoolsComponentHook( + DevtoolsHooks.COMPONENT_ADDED, +) + +export const devtoolsComponentUpdated = + /*#__PURE__*/ createDevtoolsComponentHook(DevtoolsHooks.COMPONENT_UPDATED) + +const _devtoolsComponentRemoved = /*#__PURE__*/ createDevtoolsComponentHook( + DevtoolsHooks.COMPONENT_REMOVED, +) + +export const devtoolsComponentRemoved = ( + component: ComponentInternalInstance, +) => { + if ( + devtools && + typeof devtools.cleanupBuffer === 'function' && + // remove the component if it wasn't buffered + !devtools.cleanupBuffer(component) + ) { + _devtoolsComponentRemoved(component) + } +} + +/*! #__NO_SIDE_EFFECTS__ */ +function createDevtoolsComponentHook(hook: DevtoolsHooks) { + return (component: ComponentInternalInstance) => { + emit( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : undefined, + component, + ) + } +} + +export const devtoolsPerfStart = /*#__PURE__*/ createDevtoolsPerformanceHook( + DevtoolsHooks.PERFORMANCE_START, +) + +export const devtoolsPerfEnd = /*#__PURE__*/ createDevtoolsPerformanceHook( + DevtoolsHooks.PERFORMANCE_END, +) + +function createDevtoolsPerformanceHook(hook: DevtoolsHooks) { + return (component: ComponentInternalInstance, type: string, time: number) => { + emit(hook, component.appContext.app, component.uid, component, type, time) + } +} + +export function devtoolsComponentEmit( + component: ComponentInternalInstance, + event: string, + params: any[], +) { + emit( + DevtoolsHooks.COMPONENT_EMIT, + component.appContext.app, + component, + event, + params, + ) +} diff --git a/packages/runtime-vapor/src/helpers/resolveAssets.ts b/packages/runtime-vapor/src/helpers/resolveAssets.ts index 18eba78cb..5454de687 100644 --- a/packages/runtime-vapor/src/helpers/resolveAssets.ts +++ b/packages/runtime-vapor/src/helpers/resolveAssets.ts @@ -1,7 +1,7 @@ import { camelize, capitalize } from '@vue/shared' import { type Directive, warn } from '..' import { type Component, currentInstance } from '../component' -import { getComponentName } from '../warning' +import { getComponentName } from '../component' export const COMPONENTS = 'components' export const DIRECTIVES = 'directives' diff --git a/packages/runtime-vapor/src/profiling.ts b/packages/runtime-vapor/src/profiling.ts new file mode 100644 index 000000000..3caae0a67 --- /dev/null +++ b/packages/runtime-vapor/src/profiling.ts @@ -0,0 +1,54 @@ +/* eslint-disable no-restricted-globals */ +import { + type ComponentInternalInstance, + formatComponentName, +} from './component' +import { devtoolsPerfEnd, devtoolsPerfStart } from './devtools' + +let supported: boolean +let perf: Performance + +export function startMeasure( + instance: ComponentInternalInstance, + type: string, +) { + if (instance.appContext.config.performance && isSupported()) { + perf.mark(`vue-${type}-${instance.uid}`) + } + + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsPerfStart(instance, type, isSupported() ? perf.now() : Date.now()) + } +} + +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, instance.component)}> ${type}`, + startTag, + endTag, + ) + perf.clearMarks(startTag) + perf.clearMarks(endTag) + } + + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsPerfEnd(instance, type, isSupported() ? perf.now() : Date.now()) + } +} + +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-vapor/src/warning.ts b/packages/runtime-vapor/src/warning.ts index a4cf864e4..63ee30fed 100644 --- a/packages/runtime-vapor/src/warning.ts +++ b/packages/runtime-vapor/src/warning.ts @@ -1,7 +1,7 @@ import { - type Component, type ComponentInternalInstance, currentInstance, + formatComponentName, } from './component' import { isFunction, isString } from '@vue/shared' import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity' @@ -155,44 +155,3 @@ function formatProp(key: string, value: unknown, raw?: boolean): any { return raw ? value : [`${key}=`, value] } } - -export function getComponentName( - Component: Component, - includeInferred = true, -): string | false | undefined { - return isFunction(Component) - ? Component.displayName || Component.name - : Component.name || (includeInferred && Component.__name) -} - -export function formatComponentName( - instance: ComponentInternalInstance | null, - Component: Component, - isRoot = false, -): string { - let name = getComponentName(Component) - if (!name && Component.__file) { - const match = Component.__file.match(/([^/\\]+)\.\w+$/) - if (match) { - name = match[1] - } - } - - if (!name && instance && instance.parent) { - // try to infer the name based on reverse resolution - const inferFromRegistry = (registry: Record | undefined) => { - for (const key in registry) { - if (registry[key] === Component) { - return key - } - } - } - name = inferFromRegistry(instance.appContext.components) - } - - return name ? classify(name) : isRoot ? `App` : `Anonymous` -} - -const classifyRE = /(?:^|[-_])(\w)/g -const classify = (str: string): string => - str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')