Browse Source

feat(runtime-vapor): implement app.config.performance (#230)

* 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 <sxzz@sxzz.moe>
XiaoDong 2 years ago
parent
commit
3ac951b5b5

+ 2 - 0
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,

+ 16 - 0
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
 }
 

+ 41 - 0
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<string, any> | 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, '')

+ 160 - 0
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<string, string | Symbol>
+}
+
+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,
+  )
+}

+ 1 - 1
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'

+ 54 - 0
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
+}

+ 1 - 42
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<string, any> | 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, '')