Skip to content

Commit f1d534f

Browse files
committed
perf(timeline): optimize group render and scrolling
1 parent 4bed049 commit f1d534f

File tree

3 files changed

+89
-87
lines changed

3 files changed

+89
-87
lines changed

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

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ const LAYER_SIZE = 16
4747
const GROUP_SIZE = 6
4848
const MIN_CAMERA_SIZE = 10
4949
50+
const propConfig = {
51+
writable: true,
52+
configurable: false,
53+
}
54+
5055
installUnsafeEval(PIXI)
5156
5257
export default defineComponent({
@@ -57,15 +62,16 @@ export default defineComponent({
5762
const { startTime, endTime, minTime, maxTime } = useTime()
5863
const { darkMode } = useDarkMode()
5964
60-
// Optimize for read in loops
61-
const nonReactiveTime = {
65+
// Optimize for read in loops and hot code
66+
const nonReactiveState = {
6267
startTime: nonReactive(startTime),
6368
endTime: nonReactive(endTime),
6469
minTime: nonReactive(minTime),
70+
darkMode: nonReactive(darkMode),
6571
}
6672
6773
function getTimePosition (time: number) {
68-
return (time - nonReactiveTime.minTime.value) / (nonReactiveTime.endTime.value - nonReactiveTime.startTime.value) * app.view.width / window.devicePixelRatio
74+
return (time - nonReactiveState.minTime.value) / (nonReactiveState.endTime.value - nonReactiveState.startTime.value) * app.view.width / window.devicePixelRatio
6975
}
7076
7177
// Reset
@@ -159,7 +165,7 @@ export default defineComponent({
159165
})
160166
161167
function updateBackground () {
162-
if (darkMode.value) {
168+
if (nonReactiveState.darkMode.value) {
163169
app && (app.renderer.backgroundColor = 0x0b1015)
164170
} else {
165171
app && (app.renderer.backgroundColor = 0xffffff)
@@ -355,6 +361,7 @@ export default defineComponent({
355361
// Events
356362
357363
const { selectedEvent } = useSelectedEvent()
364+
const nonReactiveSelectedEvent = nonReactive(selectedEvent)
358365
359366
let events: TimelineEvent[] = []
360367
@@ -418,8 +425,15 @@ export default defineComponent({
418425
draw()
419426
}
420427
428+
let isEventIgnoredCache: Record<TimelineEvent['id'], boolean> = {}
429+
421430
function isEventIgnored (event: TimelineEvent) {
422-
return event.layer.ignoreNoDurationGroups && event.group && event.group.duration <= 0
431+
let result = isEventIgnoredCache[event.id]
432+
if (result == null) {
433+
result = event.layer.ignoreNoDurationGroups && event.group?.nonReactiveDuration <= 0
434+
isEventIgnoredCache[event.id] = result
435+
}
436+
return result
423437
}
424438
425439
function computeEventVerticalPosition (event: TimelineEvent) {
@@ -434,7 +448,7 @@ export default defineComponent({
434448
// Collision offset for non-flamecharts
435449
const offset = event.layer.groupsOnly ? 0 : 12
436450
// For flamechart allow 1-pixel overlap at the end of a group
437-
const lastOffset = event.layer.groupsOnly && event.group?.duration > 0 ? -1 : 0
451+
const lastOffset = event.layer.groupsOnly && event.group?.nonReactiveDuration > 0 ? -1 : 0
438452
// Flamechart uses time instead of pixel position
439453
const getPos = event.layer.groupsOnly ? (time: number) => time : getTimePosition
440454
@@ -483,7 +497,7 @@ export default defineComponent({
483497
)
484498
)) {
485499
// Collision!
486-
if (event.group && event.group.duration > otherGroup.duration && firstEvent.time <= otherGroup.firstEvent.time) {
500+
if (event.group && event.group.nonReactiveDuration > otherGroup.nonReactiveDuration && firstEvent.time <= otherGroup.firstEvent.time) {
487501
// Invert positions because current group has higher priority
488502
if (!updateEventVerticalPositionQueue.has(otherGroup.firstEvent)) {
489503
queueEventPositionUpdate([otherGroup.firstEvent], event.layer.groupsOnly)
@@ -524,32 +538,18 @@ export default defineComponent({
524538
525539
if (!event.layer.groupsOnly || (event.group?.firstEvent === event)) {
526540
eventContainer = new PIXI.Container()
527-
Object.defineProperty(event, 'container', {
528-
value: eventContainer,
529-
writable: true,
530-
configurable: false,
531-
})
541+
Object.defineProperty(event, 'container', { value: eventContainer, ...propConfig })
532542
layerContainer.addChild(eventContainer)
533543
}
534544
535545
// Group graphics
536546
if (event.group) {
537547
if (event.group.firstEvent === event) {
538548
const groupG = new PIXI.Graphics()
539-
Object.defineProperty(event, 'groupG', {
540-
value: groupG,
541-
writable: true,
542-
configurable: false,
543-
})
544-
Object.defineProperty(event, 'groupT', {
545-
value: null,
546-
writable: true,
547-
configurable: false,
548-
})
549-
Object.defineProperty(event, 'groupText', {
550-
value: null,
551-
writable: true,
552-
configurable: false,
549+
Object.defineProperties(event, {
550+
groupG: { value: groupG, ...propConfig },
551+
groupT: { value: null, ...propConfig },
552+
groupText: { value: null, ...propConfig },
553553
})
554554
eventContainer.addChild(groupG)
555555
event.group.oldSize = null
@@ -565,11 +565,7 @@ export default defineComponent({
565565
// Graphics
566566
if (eventContainer) {
567567
const g = new PIXI.Graphics()
568-
Object.defineProperty(event, 'g', {
569-
value: g,
570-
writable: true,
571-
configurable: false,
572-
})
568+
Object.defineProperty(event, 'g', { value: g, ...propConfig })
573569
eventContainer.addChild(g)
574570
}
575571
@@ -618,6 +614,7 @@ export default defineComponent({
618614
e.container = null
619615
}
620616
events = []
617+
isEventIgnoredCache = {}
621618
}
622619
623620
function resetEvents () {
@@ -736,8 +733,8 @@ export default defineComponent({
736733
if (selected) {
737734
// Border-only style
738735
size--
739-
g.lineStyle(2, boostColor(color, darkMode.value))
740-
g.beginFill(dimColor(color, darkMode.value))
736+
g.lineStyle(2, boostColor(color, nonReactiveState.darkMode.value))
737+
g.beginFill(dimColor(color, nonReactiveState.darkMode.value))
741738
if (!event.group || event.group.firstEvent !== event) {
742739
event.container.zIndex = 999999999
743740
}
@@ -759,7 +756,7 @@ export default defineComponent({
759756
const drawUnselectedEvent = drawEvent.bind(null, false)
760757
761758
function refreshEventGraphics (event: TimelineEvent) {
762-
if (selectedEvent.value === event) {
759+
if (nonReactiveSelectedEvent.value === event) {
763760
drawSelectedEvent(event)
764761
} else {
765762
drawUnselectedEvent(event)
@@ -775,8 +772,8 @@ export default defineComponent({
775772
776773
function selectPreviousEvent () {
777774
let index
778-
if (selectedEvent.value) {
779-
index = events.indexOf(selectedEvent.value)
775+
if (nonReactiveSelectedEvent.value) {
776+
index = events.indexOf(nonReactiveSelectedEvent.value)
780777
} else {
781778
index = events.length
782779
}
@@ -797,8 +794,8 @@ export default defineComponent({
797794
798795
function selectNextEvent () {
799796
let index
800-
if (selectedEvent.value) {
801-
index = events.indexOf(selectedEvent.value)
797+
if (nonReactiveSelectedEvent.value) {
798+
index = events.indexOf(nonReactiveSelectedEvent.value)
802799
} else {
803800
index = -1
804801
}
@@ -862,38 +859,40 @@ export default defineComponent({
862859
app.stage.addListener('mousemove', mouseEvent => {
863860
const text: string[] = []
864861
865-
// Event tooltip
866-
const event = getEventAtPosition(mouseEvent.data.global.x, mouseEvent.data.global.y)
867-
if (event) {
868-
text.push(event.title ?? 'Event')
869-
if (event.subtitle) {
870-
text.push(event.subtitle)
871-
}
872-
text.push(formatTime(event.time, 'ms'))
862+
if (!cameraDragging) {
863+
// Event tooltip
864+
const event = getEventAtPosition(mouseEvent.data.global.x, mouseEvent.data.global.y)
865+
if (event) {
866+
text.push(event.title ?? 'Event')
867+
if (event.subtitle) {
868+
text.push(event.subtitle)
869+
}
870+
text.push(formatTime(event.time, 'ms'))
873871
874-
if (event.group) {
875-
text.push(`Group: ${event.group.duration}ms (${event.group.events.length} event${event.group.events.length > 1 ? 's' : ''})`)
876-
}
872+
if (event.group) {
873+
text.push(`Group: ${event.group.nonReactiveDuration}ms (${event.group.events.length} event${event.group.events.length > 1 ? 's' : ''})`)
874+
}
877875
878-
if (event?.container) {
879-
event.container.alpha = 0.5
880-
}
881-
} else {
882-
// Marker tooltip
883-
const marker = getMarkerAtPosition(mouseEvent.data.global.x)
884-
if (marker) {
885-
text.push(marker.label)
886-
text.push(formatTime(marker.time, 'ms'))
887-
text.push('(marker)')
876+
if (event?.container) {
877+
event.container.alpha = 0.5
878+
}
879+
} else {
880+
// Marker tooltip
881+
const marker = getMarkerAtPosition(mouseEvent.data.global.x)
882+
if (marker) {
883+
text.push(marker.label)
884+
text.push(formatTime(marker.time, 'ms'))
885+
text.push('(marker)')
886+
}
888887
}
889-
}
890-
if (event !== hoverEvent) {
891-
if (hoverEvent?.container) {
892-
hoverEvent.container.alpha = 1
888+
if (event !== hoverEvent) {
889+
if (hoverEvent?.container) {
890+
hoverEvent.container.alpha = 1
891+
}
892+
draw()
893893
}
894-
draw()
894+
hoverEvent = event
895895
}
896-
hoverEvent = event
897896
898897
if (text.length) {
899898
// Draw tooltip
@@ -929,7 +928,7 @@ export default defineComponent({
929928
930929
function drawEventGroup (event: TimelineEvent) {
931930
if (event.groupG) {
932-
const drawAsSelected = event === selectedEvent.value && event.layer.groupsOnly
931+
const drawAsSelected = event === nonReactiveSelectedEvent.value && event.layer.groupsOnly
933932
934933
/** @type {PIXI.Graphics} */
935934
const g = event.groupG
@@ -938,14 +937,14 @@ export default defineComponent({
938937
g.clear()
939938
if (event.layer.groupsOnly) {
940939
if (drawAsSelected) {
941-
g.lineStyle(2, boostColor(event.layer.color, darkMode.value))
942-
g.beginFill(dimColor(event.layer.color, darkMode.value, 30))
940+
g.lineStyle(2, boostColor(event.layer.color, nonReactiveState.darkMode.value))
941+
g.beginFill(dimColor(event.layer.color, nonReactiveState.darkMode.value, 30))
943942
} else {
944943
g.beginFill(event.layer.color, 0.5)
945944
}
946945
} else {
947-
g.lineStyle(1, dimColor(event.layer.color, darkMode.value))
948-
g.beginFill(dimColor(event.layer.color, darkMode.value, 25))
946+
g.lineStyle(1, dimColor(event.layer.color, nonReactiveState.darkMode.value))
947+
g.beginFill(dimColor(event.layer.color, nonReactiveState.darkMode.value, 25))
949948
}
950949
if (event.layer.groupsOnly) {
951950
g.drawRect(0, -LAYER_SIZE / 2, size - 1, LAYER_SIZE - 1)
@@ -959,16 +958,18 @@ export default defineComponent({
959958
if (event.layer.groupsOnly && event.title && size > 32) {
960959
let t = event.groupT
961960
let text = event.groupText
962-
if (!t) {
961+
if (!text) {
963962
text = `${SharedData.debugInfo ? `${event.id} ` : ''}${event.title} ${event.subtitle}`
963+
event.groupText = text
964+
}
965+
if (!t) {
964966
t = event.groupT = new PIXI.BitmapText('', {
965967
fontName: 'Roboto Mono',
966-
tint: darkMode.value ? 0xffffff : 0,
968+
tint: nonReactiveState.darkMode.value ? 0xffffff : 0,
967969
})
968970
t.x = 1
969971
t.y = Math.round(-t.height / 2)
970972
t.dirty = false
971-
event.groupText = text
972973
event.container.addChild(t)
973974
}
974975
t.text = text.slice(0, Math.floor((size - 1) / 6))

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ export function onEventAdd (cb: AddEventCb) {
3737
}
3838

3939
export function addEvent (appId: string, eventOptions: TimelineEvent, layer: Layer) {
40+
const descriptor = {
41+
writable: true,
42+
configurable: false,
43+
}
44+
4045
// Non-reactive content
4146
const event = {} as TimelineEvent
4247
for (const key in eventOptions) {
43-
Object.defineProperty(event, key, {
44-
value: eventOptions[key],
45-
writable: true,
46-
configurable: false,
47-
})
48+
Object.defineProperty(event, key, { value: eventOptions[key], ...descriptor })
4849
}
4950

5051
if (layer.eventsMap[event.id]) return
@@ -54,8 +55,10 @@ export function addEvent (appId: string, eventOptions: TimelineEvent, layer: Lay
5455
resetTime()
5556
}
5657

57-
event.layer = layer
58-
event.appId = appId
58+
Object.defineProperties(event, {
59+
layer: { value: layer, ...descriptor },
60+
appId: { value: appId, ...descriptor },
61+
})
5962
layer.events.push(event)
6063
layer.eventsMap[event.id] = event
6164

@@ -65,17 +68,14 @@ export function addEvent (appId: string, eventOptions: TimelineEvent, layer: Lay
6568
if (!group) {
6669
group = layer.groupsMap[event.groupId] = {
6770
events: [],
68-
firstEvent: event,
69-
lastEvent: event,
7071
duration: 0,
7172
} as EventGroup
72-
const descriptor = {
73-
writable: true,
74-
configurable: false,
75-
}
7673
Object.defineProperties(group, {
7774
id: { value: event.groupId, ...descriptor },
7875
y: { value: 0, ...descriptor },
76+
firstEvent: { value: event, ...descriptor },
77+
lastEvent: { value: event, ...descriptor },
78+
nonReactiveDuration: { value: 0, ...descriptor },
7979
oldSize: { value: null, ...descriptor },
8080
oldSelected: { value: null, ...descriptor },
8181
})
@@ -86,7 +86,7 @@ export function addEvent (appId: string, eventOptions: TimelineEvent, layer: Lay
8686
}
8787
group.events.push(event)
8888
group.lastEvent = event
89-
group.duration = event.time - group.firstEvent.time
89+
group.duration = group.nonReactiveDuration = event.time - group.firstEvent.time
9090
event.group = group
9191
}
9292

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface EventGroup {
1717
firstEvent: TimelineEvent
1818
lastEvent: TimelineEvent
1919
duration: number
20+
nonReactiveDuration: number
2021
y: number
2122
oldSize?: number
2223
oldSelected?: boolean

0 commit comments

Comments
 (0)