Skip to content

Commit d1e3a00

Browse files
authoredOct 16, 2024··
feat: timeline panel (#627)
1 parent c3ec0bc commit d1e3a00

File tree

23 files changed

+581
-16
lines changed

23 files changed

+581
-16
lines changed
 

‎docs/getting-started/features.md

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ Components tab shows your components information, including the node tree, state
2020

2121
![components](/features/components.png)
2222

23+
## Timeline
24+
25+
Timeline tab shows the performance of your app, including the time spent on rendering, updating, and so on.
26+
27+
![timeline](/features/timeline.png)
28+
2329
## Assets(Vite only)
2430

2531
Assets tab shows your files from the project directory, you can see the information of selected file with some helpful actions.

‎docs/guide/migration.md

+2-11
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,11 @@ The v7 version of devtools only supports Vue3. If your application is still usin
88

99
In v7, we've made some feature-level adjustments compared to v6. You can view the v7 feature overview in the [Features](/getting-started/features). Here, we mainly mention some of the main feature changes.
1010

11-
### Deprecated Features
12-
13-
Due to high performance costs and potential memory leak risks, we have removed some features in v7. These features are:
14-
15-
- `Performance` Timeline
16-
- `Component Events` Timeline
17-
18-
💡 By the way, we are looking for a balanced approach to re-enable it with better performance. You can follow the latest progress [here](https://github.com/vuejs/devtools-next/issues/609).
19-
2011
### Feature Adjustments
2112

22-
- Timeline Tab
13+
- Plugin Timeline Tab
2314

24-
In v7, we moved the timeline tab to be managed within each plugin's menu. Here is a screenshot of the pinia devtools plugin:
15+
In v7, we moved the plugin timeline tab to be managed within each plugin's menu. Here is a screenshot of the pinia devtools plugin:
2516

2617
![pinia-timeline](/features/pinia-timeline.png)
2718

‎docs/public/features/timeline.png

181 KB
Loading

‎packages/applet/src/components/timeline/index.vue

+13-3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import RootStateViewer from '~/components/state/RootStateViewer.vue'
1212
import { createExpandedContext } from '~/composables/toggle-expanded'
1313
import EventList from './EventList.vue'
1414
15-
const props = defineProps<{
15+
const props = withDefaults(defineProps<{
1616
layerIds: string[]
1717
docLink: string
1818
githubRepoLink?: string
19-
}>()
19+
headerVisible?: boolean
20+
}>(), {
21+
headerVisible: true,
22+
})
2023
2124
const { expanded: expandedStateNodes } = createExpandedContext('timeline-state')
2225
@@ -92,11 +95,18 @@ rpc.functions.on(DevToolsMessagingEvents.TIMELINE_EVENT_UPDATED, onTimelineEvent
9295
onUnmounted(() => {
9396
rpc.functions.off(DevToolsMessagingEvents.TIMELINE_EVENT_UPDATED, onTimelineEventUpdated)
9497
})
98+
99+
defineExpose({
100+
clear() {
101+
eventList.value = []
102+
groupList.value.clear()
103+
},
104+
})
95105
</script>
96106

97107
<template>
98108
<div class="h-full flex flex-col">
99-
<DevToolsHeader :doc-link="docLink" :github-repo-link="githubRepoLink">
109+
<DevToolsHeader v-if="headerVisible" :doc-link="docLink" :github-repo-link="githubRepoLink">
100110
<Navbar />
101111
</DevToolsHeader>
102112
<template v-if="eventList.length">

‎packages/applet/src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import SelectiveList from './components/basic/SelectiveList.vue'
2+
import Timeline from './components/timeline/index.vue'
13
import 'uno.css'
24
import '@unocss/reset/tailwind.css'
35
import './styles/base.css'
@@ -9,3 +11,8 @@ export * from './modules/components'
911
export * from './modules/custom-inspector'
1012
export * from './modules/pinia'
1113
export * from './modules/router'
14+
15+
export {
16+
SelectiveList,
17+
Timeline,
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<script setup lang="ts">
2+
import { rpc, useDevToolsState } from '@vue/devtools-core'
3+
import { useDevToolsColorMode, vTooltip, VueIcIcon } from '@vue/devtools-ui'
4+
import { defineModel } from 'vue'
5+
6+
defineProps<{ data: {
7+
id: string
8+
label: string
9+
}[] }>()
10+
11+
const emit = defineEmits(['select', 'clear'])
12+
const devtoolsState = useDevToolsState()
13+
const recordingState = computed(() => devtoolsState.timelineLayersState.value.recordingState)
14+
const timelineLayersState = computed(() => devtoolsState.timelineLayersState.value)
15+
const recordingTooltip = computed(() => recordingState.value ? 'Stop recording' : 'Start recording')
16+
const { colorMode } = useDevToolsColorMode()
17+
const isDark = computed(() => colorMode.value === 'dark')
18+
const selected = defineModel()
19+
function select(id: string) {
20+
selected.value = id
21+
emit('select', id)
22+
rpc.value.updateTimelineLayersState({
23+
selected: id,
24+
})
25+
}
26+
27+
watch(() => timelineLayersState.value.selected, (state: string) => {
28+
selected.value = state
29+
}, {
30+
immediate: true,
31+
})
32+
33+
function getTimelineLayerEnabled(id: string) {
34+
return {
35+
'mouse': timelineLayersState.value.mouseEventEnabled,
36+
'keyboard': timelineLayersState.value.keyboardEventEnabled,
37+
'component-event': timelineLayersState.value.componentEventEnabled,
38+
'performance': timelineLayersState.value.performanceEventEnabled,
39+
}[id]
40+
}
41+
42+
function toggleRecordingState() {
43+
rpc.value.updateTimelineLayersState({
44+
recordingState: !recordingState.value,
45+
})
46+
}
47+
48+
function toggleTimelineLayerEnabled(id: string) {
49+
const normalizedId = {
50+
'mouse': 'mouseEventEnabled',
51+
'keyboard': 'keyboardEventEnabled',
52+
'component-event': 'componentEventEnabled',
53+
'performance': 'performanceEventEnabled',
54+
}[id]
55+
rpc.value.updateTimelineLayersState({
56+
[normalizedId]: !getTimelineLayerEnabled(id),
57+
})
58+
}
59+
</script>
60+
61+
<template>
62+
<div h-full flex flex-col p2>
63+
<div class="mb-1 flex justify-end pb-1" border="b dashed base">
64+
<div class="flex items-center gap-2 px-1">
65+
<div v-tooltip.bottom-end="{ content: recordingTooltip }" class="flex items-center gap1" @click="toggleRecordingState">
66+
<span v-if="recordingState" class="recording recording-btn bg-[#ef4444]" />
67+
<span v-else class="recording-btn bg-black op70 dark:(bg-white) hover:op100" />
68+
</div>
69+
<div v-tooltip.bottom-end="{ content: 'Clear all timelines' }" class="flex items-center gap1" @click="emit('clear')">
70+
<VueIcIcon name="baseline-delete" cursor-pointer text-xl op70 hover:op100 />
71+
</div>
72+
<div v-tooltip.bottom-end="{ content: '<p style=\'width: 285px\'>Timeline events can cause significant performance overhead in large applications, so we recommend enabling it only when needed and on-demand. </p>', html: true }" class="flex items-center gap1">
73+
<VueIcIcon name="baseline-tips-and-updates" cursor-pointer text-xl op70 hover:op100 />
74+
</div>
75+
</div>
76+
</div>
77+
<ul class="p2">
78+
<li
79+
v-for="item in data" :key="item.id"
80+
class="group relative selectable-item"
81+
:class="{ active: item.id === selected }"
82+
@click="select(item.id)"
83+
>
84+
{{ item.label }}
85+
<span class="absolute right-2 rounded-1 bg-primary-500 px1 text-3 text-white op0 [.active_&]:(bg-primary-400 dark:bg-gray-600) group-hover:op80 hover:op100!" @click.stop="toggleTimelineLayerEnabled(item.id)">
86+
{{ getTimelineLayerEnabled(item.id) ? 'Disabled' : 'Enabled' }}
87+
</span>
88+
</li>
89+
</ul>
90+
</div>
91+
</template>
92+
93+
<style scoped>
94+
@keyframes pulse {
95+
50% {
96+
opacity: 0.5;
97+
}
98+
}
99+
.recording-btn {
100+
--at-apply: w-3.5 h-3.5 inline-flex cursor-pointer rounded-50%;
101+
}
102+
.recording {
103+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
104+
transition-duration: 1s;
105+
box-shadow: #ef4444 0 0 8px;
106+
}
107+
</style>

‎packages/client/src/constants/tab.ts

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export const builtinTab: [string, ModuleBuiltinTab[]][] = [
2525
path: 'pages',
2626
title: 'Pages',
2727
},
28+
{
29+
icon: 'i-carbon-roadmap',
30+
name: 'Timeline',
31+
order: -100,
32+
path: 'timeline',
33+
title: 'Timeline',
34+
},
2835
{
2936
icon: 'i-carbon-image-copy',
3037
name: 'assets',

‎packages/client/src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Pages from '~/pages/pages.vue'
1818
import PiniaPage from '~/pages/pinia.vue'
1919
import RouterPage from '~/pages/router.vue'
2020
import Settings from '~/pages/settings.vue'
21+
import Timeline from '~/pages/timeline.vue'
2122
import App from './App.vue'
2223
import '@unocss/reset/tailwind.css'
2324
import 'uno.css'
@@ -32,6 +33,7 @@ const routes = [
3233
{ path: '/pinia', component: PiniaPage },
3334
{ path: '/router', component: RouterPage },
3435
{ path: '/pages', component: Pages },
36+
{ path: '/timeline', component: Timeline },
3537
{ path: '/assets', component: Assets },
3638
{ path: '/graph', component: Graph },
3739
{ path: '/settings', component: Settings },
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script setup lang="ts">
2+
import { SelectiveList, Timeline } from '@vue/devtools-applet'
3+
import {
4+
rpc,
5+
useDevToolsState,
6+
} from '@vue/devtools-core'
7+
import { Pane, Splitpanes } from 'splitpanes'
8+
9+
const timelineRef = ref()
10+
11+
// responsive layout
12+
const splitpanesRef = ref<HTMLDivElement>()
13+
const splitpanesReady = ref(false)
14+
const { width: splitpanesWidth } = useElementSize(splitpanesRef)
15+
// prevent `Splitpanes` layout from being changed before it ready
16+
const horizontal = computed(() => splitpanesReady.value ? splitpanesWidth.value < 700 : false)
17+
18+
// #region toggle app
19+
const devtoolsState = useDevToolsState()
20+
const appRecords = computed(() => devtoolsState.appRecords.value.map(app => ({
21+
label: app.name + (app.version ? ` (${app.version})` : ''),
22+
value: app.id,
23+
})))
24+
25+
const normalizedAppRecords = computed(() => appRecords.value.map(app => ({
26+
label: app.label,
27+
id: app.value,
28+
})))
29+
30+
const activeAppRecordId = ref(devtoolsState.activeAppRecordId.value)
31+
watchEffect(() => {
32+
activeAppRecordId.value = devtoolsState.activeAppRecordId.value
33+
})
34+
35+
function toggleApp(id: string) {
36+
rpc.value.toggleApp(id).then(() => {
37+
clearTimelineEvents()
38+
})
39+
}
40+
41+
// #endregion
42+
const activeTimelineLayer = ref('')
43+
const timelineLayers = [
44+
{
45+
label: 'Mouse',
46+
id: 'mouse',
47+
},
48+
{
49+
label: 'Keyboard',
50+
id: 'keyboard',
51+
},
52+
{
53+
label: 'Component events',
54+
id: 'component-event',
55+
},
56+
{
57+
label: 'Performance',
58+
id: 'performance',
59+
},
60+
]
61+
62+
function clearTimelineEvents() {
63+
timelineRef.value?.clear()
64+
}
65+
66+
function toggleTimelineLayer() {
67+
clearTimelineEvents()
68+
}
69+
</script>
70+
71+
<template>
72+
<div class="h-full w-full">
73+
<Splitpanes ref="splitpanesRef" class="flex-1 overflow-auto" :horizontal="horizontal" @ready="splitpanesReady = true">
74+
<Pane v-if="appRecords.length > 1" border="base h-full" size="20">
75+
<div class="no-scrollbar h-full flex select-none gap-2 overflow-scroll">
76+
<SelectiveList v-model="activeAppRecordId" :data="normalizedAppRecords" class="w-full" @select="toggleApp" />
77+
</div>
78+
</Pane>
79+
<Pane border="base" h-full>
80+
<div class="h-full flex flex-col">
81+
<div class="no-scrollbar h-full flex select-none gap-2 overflow-scroll">
82+
<TimelineLayers v-model="activeTimelineLayer" :data="timelineLayers" class="w-full" @select="toggleTimelineLayer" @clear="clearTimelineEvents" />
83+
</div>
84+
</div>
85+
</Pane>
86+
<Pane relative h-full size="65">
87+
<div class="h-full flex flex-col p2">
88+
<Timeline ref="timelineRef" :layer-ids="[activeTimelineLayer]" :header-visible="false" doc-link="" />
89+
</div>
90+
</Pane>
91+
</Splitpanes>
92+
</div>
93+
</template>

‎packages/core/src/rpc/global.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DevToolsV6PluginAPIHookKeys, DevToolsV6PluginAPIHookPayloads, OpenInEditorOptions } from '@vue/devtools-kit'
2-
import { devtools, DevToolsContextHookKeys, DevToolsMessagingHookKeys, devtoolsRouter, devtoolsRouterInfo, getActiveInspectors, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getRpcClient, getRpcServer, stringify, toggleClientConnected, updateDevToolsClientDetected } from '@vue/devtools-kit'
2+
import { devtools, DevToolsContextHookKeys, DevToolsMessagingHookKeys, devtoolsRouter, devtoolsRouterInfo, getActiveInspectors, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getRpcClient, getRpcServer, stringify, toggleClientConnected, updateDevToolsClientDetected, updateTimelineLayersState } from '@vue/devtools-kit'
33
import { createHooks } from 'hookable'
44

55
const hooks = createHooks()
@@ -32,6 +32,7 @@ function getDevToolsState() {
3232
routerId: item.routerId,
3333
})),
3434
activeAppRecordId: state.activeAppRecordId,
35+
timelineLayersState: state.timelineLayersState,
3536
}
3637
}
3738

@@ -93,6 +94,9 @@ export const functions = {
9394
getInspectorActions(id: string) {
9495
return getInspectorActions(id)
9596
},
97+
updateTimelineLayersState(state: Record<string, boolean>) {
98+
return updateTimelineLayersState(state)
99+
},
96100
callInspectorNodeAction(inspectorId: string, actionIndex: number, nodeId: string) {
97101
const nodeActions = getInspectorNodeActions(inspectorId)
98102
if (nodeActions?.length) {

‎packages/core/src/vue-plugin/devtools-state.ts

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface DevToolsState {
1212
vitePluginDetected: boolean
1313
appRecords: AppRecord[]
1414
activeAppRecordId: string
15+
timelineLayersState: Record<string, boolean>
1516
}
1617

1718
type DevToolsRefState = {
@@ -44,6 +45,7 @@ export function createDevToolsStateContext() {
4445
const vitePluginDetected = ref(false)
4546
const appRecords = ref<Array<AppRecord>>([])
4647
const activeAppRecordId = ref('')
48+
const timelineLayersState = ref<Record<string, boolean>>({})
4749

4850
function updateState(data: DevToolsState) {
4951
connected.value = data.connected
@@ -54,6 +56,7 @@ export function createDevToolsStateContext() {
5456
vitePluginDetected.value = data.vitePluginDetected
5557
appRecords.value = data.appRecords
5658
activeAppRecordId.value = data.activeAppRecordId!
59+
timelineLayersState.value = data.timelineLayersState!
5760
}
5861

5962
function getDevToolsState() {
@@ -76,6 +79,7 @@ export function createDevToolsStateContext() {
7679
vitePluginDetected,
7780
appRecords,
7881
activeAppRecordId,
82+
timelineLayersState,
7983
}
8084
}
8185

‎packages/devtools-kit/src/core/app/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function createAppRecord(app: VueAppInstance['appContext']['app']): AppRe
5757
id,
5858
name,
5959
instanceMap: new Map(),
60+
perfGroupIds: new Map(),
6061
rootInstance,
6162
}
6263

‎packages/devtools-kit/src/core/plugin/components.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ComponentWalker } from '../../core/component/tree/walker'
66
import { getAppRecord, getComponentId, getComponentInstance } from '../../core/component/utils'
77
import { activeAppRecord, devtoolsContext, devtoolsState, DevToolsV6PluginAPIHookKeys } from '../../ctx'
88
import { hook } from '../../hook'
9+
import { setupBuiltinTimelineLayers } from '../timeline'
910
import { exposeInstanceToWindow } from '../vm'
1011

1112
const INSPECTOR_ID = 'components'
@@ -24,6 +25,8 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu
2425
treeFilterPlaceholder: 'Search components',
2526
})
2627

28+
setupBuiltinTimelineLayers(api)
29+
2730
api.on.getInspectorTree(async (payload) => {
2831
if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
2932
// @ts-expect-error skip type @TODO
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { DevToolsV6PluginAPI } from '../../api/v6'
2+
import { isBrowser } from '@vue/devtools-shared'
3+
import { getAppRecord, getInstanceName } from '../../core/component/utils'
4+
import { devtoolsState } from '../../ctx/state'
5+
import { hook } from '../../hook'
6+
import { PERFORMANCE_EVENT_LAYER_ID, performanceMarkEnd, performanceMarkStart } from './perf'
7+
8+
const COMPONENT_EVENT_LAYER_ID = 'component-event'
9+
10+
export function setupBuiltinTimelineLayers(api: DevToolsV6PluginAPI) {
11+
if (!isBrowser)
12+
return
13+
14+
// Mouse events timeline layer
15+
16+
api.addTimelineLayer({
17+
id: 'mouse',
18+
label: 'Mouse',
19+
color: 0xA451AF,
20+
})
21+
22+
;(['mousedown', 'mouseup', 'click', 'dblclick'] as const).forEach((eventType) => {
23+
if (!devtoolsState.timelineLayersState.recordingState || !devtoolsState.timelineLayersState.mouseEventEnabled)
24+
return
25+
window.addEventListener(eventType, async (event: MouseEvent) => {
26+
await api.addTimelineEvent({
27+
layerId: 'mouse',
28+
event: {
29+
time: Date.now(),
30+
data: {
31+
type: eventType,
32+
x: event.clientX,
33+
y: event.clientY,
34+
},
35+
title: eventType,
36+
},
37+
})
38+
}, {
39+
capture: true,
40+
passive: true,
41+
})
42+
})
43+
44+
// Keyboard events timeline layer
45+
46+
api.addTimelineLayer({
47+
id: 'keyboard',
48+
label: 'Keyboard',
49+
color: 0x8151AF,
50+
})
51+
52+
;(['keyup', 'keydown', 'keypress'] as const).forEach((eventType) => {
53+
window.addEventListener(eventType, async (event: KeyboardEvent) => {
54+
if (!devtoolsState.timelineLayersState.recordingState || !devtoolsState.timelineLayersState.keyboardEventEnabled)
55+
return
56+
await api.addTimelineEvent({
57+
layerId: 'keyboard',
58+
event: {
59+
time: Date.now(),
60+
data: {
61+
type: eventType,
62+
key: event.key,
63+
ctrlKey: event.ctrlKey,
64+
shiftKey: event.shiftKey,
65+
altKey: event.altKey,
66+
metaKey: event.metaKey,
67+
},
68+
title: event.key,
69+
},
70+
})
71+
}, {
72+
capture: true,
73+
passive: true,
74+
})
75+
})
76+
77+
// Component events timeline layer
78+
79+
api.addTimelineLayer({
80+
id: COMPONENT_EVENT_LAYER_ID,
81+
label: 'Component events',
82+
color: 0x4FC08D,
83+
})
84+
85+
hook.on.componentEmit(async (app, instance, event, params) => {
86+
if (!devtoolsState.timelineLayersState.recordingState || !devtoolsState.timelineLayersState.componentEventEnabled)
87+
return
88+
89+
const appRecord = await getAppRecord(app)
90+
91+
if (!appRecord)
92+
return
93+
94+
const componentId = `${appRecord.id}:${instance.uid}`
95+
const componentName = getInstanceName(instance) || 'Unknown Component'
96+
97+
api.addTimelineEvent({
98+
layerId: COMPONENT_EVENT_LAYER_ID,
99+
event: {
100+
time: Date.now(),
101+
data: {
102+
component: {
103+
_custom: {
104+
type: 'component-definition',
105+
display: componentName,
106+
},
107+
},
108+
event,
109+
params,
110+
},
111+
title: event,
112+
subtitle: `by ${componentName}`,
113+
meta: {
114+
componentId,
115+
},
116+
},
117+
})
118+
})
119+
120+
// Performance timeline layer
121+
122+
api.addTimelineLayer({
123+
id: 'performance',
124+
label: PERFORMANCE_EVENT_LAYER_ID,
125+
color: 0x41B86A,
126+
})
127+
128+
hook.on.perfStart((app, uid, vm, type, time) => {
129+
if (!devtoolsState.timelineLayersState.recordingState || !devtoolsState.timelineLayersState.performanceEventEnabled)
130+
return
131+
performanceMarkStart(api, app, uid, vm, type, time)
132+
})
133+
134+
hook.on.perfEnd((app, uid, vm, type, time) => {
135+
if (!devtoolsState.timelineLayersState.recordingState || !devtoolsState.timelineLayersState.performanceEventEnabled)
136+
return
137+
performanceMarkEnd(api, app, uid, vm, type, time)
138+
})
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { App } from 'vue'
2+
import type { DevToolsV6PluginAPI } from '../../api/v6'
3+
import type { VueAppInstance } from '../../types'
4+
import { getAppRecord, getInstanceName } from '../../core/component/utils'
5+
import { devtoolsState } from '../../ctx/state'
6+
7+
type HookAppInstance = App & VueAppInstance
8+
const markEndQueue = new Map<string, {
9+
app: App
10+
uid: number
11+
instance: HookAppInstance
12+
type: string
13+
time: number
14+
}>()
15+
export const PERFORMANCE_EVENT_LAYER_ID = 'performance'
16+
17+
export async function performanceMarkStart(api: DevToolsV6PluginAPI, app: App, uid: number, vm: HookAppInstance, type: string, time: number) {
18+
const appRecord = await getAppRecord(app)
19+
if (!appRecord) {
20+
return
21+
}
22+
const componentName = getInstanceName(vm) || 'Unknown Component'
23+
const groupId = devtoolsState.perfUniqueGroupId++
24+
const groupKey = `${uid}-${type}`
25+
appRecord.perfGroupIds.set(groupKey, { groupId, time })
26+
await api.addTimelineEvent({
27+
layerId: PERFORMANCE_EVENT_LAYER_ID,
28+
event: {
29+
time: Date.now(),
30+
data: {
31+
component: componentName,
32+
type,
33+
measure: 'start',
34+
},
35+
title: componentName,
36+
subtitle: type,
37+
groupId,
38+
},
39+
})
40+
if (markEndQueue.has(groupKey)) {
41+
const {
42+
app,
43+
uid,
44+
instance,
45+
type,
46+
time,
47+
} = markEndQueue.get(groupKey)!
48+
markEndQueue.delete(groupKey)
49+
await performanceMarkEnd(
50+
api,
51+
app,
52+
uid,
53+
instance,
54+
type,
55+
time,
56+
)
57+
}
58+
}
59+
60+
export function performanceMarkEnd(api: DevToolsV6PluginAPI, app: App, uid: number, vm: HookAppInstance, type: string, time: number) {
61+
const appRecord = getAppRecord(app)
62+
if (!appRecord)
63+
return
64+
65+
const componentName = getInstanceName(vm) || 'Unknown Component'
66+
const groupKey = `${uid}-${type}`
67+
const groupInfo = appRecord.perfGroupIds.get(groupKey)
68+
if (groupInfo) {
69+
const groupId = groupInfo.groupId
70+
const startTime = groupInfo.time
71+
const duration = time - startTime
72+
api.addTimelineEvent({
73+
layerId: PERFORMANCE_EVENT_LAYER_ID,
74+
event: {
75+
time: Date.now(),
76+
data: {
77+
component: componentName,
78+
type,
79+
measure: 'end',
80+
duration: {
81+
_custom: {
82+
type: 'Duration',
83+
value: duration,
84+
display: `${duration} ms`,
85+
},
86+
},
87+
},
88+
title: componentName,
89+
subtitle: type,
90+
groupId,
91+
},
92+
})
93+
}
94+
else {
95+
markEndQueue.set(groupKey, { app, uid, instance: vm, type, time })
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { isBrowser } from '@vue/devtools-shared'
2+
3+
const TIMELINE_LAYERS_STATE_STORAGE_ID = '__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS_STATE__'
4+
5+
export function addTimelineLayersStateToStorage(state: Record<string, boolean | string>) {
6+
if (!isBrowser) {
7+
return
8+
}
9+
localStorage.setItem(TIMELINE_LAYERS_STATE_STORAGE_ID, JSON.stringify(state))
10+
}
11+
12+
export function getTimelineLayersStateFromStorage() {
13+
if (!isBrowser) {
14+
return {
15+
recordingState: false,
16+
mouseEventEnabled: false,
17+
keyboardEventEnabled: false,
18+
componentEventEnabled: false,
19+
performanceEventEnabled: false,
20+
selected: '',
21+
}
22+
}
23+
const state = localStorage.getItem(TIMELINE_LAYERS_STATE_STORAGE_ID)
24+
return state
25+
? JSON.parse(state)
26+
: {
27+
recordingState: false,
28+
mouseEventEnabled: false,
29+
keyboardEventEnabled: false,
30+
componentEventEnabled: false,
31+
performanceEventEnabled: false,
32+
selected: '',
33+
}
34+
}

‎packages/devtools-kit/src/ctx/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from './inspector'
1414
export * from './plugin'
1515
export * from './router'
1616
export * from './state'
17+
export { updateTimelineLayersState } from './timeline'
1718

1819
export interface DevtoolsContext {
1920
hooks: Hookable<DevToolsContextHooks & DevToolsMessagingHooks, HookKeys<DevToolsContextHooks & DevToolsMessagingHooks>>

‎packages/devtools-kit/src/ctx/state.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AppRecord, CustomCommand, CustomTab } from '../types'
22
import { target as global, isUrlString } from '@vue/devtools-shared'
33
import { debounce } from 'perfect-debounce'
44
import { devtoolsContext } from '.'
5+
import { getTimelineLayersStateFromStorage } from '../core/timeline/storage'
56
import { DevToolsMessagingHookKeys } from './hook'
67

78
export interface DevToolsAppRecords extends AppRecord {}
@@ -18,6 +19,8 @@ export interface DevToolsState {
1819
devtoolsClientDetected: {
1920
[key: string]: boolean
2021
}
22+
perfUniqueGroupId: number
23+
timelineLayersState: Record<string, boolean>
2124
}
2225

2326
global.__VUE_DEVTOOLS_KIT_APP_RECORDS__ ??= []
@@ -38,6 +41,8 @@ function initStateFactory() {
3841
commands: [],
3942
highPerfModeEnabled: true,
4043
devtoolsClientDetected: {},
44+
perfUniqueGroupId: 0,
45+
timelineLayersState: getTimelineLayersStateFromStorage(),
4146
}
4247
}
4348
global[STATE_KEY] ??= initStateFactory()

‎packages/devtools-kit/src/ctx/timeline.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { PluginDescriptor, TimelineLayerOptions } from '../types'
22
import { target } from '@vue/devtools-shared'
33
import { getAppRecord } from '../core/component/utils'
4+
import { addTimelineLayersStateToStorage } from '../core/timeline/storage'
5+
import { devtoolsState, updateDevToolsState } from './state'
46

57
interface DevToolsKitTimelineLayer extends TimelineLayerOptions {
68
appRecord: unknown
@@ -22,3 +24,14 @@ export function addTimelineLayer(options: TimelineLayerOptions, descriptor: Plug
2224
appRecord: getAppRecord(descriptor.app),
2325
})
2426
}
27+
28+
export function updateTimelineLayersState(state: Record<string, boolean>) {
29+
const updatedState = {
30+
...devtoolsState.timelineLayersState,
31+
...state,
32+
}
33+
addTimelineLayersStateToStorage(updatedState)
34+
updateDevToolsState({
35+
timelineLayersState: updatedState,
36+
})
37+
}

‎packages/devtools-kit/src/hook/index.ts

+27
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const on: VueHooks['on'] = {
2121
componentAdded(fn) {
2222
return devtoolsHooks.hook(DevToolsHooks.COMPONENT_ADDED, fn)
2323
},
24+
componentEmit(fn) {
25+
return devtoolsHooks.hook(DevToolsHooks.COMPONENT_EMIT, fn)
26+
},
2427
componentUpdated(fn) {
2528
return devtoolsHooks.hook(DevToolsHooks.COMPONENT_UPDATED, fn)
2629
},
@@ -30,6 +33,12 @@ const on: VueHooks['on'] = {
3033
setupDevtoolsPlugin(fn) {
3134
devtoolsHooks.hook(DevToolsHooks.SETUP_DEVTOOLS_PLUGIN, fn)
3235
},
36+
perfStart(fn) {
37+
return devtoolsHooks.hook(DevToolsHooks.PERFORMANCE_START, fn)
38+
},
39+
perfEnd(fn) {
40+
return devtoolsHooks.hook(DevToolsHooks.PERFORMANCE_END, fn)
41+
},
3342
}
3443

3544
export function createDevToolsHook(): DevToolsHook {
@@ -113,6 +122,24 @@ export function subscribeDevToolsHook() {
113122
devtoolsHooks.callHook(DevToolsHooks.COMPONENT_REMOVED, app, uid, parentUid, component)
114123
})
115124

125+
hook.on<DevToolsEvent[DevToolsHooks.COMPONENT_EMIT]>(DevToolsHooks.COMPONENT_EMIT, async (app, instance, event, params) => {
126+
if (!app || !instance || devtoolsState.highPerfModeEnabled)
127+
return
128+
devtoolsHooks.callHook(DevToolsHooks.COMPONENT_EMIT, app, instance, event, params)
129+
})
130+
131+
hook.on<DevToolsEvent[DevToolsHooks.PERFORMANCE_START]>(DevToolsHooks.PERFORMANCE_START, (app, uid, vm, type, time) => {
132+
if (!app || devtoolsState.highPerfModeEnabled)
133+
return
134+
devtoolsHooks.callHook(DevToolsHooks.PERFORMANCE_START, app, uid, vm, type, time)
135+
})
136+
137+
hook.on<DevToolsEvent[DevToolsHooks.PERFORMANCE_END]>(DevToolsHooks.PERFORMANCE_END, (app, uid, vm, type, time) => {
138+
if (!app || devtoolsState.highPerfModeEnabled)
139+
return
140+
devtoolsHooks.callHook(DevToolsHooks.PERFORMANCE_END, app, uid, vm, type, time)
141+
})
142+
116143
// devtools plugin setup
117144
hook.on<DevToolsEvent[DevToolsHooks.SETUP_DEVTOOLS_PLUGIN]>(DevToolsHooks.SETUP_DEVTOOLS_PLUGIN, (pluginDescriptor, setupFn, options) => {
118145
if (options?.target === 'legacy')

‎packages/devtools-kit/src/types/app.ts

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface AppRecord {
5050
version?: string
5151
types?: Record<string, string | symbol>
5252
instanceMap: Map<string, VueAppInstance>
53+
perfGroupIds: Map<string, { groupId: number, time: number }>
5354
rootInstance: VueAppInstance
5455
routerId?: string
5556
}

‎packages/devtools-kit/src/types/hook.ts

+6
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ export interface DevToolsEvent {
2626
[DevToolsHooks.APP_CONNECTED]: () => void
2727
[DevToolsHooks.APP_UNMOUNT]: (app: VueAppInstance['appContext']['app']) => void | Promise<void>
2828
[DevToolsHooks.COMPONENT_ADDED]: (app: HookAppInstance, uid: number, parentUid: number, component: VueAppInstance) => void | Promise<void>
29+
[DevToolsHooks.COMPONENT_EMIT]: (app: HookAppInstance, instance: VueAppInstance, event: string, params: unknown) => void | Promise<void>
2930
[DevToolsHooks.COMPONENT_UPDATED]: DevToolsEvent['component:added']
3031
[DevToolsHooks.COMPONENT_REMOVED]: DevToolsEvent['component:added']
3132
[DevToolsHooks.SETUP_DEVTOOLS_PLUGIN]: (pluginDescriptor: PluginDescriptor, setupFn: PluginSetupFunction, options?: { target?: string }) => void
33+
[DevToolsHooks.PERFORMANCE_START]: (app: App, uid: number, vm: HookAppInstance, type: string, time: number) => void
34+
[DevToolsHooks.PERFORMANCE_END]: (app: App, uid: number, vm: HookAppInstance, type: string, time: number) => void
3235
}
3336

3437
export interface DevToolsHook {
@@ -51,9 +54,12 @@ export interface VueHooks {
5154
vueAppUnmount: (fn: DevToolsEvent[DevToolsHooks.APP_UNMOUNT]) => void
5255
vueAppConnected: (fn: DevToolsEvent[DevToolsHooks.APP_CONNECTED]) => void
5356
componentAdded: (fn: DevToolsEvent[DevToolsHooks.COMPONENT_ADDED]) => () => void
57+
componentEmit: (fn: DevToolsEvent[DevToolsHooks.COMPONENT_EMIT]) => () => void
5458
componentUpdated: (fn: DevToolsEvent[DevToolsHooks.COMPONENT_UPDATED]) => () => void
5559
componentRemoved: (fn: DevToolsEvent[DevToolsHooks.COMPONENT_REMOVED]) => () => void
5660
setupDevtoolsPlugin: (fn: DevToolsEvent[DevToolsHooks.SETUP_DEVTOOLS_PLUGIN]) => void
61+
perfStart: (fn: DevToolsEvent[DevToolsHooks.PERFORMANCE_START]) => void
62+
perfEnd: (fn: DevToolsEvent[DevToolsHooks.PERFORMANCE_END]) => void
5763
}
5864
setupDevToolsPlugin: (pluginDescriptor: PluginDescriptor, setupFn: PluginSetupFunction) => void
5965
}

‎packages/playground/basic/src/pages/Home.vue

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import Foo from '../components/Foo.vue'
33
import IndexComponent from '../components/IndexComponent/index.vue'
44
5+
const emit = defineEmits(['update'])
56
const visible = ref(false)
6-
77
const obj = reactive<{
88
count: number
99
foo?: number
@@ -17,12 +17,19 @@ obj.foo = toRef(obj, 'count')
1717
// @ts-expect-error type guard
1818
obj.bar = ref('bar')
1919
20+
function trigger() {
21+
emit('update', 1)
22+
}
23+
2024
const toRefObj = toRefs(obj)
2125
</script>
2226

2327
<template>
2428
<div class="m-auto mt-3 h-80 w-120 flex flex-col items-center justify-center rounded bg-[#363636]">
2529
Home
30+
<button @click="trigger">
31+
Click me
32+
</button>
2633
<Foo v-if="visible" />
2734
<IndexComponent v-if="visible" />
2835
<img src="/vite.svg" alt="Vite Logo">

0 commit comments

Comments
 (0)
Please sign in to comment.