Skip to content

Commit d4159f9

Browse files
Timeline perf improvements (#662)
* move get keyboard tracks out of update function * cache flattened tracks * Return a defensive copy from getAllTracks() Copilot review: getAllTracks() returned the internal flatTracks array by reference, letting callers mutate cached internal state and changing prior per-call-fresh-array behavior. Return a shallow copy instead; the cached array is still used directly on hot internal paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 7334c2e commit d4159f9

2 files changed

Lines changed: 39 additions & 34 deletions

File tree

packages/timeline/src/TimelineInput.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ export const TIMELINE_OPTION_NODE_CONFIGS = defineOptionNodeConfigs([
4343
// We can use TimelineOptionNodes for strong typing when using useNodeOptionNodes
4444
export type TimelineOptionNodes = OptionNodesFromConfigs<typeof TIMELINE_OPTION_NODE_CONFIGS>
4545

46+
const getKeyframeTracks = (tracks: TimelineManagerTrack[]): TimelineManagerKeyframeTrack[] => {
47+
const keyframeTracks: TimelineManagerKeyframeTrack[] = []
48+
49+
for (const track of tracks) {
50+
if (track.trackType === 'keyframe') {
51+
keyframeTracks.push(track)
52+
continue
53+
}
54+
55+
if (track.trackType === 'vector') {
56+
keyframeTracks.push(...getKeyframeTracks(track.childTracks))
57+
}
58+
}
59+
60+
return keyframeTracks
61+
}
62+
4663
export class TimelineInput implements IPlugin {
4764
public readonly id = 'timeline-input'
4865
public readonly name = 'Timeline Input'
@@ -101,26 +118,6 @@ export class TimelineInput implements IPlugin {
101118
const changedTrackIds = Object.keys(changed)
102119
if (changedTrackIds.length > 0) {
103120
const allTracks = getTimelineTracks(engine.getStoreState(), timelineId)
104-
105-
const getKeyframeTracks = (
106-
tracks: TimelineManagerTrack[],
107-
): TimelineManagerKeyframeTrack[] => {
108-
const keyframeTracks: TimelineManagerKeyframeTrack[] = []
109-
110-
for (const track of tracks) {
111-
if (track.trackType === 'keyframe') {
112-
keyframeTracks.push(track)
113-
continue
114-
}
115-
116-
if (track.trackType === 'vector') {
117-
keyframeTracks.push(...getKeyframeTracks(track.childTracks))
118-
}
119-
}
120-
121-
return keyframeTracks
122-
}
123-
124121
const tracks = getKeyframeTracks(allTracks)
125122

126123
const paramTargetNodeIds: string[] = []

packages/timeline/src/TimelineManager.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,34 @@ export class TimelineManager {
2727
// Number of shot keyframes at or before the position as of the last check, per track.
2828
private shotKeyframeCount: Map<string, number> = new Map()
2929
private clock: Clock | null = null
30+
// Flattened (vector tracks expanded to their childTracks) view of timelineData.tracks, kept in
31+
// sync by buildCache() - avoids re-walking the track tree on every computeValues/emitUpdate call.
32+
private flatTracks: TimelineManagerTrack[] = []
3033

3134
constructor(timeline: TimelineManagerData, clock?: Clock) {
3235
this.timelineData = timeline
3336
this.clock = clock ?? null
3437
this.buildCache()
3538
}
3639

40+
private flattenTracks(tracks: TimelineManagerTrack[]): TimelineManagerTrack[] {
41+
const allTracks: TimelineManagerTrack[] = []
42+
43+
for (const track of tracks) {
44+
allTracks.push(track)
45+
if (track.trackType === 'vector') {
46+
allTracks.push(...this.flattenTracks(track.childTracks))
47+
}
48+
}
49+
50+
return allTracks
51+
}
52+
3753
private buildCache() {
3854
this.sortedKeyframesCache.clear()
3955
this.resetKeyframeIndexes()
40-
for (const track of this.getAllTracks()) {
56+
this.flatTracks = this.flattenTracks(this.timelineData.tracks)
57+
for (const track of this.flatTracks) {
4158
switch (track.trackType) {
4259
case 'audio':
4360
if (this.audioCache.get(track.id)?.src !== track.audioUrl) {
@@ -104,7 +121,7 @@ export class TimelineManager {
104121

105122
private computeValues(): TrackValues {
106123
const values: TrackValues = {}
107-
for (const track of this.getAllTracks()) {
124+
for (const track of this.flatTracks) {
108125
if (track.trackType !== 'keyframe') continue
109126
const value = this.getTrackValue(track)
110127
if (value !== undefined) {
@@ -143,7 +160,7 @@ export class TimelineManager {
143160
track: TimelineManagerAudioTrack
144161
audio: HTMLAudioElement
145162
}[] {
146-
return this.getAllTracks()
163+
return this.flatTracks
147164
.filter(
148165
(track): track is TimelineManagerAudioTrack =>
149166
track.trackType === 'audio' && this.audioCache.has(track.id),
@@ -176,17 +193,8 @@ export class TimelineManager {
176193
this.rafId = requestAnimationFrame(this.tick)
177194
}
178195

179-
getAllTracks(tracks = this.timelineData.tracks): TimelineManagerTrack[] {
180-
const allTracks: TimelineManagerTrack[] = []
181-
182-
for (const track of tracks) {
183-
allTracks.push(track)
184-
if (track.trackType === 'vector') {
185-
allTracks.push(...this.getAllTracks(track.childTracks))
186-
}
187-
}
188-
189-
return allTracks
196+
getAllTracks(): TimelineManagerTrack[] {
197+
return [...this.flatTracks]
190198
}
191199

192200
play() {

0 commit comments

Comments
 (0)