@hyperframes/engine 0.6.111 → 0.6.112
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/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/services/frameCapture.d.ts +19 -0
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +350 -1
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +13 -0
- package/src/services/frameCapture-staticDedupIndex.test.ts +76 -0
- package/src/services/frameCapture.ts +417 -1
- package/src/types.ts +20 -0
package/dist/config.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface EngineConfig {
|
|
|
42
42
|
expectedChromiumMajor?: number;
|
|
43
43
|
/** Force screenshot capture mode (skip BeginFrame even on Linux). */
|
|
44
44
|
forceScreenshot: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Static-frame dedup: reuse byte-identical frames instead of re-seeking +
|
|
47
|
+
* re-screenshotting (anchor-verified at init). Default ON; disable via
|
|
48
|
+
* `HF_STATIC_DEDUP` in {false,0,off}. Only arms in screenshot capture mode.
|
|
49
|
+
*/
|
|
50
|
+
staticFrameDedup: boolean;
|
|
45
51
|
/**
|
|
46
52
|
* Low-memory render profile. When `true`, the orchestrator collapses the
|
|
47
53
|
* pipeline to its cheapest shape on memory-constrained hosts: it skips the
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAE3B,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IAClB,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACvC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IAGpB,yDAAyD;IACzD,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,sCAAsC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,oBAAoB,EAAE,MAAM,CAAC;IAG7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;;OAQG;IACH,cAAc,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IACjD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,yBAAyB,EAAE,OAAO,CAAC;IAGnC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,iCAAiC,EAAE,MAAM,CAAC;IAG1C,+DAA+D;IAC/D,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAG/B,kEAAkE;IAClE,GAAG,EAAE;QAAE,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,GAAG,KAAK,CAAC;IACxC,yEAAyE;IACzE,aAAa,EAAE,OAAO,CAAC;IAGvB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,6BAA6B,EAAE,MAAM,CAAC;IAGtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAC;IAG9B,kDAAkD;IAClD,aAAa,EAAE,OAAO,CAAC;IACvB,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAG7B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,qEAAqE;AACrE,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAE3B,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IAClB,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACvC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IAGpB,yDAAyD;IACzD,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,sCAAsC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,oBAAoB,EAAE,MAAM,CAAC;IAG7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;;OAQG;IACH,cAAc,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IACjD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,eAAe,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;;;;;OAQG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,yBAAyB,EAAE,OAAO,CAAC;IAGnC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,iCAAiC,EAAE,MAAM,CAAC;IAG1C,+DAA+D;IAC/D,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAG/B,kEAAkE;IAClE,GAAG,EAAE;QAAE,QAAQ,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,GAAG,KAAK,CAAC;IACxC,yEAAyE;IACzE,aAAa,EAAE,OAAO,CAAC;IAGvB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,6BAA6B,EAAE,MAAM,CAAC;IAGtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAC;IAG9B,kDAAkD;IAClD,aAAa,EAAE,OAAO,CAAC;IACvB,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAG7B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,qEAAqE;AACrE,eAAO,MAAM,cAAc,EAAE,YA8C5B,CAAC;AAgBF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAqI7E"}
|
package/dist/config.js
CHANGED
|
@@ -22,6 +22,7 @@ export const DEFAULT_CONFIG = {
|
|
|
22
22
|
browserTimeout: 120_000,
|
|
23
23
|
protocolTimeout: 300_000,
|
|
24
24
|
forceScreenshot: false,
|
|
25
|
+
staticFrameDedup: true,
|
|
25
26
|
// Auto-detected per host in `resolveConfig`; defaults off for the raw
|
|
26
27
|
// DEFAULT_CONFIG (used directly by tests and worker-sizing fallbacks).
|
|
27
28
|
lowMemoryMode: false,
|
|
@@ -95,6 +96,11 @@ export function resolveConfig(overrides) {
|
|
|
95
96
|
return false;
|
|
96
97
|
return isLowMemorySystem();
|
|
97
98
|
};
|
|
99
|
+
// Opt-OUT: default ON, disabled only by an explicit falsey value.
|
|
100
|
+
const resolveStaticFrameDedup = () => {
|
|
101
|
+
const raw = env("HF_STATIC_DEDUP")?.trim().toLowerCase();
|
|
102
|
+
return !(raw === "false" || raw === "off" || raw === "0");
|
|
103
|
+
};
|
|
98
104
|
// Env-var layer (backward compat)
|
|
99
105
|
const fromEnv = {
|
|
100
106
|
concurrency: env("PRODUCER_MAX_WORKERS") ? Number(env("PRODUCER_MAX_WORKERS")) : undefined,
|
|
@@ -111,6 +117,7 @@ export function resolveConfig(overrides) {
|
|
|
111
117
|
? Number(env("PRODUCER_EXPECTED_CHROMIUM_MAJOR"))
|
|
112
118
|
: undefined,
|
|
113
119
|
forceScreenshot: envBool("PRODUCER_FORCE_SCREENSHOT", DEFAULT_CONFIG.forceScreenshot),
|
|
120
|
+
staticFrameDedup: resolveStaticFrameDedup(),
|
|
114
121
|
lowMemoryMode: resolveLowMemoryMode(),
|
|
115
122
|
enablePageSideCompositing: envBool("HF_PAGE_SIDE_COMPOSITING", DEFAULT_CONFIG.enablePageSideCompositing),
|
|
116
123
|
enableChunkedEncode: envBool("PRODUCER_ENABLE_CHUNKED_ENCODE", DEFAULT_CONFIG.enableChunkedEncode),
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,6BAA6B,GAC9B,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,6BAA6B,GAC9B,MAAM,4BAA4B,CAAC;AA0LpC,qEAAqE;AACrE,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C,GAAG,EAAE,EAAE;IACP,OAAO,EAAE,UAAU;IACnB,MAAM,EAAE,MAAM;IACd,WAAW,EAAE,EAAE;IAEf,WAAW,EAAE,MAAM;IACnB,cAAc,EAAE,GAAG;IACnB,iBAAiB,EAAE,GAAG;IACtB,oBAAoB,EAAE,IAAI;IAE1B,UAAU,EAAE,KAAK;IACjB,cAAc,EAAE,UAAU;IAC1B,iBAAiB,EAAE,IAAI;IACvB,cAAc,EAAE,OAAO;IACvB,eAAe,EAAE,OAAO;IACxB,eAAe,EAAE,KAAK;IACtB,gBAAgB,EAAE,IAAI;IACtB,sEAAsE;IACtE,uEAAuE;IACvE,aAAa,EAAE,KAAK;IACpB,yBAAyB,EAAE,IAAI;IAE/B,mBAAmB,EAAE,KAAK;IAC1B,eAAe,EAAE,GAAG;IACpB,qBAAqB,EAAE,IAAI;IAC3B,iCAAiC,EAAE,GAAG;IAEtC,mBAAmB,EAAE,OAAO;IAC5B,oBAAoB,EAAE,OAAO;IAC7B,sBAAsB,EAAE,OAAO;IAE/B,GAAG,EAAE,KAAK;IACV,aAAa,EAAE,IAAI;IAEnB,SAAS,EAAE,CAAC;IACZ,sBAAsB,EAAE,GAAG;IAC3B,6BAA6B,EAAE,IAAI;IAEnC,kBAAkB,EAAE,MAAM;IAC1B,kBAAkB,EAAE,MAAM;IAC1B,qBAAqB,EAAE,MAAM;IAE7B,aAAa,EAAE,IAAI;IAEnB,KAAK,EAAE,KAAK;CACb,CAAC;AAEF,SAAS,wBAAwB;IAC/B,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,EAAE,CAAC;IAC5B,IAAI,KAAK,IAAI,6BAA6B;QAAE,OAAO,EAAE,CAAC;IACtD,OAAO,cAAc,CAAC,sBAAsB,CAAC;AAC/C,CAAC;AAED,SAAS,0BAA0B;IACjC,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC;IAC7B,IAAI,KAAK,IAAI,6BAA6B;QAAE,OAAO,GAAG,CAAC;IACvD,OAAO,cAAc,CAAC,6BAA6B,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiC;IAC7D,MAAM,GAAG,GAAG,CAAC,GAAW,EAAsB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,QAAgB,EAAU,EAAE;QACvD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACrB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;YAAE,OAAO,QAAQ,CAAC;QACrD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC3C,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,QAAiB,EAAW,EAAE;QAC1D,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACrB,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;QACvC,OAAO,GAAG,KAAK,MAAM,CAAC;IACxB,CAAC,CAAC;IACF,MAAM,iBAAiB,GAAG,GAAmC,EAAE;QAC7D,MAAM,GAAG,GAAG,GAAG,CAAC,2BAA2B,CAAC,CAAC;QAC7C,IAAI,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,MAAM;YAAE,OAAO,GAAG,CAAC;QAC3E,OAAO,cAAc,CAAC,cAAc,CAAC;IACvC,CAAC,CAAC;IACF,4EAA4E;IAC5E,MAAM,oBAAoB,GAAG,GAAY,EAAE;QACzC,MAAM,GAAG,GAAG,GAAG,CAAC,0BAA0B,CAAC,EAAE,WAAW,EAAE,CAAC;QAC3D,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAC/D,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG;YAAE,OAAO,KAAK,CAAC;QAClE,OAAO,iBAAiB,EAAE,CAAC;IAC7B,CAAC,CAAC;IACF,kEAAkE;IAClE,MAAM,uBAAuB,GAAG,GAAY,EAAE;QAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzD,OAAO,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC;IAC5D,CAAC,CAAC;IAEF,kCAAkC;IAClC,MAAM,OAAO,GAA0B;QACrC,WAAW,EAAE,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;QAC1F,cAAc,EAAE,MAAM,CAAC,2BAA2B,EAAE,cAAc,CAAC,cAAc,CAAC;QAClF,iBAAiB,EAAE,MAAM,CAAC,8BAA8B,EAAE,cAAc,CAAC,iBAAiB,CAAC;QAC3F,oBAAoB,EAAE,MAAM,CAC1B,iCAAiC,EACjC,cAAc,CAAC,oBAAoB,CACpC;QAED,UAAU,EAAE,GAAG,CAAC,8BAA8B,CAAC;QAC/C,UAAU,EAAE,OAAO,CAAC,sBAAsB,EAAE,cAAc,CAAC,UAAU,CAAC;QACtE,cAAc,EAAE,iBAAiB,EAAE;QACnC,iBAAiB,EAAE,OAAO,CAAC,8BAA8B,EAAE,cAAc,CAAC,iBAAiB,CAAC;QAC5F,cAAc,EAAE,MAAM,CAAC,sCAAsC,EAAE,cAAc,CAAC,cAAc,CAAC;QAC7F,eAAe,EAAE,MAAM,CACrB,wCAAwC,EACxC,cAAc,CAAC,eAAe,CAC/B;QACD,qBAAqB,EAAE,GAAG,CAAC,kCAAkC,CAAC;YAC5D,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;YACjD,CAAC,CAAC,SAAS;QAEb,eAAe,EAAE,OAAO,CAAC,2BAA2B,EAAE,cAAc,CAAC,eAAe,CAAC;QACrF,gBAAgB,EAAE,uBAAuB,EAAE;QAC3C,aAAa,EAAE,oBAAoB,EAAE;QACrC,yBAAyB,EAAE,OAAO,CAChC,0BAA0B,EAC1B,cAAc,CAAC,yBAAyB,CACzC;QAED,mBAAmB,EAAE,OAAO,CAC1B,gCAAgC,EAChC,cAAc,CAAC,mBAAmB,CACnC;QACD,eAAe,EAAE,IAAI,CAAC,GAAG,CACvB,GAAG,EACH,MAAM,CAAC,4BAA4B,EAAE,cAAc,CAAC,eAAe,CAAC,CACrE;QACD,qBAAqB,EAAE,OAAO,CAC5B,kCAAkC,EAClC,cAAc,CAAC,qBAAqB,CACrC;QACD,iCAAiC,EAAE,IAAI,CAAC,GAAG,CACzC,CAAC,EACD,MAAM,CACJ,gDAAgD,EAChD,cAAc,CAAC,iCAAiC,CACjD,CACF;QAED,mBAAmB,EAAE,MAAM,CAAC,0BAA0B,EAAE,cAAc,CAAC,mBAAmB,CAAC;QAC3F,oBAAoB,EAAE,MAAM,CAAC,2BAA2B,EAAE,cAAc,CAAC,oBAAoB,CAAC;QAC9F,sBAAsB,EAAE,MAAM,CAC5B,6BAA6B,EAC7B,cAAc,CAAC,sBAAsB,CACtC;QAED,GAAG,EAAE,CAAC,GAAG,EAAE;YACT,MAAM,GAAG,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACzC,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,EAAE;QACJ,aAAa,EAAE,OAAO,CAAC,0BAA0B,EAAE,cAAc,CAAC,aAAa,CAAC;QAEhF,SAAS,EAAE,MAAM,CAAC,qBAAqB,EAAE,cAAc,CAAC,SAAS,CAAC;QAClE,sBAAsB,EAAE,IAAI,CAAC,GAAG,CAC9B,EAAE,EACF,MAAM,CAAC,qCAAqC,EAAE,wBAAwB,EAAE,CAAC,CAC1E;QACD,6BAA6B,EAAE,IAAI,CAAC,GAAG,CACrC,EAAE,EACF,MAAM,CAAC,wCAAwC,EAAE,0BAA0B,EAAE,CAAC,CAC/E;QAED,kBAAkB,EAAE,MAAM,CACxB,kCAAkC,EAClC,cAAc,CAAC,kBAAkB,CAClC;QACD,kBAAkB,EAAE,MAAM,CACxB,kCAAkC,EAClC,cAAc,CAAC,kBAAkB,CAClC;QACD,qBAAqB,EAAE,MAAM,CAC3B,qCAAqC,EACrC,cAAc,CAAC,qBAAqB,CACrC;QAED,aAAa,EAAE,GAAG,CAAC,oCAAoC,CAAC,KAAK,OAAO;QACpE,mBAAmB,EAAE,GAAG,CAAC,mCAAmC,CAAC;QAE7D,eAAe,EAAE,GAAG,CAAC,+BAA+B,CAAC;KACtD,CAAC;IAEF,0DAA0D;IAC1D,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC;IAEhG,OAAO;QACL,GAAG,cAAc;QACjB,GAAG,QAAQ;QACX,GAAG,SAAS;KACb,CAAC;AACJ,CAAC"}
|
|
@@ -21,6 +21,25 @@ export interface CaptureSession {
|
|
|
21
21
|
outputDir: string;
|
|
22
22
|
onBeforeCapture: BeforeCaptureHook | null;
|
|
23
23
|
isInitialized: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Static-frame dedup (default-on; opt out with `HF_STATIC_DEDUP=false`): indices of frames byte-identical
|
|
26
|
+
* to their predecessor (no GSAP tween / clip cut active in either), predicted from
|
|
27
|
+
* window.__timelines and empirically anchor-verified. These reuse `lastFrameBuffer`
|
|
28
|
+
* instead of re-seeking + re-screenshotting. Undefined when disabled or ineligible.
|
|
29
|
+
*/
|
|
30
|
+
staticFrames?: Set<number>;
|
|
31
|
+
/** Last non-deduped frame buffer, reused for every `staticFrames` index in its run. */
|
|
32
|
+
lastFrameBuffer?: Buffer;
|
|
33
|
+
/** Count of frames served from a reused buffer (dedup telemetry). */
|
|
34
|
+
staticDedupCount?: number;
|
|
35
|
+
/** Dedup was enabled for this render (default-on; opt out with `HF_STATIC_DEDUP=false`). */
|
|
36
|
+
staticDedupEnabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Short machine code for WHY dedup did not arm, for a low-cardinality breakdown.
|
|
39
|
+
* One of: `capture_mode` | `video_injection` | `page_composite` |
|
|
40
|
+
* `ineligible` | `verification_failed` | `verification_budget`. Undefined when armed or disabled.
|
|
41
|
+
*/
|
|
42
|
+
staticDedupSkipReason?: string;
|
|
24
43
|
pageReleased?: boolean;
|
|
25
44
|
browserReleased?: boolean;
|
|
26
45
|
browserConsoleBuffer: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frameCapture.d.ts","sourceRoot":"","sources":["../../src/services/frameCapture.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,IAAI,EAAsC,MAAM,gBAAgB,CAAC;AAM7F,OAAO,EAOL,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAO7B,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,KAAK,EACV,cAAc,EAEd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;AAEvF,wGAAwG;AACxG,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5E,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC1C,aAAa,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"frameCapture.d.ts","sourceRoot":"","sources":["../../src/services/frameCapture.ts"],"names":[],"mappings":"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"}
|
|
@@ -739,6 +739,7 @@ export async function initializeSession(session) {
|
|
|
739
739
|
if (session.options.format === "png") {
|
|
740
740
|
await initTransparentBackground(session.page);
|
|
741
741
|
}
|
|
742
|
+
await armStaticDedup(session, session.page, logInitPhase);
|
|
742
743
|
session.isInitialized = true;
|
|
743
744
|
return;
|
|
744
745
|
}
|
|
@@ -866,6 +867,7 @@ export async function initializeSession(session) {
|
|
|
866
867
|
if (session.options.format === "png") {
|
|
867
868
|
await initTransparentBackground(session.page);
|
|
868
869
|
}
|
|
870
|
+
await armStaticDedup(session, session.page, logInitPhase);
|
|
869
871
|
session.isInitialized = true;
|
|
870
872
|
}
|
|
871
873
|
async function captureFrameErrorDiagnostics(session, frameIndex, time, error) {
|
|
@@ -947,6 +949,295 @@ async function prepareFrameForCapture(session, frameIndex, time) {
|
|
|
947
949
|
}
|
|
948
950
|
return { quantizedTime, seekMs, beforeCaptureMs };
|
|
949
951
|
}
|
|
952
|
+
// ── Static-frame dedup (default-on, opt-out HF_STATIC_DEDUP=false) ─────────────
|
|
953
|
+
// Skip re-seeking + re-screenshotting frames that are byte-identical to their
|
|
954
|
+
// predecessor. A frame is dedupable iff no GSAP tween or clip cut is active in it or
|
|
955
|
+
// its predecessor (predicted from window.__timelines), AND an empirical anchor-compare
|
|
956
|
+
// confirms it. Capture-mode-independent (works on screenshot + beginframe), lossless
|
|
957
|
+
// (verification disables the whole comp on any drift), default off. Pays on
|
|
958
|
+
// static-hold content (title cards, slideshows, data-viz pauses); a no-op on
|
|
959
|
+
// continuously-animated comps and disqualified by video/canvas/non-GSAP animation.
|
|
960
|
+
/**
|
|
961
|
+
* Clip-cut boundary frames (±1) from the [data-start] schedule. A hard scene swap at a
|
|
962
|
+
* cut changes content with no tween; treat those frames as animated so the post-cut
|
|
963
|
+
* frame is captured fresh and later static frames reuse the correct scene.
|
|
964
|
+
*/
|
|
965
|
+
async function computeClipBoundaryFrames(page, fps) {
|
|
966
|
+
const schedule = await page.evaluate(() => Array.from(document.querySelectorAll("[data-start]")).map((el) => ({
|
|
967
|
+
start: parseFloat(el.dataset.start || ""),
|
|
968
|
+
dur: parseFloat(el.dataset.duration || ""),
|
|
969
|
+
})));
|
|
970
|
+
const frames = new Set();
|
|
971
|
+
for (const { start, dur } of schedule) {
|
|
972
|
+
if (Number.isNaN(start))
|
|
973
|
+
continue;
|
|
974
|
+
const edges = [Math.round(start * fps)];
|
|
975
|
+
if (!Number.isNaN(dur))
|
|
976
|
+
edges.push(Math.round((start + dur) * fps));
|
|
977
|
+
for (const e of edges) {
|
|
978
|
+
for (const f of [e - 1, e, e + 1]) {
|
|
979
|
+
if (f >= 0)
|
|
980
|
+
frames.add(f);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return frames;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Predict the dedupable (static) frame set from window.__timelines. A frame f (f>0) is
|
|
988
|
+
* static iff NEITHER f NOR f-1 falls inside any GSAP tween interval — content didn't
|
|
989
|
+
* change f-1→f, so f can reuse f-1's buffer. Requiring BOTH neighbours static under-
|
|
990
|
+
* claims by one frame at each tween edge (the SAFE direction). Disqualifies the whole
|
|
991
|
+
* comp on any signal the tween-walker can't see: video / canvas / webgl (redraw without
|
|
992
|
+
* a tween), zero tweens (non-GSAP animation), or a running CSS/WAAPI animation.
|
|
993
|
+
*/
|
|
994
|
+
async function computeStaticFrameSet(page, fps) {
|
|
995
|
+
const result = await page.evaluate(() => {
|
|
996
|
+
const intervals = [];
|
|
997
|
+
let tweenCount = 0;
|
|
998
|
+
// totalDuration() (NOT duration()): a repeat/yoyo tween animates past one iteration;
|
|
999
|
+
// a repeating timeline is marked opaque over its whole span (conservative).
|
|
1000
|
+
function walk(tl, offset) {
|
|
1001
|
+
if (typeof tl.getChildren !== "function")
|
|
1002
|
+
return;
|
|
1003
|
+
for (const child of tl.getChildren(false, true, true)) {
|
|
1004
|
+
const start = offset + (typeof child.startTime === "function" ? child.startTime() : 0);
|
|
1005
|
+
const single = typeof child.duration === "function" ? child.duration() : 0;
|
|
1006
|
+
const total = typeof child.totalDuration === "function" ? child.totalDuration() : single;
|
|
1007
|
+
if (typeof child.getChildren === "function") {
|
|
1008
|
+
if (total > single + 1e-6)
|
|
1009
|
+
intervals.push({ start, end: start + total });
|
|
1010
|
+
else
|
|
1011
|
+
walk(child, start);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
tweenCount++;
|
|
1015
|
+
intervals.push({ start, end: start + total });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const w = window;
|
|
1020
|
+
for (const tl of Object.values(w.__timelines || {})) {
|
|
1021
|
+
if (tl && typeof tl.getChildren === "function")
|
|
1022
|
+
walk(tl, 0);
|
|
1023
|
+
}
|
|
1024
|
+
const hasVideo = !!document.querySelector("video");
|
|
1025
|
+
const hasCanvas = !!document.querySelector("canvas");
|
|
1026
|
+
// A non-numeric data-start (reference expression like "intro+0.5") can't be turned
|
|
1027
|
+
// into a clip-cut boundary by computeClipBoundaryFrames' parseFloat, so the cut goes
|
|
1028
|
+
// unprotected and could be deduped into the previous scene. Disqualify the comp.
|
|
1029
|
+
const hasUnresolvableClipStart = Array.from(document.querySelectorAll("[data-start]")).some((el) => {
|
|
1030
|
+
const v = el.dataset.start;
|
|
1031
|
+
return v != null && v.trim() !== "" && !Number.isFinite(parseFloat(v));
|
|
1032
|
+
});
|
|
1033
|
+
// Non-GSAP animation (CSS @keyframes / transitions / WAAPI) surfaces via
|
|
1034
|
+
// getAnimations(); any running/paused one can change content without a tween.
|
|
1035
|
+
let hasNonGsapAnim = false;
|
|
1036
|
+
try {
|
|
1037
|
+
const docAnims = document.getAnimations;
|
|
1038
|
+
if (typeof docAnims === "function") {
|
|
1039
|
+
hasNonGsapAnim = docAnims.call(document).some((a) => {
|
|
1040
|
+
const t = a;
|
|
1041
|
+
return t.playState === "running" || t.playState === "paused";
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
hasNonGsapAnim = true;
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
intervals,
|
|
1050
|
+
tweenCount,
|
|
1051
|
+
duration: w.__hf?.duration ?? 0,
|
|
1052
|
+
hasVideo,
|
|
1053
|
+
hasCanvas,
|
|
1054
|
+
hasNonGsapAnim,
|
|
1055
|
+
hasUnresolvableClipStart,
|
|
1056
|
+
};
|
|
1057
|
+
});
|
|
1058
|
+
const { intervals, tweenCount, duration, hasVideo, hasCanvas, hasNonGsapAnim, hasUnresolvableClipStart, } = result;
|
|
1059
|
+
const totalFrames = Math.max(1, Math.ceil(duration * fps));
|
|
1060
|
+
const animated = new Set();
|
|
1061
|
+
for (const { start, end } of intervals) {
|
|
1062
|
+
const lo = Math.max(0, Math.floor(start * fps));
|
|
1063
|
+
const hi = Math.min(totalFrames - 1, Math.ceil(end * fps));
|
|
1064
|
+
for (let f = lo; f <= hi; f++)
|
|
1065
|
+
animated.add(f);
|
|
1066
|
+
}
|
|
1067
|
+
for (const f of await computeClipBoundaryFrames(page, fps))
|
|
1068
|
+
animated.add(f);
|
|
1069
|
+
const reasons = [];
|
|
1070
|
+
if (!(duration > 0))
|
|
1071
|
+
reasons.push("unknown/zero duration");
|
|
1072
|
+
if (hasVideo)
|
|
1073
|
+
reasons.push("video");
|
|
1074
|
+
if (hasCanvas)
|
|
1075
|
+
reasons.push("canvas/webgl");
|
|
1076
|
+
if (tweenCount === 0)
|
|
1077
|
+
reasons.push("no GSAP tweens (non-GSAP animation)");
|
|
1078
|
+
if (hasNonGsapAnim)
|
|
1079
|
+
reasons.push("running CSS/WAAPI animation");
|
|
1080
|
+
if (hasUnresolvableClipStart)
|
|
1081
|
+
reasons.push("unresolvable clip start (reference expression)");
|
|
1082
|
+
const eligible = reasons.length === 0;
|
|
1083
|
+
const staticFrameSet = new Set();
|
|
1084
|
+
if (eligible) {
|
|
1085
|
+
for (let f = 1; f < totalFrames; f++) {
|
|
1086
|
+
if (!animated.has(f) && !animated.has(f - 1))
|
|
1087
|
+
staticFrameSet.add(f);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
totalFrames,
|
|
1092
|
+
staticFrameSet,
|
|
1093
|
+
hasVideo,
|
|
1094
|
+
hasCanvas,
|
|
1095
|
+
hasNonGsapAnim,
|
|
1096
|
+
tweenCount,
|
|
1097
|
+
eligible,
|
|
1098
|
+
reason: eligible ? "eligible" : reasons.join("+"),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Empirically verify the predicted-static set before trusting it. Group static frames
|
|
1103
|
+
* into runs; each run [a..b] reuses anchor a-1. CRITICAL: compare against the ANCHOR,
|
|
1104
|
+
* not the predecessor — a slow drift with sub-quantization per-frame deltas is byte-
|
|
1105
|
+
* identical frame-to-frame yet drifts far from the anchor by the run's end (the real
|
|
1106
|
+
* frozen error). Capture each run's anchor once, compare END + a midpoint to it; any
|
|
1107
|
+
* mismatch ⇒ the run isn't truly static ⇒ disable dedup whole-comp. Capture-mode-
|
|
1108
|
+
* independent (seeks + screenshots in normal DOM). Returns the first bad frame, or null.
|
|
1109
|
+
*/
|
|
1110
|
+
async function verifyStaticFramesSafe(session, page, staticFrames, fps, sampleCount) {
|
|
1111
|
+
const frames = [...staticFrames].sort((a, b) => a - b);
|
|
1112
|
+
if (frames.length === 0)
|
|
1113
|
+
return null;
|
|
1114
|
+
// Runs are maximal-contiguous (adjacent frames merge), so a run's anchor a-1 is
|
|
1115
|
+
// guaranteed NOT static — always a freshly-captured frame.
|
|
1116
|
+
const runs = [];
|
|
1117
|
+
for (const f of frames) {
|
|
1118
|
+
const last = runs[runs.length - 1];
|
|
1119
|
+
if (last && f === last.b + 1)
|
|
1120
|
+
last.b = f;
|
|
1121
|
+
else
|
|
1122
|
+
runs.push({ a: f, b: f });
|
|
1123
|
+
}
|
|
1124
|
+
const seekCapture = async (frameIdx) => {
|
|
1125
|
+
const t = quantizeTimeToFrame(frameIdx / fps, fps);
|
|
1126
|
+
await page.evaluate((tt) => {
|
|
1127
|
+
const hf = window.__hf;
|
|
1128
|
+
if (hf && typeof hf.seek === "function")
|
|
1129
|
+
hf.seek(tt);
|
|
1130
|
+
}, t);
|
|
1131
|
+
return pageScreenshotCapture(page, session.options);
|
|
1132
|
+
};
|
|
1133
|
+
// Verify EVERY run in order (no longest-first truncation that would leave runs armed
|
|
1134
|
+
// but unverified). Per run, compare the FIRST reused frame `a`, the END `b` (max
|
|
1135
|
+
// accumulated drift), and interior points at a stride — against the anchor the run
|
|
1136
|
+
// actually reuses. `sampleCount` sets the interior density (points per run ~ that many
|
|
1137
|
+
// for a long run); a hard cap bounds pathological run counts, and hitting it DISABLES
|
|
1138
|
+
// dedup (conservative: never trust an unverified set).
|
|
1139
|
+
const perRun = Math.max(3, Math.min(sampleCount, 8));
|
|
1140
|
+
const hardCap = Math.max(sampleCount * 8, 400);
|
|
1141
|
+
let spent = 0;
|
|
1142
|
+
for (const { a, b } of runs) {
|
|
1143
|
+
const anchor = a - 1;
|
|
1144
|
+
if (anchor < 0)
|
|
1145
|
+
continue;
|
|
1146
|
+
const anchorBuf = await seekCapture(anchor);
|
|
1147
|
+
spent++;
|
|
1148
|
+
const span = b - a;
|
|
1149
|
+
const stride = span > 0 ? Math.max(1, Math.floor(span / (perRun - 1))) : 1;
|
|
1150
|
+
const pts = new Set();
|
|
1151
|
+
for (let f = a; f <= b; f += stride)
|
|
1152
|
+
pts.add(f);
|
|
1153
|
+
pts.add(b); // always include the end (max drift)
|
|
1154
|
+
for (const f of [...pts].sort((x, y) => x - y)) {
|
|
1155
|
+
const cur = await seekCapture(f);
|
|
1156
|
+
spent++;
|
|
1157
|
+
if (!anchorBuf.equals(cur))
|
|
1158
|
+
return { badFrame: f, budgetExhausted: false };
|
|
1159
|
+
}
|
|
1160
|
+
// Budget exhausted → can't fully verify → disarm. Reported distinctly from real
|
|
1161
|
+
// drift so a `verification_budget` spike in telemetry signals "tune HF_STATIC_DEDUP_SAMPLES",
|
|
1162
|
+
// not "compositions are non-static".
|
|
1163
|
+
if (spent > hardCap)
|
|
1164
|
+
return { badFrame: a, budgetExhausted: true };
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Arm static-frame dedup for this render (default-on; opt out with HF_STATIC_DEDUP=false).
|
|
1170
|
+
* Runs at init in normal DOM state so the verification screenshots are valid. Predicts
|
|
1171
|
+
* the static set, anchor-verifies it (skip with HF_STATIC_DEDUP_VERIFY=false — unsafe),
|
|
1172
|
+
* and on success stores it on the session for captureFrameCore to reuse. Sample budget
|
|
1173
|
+
* via HF_STATIC_DEDUP_SAMPLES (default 24).
|
|
1174
|
+
*/
|
|
1175
|
+
async function armStaticDedup(session, page, logInitPhase) {
|
|
1176
|
+
// Default ON for everyone; opt out via HF_STATIC_DEDUP in {false,0,off} (resolved into
|
|
1177
|
+
// EngineConfig.staticFrameDedup by resolveConfig). Verification is the safety net at scale.
|
|
1178
|
+
// Default-on: only an explicit `staticFrameDedup === false` (resolved from
|
|
1179
|
+
// HF_STATIC_DEDUP) disables; a missing config leaves dedup enabled.
|
|
1180
|
+
session.staticDedupEnabled = session.config?.staticFrameDedup !== false;
|
|
1181
|
+
if (!session.staticDedupEnabled)
|
|
1182
|
+
return;
|
|
1183
|
+
// Conservative gates: dedup is verified against the plain screenshot path, so only arm
|
|
1184
|
+
// where the production capture matches what verification measures, and where reuse is
|
|
1185
|
+
// sound. Skip when:
|
|
1186
|
+
// - capture mode is not screenshot (BeginFrame advances the compositor clock per
|
|
1187
|
+
// frame; skipping beginFrame for static frames gaps the tick sequence, and the
|
|
1188
|
+
// verifier uses pageScreenshotCapture not beginFrameCapture — its proof wouldn't
|
|
1189
|
+
// transfer);
|
|
1190
|
+
// - a before-capture hook is set (per-frame video-frame injection — those frames are
|
|
1191
|
+
// NOT static even if the GSAP timeline is idle, and the injector is skipped on reuse);
|
|
1192
|
+
// - page-side compositing is active (shader transitions / drawElement composite paint
|
|
1193
|
+
// a frame the plain verification screenshot doesn't reproduce).
|
|
1194
|
+
if (session.captureMode !== "screenshot") {
|
|
1195
|
+
session.staticDedupSkipReason = "capture_mode";
|
|
1196
|
+
logInitPhase(`static-frame dedup: disabled (capture mode ${session.captureMode}, not screenshot)`);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (session.onBeforeCapture) {
|
|
1200
|
+
session.staticDedupSkipReason = "video_injection";
|
|
1201
|
+
logInitPhase("static-frame dedup: disabled (before-capture hook / video injection active)");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const pageComposite = await page
|
|
1205
|
+
.evaluate(() => typeof window
|
|
1206
|
+
.__hf_page_composite_prepare === "function")
|
|
1207
|
+
.catch(() => true); // fail CLOSED: if we can't determine, assume compositing → skip dedup
|
|
1208
|
+
if (pageComposite) {
|
|
1209
|
+
session.staticDedupSkipReason = "page_composite";
|
|
1210
|
+
logInitPhase("static-frame dedup: disabled (page-side compositing active)");
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const fps = fpsToNumber(session.options.fps);
|
|
1214
|
+
const stats = await computeStaticFrameSet(page, fps);
|
|
1215
|
+
if (!stats.eligible || stats.staticFrameSet.size === 0) {
|
|
1216
|
+
session.staticDedupSkipReason = "ineligible";
|
|
1217
|
+
logInitPhase(`static-frame dedup: disabled (${stats.reason})`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const rawSamples = Number(process.env.HF_STATIC_DEDUP_SAMPLES ?? "24");
|
|
1221
|
+
const samples = Number.isFinite(rawSamples) && rawSamples >= 1 ? rawSamples : 24;
|
|
1222
|
+
const verdict = process.env.HF_STATIC_DEDUP_VERIFY === "false"
|
|
1223
|
+
? null
|
|
1224
|
+
: await verifyStaticFramesSafe(session, page, stats.staticFrameSet, fps, samples);
|
|
1225
|
+
if (verdict !== null) {
|
|
1226
|
+
session.staticDedupSkipReason = verdict.budgetExhausted
|
|
1227
|
+
? "verification_budget"
|
|
1228
|
+
: "verification_failed";
|
|
1229
|
+
logInitPhase(verdict.budgetExhausted
|
|
1230
|
+
? `static-frame dedup: disabled (verification budget exhausted before frame ${verdict.badFrame}; ` +
|
|
1231
|
+
`raise HF_STATIC_DEDUP_SAMPLES to verify more)`
|
|
1232
|
+
: `static-frame dedup: disabled (verification failed — content drifts from anchor at ` +
|
|
1233
|
+
`predicted-static frame ${verdict.badFrame})`);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
// armed + predicted are derived from staticFrames in getCapturePerfSummary.
|
|
1237
|
+
session.staticFrames = stats.staticFrameSet;
|
|
1238
|
+
logInitPhase(`static-frame dedup: ${stats.staticFrameSet.size}/${stats.totalFrames} frame(s) reusable ` +
|
|
1239
|
+
`(${Math.round((stats.staticFrameSet.size / stats.totalFrames) * 100)}%, verified)`);
|
|
1240
|
+
}
|
|
950
1241
|
/**
|
|
951
1242
|
* Internal core: prepare, screenshot, and track perf.
|
|
952
1243
|
* Shared by captureFrame (disk) and captureFrameToBuffer (buffer).
|
|
@@ -955,6 +1246,29 @@ async function prepareFrameForCapture(session, frameIndex, time) {
|
|
|
955
1246
|
async function captureFrameCore(session, frameIndex, time) {
|
|
956
1247
|
const { page, options } = session;
|
|
957
1248
|
const startTime = Date.now();
|
|
1249
|
+
// Static-frame dedup: this frame is byte-identical to its predecessor (predicted +
|
|
1250
|
+
// anchor-verified at init) → reuse the prior buffer, skip the seek + screenshot.
|
|
1251
|
+
// KEY: index by the ABSOLUTE composition frame (derived from `time`), NOT the
|
|
1252
|
+
// `frameIndex` arg — chunked/parallel/distributed callers pass a chunk-RELATIVE
|
|
1253
|
+
// frameIndex (captureStage passes the loop `i`, parallelCoordinator passes
|
|
1254
|
+
// `i-outputFrameOffset`) while staticFrames is keyed in absolute frames. Using `time`
|
|
1255
|
+
// is correct on every path (sequential, per-worker range, distributed chunk) because
|
|
1256
|
+
// `time` is always the absolute composition time for the frame. Each session captures
|
|
1257
|
+
// its range in ascending order, so lastFrameBuffer is the correct in-range anchor (and
|
|
1258
|
+
// since a static run is verified identical, reusing the run's first in-range capture
|
|
1259
|
+
// equals reusing the global anchor). Telemetry: count reuses separately; do NOT bump
|
|
1260
|
+
// capturePerf.frames (that would dilute the per-frame timing averages).
|
|
1261
|
+
// Use the SAME floor+epsilon idiom as quantizeTimeToFrame so the dedup lookup agrees
|
|
1262
|
+
// with the frame the seek actually lands on, even if `time` ever isn't exactly i/fps.
|
|
1263
|
+
const absFrameIndex = Math.floor(time * fpsToNumber(options.fps) + 1e-9);
|
|
1264
|
+
if (session.staticFrames?.has(absFrameIndex) && session.lastFrameBuffer) {
|
|
1265
|
+
session.staticDedupCount = (session.staticDedupCount ?? 0) + 1;
|
|
1266
|
+
return {
|
|
1267
|
+
buffer: session.lastFrameBuffer,
|
|
1268
|
+
quantizedTime: quantizeTimeToFrame(time, fpsToNumber(options.fps)),
|
|
1269
|
+
captureTimeMs: Date.now() - startTime,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
958
1272
|
try {
|
|
959
1273
|
const { quantizedTime, seekMs, beforeCaptureMs } = await prepareFrameForCapture(session, frameIndex, time);
|
|
960
1274
|
const screenshotStart = Date.now();
|
|
@@ -978,6 +1292,9 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
978
1292
|
session.capturePerf.beforeCaptureMs += beforeCaptureMs;
|
|
979
1293
|
session.capturePerf.screenshotMs += screenshotMs;
|
|
980
1294
|
session.capturePerf.totalMs += captureTimeMs;
|
|
1295
|
+
// Retain this freshly-captured buffer so the following static frames can reuse it.
|
|
1296
|
+
if (session.staticFrames)
|
|
1297
|
+
session.lastFrameBuffer = screenshotBuffer;
|
|
981
1298
|
return { buffer: screenshotBuffer, quantizedTime, captureTimeMs };
|
|
982
1299
|
}
|
|
983
1300
|
catch (captureError) {
|
|
@@ -1038,18 +1355,38 @@ export async function discardWarmupCapture(session, frameIndex = 0, time = 0, in
|
|
|
1038
1355
|
const perfBefore = { ...session.capturePerf };
|
|
1039
1356
|
const hasDamageBefore = session.beginFrameHasDamageCount;
|
|
1040
1357
|
const noDamageBefore = session.beginFrameNoDamageCount;
|
|
1358
|
+
const dedupCountBefore = session.staticDedupCount;
|
|
1359
|
+
const lastFrameBufferBefore = session.lastFrameBuffer;
|
|
1041
1360
|
try {
|
|
1042
1361
|
await innerCapture(session, frameIndex, time);
|
|
1043
1362
|
}
|
|
1044
1363
|
finally {
|
|
1045
1364
|
// Always restore — even on error. A failed warmup capture should not
|
|
1046
|
-
// leak inflated perf counters
|
|
1365
|
+
// leak inflated perf counters, a phantom dedup reuse, or a warmup-era
|
|
1366
|
+
// lastFrameBuffer anchor into the real capture summary/state.
|
|
1047
1367
|
session.capturePerf = perfBefore;
|
|
1048
1368
|
session.beginFrameHasDamageCount = hasDamageBefore;
|
|
1049
1369
|
session.beginFrameNoDamageCount = noDamageBefore;
|
|
1370
|
+
session.staticDedupCount = dedupCountBefore;
|
|
1371
|
+
session.lastFrameBuffer = lastFrameBufferBefore;
|
|
1050
1372
|
}
|
|
1051
1373
|
}
|
|
1052
1374
|
export async function closeCaptureSession(session) {
|
|
1375
|
+
// Realized static-dedup telemetry: how much the cache actually helped this
|
|
1376
|
+
// render (vs the prediction logged at arm time). Both capture paths
|
|
1377
|
+
// (sequential orchestrator + parallel workers) close their session here, so
|
|
1378
|
+
// this is the one uniform emit point. Zero the count afterward so the
|
|
1379
|
+
// idempotent re-close (HDR cleanup) doesn't double-log.
|
|
1380
|
+
const reused = session.staticDedupCount ?? 0;
|
|
1381
|
+
if (session.staticFrames && reused > 0) {
|
|
1382
|
+
const captured = session.capturePerf.frames; // excludes reuses by design
|
|
1383
|
+
const total = captured + reused;
|
|
1384
|
+
const pct = total > 0 ? Math.round((reused / total) * 100) : 0;
|
|
1385
|
+
const avgTotalMs = captured > 0 ? Math.round(session.capturePerf.totalMs / captured) : 0;
|
|
1386
|
+
console.log(`[static-dedup] reused ${reused}/${total} frame(s) (${pct}%), ` +
|
|
1387
|
+
`est. ~${reused * avgTotalMs}ms saved (avg ${avgTotalMs}ms/frame)`);
|
|
1388
|
+
session.staticDedupCount = 0;
|
|
1389
|
+
}
|
|
1053
1390
|
// INVARIANT: closeCaptureSession is idempotent. The renderOrchestrator HDR
|
|
1054
1391
|
// cleanup path tracks a `domSessionClosed` flag and may still re-call this
|
|
1055
1392
|
// in the outer finally if the inner cleanup raised before the flag flipped.
|
|
@@ -1099,6 +1436,12 @@ export function prepareCaptureSessionForReuse(session, outputDir, onBeforeCaptur
|
|
|
1099
1436
|
};
|
|
1100
1437
|
session.beginFrameHasDamageCount = 0;
|
|
1101
1438
|
session.beginFrameNoDamageCount = 0;
|
|
1439
|
+
// Reset per-render dedup state so a buffer captured by the prior render/probe can't
|
|
1440
|
+
// bleed into this render's first static frame. staticFrames (the armed set) is left
|
|
1441
|
+
// intact: it's keyed in absolute frames and stays valid for a same-composition reuse;
|
|
1442
|
+
// lastFrameBuffer must be re-seeded by this render's first fresh capture.
|
|
1443
|
+
session.lastFrameBuffer = undefined;
|
|
1444
|
+
session.staticDedupCount = 0;
|
|
1102
1445
|
}
|
|
1103
1446
|
export async function getCompositionDuration(session) {
|
|
1104
1447
|
if (!session.isInitialized)
|
|
@@ -1115,6 +1458,12 @@ export function getCapturePerfSummary(session) {
|
|
|
1115
1458
|
avgSeekMs: Math.round(session.capturePerf.seekMs / frames),
|
|
1116
1459
|
avgBeforeCaptureMs: Math.round(session.capturePerf.beforeCaptureMs / frames),
|
|
1117
1460
|
avgScreenshotMs: Math.round(session.capturePerf.screenshotMs / frames),
|
|
1461
|
+
staticDedupReused: session.staticDedupCount ?? 0,
|
|
1462
|
+
staticDedupEnabled: session.staticDedupEnabled ?? false,
|
|
1463
|
+
// armed ⟺ a non-empty static set survived verification; predicted === its size.
|
|
1464
|
+
staticDedupArmed: (session.staticFrames?.size ?? 0) > 0,
|
|
1465
|
+
staticDedupPredicted: session.staticFrames?.size ?? 0,
|
|
1466
|
+
staticDedupSkipReason: session.staticDedupSkipReason,
|
|
1118
1467
|
};
|
|
1119
1468
|
}
|
|
1120
1469
|
//# sourceMappingURL=frameCapture.js.map
|