@aelionsdk/render-ir 1.2.0-rc.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +109 -28
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ evaluation for AelionSDK.
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npm install @aelionsdk/render-ir
|
|
9
|
+
npm install @aelionsdk/render-ir
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`latest` currently resolves to `2.0.0`. Product applications should use
|
|
13
13
|
`@aelionsdk/sdk`; direct use is for custom renderers, exporters and engine
|
|
14
14
|
instrumentation.
|
|
15
15
|
|
package/dist/compiler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAqC,MAAM,2BAA2B,CAAC;AAGlG,OAAO,KAAK,EAaV,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAkgBpB,qBAAa,yBAAyB;;IAcpC;;;;OAIG;IACI,IAAI,IAAI,yBAAyB;IAcxC,4EAA4E;IACrE,KAAK,IAAI,IAAI;IAOb,OAAO,CACZ,OAAO,EAAE,aAAa,EACtB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,uBAAuB,GACnB,oBAAoB,GACpB,mBAAmB,CAAC,OAAO,CAAC,CAAC,gBAAgB,CAAM,GACtD,mBAAmB;CAyOvB"}
|
package/dist/compiler.js
CHANGED
|
@@ -1,15 +1,59 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {} from '@aelionsdk/project-schema';
|
|
2
|
+
/**
|
|
3
|
+
* Objects `deepFreezePlain` has already frozen, along with everything beneath
|
|
4
|
+
* them.
|
|
5
|
+
*
|
|
6
|
+
* An incremental compile reuses most of the previous IR verbatim, and those
|
|
7
|
+
* objects were deep-frozen by the compile that produced them. Re-walking them
|
|
8
|
+
* dominated incremental compile time -- roughly three quarters of a no-op
|
|
9
|
+
* compile at a thousand clips. Membership means the whole subtree is already
|
|
10
|
+
* frozen, so the walk can stop at the first reused node.
|
|
11
|
+
*
|
|
12
|
+
* Entries are held weakly, so a released IR is still collectable.
|
|
13
|
+
*/
|
|
14
|
+
const deepFrozen = new WeakSet();
|
|
15
|
+
/** Item types the Render IR can compile, as a set so the check is one lookup. */
|
|
16
|
+
const COMPILABLE_ITEM_TYPES = new Set([
|
|
17
|
+
'video',
|
|
18
|
+
'image',
|
|
19
|
+
'audio',
|
|
20
|
+
'text',
|
|
21
|
+
'caption',
|
|
22
|
+
'nested-sequence',
|
|
23
|
+
'generator',
|
|
24
|
+
'shape',
|
|
25
|
+
'material-content',
|
|
26
|
+
'adjustment',
|
|
27
|
+
// Compiles to no clip at all; see the Gap branch in the Track loop below.
|
|
28
|
+
'gap',
|
|
29
|
+
]);
|
|
2
30
|
function deepFreezePlain(value, seen = new WeakSet()) {
|
|
3
31
|
if (value === null || typeof value !== 'object')
|
|
4
32
|
return value;
|
|
5
|
-
if (
|
|
33
|
+
if (deepFrozen.has(value))
|
|
6
34
|
return value;
|
|
7
|
-
|
|
8
|
-
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
|
|
35
|
+
if (seen.has(value))
|
|
9
36
|
return value;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
seen.add(value);
|
|
39
|
+
for (const entry of value)
|
|
40
|
+
deepFreezePlain(entry, seen);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
45
|
+
return value;
|
|
46
|
+
seen.add(value);
|
|
47
|
+
// `for...in` over a plain object walks V8's enumeration cache without
|
|
48
|
+
// materializing a key or value array for every node in the IR, which at a
|
|
49
|
+
// thousand clips is tens of thousands of throwaway arrays per compile.
|
|
50
|
+
for (const key in value) {
|
|
51
|
+
deepFreezePlain(value[key], seen);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Recorded only after the whole subtree is frozen, so membership never
|
|
55
|
+
// promises more than has actually been done.
|
|
56
|
+
deepFrozen.add(value);
|
|
13
57
|
return Object.freeze(value);
|
|
14
58
|
}
|
|
15
59
|
function object(value, context) {
|
|
@@ -185,22 +229,37 @@ function nestedSequenceSource(item) {
|
|
|
185
229
|
boundary: boundary,
|
|
186
230
|
};
|
|
187
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Serializes a value for fingerprint comparison.
|
|
234
|
+
*
|
|
235
|
+
* A fingerprint is only ever compared against the fingerprint the previous
|
|
236
|
+
* compile recorded for the same entity, so it needs one property: two entities
|
|
237
|
+
* that differ must serialize differently. Canonical ordering buys the stronger
|
|
238
|
+
* property that two *equal* entities always agree even if their keys were
|
|
239
|
+
* written in a different order -- which here would at worst cause a recompile
|
|
240
|
+
* that produces the same clip, and costs a hand-written walk over every value
|
|
241
|
+
* in the Sequence to avoid. `JSON.stringify` is exact for the plain JSON an
|
|
242
|
+
* admitted Project contains, and the engine runs it natively.
|
|
243
|
+
*/
|
|
244
|
+
function fingerprintOf(value) {
|
|
245
|
+
return JSON.stringify(value);
|
|
246
|
+
}
|
|
188
247
|
function clipFingerprint(item, materials, assets) {
|
|
189
248
|
const source = object(item.source ?? {}, `item ${item.id}.source`);
|
|
190
249
|
const sourceAssetId = typeof source.assetId === 'string' ? source.assetId : undefined;
|
|
191
250
|
const sourceAsset = sourceAssetId === undefined ? undefined : assets[sourceAssetId];
|
|
192
251
|
return [
|
|
193
|
-
|
|
252
|
+
fingerprintOf(item),
|
|
194
253
|
...(sourceAsset?.kind === 'image-sequence'
|
|
195
|
-
? [
|
|
254
|
+
? [fingerprintOf(sourceAsset.imageSequence ?? null)]
|
|
196
255
|
: []),
|
|
197
256
|
...item.materialInstanceIds.map(id => materialFingerprint(materials[id])),
|
|
198
257
|
].join('|');
|
|
199
258
|
}
|
|
200
259
|
function materialFingerprint(instance) {
|
|
201
260
|
if (instance === undefined)
|
|
202
|
-
return
|
|
203
|
-
return
|
|
261
|
+
return 'null';
|
|
262
|
+
return fingerprintOf({
|
|
204
263
|
id: instance.id,
|
|
205
264
|
definition: {
|
|
206
265
|
packageId: instance.definition.packageId,
|
|
@@ -416,6 +475,16 @@ function contentDuration(project, trackIds) {
|
|
|
416
475
|
export class IncrementalRenderCompiler {
|
|
417
476
|
#previous;
|
|
418
477
|
#compiling = false;
|
|
478
|
+
/**
|
|
479
|
+
* Track fingerprints keyed by the source track object.
|
|
480
|
+
*
|
|
481
|
+
* A track fingerprint serializes the whole Track, and a Track carries every
|
|
482
|
+
* item id on it, so recomputing it for a thousand-clip track dominated the cost
|
|
483
|
+
* of an incremental compile even when nothing on that track changed. Commits
|
|
484
|
+
* share structure: editing one item leaves every untouched track at the same
|
|
485
|
+
* object identity, so identity is a sound key and an edited track simply misses.
|
|
486
|
+
*/
|
|
487
|
+
#trackFingerprints = new WeakMap();
|
|
419
488
|
/**
|
|
420
489
|
* Creates an isolated compiler that reuses this compiler's immutable baseline.
|
|
421
490
|
* Compiling on the fork cannot advance or corrupt the parent baseline; a host
|
|
@@ -426,6 +495,14 @@ export class IncrementalRenderCompiler {
|
|
|
426
495
|
fork.#previous = this.#previous;
|
|
427
496
|
return fork;
|
|
428
497
|
}
|
|
498
|
+
#trackFingerprint(track) {
|
|
499
|
+
const cached = this.#trackFingerprints.get(track);
|
|
500
|
+
if (cached !== undefined)
|
|
501
|
+
return cached;
|
|
502
|
+
const fingerprint = fingerprintOf(track);
|
|
503
|
+
this.#trackFingerprints.set(track, fingerprint);
|
|
504
|
+
return fingerprint;
|
|
505
|
+
}
|
|
429
506
|
/** Releases the incremental baseline retained for clip/transition reuse. */
|
|
430
507
|
clear() {
|
|
431
508
|
if (this.#compiling) {
|
|
@@ -465,36 +542,40 @@ export class IncrementalRenderCompiler {
|
|
|
465
542
|
const track = project.tracks[trackId];
|
|
466
543
|
if (track === undefined)
|
|
467
544
|
throw new RangeError(`Track ${trackId} does not exist`);
|
|
468
|
-
|
|
545
|
+
// A plain loop rather than `flatMap`: the reuse path runs once per item on
|
|
546
|
+
// every commit, and wrapping each result in a throwaway single-element array
|
|
547
|
+
// was the largest remaining cost of an incremental compile.
|
|
548
|
+
const clips = [];
|
|
549
|
+
for (const itemId of track.itemIds) {
|
|
469
550
|
const item = project.items[itemId];
|
|
470
551
|
if (item === undefined)
|
|
471
552
|
throw new RangeError(`Item ${itemId} does not exist`);
|
|
472
|
-
if (item.type
|
|
473
|
-
item.type !== 'image' &&
|
|
474
|
-
item.type !== 'audio' &&
|
|
475
|
-
item.type !== 'text' &&
|
|
476
|
-
item.type !== 'caption' &&
|
|
477
|
-
item.type !== 'nested-sequence' &&
|
|
478
|
-
item.type !== 'generator' &&
|
|
479
|
-
item.type !== 'shape' &&
|
|
480
|
-
item.type !== 'material-content' &&
|
|
481
|
-
item.type !== 'adjustment')
|
|
553
|
+
if (!COMPILABLE_ITEM_TYPES.has(item.type)) {
|
|
482
554
|
throw new TypeError(`Render IR cannot compile item type ${item.type}`);
|
|
555
|
+
}
|
|
556
|
+
// A Gap is time and nothing else. It still lengthens the Sequence,
|
|
557
|
+
// because `contentDuration` reads the Project rather than the clip
|
|
558
|
+
// list, but it produces no clip for the renderer or the mixer to
|
|
559
|
+
// consider.
|
|
560
|
+
if (item.type === 'gap')
|
|
561
|
+
continue;
|
|
483
562
|
const previous = previousClips.get(itemId);
|
|
484
563
|
if (canReuseByEntity &&
|
|
485
564
|
previous !== undefined &&
|
|
486
565
|
!previous.dependencyEntityIds.some(id => affectedEntityIds.has(id))) {
|
|
487
566
|
reusedClips += 1;
|
|
488
|
-
|
|
567
|
+
clips.push(previous);
|
|
568
|
+
continue;
|
|
489
569
|
}
|
|
490
570
|
const candidate = compileClip(item, materials, project.assets);
|
|
491
571
|
if (previous?.fingerprint === candidate.fingerprint) {
|
|
492
572
|
reusedClips += 1;
|
|
493
|
-
|
|
573
|
+
clips.push(previous);
|
|
574
|
+
continue;
|
|
494
575
|
}
|
|
495
576
|
compiledClips += 1;
|
|
496
|
-
|
|
497
|
-
}
|
|
577
|
+
clips.push(candidate);
|
|
578
|
+
}
|
|
498
579
|
return {
|
|
499
580
|
id: track.id,
|
|
500
581
|
kind: track.kind,
|
|
@@ -506,7 +587,7 @@ export class IncrementalRenderCompiler {
|
|
|
506
587
|
: {}),
|
|
507
588
|
clips,
|
|
508
589
|
materialInstanceIds: [...track.materialInstanceIds],
|
|
509
|
-
fingerprint:
|
|
590
|
+
fingerprint: this.#trackFingerprint(track),
|
|
510
591
|
};
|
|
511
592
|
});
|
|
512
593
|
const transitions = sequence.transitionIds.map(id => {
|
|
@@ -529,7 +610,7 @@ export class IncrementalRenderCompiler {
|
|
|
529
610
|
materialInstanceId: value.materialInstanceId,
|
|
530
611
|
dependencyEntityIds: [id, value.fromItemId, value.toItemId, value.materialInstanceId],
|
|
531
612
|
fingerprint: [
|
|
532
|
-
|
|
613
|
+
fingerprintOf(value),
|
|
533
614
|
materialFingerprint(materials[value.materialInstanceId]),
|
|
534
615
|
].join('|'),
|
|
535
616
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aelionsdk/render-ir",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Incremental render intermediate representation compiler for AelionSDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"provenance": true
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@aelionsdk/core": "
|
|
38
|
-
"@aelionsdk/material-compiler": "
|
|
39
|
-
"@aelionsdk/project-schema": "
|
|
37
|
+
"@aelionsdk/core": "2.0.0",
|
|
38
|
+
"@aelionsdk/material-compiler": "2.0.0",
|
|
39
|
+
"@aelionsdk/project-schema": "2.0.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -b",
|