Skip to content

Commit 021ea73

Browse files
authored
Merge pull request #103 from SuperFlyTV/fix/better-trace-resolving
Add tracing of resolve, for troubleshooting
2 parents e30b75b + 7723b89 commit 021ea73

File tree

11 files changed

+406
-198
lines changed

11 files changed

+406
-198
lines changed

src/__tests__/basic.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable jest/no-standalone-expect */
22

33
import { EventType, ResolvedTimelineObject, TimelineObjectInstance, getResolvedState, resolveTimeline } from '..'
4+
45
import { baseInstances } from '../resolver/lib/instance'
56
import { describeVariants } from './testlib'
67

@@ -1695,6 +1696,7 @@ describeVariants(
16951696
resolvedKeyframeCount: 0,
16961697
resolvingObjectCount: 0,
16971698
resolvingCount: 0,
1699+
resolveTrace: [],
16981700
})
16991701
})
17001702
test('Class state overrides', () => {

src/__tests__/index.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
resolveTimeline,
88
getResolvedState,
99
applyKeyframeContent,
10+
ResolveError,
1011
} from '../index'
1112
import { baseInstances } from '../resolver/lib/instance'
1213
import { clone } from '../resolver/lib/lib'
@@ -879,4 +880,68 @@ describe('index', () => {
879880

880881
expect(state1.layers['layerA']).toEqual(state1NoCache.layers['layerA'])
881882
})
883+
test('traceResolving', () => {
884+
const timeline: TimelineObject<any>[] = [
885+
{
886+
id: 'A',
887+
layer: 'L',
888+
enable: { start: 100, duration: 1 },
889+
content: {},
890+
priority: 0,
891+
},
892+
]
893+
894+
const resolved = resolveTimeline(timeline, {
895+
cache: {},
896+
time: 0,
897+
traceResolving: true,
898+
})
899+
900+
// Note: The exact content of the trace isn't that important, since it's for troubleshooting purposes
901+
expect(resolved.statistics.resolveTrace).toMatchObject([
902+
'init',
903+
'resolveTimeline start',
904+
'timeline object count 1',
905+
'objects: 1',
906+
'using cache',
907+
'cache: init',
908+
'cache: canUseIncomingCache: false',
909+
'cache: cached objects: []',
910+
'cache: object "A" is new',
911+
'Resolver: Step 1a',
912+
'Resolver: Resolve object "A"',
913+
'Resolver: object "A" resolved.instances: [{"id":"@A_0","start":100,"end":101,"references":[]}]',
914+
'Resolver: object "A" directReferences: []',
915+
'Resolver: Step 1b',
916+
'Resolver: Resolve conflicts for layers: ["L"]',
917+
'LayerState: Resolve conflicts for layer "L", objects: A',
918+
'Resolver: Step 2',
919+
'resolveTimeline done!',
920+
])
921+
})
922+
test('thrown errors should be ResolveError', () => {
923+
const timeline: TimelineObject<any>[] = [
924+
{
925+
id: 'A',
926+
layer: 'L',
927+
enable: { start: 'badexpression((' },
928+
content: {},
929+
priority: 0,
930+
},
931+
]
932+
933+
{
934+
let error: any = null
935+
try {
936+
resolveTimeline(timeline, {
937+
time: 0,
938+
traceResolving: true,
939+
})
940+
} catch (e) {
941+
error = e
942+
}
943+
expect(error).toBeInstanceOf(ResolveError)
944+
expect(error.resolvedTimeline).toBeTruthy()
945+
}
946+
})
882947
})

src/api/resolvedTimeline.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export interface ResolvedTimeline<TContent extends Content = Content> {
4848
* (is affected when using cache)
4949
*/
5050
resolvingCount: number
51+
52+
/** If traceResolving option is enabled, will contain a trace of the steps the resolver did while resolving */
53+
resolveTrace: string[]
5154
}
5255
/** Is set if there was an error during Resolving and options.dontThrowOnError is set. */
5356
error?: Error

src/api/resolver.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface ResolveOptions {
3737
*/
3838
debug?: boolean
3939

40+
/**
41+
* If true, will store traces of the resolving into resolvedTimeline.statistics.resolveTrace.
42+
* This decreases performance slightly.
43+
*/
44+
traceResolving?: boolean
45+
4046
/**
4147
* Skip timeline validation.
4248
* This improves performance slightly, but will not catch errors in the input timeline so use with caution.
@@ -46,7 +52,7 @@ export interface ResolveOptions {
4652
/** Skip generating statistics, this improves performance slightly. */
4753
skipStatistics?: boolean
4854

49-
/** Don't throw when an error (such as circular dependency) occurs. The Error will instead be written to resolvedTimeline.error */
55+
/** Don't throw when a timeline-error (such as circular dependency) occurs. The Error will instead be written to resolvedTimeline.error */
5056
dontThrowOnError?: boolean
5157
}
5258
export interface ResolverCache<TContent extends Content = Content> {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
TimelineObject,
1212
} from './api'
1313
export * from './api'
14+
export { ResolveError } from './resolver/lib/Error'
1415

1516
import { StateHandler } from './resolver/StateHandler'
1617
import { ExpressionHandler } from './resolver/ExpressionHandler'

src/resolver/CacheHandler.ts

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,26 @@ export class CacheHandler {
1717

1818
if (!cache.canBeUsed) {
1919
// Reset the cache:
20-
cache.objHashes = {}
21-
cache.objects = {}
20+
CacheHandler.resetCache(cache)
2221

2322
this.canUseIncomingCache = false
2423
} else {
2524
this.canUseIncomingCache = true
2625
}
2726

27+
if (this.resolvedTimeline.traceResolving) {
28+
this.resolvedTimeline.addResolveTrace(`cache: init`)
29+
this.resolvedTimeline.addResolveTrace(`cache: canUseIncomingCache: ${this.canUseIncomingCache}`)
30+
this.resolvedTimeline.addResolveTrace(
31+
`cache: cached objects: ${JSON.stringify(Object.keys(cache.objects))}`
32+
)
33+
}
34+
2835
// cache.canBeUsed will be set in this.persistData()
2936
cache.canBeUsed = false
3037

3138
this.cache = cache as ResolverCache
3239
}
33-
private debug(...args: any[]) {
34-
if (this.resolvedTimeline.options.debug) console.log(...args)
35-
}
3640
public determineChangedObjects(): void {
3741
const toc = tic(' cache.determineChangedObjects')
3842
// Go through all new objects, and determine whether they have changed:
@@ -45,8 +49,15 @@ export class CacheHandler {
4549
const newHash = hashTimelineObject(obj)
4650
allNewObjects[obj.id] = true
4751

48-
if (!oldHash) this.debug(`Cache: Object "${obj.id}" is new`)
49-
else if (oldHash !== newHash) this.debug(`Cache: Object "${obj.id}" has changed`)
52+
if (!oldHash) {
53+
if (this.resolvedTimeline.traceResolving) {
54+
this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" is new`)
55+
}
56+
} else if (oldHash !== newHash) {
57+
if (this.resolvedTimeline.traceResolving) {
58+
this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" has changed`)
59+
}
60+
}
5061
if (
5162
// Object is new:
5263
!oldHash ||
@@ -61,16 +72,20 @@ export class CacheHandler {
6172
} else {
6273
// No timing-affecting changes detected
6374
/* istanbul ignore if */
64-
if (!oldHash) this.debug(`Cache: Object "${obj.id}" is similar`)
75+
if (!oldHash) {
76+
if (this.resolvedTimeline.traceResolving) {
77+
this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" is similar`)
78+
}
79+
}
6580

6681
// Even though the timeline-properties hasn't changed,
6782
// the content (and other properties) might have:
6883
const oldObj = this.cache.objects[obj.id]
6984

7085
/* istanbul ignore if */
7186
if (!oldObj) {
72-
console.error('oldHash', oldHash)
73-
console.error('ids', Object.keys(this.cache.objects))
87+
console.error(`oldHash: "${oldHash}"`)
88+
console.error(`ids: ${JSON.stringify(Object.keys(this.cache.objects))}`)
7489
throw new Error(`Internal Error: obj "${obj.id}" not found in cache, even though hashes match!`)
7590
}
7691

@@ -149,15 +164,28 @@ export class CacheHandler {
149164
for (const reference of changedTracker.listChanged()) {
150165
invalidator.invalidateObjectsWithReference(reference)
151166
}
167+
if (this.resolvedTimeline.traceResolving) {
168+
this.resolvedTimeline.addResolveTrace(
169+
`cache: changed references: ${JSON.stringify(Array.from(changedTracker.listChanged()))}`
170+
)
171+
this.resolvedTimeline.addResolveTrace(
172+
`cache: invalidated objects: ${JSON.stringify(Array.from(invalidator.getInValidObjectIds()))}`
173+
)
174+
this.resolvedTimeline.addResolveTrace(
175+
`cache: unchanged objects: ${JSON.stringify(invalidator.getValidObjects().map((o) => o.id))}`
176+
)
177+
}
152178

153179
// At this point, the objects that are left in validObjects are still valid (ie has not changed or is affected by any others).
154180
// We can reuse the old resolving for those:
155181
for (const obj of invalidator.getValidObjects()) {
156-
if (!this.cache.objects[obj.id])
182+
if (!this.cache.objects[obj.id]) {
157183
/* istanbul ignore next */
158184
throw new Error(
159-
`Something went wrong: "${obj.id}" does not exist in cache.resolvedTimeline.objects`
185+
`Internal Error: Something went wrong: "${obj.id}" does not exist in cache.resolvedTimeline.objects`
160186
)
187+
}
188+
161189
this.resolvedTimeline.objectsMap.set(obj.id, this.cache.objects[obj.id])
162190
}
163191
}
@@ -179,6 +207,12 @@ export class CacheHandler {
179207

180208
toc()
181209
}
210+
/** Resets / Clears the cache */
211+
static resetCache(cache: Partial<ResolverCache>): void {
212+
delete cache.canBeUsed
213+
cache.objHashes = {}
214+
cache.objects = {}
215+
}
182216
}
183217
/** Return a "hash-string" which changes whenever anything that affects timing of a timeline-object has changed. */
184218
export function hashTimelineObject(obj: ResolvedTimelineObject): string {
@@ -210,21 +244,26 @@ function getAllReferencesThisObjectAffects(newObj: ResolvedTimelineObject): Refe
210244
}
211245
return references
212246
}
247+
/**
248+
* Keeps track of which timeline object have been changed
249+
*/
213250
class ChangedTracker {
214251
private changedReferences = new Set<Reference>()
215252

253+
/**
254+
* Mark an object as "has changed".
255+
* Will store all references that are affected by this object.
256+
*/
216257
public addChangedObject(obj: ResolvedTimelineObject) {
217-
const references = getAllReferencesThisObjectAffects(obj)
218-
for (const ref of references) {
258+
for (const ref of getAllReferencesThisObjectAffects(obj)) {
219259
this.changedReferences.add(ref)
220260
}
221-
if (objHasLayer(obj)) {
222-
this.changedReferences.add(`$${obj.layer}`)
223-
}
224261
}
262+
/** Returns true if a reference has changed */
225263
public isChanged(ref: Reference): boolean {
226264
return this.changedReferences.has(ref)
227265
}
266+
/** Returns a list of all changed references */
228267
public listChanged(): IterableIterator<Reference> {
229268
return this.changedReferences.keys()
230269
}
@@ -236,6 +275,7 @@ class Invalidator {
236275
/** All references that depend on another reference (ie objects, class or layers): */
237276
private affectReferenceMap: { [ref: Reference]: Reference[] } = {}
238277
private validObjects: ResolvedTimelineObjects = {}
278+
private inValidObjectIds: string[] = []
239279
/** Map of which objects can be affected by any other object, per layer */
240280
private objectLayerMap: { [layer: string]: string[] } = {}
241281

@@ -245,6 +285,9 @@ class Invalidator {
245285
public getValidObjects(): ResolvedTimelineObject[] {
246286
return Object.values<ResolvedTimelineObject>(this.validObjects)
247287
}
288+
public getInValidObjectIds(): string[] {
289+
return this.inValidObjectIds
290+
}
248291
public addObjectOnLayer(layer: string, obj: ResolvedTimelineObject) {
249292
if (!this.objectLayerMap[layer]) this.objectLayerMap[layer] = []
250293
this.objectLayerMap[layer].push(obj.id)
@@ -263,6 +306,7 @@ class Invalidator {
263306
const objId = getRefObjectId(reference)
264307
if (this.validObjects[objId]) {
265308
delete this.validObjects[objId]
309+
this.inValidObjectIds.push(objId)
266310
}
267311
}
268312
if (isLayerReference(reference)) {

0 commit comments

Comments
 (0)