Skip to content

Commit bcf57c0

Browse files
committed
feat(timeline): markers
1 parent 57beb33 commit bcf57c0

12 files changed

Lines changed: 276 additions & 38 deletions

File tree

packages/api/src/api/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ export interface TimelineEvent<TData = any, TMeta = any> {
7070
subtitle?: string
7171
}
7272

73+
export interface TimelineMarkerOptions {
74+
id: string
75+
time: number
76+
color: number
77+
label: string
78+
all?: boolean
79+
}
80+
7381
export interface CustomInspectorOptions {
7482
id: string
7583
label: string

packages/api/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@ export interface PluginDescriptor {
1515
componentStateTypes?: string[]
1616
logo?: string
1717
disableAppScope?: boolean
18+
/**
19+
* Run the plugin setup and expose the api even if the devtools is not opened yet.
20+
* Useful to record timeline events early.
21+
*/
22+
enableEarlyProxy?: boolean
1823
}
1924

2025
export type SetupFunction = (api: DevtoolsPluginApi) => void
2126

2227
export function setupDevtoolsPlugin (pluginDescriptor: PluginDescriptor, setupFn: SetupFunction) {
2328
const target = getTarget()
2429
const hook = getDevtoolsGlobalHook()
25-
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !isProxyAvailable)) {
30+
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !isProxyAvailable || !pluginDescriptor.enableEarlyProxy)) {
2631
hook.emit(HOOK_SETUP, pluginDescriptor, setupFn)
2732
} else {
2833
const proxy = isProxyAvailable ? new ApiProxy() : null

packages/app-backend-api/src/backend-context.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
CustomInspectorOptions,
66
TimelineEventOptions,
77
WithId,
8-
ID
8+
ID,
9+
TimelineMarkerOptions
910
} from '@vue/devtools-api'
1011
import { AppRecord } from './app-record'
1112
import { DevtoolsApi } from './api'
@@ -27,6 +28,7 @@ export interface BackendContext {
2728
timelineEventMap: Map<ID, TimelineEventOptions & WithId>
2829
perfUniqueGroupId: number
2930
customInspectors: CustomInspector[]
31+
timelineMarkers: TimelineMarker[]
3032
}
3133

3234
export interface TimelineLayer extends TimelineLayerOptions {
@@ -35,6 +37,10 @@ export interface TimelineLayer extends TimelineLayerOptions {
3537
events: (TimelineEventOptions & WithId)[]
3638
}
3739

40+
export interface TimelineMarker extends TimelineMarkerOptions {
41+
app: App | null
42+
}
43+
3844
export interface CustomInspector extends CustomInspectorOptions {
3945
app: App
4046
plugin: Plugin
@@ -62,7 +68,8 @@ export function createBackendContext (options: CreateBackendContextOptions): Bac
6268
nextTimelineEventId: 0,
6369
timelineEventMap: new Map(),
6470
perfUniqueGroupId: 0,
65-
customInspectors: []
71+
customInspectors: [],
72+
timelineMarkers: []
6673
}
6774
ctx.api = new DevtoolsApi(options.bridge, ctx)
6875
return ctx

packages/app-backend-core/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { sendInspectorTree, getInspector, getInspectorWithAppId, sendInspectorSt
3737
import { showScreenshot } from './timeline-screenshot'
3838
import { handleAddPerformanceTag, performanceMarkEnd, performanceMarkStart } from './perf'
3939
import { initOnPageConfig } from './page-config'
40+
import { sendTimelineMarkers, addTimelineMarker } from './timeline-marker'
4041

4142
let ctx: BackendContext = target.__vdevtools_ctx ?? null
4243
let connected = target.__vdevtools_connected ?? false
@@ -83,6 +84,14 @@ export async function initBackend (bridge: Bridge) {
8384
ctx.bridge = bridge
8485
connectBridge()
8586
}
87+
88+
addTimelineMarker({
89+
id: 'vue-devtools-init-backend',
90+
time: Date.now(),
91+
label: 'Vue Devtools init',
92+
color: 0x41B883,
93+
all: true
94+
}, ctx)
8695
}
8796

8897
async function connect () {
@@ -475,6 +484,10 @@ function connectBridge () {
475484
sendTimelineLayerEvents(appId, layerId, ctx)
476485
})
477486

487+
ctx.bridge.on(BridgeEvents.TO_BACK_TIMELINE_LOAD_MARKERS, async () => {
488+
await sendTimelineMarkers(ctx)
489+
})
490+
478491
// Custom inspectors
479492

480493
ctx.bridge.on(BridgeEvents.TO_BACK_CUSTOM_INSPECTOR_LIST, () => {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { BackendContext, TimelineMarker } from '@vue-devtools/app-backend-api'
2+
import { getAppRecordId } from './app'
3+
import { BridgeEvents } from '@vue-devtools/shared-utils'
4+
import { TimelineMarkerOptions } from '@vue/devtools-api'
5+
6+
export async function addTimelineMarker (options: TimelineMarkerOptions, ctx: BackendContext) {
7+
if (!ctx.currentAppRecord) {
8+
options.all = true
9+
}
10+
const marker: TimelineMarker = {
11+
...options,
12+
app: options.all ? null : ctx.currentAppRecord?.options.app
13+
}
14+
ctx.timelineMarkers.push(marker)
15+
ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_MARKER, {
16+
marker: await serializeMarker(marker),
17+
appId: ctx.currentAppRecord.id
18+
})
19+
}
20+
21+
export async function sendTimelineMarkers (ctx: BackendContext) {
22+
const markers = ctx.timelineMarkers.filter(marker => marker.all || marker.app === ctx.currentAppRecord.options.app)
23+
const result = []
24+
for (const marker of markers) {
25+
result.push(await serializeMarker(marker))
26+
}
27+
ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_LOAD_MARKERS, {
28+
markers: result,
29+
appId: ctx.currentAppRecord.id
30+
})
31+
}
32+
33+
async function serializeMarker (marker: TimelineMarker) {
34+
return {
35+
id: marker.id,
36+
appId: marker.app ? await getAppRecordId(marker.app) : null,
37+
all: marker.all,
38+
time: marker.time,
39+
label: marker.label,
40+
color: marker.color
41+
}
42+
}

packages/app-frontend/src/features/timeline/TimelineView.vue

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ import {
1919
onEventAdd,
2020
useCursor,
2121
Layer,
22-
TimelineEvent
22+
TimelineEvent,
23+
useMarkers,
24+
TimelineMarker
2325
} from './composable'
2426
import { useApps } from '@front/features/apps'
2527
import { onKeyUp } from '@front/util/keyboard'
2628
import SharedData from '@utils/shared-data'
2729
import { useDarkMode } from '@front/util/theme'
2830
import { dimColor, boostColor } from '@front/util/color'
31+
import { formatTime } from '@front/util/format'
2932
3033
const LAYER_SIZE = 16
3134
const GROUP_SIZE = 6
@@ -41,6 +44,10 @@ export default defineComponent({
4144
const { startTime, endTime, minTime, maxTime } = useTime()
4245
const { darkMode } = useDarkMode()
4346
47+
function getTimePosition (time: number) {
48+
return (time - minTime.value) / (endTime.value - startTime.value) * app.view.width
49+
}
50+
4451
// Reset
4552
4653
type ResetCb = () => void
@@ -106,6 +113,53 @@ export default defineComponent({
106113
updateBackground()
107114
})
108115
116+
// Markers
117+
118+
const { currentAppMarkers } = useMarkers()
119+
120+
let markerContainer: PIXI.Graphics
121+
122+
onMounted(() => {
123+
markerContainer = new PIXI.Graphics()
124+
app.stage.addChild(markerContainer)
125+
drawMarkers()
126+
})
127+
128+
function drawMarkers () {
129+
console.log('markers', currentAppMarkers.value)
130+
markerContainer.clear()
131+
for (const marker of currentAppMarkers.value) {
132+
markerContainer.lineStyle(1, marker.color, 0.5, 0, true)
133+
const x = getTimePosition(marker.time)
134+
marker.x = x
135+
markerContainer.moveTo(x, 0)
136+
markerContainer.lineTo(x, app.view.height)
137+
}
138+
markerContainer.x = horizontalScrollingContainer.x
139+
}
140+
141+
watch(currentAppMarkers, () => {
142+
if (markerContainer) {
143+
drawMarkers()
144+
}
145+
})
146+
147+
function getMarkerAtPosition (targetX: number): TimelineMarker | null {
148+
let choice: TimelineMarker = null
149+
let dist: number
150+
151+
for (const marker of currentAppMarkers.value) {
152+
const globalX = marker.x + markerContainer.x
153+
const currentDist = Math.abs(targetX - globalX)
154+
if (currentDist <= 50 && (currentDist < dist || !choice)) {
155+
dist = currentDist
156+
choice = marker
157+
}
158+
}
159+
160+
return choice
161+
}
162+
109163
// Layers
110164
111165
const {
@@ -247,10 +301,6 @@ export default defineComponent({
247301
248302
let events: TimelineEvent[] = []
249303
250-
function getEventPosition (event: TimelineEvent) {
251-
return (event.time - minTime.value) / (endTime.value - startTime.value) * app.view.width
252-
}
253-
254304
const updateEventPositionQueue = new Set<TimelineEvent>()
255305
let currentEventPositionUpdate: TimelineEvent = null
256306
let updateEventPositionQueued = false
@@ -262,7 +312,7 @@ export default defineComponent({
262312
e.container.visible = !ignored
263313
if (ignored) continue
264314
// Update horizontal position immediately
265-
e.container.x = getEventPosition(e)
315+
e.container.x = getTimePosition(e.time)
266316
// Queue vertical position compute
267317
updateEventPositionQueue.add(e)
268318
}
@@ -316,13 +366,13 @@ export default defineComponent({
316366
(
317367
// Horizontal intersection (first event)
318368
(
319-
getEventPosition(firstEvent) >= getEventPosition(otherGroup.firstEvent) - offset &&
320-
getEventPosition(firstEvent) <= getEventPosition(otherGroup.lastEvent) + offset + lastOffset
369+
getTimePosition(firstEvent.time) >= getTimePosition(otherGroup.firstEvent.time) - offset &&
370+
getTimePosition(firstEvent.time) <= getTimePosition(otherGroup.lastEvent.time) + offset + lastOffset
321371
) ||
322372
// Horizontal intersection (last event)
323373
(
324-
getEventPosition(lastEvent) >= getEventPosition(otherGroup.firstEvent) - offset - lastOffset &&
325-
getEventPosition(lastEvent) <= getEventPosition(otherGroup.lastEvent) + offset
374+
getTimePosition(lastEvent.time) >= getTimePosition(otherGroup.firstEvent.time) - offset - lastOffset &&
375+
getTimePosition(lastEvent.time) <= getTimePosition(otherGroup.lastEvent.time) + offset
326376
)
327377
)
328378
) {
@@ -657,10 +707,11 @@ export default defineComponent({
657707
eventTooltip.addChild(eventTooltipText)
658708
659709
app.stage.addListener('mousemove', mouseEvent => {
710+
const text: string[] = []
711+
712+
// Event tooltip
660713
const event = getEventAtPosition(mouseEvent.data.global.x, mouseEvent.data.global.y)
661714
if (event) {
662-
const text = []
663-
664715
text.push(event.title ?? 'Event')
665716
if (event.subtitle) {
666717
text.push(event.subtitle)
@@ -670,34 +721,41 @@ export default defineComponent({
670721
text.push(`Group: ${event.group.duration}ms (${event.group.events.length} event${event.group.events.length > 1 ? 's' : ''})`)
671722
}
672723
673-
if (!text.length) {
674-
eventTooltip.visible = false
675-
} else {
676-
eventTooltipText.text = text.join('\n')
677-
678-
eventTooltipGraphics.clear()
679-
eventTooltipGraphics.beginFill(0xffffff)
680-
eventTooltipGraphics.lineStyle(1, 0x000000, 0.2, 1)
681-
eventTooltipGraphics.drawRoundedRect(0, 0, eventTooltipText.width + 8, eventTooltipText.height + 8, 4)
682-
683-
eventTooltip.x = mouseEvent.data.global.x + 12
684-
if (eventTooltip.x + eventTooltip.width > app.renderer.width) {
685-
eventTooltip.x = mouseEvent.data.global.x - eventTooltip.width - 12
686-
}
687-
eventTooltip.y = mouseEvent.data.global.y + 12
688-
if (eventTooltip.y + eventTooltip.height > app.renderer.height) {
689-
eventTooltip.y = mouseEvent.data.global.y - eventTooltip.height - 12
690-
}
691-
eventTooltip.visible = true
692-
}
693-
694724
if (hoverEvent?.container && hoverEvent !== event) {
695725
hoverEvent.container.alpha = 1
696726
}
697727
hoverEvent = event
698728
if (hoverEvent?.container) {
699729
hoverEvent.container.alpha = 0.5
700730
}
731+
} else {
732+
// Marker tooltip
733+
const marker = getMarkerAtPosition(mouseEvent.data.global.x)
734+
if (marker) {
735+
text.push(marker.label)
736+
text.push(formatTime(marker.time, 'ms'))
737+
text.push('(marker)')
738+
}
739+
}
740+
741+
if (text.length) {
742+
// Draw tooltip
743+
eventTooltipText.text = text.join('\n')
744+
745+
eventTooltipGraphics.clear()
746+
eventTooltipGraphics.beginFill(0xffffff)
747+
eventTooltipGraphics.lineStyle(1, 0x000000, 0.2, 1)
748+
eventTooltipGraphics.drawRoundedRect(0, 0, eventTooltipText.width + 8, eventTooltipText.height + 8, 4)
749+
750+
eventTooltip.x = mouseEvent.data.global.x + 12
751+
if (eventTooltip.x + eventTooltip.width > app.renderer.width) {
752+
eventTooltip.x = mouseEvent.data.global.x - eventTooltip.width - 12
753+
}
754+
eventTooltip.y = mouseEvent.data.global.y + 12
755+
if (eventTooltip.y + eventTooltip.height > app.renderer.height) {
756+
eventTooltip.y = mouseEvent.data.global.y - eventTooltip.height - 12
757+
}
758+
eventTooltip.visible = true
701759
} else {
702760
if (hoverEvent?.container) {
703761
hoverEvent.container.alpha = 1
@@ -716,7 +774,7 @@ export default defineComponent({
716774
/** @type {PIXI.Graphics} */
717775
const g = event.groupG
718776
g.clear()
719-
const size = getEventPosition(event.group.lastEvent) - getEventPosition(event.group.firstEvent)
777+
const size = getTimePosition(event.group.lastEvent.time) - getTimePosition(event.group.firstEvent.time)
720778
if (event.layer.groupsOnly) {
721779
if (drawAsSelected) {
722780
g.lineStyle(2, boostColor(event.layer.color, darkMode.value))
@@ -861,6 +919,7 @@ export default defineComponent({
861919
horizontalScrollingContainer.x = -(startTime.value - minTime.value) / (endTime.value - startTime.value) * app.view.width
862920
drawLayerBackgroundEffects()
863921
drawTimeGrid()
922+
drawMarkers()
864923
}
865924
866925
watch(startTime, () => queueCameraUpdate())

packages/app-frontend/src/features/timeline/composable/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './events'
22
export * from './layers'
3+
export * from './markers'
34
export * from './reset'
45
export * from './screenshot'
56
export * from './setup'
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useCurrentApp } from '@front/features/apps'
2+
import { computed, watch } from '@vue/composition-api'
3+
import { markersAllApps, markersPerApp } from './store'
4+
import { getBridge } from '@front/features/bridge'
5+
import { BridgeEvents } from '@vue-devtools/shared-utils'
6+
7+
export function useMarkers () {
8+
const { currentAppId } = useCurrentApp()
9+
const currentAppMarkers = computed(() => markersAllApps.value.concat(markersPerApp.value[currentAppId.value] ?? []))
10+
11+
watch(currentAppId, () => {
12+
loadMarkers()
13+
}, {
14+
immediate: true
15+
})
16+
17+
return {
18+
currentAppMarkers
19+
}
20+
}
21+
22+
function loadMarkers () {
23+
getBridge().send(BridgeEvents.TO_BACK_TIMELINE_LOAD_MARKERS)
24+
}

0 commit comments

Comments
 (0)