@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 CHANGED
@@ -6,10 +6,10 @@ evaluation for AelionSDK.
6
6
  ## Install
7
7
 
8
8
  ```bash
9
- npm install @aelionsdk/render-ir@next
9
+ npm install @aelionsdk/render-ir
10
10
  ```
11
11
 
12
- `next` currently resolves to `1.2.0-rc.5`. Product applications should use
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
 
@@ -1 +1 @@
1
- {"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAmB,MAAM,2BAA2B,CAAC;AAGpG,OAAO,KAAK,EAaV,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAscpB,qBAAa,yBAAyB;;IAIpC;;;;OAIG;IACI,IAAI,IAAI,yBAAyB;IAMxC,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;CAwOvB"}
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 { canonicalStringify } from '@aelionsdk/project-schema';
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 (seen.has(value))
33
+ if (deepFrozen.has(value))
6
34
  return value;
7
- const prototype = Object.getPrototypeOf(value);
8
- if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
35
+ if (seen.has(value))
9
36
  return value;
10
- seen.add(value);
11
- for (const entry of Object.values(value))
12
- deepFreezePlain(entry, seen);
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
- canonicalStringify(item),
252
+ fingerprintOf(item),
194
253
  ...(sourceAsset?.kind === 'image-sequence'
195
- ? [canonicalStringify(sourceAsset.imageSequence ?? null)]
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 canonicalStringify(null);
203
- return canonicalStringify({
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
- const clips = track.itemIds.flatMap(itemId => {
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 !== 'video' &&
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
- return [previous];
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
- return [previous];
573
+ clips.push(previous);
574
+ continue;
494
575
  }
495
576
  compiledClips += 1;
496
- return [candidate];
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: canonicalStringify(track),
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
- canonicalStringify(value),
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": "1.2.0-rc.5",
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": "1.2.0-rc.5",
38
- "@aelionsdk/material-compiler": "1.2.0-rc.5",
39
- "@aelionsdk/project-schema": "1.2.0-rc.5"
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",