Skip to content

Commit

Permalink
Merge pull request #103 from SuperFlyTV/fix/better-trace-resolving
Browse files Browse the repository at this point in the history
Add tracing of resolve, for troubleshooting
  • Loading branch information
nytamin authored Oct 10, 2024
2 parents e30b75b + 7723b89 commit 021ea73
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 198 deletions.
2 changes: 2 additions & 0 deletions src/__tests__/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable jest/no-standalone-expect */

import { EventType, ResolvedTimelineObject, TimelineObjectInstance, getResolvedState, resolveTimeline } from '..'

import { baseInstances } from '../resolver/lib/instance'
import { describeVariants } from './testlib'

Expand Down Expand Up @@ -1695,6 +1696,7 @@ describeVariants(
resolvedKeyframeCount: 0,
resolvingObjectCount: 0,
resolvingCount: 0,
resolveTrace: [],
})
})
test('Class state overrides', () => {
Expand Down
65 changes: 65 additions & 0 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
resolveTimeline,
getResolvedState,
applyKeyframeContent,
ResolveError,
} from '../index'
import { baseInstances } from '../resolver/lib/instance'
import { clone } from '../resolver/lib/lib'
Expand Down Expand Up @@ -879,4 +880,68 @@ describe('index', () => {

expect(state1.layers['layerA']).toEqual(state1NoCache.layers['layerA'])
})
test('traceResolving', () => {
const timeline: TimelineObject<any>[] = [
{
id: 'A',
layer: 'L',
enable: { start: 100, duration: 1 },
content: {},
priority: 0,
},
]

const resolved = resolveTimeline(timeline, {
cache: {},
time: 0,
traceResolving: true,
})

// Note: The exact content of the trace isn't that important, since it's for troubleshooting purposes
expect(resolved.statistics.resolveTrace).toMatchObject([
'init',
'resolveTimeline start',
'timeline object count 1',
'objects: 1',
'using cache',
'cache: init',
'cache: canUseIncomingCache: false',
'cache: cached objects: []',
'cache: object "A" is new',
'Resolver: Step 1a',
'Resolver: Resolve object "A"',
'Resolver: object "A" resolved.instances: [{"id":"@A_0","start":100,"end":101,"references":[]}]',
'Resolver: object "A" directReferences: []',
'Resolver: Step 1b',
'Resolver: Resolve conflicts for layers: ["L"]',
'LayerState: Resolve conflicts for layer "L", objects: A',
'Resolver: Step 2',
'resolveTimeline done!',
])
})
test('thrown errors should be ResolveError', () => {
const timeline: TimelineObject<any>[] = [
{
id: 'A',
layer: 'L',
enable: { start: 'badexpression((' },
content: {},
priority: 0,
},
]

{
let error: any = null
try {
resolveTimeline(timeline, {
time: 0,
traceResolving: true,
})
} catch (e) {
error = e
}
expect(error).toBeInstanceOf(ResolveError)
expect(error.resolvedTimeline).toBeTruthy()
}
})
})
3 changes: 3 additions & 0 deletions src/api/resolvedTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export interface ResolvedTimeline<TContent extends Content = Content> {
* (is affected when using cache)
*/
resolvingCount: number

/** If traceResolving option is enabled, will contain a trace of the steps the resolver did while resolving */
resolveTrace: string[]
}
/** Is set if there was an error during Resolving and options.dontThrowOnError is set. */
error?: Error
Expand Down
8 changes: 7 additions & 1 deletion src/api/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export interface ResolveOptions {
*/
debug?: boolean

/**
* If true, will store traces of the resolving into resolvedTimeline.statistics.resolveTrace.
* This decreases performance slightly.
*/
traceResolving?: boolean

/**
* Skip timeline validation.
* This improves performance slightly, but will not catch errors in the input timeline so use with caution.
Expand All @@ -46,7 +52,7 @@ export interface ResolveOptions {
/** Skip generating statistics, this improves performance slightly. */
skipStatistics?: boolean

/** Don't throw when an error (such as circular dependency) occurs. The Error will instead be written to resolvedTimeline.error */
/** Don't throw when a timeline-error (such as circular dependency) occurs. The Error will instead be written to resolvedTimeline.error */
dontThrowOnError?: boolean
}
export interface ResolverCache<TContent extends Content = Content> {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TimelineObject,
} from './api'
export * from './api'
export { ResolveError } from './resolver/lib/Error'

import { StateHandler } from './resolver/StateHandler'
import { ExpressionHandler } from './resolver/ExpressionHandler'
Expand Down
78 changes: 61 additions & 17 deletions src/resolver/CacheHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,26 @@ export class CacheHandler {

if (!cache.canBeUsed) {
// Reset the cache:
cache.objHashes = {}
cache.objects = {}
CacheHandler.resetCache(cache)

this.canUseIncomingCache = false
} else {
this.canUseIncomingCache = true
}

if (this.resolvedTimeline.traceResolving) {
this.resolvedTimeline.addResolveTrace(`cache: init`)
this.resolvedTimeline.addResolveTrace(`cache: canUseIncomingCache: ${this.canUseIncomingCache}`)
this.resolvedTimeline.addResolveTrace(
`cache: cached objects: ${JSON.stringify(Object.keys(cache.objects))}`
)
}

// cache.canBeUsed will be set in this.persistData()
cache.canBeUsed = false

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

if (!oldHash) this.debug(`Cache: Object "${obj.id}" is new`)
else if (oldHash !== newHash) this.debug(`Cache: Object "${obj.id}" has changed`)
if (!oldHash) {
if (this.resolvedTimeline.traceResolving) {
this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" is new`)
}
} else if (oldHash !== newHash) {
if (this.resolvedTimeline.traceResolving) {
this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" has changed`)
}
}
if (
// Object is new:
!oldHash ||
Expand All @@ -61,16 +72,20 @@ export class CacheHandler {
} else {
// No timing-affecting changes detected
/* istanbul ignore if */
if (!oldHash) this.debug(`Cache: Object "${obj.id}" is similar`)
if (!oldHash) {
if (this.resolvedTimeline.traceResolving) {
this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" is similar`)
}
}

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

/* istanbul ignore if */
if (!oldObj) {
console.error('oldHash', oldHash)
console.error('ids', Object.keys(this.cache.objects))
console.error(`oldHash: "${oldHash}"`)
console.error(`ids: ${JSON.stringify(Object.keys(this.cache.objects))}`)
throw new Error(`Internal Error: obj "${obj.id}" not found in cache, even though hashes match!`)
}

Expand Down Expand Up @@ -149,15 +164,28 @@ export class CacheHandler {
for (const reference of changedTracker.listChanged()) {
invalidator.invalidateObjectsWithReference(reference)
}
if (this.resolvedTimeline.traceResolving) {
this.resolvedTimeline.addResolveTrace(
`cache: changed references: ${JSON.stringify(Array.from(changedTracker.listChanged()))}`
)
this.resolvedTimeline.addResolveTrace(
`cache: invalidated objects: ${JSON.stringify(Array.from(invalidator.getInValidObjectIds()))}`
)
this.resolvedTimeline.addResolveTrace(
`cache: unchanged objects: ${JSON.stringify(invalidator.getValidObjects().map((o) => o.id))}`
)
}

// At this point, the objects that are left in validObjects are still valid (ie has not changed or is affected by any others).
// We can reuse the old resolving for those:
for (const obj of invalidator.getValidObjects()) {
if (!this.cache.objects[obj.id])
if (!this.cache.objects[obj.id]) {
/* istanbul ignore next */
throw new Error(
`Something went wrong: "${obj.id}" does not exist in cache.resolvedTimeline.objects`
`Internal Error: Something went wrong: "${obj.id}" does not exist in cache.resolvedTimeline.objects`
)
}

this.resolvedTimeline.objectsMap.set(obj.id, this.cache.objects[obj.id])
}
}
Expand All @@ -179,6 +207,12 @@ export class CacheHandler {

toc()
}
/** Resets / Clears the cache */
static resetCache(cache: Partial<ResolverCache>): void {
delete cache.canBeUsed
cache.objHashes = {}
cache.objects = {}
}
}
/** Return a "hash-string" which changes whenever anything that affects timing of a timeline-object has changed. */
export function hashTimelineObject(obj: ResolvedTimelineObject): string {
Expand Down Expand Up @@ -210,21 +244,26 @@ function getAllReferencesThisObjectAffects(newObj: ResolvedTimelineObject): Refe
}
return references
}
/**
* Keeps track of which timeline object have been changed
*/
class ChangedTracker {
private changedReferences = new Set<Reference>()

/**
* Mark an object as "has changed".
* Will store all references that are affected by this object.
*/
public addChangedObject(obj: ResolvedTimelineObject) {
const references = getAllReferencesThisObjectAffects(obj)
for (const ref of references) {
for (const ref of getAllReferencesThisObjectAffects(obj)) {
this.changedReferences.add(ref)
}
if (objHasLayer(obj)) {
this.changedReferences.add(`$${obj.layer}`)
}
}
/** Returns true if a reference has changed */
public isChanged(ref: Reference): boolean {
return this.changedReferences.has(ref)
}
/** Returns a list of all changed references */
public listChanged(): IterableIterator<Reference> {
return this.changedReferences.keys()
}
Expand All @@ -236,6 +275,7 @@ class Invalidator {
/** All references that depend on another reference (ie objects, class or layers): */
private affectReferenceMap: { [ref: Reference]: Reference[] } = {}
private validObjects: ResolvedTimelineObjects = {}
private inValidObjectIds: string[] = []
/** Map of which objects can be affected by any other object, per layer */
private objectLayerMap: { [layer: string]: string[] } = {}

Expand All @@ -245,6 +285,9 @@ class Invalidator {
public getValidObjects(): ResolvedTimelineObject[] {
return Object.values<ResolvedTimelineObject>(this.validObjects)
}
public getInValidObjectIds(): string[] {
return this.inValidObjectIds
}
public addObjectOnLayer(layer: string, obj: ResolvedTimelineObject) {
if (!this.objectLayerMap[layer]) this.objectLayerMap[layer] = []
this.objectLayerMap[layer].push(obj.id)
Expand All @@ -263,6 +306,7 @@ class Invalidator {
const objId = getRefObjectId(reference)
if (this.validObjects[objId]) {
delete this.validObjects[objId]
this.inValidObjectIds.push(objId)
}
}
if (isLayerReference(reference)) {
Expand Down
Loading

0 comments on commit 021ea73

Please sign in to comment.