Skip to content

Commit d1e3a00

Browse files
authored
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 2 additions & 11 deletions
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

Lines changed: 13 additions & 3 deletions
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

Lines changed: 7 additions & 0 deletions
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+
}
Lines changed: 107 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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 },
Lines changed: 93 additions & 0 deletions
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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

0 commit comments

Comments
 (0)