Skip to content

Commit

Permalink
perf(timeline): optimized vertical position check + don't recompute f…
Browse files Browse the repository at this point in the history
…lamecharts
  • Loading branch information
Akryum committed Nov 25, 2021
1 parent d0c9c16 commit b605da9
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 77 deletions.
173 changes: 101 additions & 72 deletions packages/app-frontend/src/features/timeline/TimelineView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { onKeyUp } from '@front/util/keyboard'
import { useDarkMode } from '@front/util/theme'
import { dimColor, boostColor } from '@front/util/color'
import { formatTime } from '@front/util/format'
import { Queue } from '@front/util/queue'
import { nonReactive } from '@front/util/reactivity'
PIXI.settings.ROUND_PIXELS = true
Expand All @@ -46,8 +48,15 @@ export default defineComponent({
const { startTime, endTime, minTime, maxTime } = useTime()
const { darkMode } = useDarkMode()
// Optimize for read in loops
const nonReactiveTime = {
startTime: nonReactive(startTime),
endTime: nonReactive(endTime),
minTime: nonReactive(minTime),
}
function getTimePosition (time: number) {
return (time - minTime.value) / (endTime.value - startTime.value) * app.view.width
return (time - nonReactiveTime.minTime.value) / (nonReactiveTime.endTime.value - nonReactiveTime.startTime.value) * app.view.width
}
// Reset
Expand Down Expand Up @@ -303,34 +312,33 @@ export default defineComponent({
let events: TimelineEvent[] = []
const updateEventPositionQueue = new Set<TimelineEvent>()
let currentEventPositionUpdate: TimelineEvent = null
let updateEventPositionQueued = false
const updateEventPositionQueue = new Queue<TimelineEvent>()
let eventPositionUpdateInProgress = false
function queueEventPositionUpdate (...events: TimelineEvent[]) {
function queueEventPositionUpdate (events: TimelineEvent[], force = false) {
for (const e of events) {
if (!e.container) continue
const ignored = isEventIgnored(e)
e.container.visible = !ignored
if (ignored) continue
// Update horizontal position immediately
e.container.x = Math.round(getTimePosition(e.time))
if (!force && e.layer.groupsOnly) continue
// Queue vertical position compute
updateEventPositionQueue.add(e)
}
if (!updateEventPositionQueued) {
updateEventPositionQueued = true
if (!eventPositionUpdateInProgress) {
eventPositionUpdateInProgress = true
Vue.nextTick(() => {
nextEventPositionUpdate()
updateEventPositionQueued = false
eventPositionUpdateInProgress = false
})
}
}
function nextEventPositionUpdate () {
if (currentEventPositionUpdate) return
const event = currentEventPositionUpdate = updateEventPositionQueue.values().next().value
if (event) {
let event: TimelineEvent
while ((event = updateEventPositionQueue.shift())) {
computeEventVerticalPosition(event)
}
}
Expand All @@ -340,53 +348,73 @@ export default defineComponent({
}
function computeEventVerticalPosition (event: TimelineEvent) {
// Skip if the event is not visible
// or if the group graphics is not visible
if ((event.time >= startTime.value && event.time <= endTime.value) ||
(event.group?.firstEvent === event && event.group.lastEvent.time >= startTime.value && event.group.lastEvent.time <= endTime.value)) {
let y = 0
if (event.group && event !== event.group.firstEvent) {
// If the event is inside a group, just use the group position
y = event.group.y
} else {
const firstEvent = event.group ? event.group.firstEvent : event
const lastEvent = event.group ? event.group.lastEvent : event
// Collision offset for non-flamecharts
const offset = event.layer.groupsOnly ? 0 : 12
let y = 0
if (event.group && event !== event.group.firstEvent) {
// If the event is inside a group, just use the group position
y = event.group.y
} else {
const firstEvent = event.group ? event.group.firstEvent : event
const lastEvent = event.group ? event.group.lastEvent : event
const lastOffset = event.layer.groupsOnly && event.group?.duration > 0 ? -1 : 0
// Check for 'collision' with other event groups
const l = event.layer.groups.length
let checkAgain = true
while (checkAgain) {
checkAgain = false
for (let i = 0; i < l; i++) {
const otherGroup = event.layer.groups[i]
if (
// Different group
(
!event.group ||
event.group !== otherGroup
) &&
// Same row
otherGroup.y === y &&
(
// Horizontal intersection (first event)
(
getTimePosition(firstEvent.time) >= getTimePosition(otherGroup.firstEvent.time) - offset &&
getTimePosition(firstEvent.time) <= getTimePosition(otherGroup.lastEvent.time) + offset + lastOffset
) ||
// Horizontal intersection (last event)
// For flamechart allow 1-pixel overlap at the end of a group
const lastOffset = event.layer.groupsOnly && event.group?.duration > 0 ? -1 : 0
// Flamechart uses time instead of pixel position
const getPos = event.layer.groupsOnly ? (time: number) => time : getTimePosition
const firstPos = getPos(firstEvent.time)
const lastPos = event.group ? getPos(lastEvent.time) : firstPos
// Check for 'collision' with other event groups
const l = event.layer.groups.length
let checkAgain = true
while (checkAgain) {
checkAgain = false
for (let i = 0; i < l; i++) {
const otherGroup = event.layer.groups[i]
if (
// Different group
(
!event.group ||
event.group !== otherGroup
) &&
// Same row
otherGroup.y === y
) {
// // eslint-disable-next-line no-console
// if (event.layer.groupsOnly) console.log('checking collision with', otherGroup.firstEvent.id, otherGroup.firstEvent.title)
const otherGroupFirstPos = getPos(otherGroup.firstEvent.time)
const otherGroupLastPos = getPos(otherGroup.lastEvent.time)
// First position is inside other group
const firstEventIntersection = (
firstPos >= otherGroupFirstPos - offset &&
firstPos <= otherGroupLastPos + offset + lastOffset
)
if (firstEventIntersection || (
// Additional checks if group
event.group && (
(
getTimePosition(lastEvent.time) >= getTimePosition(otherGroup.firstEvent.time) - offset - lastOffset &&
getTimePosition(lastEvent.time) <= getTimePosition(otherGroup.lastEvent.time) + offset
// Last position is inside other group
lastPos >= otherGroupFirstPos - offset - lastOffset &&
lastPos <= otherGroupLastPos + offset
) || (
// Other group is inside current group
firstPos < otherGroupFirstPos - offset &&
lastPos > otherGroupLastPos + offset
)
)
) {
)) {
// Collision!
if (event.group && event.group.duration > otherGroup.duration && firstEvent.time <= otherGroup.firstEvent.time) {
// Invert positions because current group has higher priority
queueEventPositionUpdate(otherGroup.firstEvent)
if (!updateEventPositionQueue.has(otherGroup.firstEvent)) {
queueEventPositionUpdate([otherGroup.firstEvent], event.layer.groupsOnly)
}
} else {
// Offset the current group/event
y++
Expand All @@ -397,29 +425,24 @@ export default defineComponent({
}
}
}
}
// If the event is the first in a group, update group position
if (event.group) {
event.group.y = y
}
// If the event is the first in a group, update group position
if (event.group) {
event.group.y = y
}
// Might update the layer's height as well
if (y + 1 > event.layer.height) {
const oldLayerHeight = event.layer.height
const newLayerHeight = event.layer.height = y + 1
if (oldLayerHeight !== newLayerHeight) {
updateLayerPositions()
drawLayerBackgroundEffects()
}
// Might update the layer's height as well
if (y + 1 > event.layer.height) {
const oldLayerHeight = event.layer.height
const newLayerHeight = event.layer.height = y + 1
if (oldLayerHeight !== newLayerHeight) {
updateLayerPositions()
drawLayerBackgroundEffects()
}
}
event.container.y = (y + 1) * LAYER_SIZE
}
// Next
updateEventPositionQueue.delete(event)
currentEventPositionUpdate = null
nextEventPositionUpdate()
event.container.y = (y + 1) * LAYER_SIZE
}
function addEvent (event: TimelineEvent, layerContainer: PIXI.Container) {
Expand Down Expand Up @@ -456,7 +479,11 @@ export default defineComponent({
events.push(event)
refreshEventGraphics(event)
queueEventPositionUpdate(event)
if (event.container) {
queueEventPositionUpdate([event], true)
} else {
queueEventPositionUpdate([event.group.firstEvent], true)
}
return event
}
Expand Down Expand Up @@ -527,10 +554,12 @@ export default defineComponent({
function updateEvents () {
for (const layer of layers.value) {
layer.height = 1
if (!layer.groupsOnly) {
layer.height = 1
}
}
updateLayerPositions()
queueEventPositionUpdate(...events)
queueEventPositionUpdate(events)
for (const event of events) {
if (event.groupG) {
drawEventGroup(event)
Expand Down Expand Up @@ -826,7 +855,7 @@ export default defineComponent({
if (event.layer.groupsOnly && event.title && size > 32) {
let t = event.groupT
if (!t) {
t = event.groupT = new PIXI.Text(`${event.title} ${event.subtitle}`, {
t = event.groupT = new PIXI.Text(`${event.id} ${event.title} ${event.subtitle}`, {
fontSize: 10,
fill: darkMode.value ? 0xffffff : 0,
})
Expand Down
13 changes: 12 additions & 1 deletion packages/app-frontend/src/features/timeline/composable/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,18 @@ export function onEventAdd (cb: AddEventCb) {
addEventCbs.push(cb)
}

export function addEvent (appId: string, event: TimelineEvent, layer: Layer) {
export function addEvent (appId: string, eventOptions: TimelineEvent, layer: Layer) {
// Non-reactive content
const event = {} as TimelineEvent
for (const key in eventOptions) {
Object.defineProperty(event, key, {
value: eventOptions[key],
writable: true,
enumerable: true,
configurable: false,
})
}

if (layer.eventsMap[event.id]) return

if (timelineIsEmpty.value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import cloneDeep from 'lodash/cloneDeep'
import Vue from 'vue'
import { Bridge, BridgeEvents, parse } from '@vue-devtools/shared-utils'
import { getApps } from '@front/features/apps'
Expand Down Expand Up @@ -33,7 +32,7 @@ export function setupTimelineBridgeEvents (bridge: Bridge) {
return
}

addEvent(appId, cloneDeep(event), layer)
addEvent(appId, event, layer)
}
})

Expand All @@ -53,7 +52,7 @@ export function setupTimelineBridgeEvents (bridge: Bridge) {
const pendingKey = `${appId}:${layer.id}`
if (pendingEvents[pendingKey] && pendingEvents[pendingKey].length) {
for (const event of pendingEvents[pendingKey]) {
addEvent(appId, cloneDeep(event), getLayers(appId).find(l => l.id === layer.id))
addEvent(appId, event, getLayers(appId).find(l => l.id === layer.id))
}
pendingEvents[pendingKey] = []
}
Expand Down Expand Up @@ -91,7 +90,7 @@ export function setupTimelineBridgeEvents (bridge: Bridge) {
}

for (const event of events) {
addEvent(appId, cloneDeep(event), layer)
addEvent(appId, event, layer)
}
})

Expand Down
48 changes: 48 additions & 0 deletions packages/app-frontend/src/util/queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export class Queue<T = any> {
private existsMap: Map<T, boolean> = new Map()
private firstItem: QueueItem<T> | null = null
private lastItem: QueueItem<T> | null = null

add (value: T) {
if (!this.existsMap.has(value)) {
this.existsMap.set(value, true)
const item = {
current: value,
next: null,
}
if (!this.firstItem) {
this.firstItem = item
}
if (this.lastItem) {
this.lastItem.next = item
}
this.lastItem = item
}
}

shift (): T | null {
if (this.firstItem) {
const item = this.firstItem
this.firstItem = item.next
if (!this.firstItem) {
this.lastItem = null
}
this.existsMap.delete(item.current)
return item.current
}
return null
}

isEmpty () {
return !this.firstItem
}

has (value: T) {
return this.existsMap.has(value)
}
}

interface QueueItem<T> {
current: T
next: QueueItem<T> | null
}
13 changes: 13 additions & 0 deletions packages/app-frontend/src/util/reactivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Ref, watch } from '@vue/composition-api'

export function nonReactive<T> (ref: Ref<T>) {
const holder = {
value: ref.value,
}

watch(ref, value => {
holder.value = value
})

return holder
}

0 comments on commit b605da9

Please sign in to comment.