@hyperframes/producer 0.9.9 → 0.9.11
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/distributed.js +40 -21
- package/dist/index.js +217 -174
- package/dist/public-server.js +217 -174
- package/dist/services/render/stallClock.d.ts +7 -0
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -1435,6 +1435,27 @@ import {
|
|
|
1435
1435
|
prepareCaptureSessionForReuse,
|
|
1436
1436
|
spawnStreamingEncoder
|
|
1437
1437
|
} from "@hyperframes/engine";
|
|
1438
|
+
|
|
1439
|
+
// src/services/render/stallClock.ts
|
|
1440
|
+
var SUSPEND_GAP_MS = 15e3;
|
|
1441
|
+
function createStallClock(now = () => Date.now()) {
|
|
1442
|
+
let since = now();
|
|
1443
|
+
let seen = since;
|
|
1444
|
+
return {
|
|
1445
|
+
reset() {
|
|
1446
|
+
since = seen = now();
|
|
1447
|
+
},
|
|
1448
|
+
idle(expectedMs) {
|
|
1449
|
+
const at = now();
|
|
1450
|
+
const gap = at - seen;
|
|
1451
|
+
seen = at;
|
|
1452
|
+
if (gap > expectedMs + SUSPEND_GAP_MS) since += gap;
|
|
1453
|
+
return at - since;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/services/render/stages/captureStreamingStage.ts
|
|
1438
1459
|
var DEFAULT_CAPTURE_STALL_MS = 6e4;
|
|
1439
1460
|
function resolveCaptureStallTimeoutMs() {
|
|
1440
1461
|
const raw = process.env.HF_DE_STALL_MS ?? process.env.HF_DE_PARALLEL_STALL_MS;
|
|
@@ -1802,35 +1823,33 @@ function watchStall(phase, rule) {
|
|
|
1802
1823
|
const detail = rule.detail ?? (() => void 0);
|
|
1803
1824
|
let last = rule.progress();
|
|
1804
1825
|
let budgetMs = rule.firstProgressMs;
|
|
1805
|
-
|
|
1826
|
+
const silence = createStallClock();
|
|
1827
|
+
const pollMs = Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs);
|
|
1806
1828
|
let attempt;
|
|
1807
1829
|
let timer;
|
|
1808
1830
|
let inTail = false;
|
|
1809
1831
|
const restartWindow = (ms) => {
|
|
1810
1832
|
last = rule.progress();
|
|
1811
1833
|
budgetMs = ms;
|
|
1812
|
-
|
|
1834
|
+
silence.reset();
|
|
1813
1835
|
};
|
|
1814
1836
|
const tripped = new Promise((_, reject) => {
|
|
1815
|
-
timer = setInterval(
|
|
1816
|
-
()
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
},
|
|
1832
|
-
Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs)
|
|
1833
|
-
);
|
|
1837
|
+
timer = setInterval(() => {
|
|
1838
|
+
const current = rule.progress();
|
|
1839
|
+
if (rule.complete?.() && !inTail) {
|
|
1840
|
+
inTail = true;
|
|
1841
|
+
return restartWindow(rule.tailMs ?? rule.noProgressMs);
|
|
1842
|
+
}
|
|
1843
|
+
if (current !== last) return restartWindow(rule.noProgressMs);
|
|
1844
|
+
if (silence.idle(pollMs) < budgetMs) return;
|
|
1845
|
+
const error = new RenderStageStalledError(phase, budgetMs, current, detail());
|
|
1846
|
+
if (attempt && !attempt.signal.aborted) {
|
|
1847
|
+
attempt.abort(error);
|
|
1848
|
+
return restartWindow(rule.noProgressMs);
|
|
1849
|
+
}
|
|
1850
|
+
clearInterval(timer);
|
|
1851
|
+
reject(error);
|
|
1852
|
+
}, pollMs);
|
|
1834
1853
|
});
|
|
1835
1854
|
const openAttempt = (parent) => {
|
|
1836
1855
|
attempt = new AbortController();
|
package/dist/index.js
CHANGED
|
@@ -3721,6 +3721,27 @@ import {
|
|
|
3721
3721
|
prepareCaptureSessionForReuse,
|
|
3722
3722
|
spawnStreamingEncoder
|
|
3723
3723
|
} from "@hyperframes/engine";
|
|
3724
|
+
|
|
3725
|
+
// src/services/render/stallClock.ts
|
|
3726
|
+
var SUSPEND_GAP_MS = 15e3;
|
|
3727
|
+
function createStallClock(now = () => Date.now()) {
|
|
3728
|
+
let since = now();
|
|
3729
|
+
let seen = since;
|
|
3730
|
+
return {
|
|
3731
|
+
reset() {
|
|
3732
|
+
since = seen = now();
|
|
3733
|
+
},
|
|
3734
|
+
idle(expectedMs) {
|
|
3735
|
+
const at = now();
|
|
3736
|
+
const gap = at - seen;
|
|
3737
|
+
seen = at;
|
|
3738
|
+
if (gap > expectedMs + SUSPEND_GAP_MS) since += gap;
|
|
3739
|
+
return at - since;
|
|
3740
|
+
}
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
// src/services/render/stages/captureStreamingStage.ts
|
|
3724
3745
|
var DEFAULT_CAPTURE_STALL_MS = 6e4;
|
|
3725
3746
|
var DE_STALL_POLL_MS = 5e3;
|
|
3726
3747
|
var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
|
|
@@ -3765,21 +3786,27 @@ var SequentialCaptureStallError = class extends Error {
|
|
|
3765
3786
|
};
|
|
3766
3787
|
function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
3767
3788
|
return new Promise((resolve13, reject) => {
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
)
|
|
3779
|
-
);
|
|
3780
|
-
},
|
|
3781
|
-
Math.max(0, deadlineMs)
|
|
3789
|
+
const clock = createStallClock();
|
|
3790
|
+
const trip = () => reject(
|
|
3791
|
+
signal?.aborted ? new Error(
|
|
3792
|
+
`[Render] Sequential ${captureModeLabel(input.session.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
|
|
3793
|
+
) : new SequentialCaptureStallError(
|
|
3794
|
+
input.session,
|
|
3795
|
+
input.stallTimeoutMs,
|
|
3796
|
+
input.frameIndex,
|
|
3797
|
+
input.totalFrames
|
|
3798
|
+
)
|
|
3782
3799
|
);
|
|
3800
|
+
const deadline = Math.max(0, deadlineMs);
|
|
3801
|
+
let wait = Math.min(deadline, DE_STALL_POLL_MS);
|
|
3802
|
+
let timer;
|
|
3803
|
+
const look = () => {
|
|
3804
|
+
const idle = clock.idle(wait);
|
|
3805
|
+
if (idle >= deadline) return trip();
|
|
3806
|
+
wait = Math.min(deadline - idle, DE_STALL_POLL_MS);
|
|
3807
|
+
timer = setTimeout(look, wait);
|
|
3808
|
+
};
|
|
3809
|
+
timer = setTimeout(look, wait);
|
|
3783
3810
|
promise.then(
|
|
3784
3811
|
(value) => {
|
|
3785
3812
|
clearTimeout(timer);
|
|
@@ -3889,26 +3916,26 @@ function createDrainFrameGuard(args) {
|
|
|
3889
3916
|
return buf;
|
|
3890
3917
|
};
|
|
3891
3918
|
}
|
|
3892
|
-
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
|
|
3919
|
+
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, resume, risk) {
|
|
3893
3920
|
let prev = null;
|
|
3894
3921
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
3895
3922
|
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
|
3896
3923
|
const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
|
|
3897
3924
|
const captureRisk = async (idx) => {
|
|
3898
3925
|
await risk.open();
|
|
3899
|
-
|
|
3926
|
+
quiet.reset();
|
|
3900
3927
|
return raceAgainstStall(
|
|
3901
3928
|
risk.capture(idx, frameTime(idx)),
|
|
3902
|
-
stallTimeoutMs -
|
|
3929
|
+
stallTimeoutMs - quiet.idle(0),
|
|
3903
3930
|
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3904
3931
|
abortSignal
|
|
3905
3932
|
);
|
|
3906
3933
|
};
|
|
3907
3934
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
3908
|
-
|
|
3935
|
+
const quiet = createStallClock();
|
|
3909
3936
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
3910
3937
|
promise,
|
|
3911
|
-
stallTimeoutMs -
|
|
3938
|
+
stallTimeoutMs - quiet.idle(0),
|
|
3912
3939
|
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3913
3940
|
abortSignal
|
|
3914
3941
|
);
|
|
@@ -3920,7 +3947,8 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3920
3947
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
|
|
3921
3948
|
reorderBuffer.advanceTo(prev.idx + 1);
|
|
3922
3949
|
job.framesRendered = prev.idx + 1;
|
|
3923
|
-
|
|
3950
|
+
resume.written = prev.idx + 1;
|
|
3951
|
+
quiet.reset();
|
|
3924
3952
|
reportFrameProgress(
|
|
3925
3953
|
job,
|
|
3926
3954
|
`Streaming frame ${prev.idx + 1}/${totalFrames}`,
|
|
@@ -3940,7 +3968,8 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3940
3968
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
|
|
3941
3969
|
reorderBuffer.advanceTo(item.idx + 1);
|
|
3942
3970
|
job.framesRendered = item.idx + 1;
|
|
3943
|
-
|
|
3971
|
+
resume.written = item.idx + 1;
|
|
3972
|
+
quiet.reset();
|
|
3944
3973
|
reportFrameProgress(
|
|
3945
3974
|
job,
|
|
3946
3975
|
`Streaming frame ${item.idx + 1}/${totalFrames}`,
|
|
@@ -3955,7 +3984,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3955
3984
|
const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
|
|
3956
3985
|
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
|
|
3957
3986
|
let prevBatch = [];
|
|
3958
|
-
let i =
|
|
3987
|
+
let i = resume.from;
|
|
3959
3988
|
while (i < totalFrames) {
|
|
3960
3989
|
assertNotAborted();
|
|
3961
3990
|
if (batchable(i)) {
|
|
@@ -3988,7 +4017,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3988
4017
|
await drainBatch(prevBatch);
|
|
3989
4018
|
return;
|
|
3990
4019
|
}
|
|
3991
|
-
for (let i =
|
|
4020
|
+
for (let i = resume.from; i < totalFrames; i++) {
|
|
3992
4021
|
assertNotAborted();
|
|
3993
4022
|
if (risk?.frames.has(i)) {
|
|
3994
4023
|
const buffer = await captureRisk(i);
|
|
@@ -4006,6 +4035,10 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
4006
4035
|
}
|
|
4007
4036
|
await drainPrev();
|
|
4008
4037
|
}
|
|
4038
|
+
var STALL_RESUMES_BEFORE_FALLBACK = 3;
|
|
4039
|
+
function resumesAfterStall(error, abortSignal, resume, resumes) {
|
|
4040
|
+
return error instanceof SequentialCaptureStallError && abortSignal?.aborted !== true && resume.written > resume.from && resumes < STALL_RESUMES_BEFORE_FALLBACK;
|
|
4041
|
+
}
|
|
4009
4042
|
async function runCaptureStreamingStage(input) {
|
|
4010
4043
|
const {
|
|
4011
4044
|
fileServer,
|
|
@@ -4079,7 +4112,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4079
4112
|
let parallelGuardRan = false;
|
|
4080
4113
|
let writtenFrames = 0;
|
|
4081
4114
|
let resumeFrom = 0;
|
|
4082
|
-
let
|
|
4115
|
+
let resumes = 0;
|
|
4083
4116
|
let workerResults;
|
|
4084
4117
|
for (; ; ) {
|
|
4085
4118
|
const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
|
|
@@ -4114,7 +4147,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4114
4147
|
}
|
|
4115
4148
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4116
4149
|
let lastCapturedFrames = 0;
|
|
4117
|
-
|
|
4150
|
+
const quiet = createStallClock();
|
|
4118
4151
|
const workerPhases = /* @__PURE__ */ new Map();
|
|
4119
4152
|
const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
|
|
4120
4153
|
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
@@ -4125,23 +4158,21 @@ async function runCaptureStreamingStage(input) {
|
|
|
4125
4158
|
captureLabel === "BeginFrame",
|
|
4126
4159
|
cfg
|
|
4127
4160
|
);
|
|
4128
|
-
const
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
Math.min(budget(), DE_STALL_POLL_MS)
|
|
4144
|
-
);
|
|
4161
|
+
const pollMs = Math.min(budget(), DE_STALL_POLL_MS);
|
|
4162
|
+
const stallTimer = setInterval(() => {
|
|
4163
|
+
if (!watchdogArmed || quiet.idle(pollMs) <= budget()) return;
|
|
4164
|
+
stalled = true;
|
|
4165
|
+
const stallErr = new ParallelCaptureStallError({
|
|
4166
|
+
variant: "drain",
|
|
4167
|
+
captureLabel,
|
|
4168
|
+
stallTimeoutMs: budget(),
|
|
4169
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4170
|
+
totalFrames,
|
|
4171
|
+
phaseSummary: phaseSummary()
|
|
4172
|
+
});
|
|
4173
|
+
parallelReorder.abort(stallErr);
|
|
4174
|
+
stallController.abort();
|
|
4175
|
+
}, pollMs);
|
|
4145
4176
|
try {
|
|
4146
4177
|
workerResults = await executeParallelCapture(
|
|
4147
4178
|
fileServer.url,
|
|
@@ -4156,7 +4187,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4156
4187
|
workerPhases.set(phase.workerId, phaseDetail(phase));
|
|
4157
4188
|
if (phase.phase === "frame_capture" && !watchdogArmed) {
|
|
4158
4189
|
watchdogArmed = true;
|
|
4159
|
-
|
|
4190
|
+
quiet.reset();
|
|
4160
4191
|
}
|
|
4161
4192
|
log.info("[Render] Parallel capture worker phase", {
|
|
4162
4193
|
workerId: phase.workerId,
|
|
@@ -4172,7 +4203,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4172
4203
|
}
|
|
4173
4204
|
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4174
4205
|
lastCapturedFrames = progress.capturedFrames;
|
|
4175
|
-
|
|
4206
|
+
quiet.reset();
|
|
4176
4207
|
watchdogArmed = true;
|
|
4177
4208
|
}
|
|
4178
4209
|
const doneFrames = resumeFrom + progress.capturedFrames;
|
|
@@ -4203,17 +4234,14 @@ async function runCaptureStreamingStage(input) {
|
|
|
4203
4234
|
}
|
|
4204
4235
|
);
|
|
4205
4236
|
} catch (err) {
|
|
4206
|
-
if (stalled && abortSignal?.aborted !== true &&
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
totalFrames,
|
|
4213
|
-
workerPhases: phaseSummary()
|
|
4214
|
-
}
|
|
4215
|
-
);
|
|
4237
|
+
if (stalled && abortSignal?.aborted !== true && writtenFrames > resumeFrom && resumes < STALL_RESUMES_BEFORE_FALLBACK) {
|
|
4238
|
+
log.warn("[Render] Parallel capture stalled; resuming from the first unwritten frame", {
|
|
4239
|
+
resumeFrom: writtenFrames,
|
|
4240
|
+
totalFrames,
|
|
4241
|
+
workerPhases: phaseSummary()
|
|
4242
|
+
});
|
|
4216
4243
|
resumeFrom = writtenFrames;
|
|
4244
|
+
resumes++;
|
|
4217
4245
|
continue;
|
|
4218
4246
|
}
|
|
4219
4247
|
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
@@ -4249,116 +4277,133 @@ async function runCaptureStreamingStage(input) {
|
|
|
4249
4277
|
}
|
|
4250
4278
|
lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
|
|
4251
4279
|
} else {
|
|
4252
|
-
const
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
videoInjector,
|
|
4258
|
-
captureCfg
|
|
4259
|
-
);
|
|
4260
|
-
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
4261
|
-
if (probeSession) {
|
|
4262
|
-
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
4263
|
-
probeSession = null;
|
|
4264
|
-
}
|
|
4265
|
-
let riskSession = null;
|
|
4266
|
-
const openRiskSession = async () => {
|
|
4267
|
-
const opened = await createCaptureSession(
|
|
4280
|
+
const resume = { from: 0, written: 0 };
|
|
4281
|
+
let resumes = 0;
|
|
4282
|
+
for (; ; ) {
|
|
4283
|
+
const videoInjector = createRenderVideoFrameInjector();
|
|
4284
|
+
const session = probeSession ?? await createCaptureSession(
|
|
4268
4285
|
fileServer.url,
|
|
4269
4286
|
framesDir,
|
|
4270
4287
|
buildCaptureOptions(),
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
...captureCfg,
|
|
4274
|
-
useDrawElement: false,
|
|
4275
|
-
staticFrameDedup: false,
|
|
4276
|
-
enableBrowserPool: false
|
|
4277
|
-
}
|
|
4288
|
+
videoInjector,
|
|
4289
|
+
captureCfg
|
|
4278
4290
|
);
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
try {
|
|
4284
|
-
if (!session.isInitialized) {
|
|
4285
|
-
await initializeSession(session);
|
|
4291
|
+
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
4292
|
+
if (probeSession) {
|
|
4293
|
+
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
4294
|
+
probeSession = null;
|
|
4286
4295
|
}
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
totalFrames,
|
|
4301
|
-
job,
|
|
4302
|
-
currentEncoder,
|
|
4303
|
-
reorderBuffer,
|
|
4304
|
-
assertNotAborted,
|
|
4305
|
-
onProgress,
|
|
4306
|
-
log,
|
|
4307
|
-
deDrainStats,
|
|
4308
|
-
abortSignal,
|
|
4309
|
-
riskFrames && riskFrames.size > 0 ? {
|
|
4310
|
-
frames: riskFrames,
|
|
4311
|
-
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4312
|
-
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4313
|
-
open: async () => {
|
|
4314
|
-
riskSession ??= await openRiskSession();
|
|
4315
|
-
},
|
|
4316
|
-
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4317
|
-
} : void 0
|
|
4296
|
+
let riskSession = null;
|
|
4297
|
+
const openRiskSession = async () => {
|
|
4298
|
+
const opened = await createCaptureSession(
|
|
4299
|
+
fileServer.url,
|
|
4300
|
+
framesDir,
|
|
4301
|
+
buildCaptureOptions(),
|
|
4302
|
+
createRenderVideoFrameInjector(),
|
|
4303
|
+
{
|
|
4304
|
+
...captureCfg,
|
|
4305
|
+
useDrawElement: false,
|
|
4306
|
+
staticFrameDedup: false,
|
|
4307
|
+
enableBrowserPool: false
|
|
4308
|
+
}
|
|
4318
4309
|
);
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
const frameProgress = (i + 1) / totalFrames;
|
|
4343
|
-
const progress = 25 + frameProgress * 55;
|
|
4344
|
-
reportFrameProgress(
|
|
4310
|
+
riskSession = opened;
|
|
4311
|
+
await initializeSession(opened);
|
|
4312
|
+
return opened;
|
|
4313
|
+
};
|
|
4314
|
+
let resumed = false;
|
|
4315
|
+
try {
|
|
4316
|
+
if (!session.isInitialized) {
|
|
4317
|
+
await initializeSession(session);
|
|
4318
|
+
}
|
|
4319
|
+
await completeDeferredDrawElementInit(session);
|
|
4320
|
+
assertNotAborted();
|
|
4321
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4322
|
+
if (session.workerEncodeEnabled) {
|
|
4323
|
+
deDrainStats ??= {
|
|
4324
|
+
verifyChecked: 0,
|
|
4325
|
+
blankSuspects: 0,
|
|
4326
|
+
blankDeterministicAccepts: 0,
|
|
4327
|
+
blankRecaptures: 0
|
|
4328
|
+
};
|
|
4329
|
+
const riskFrames = session.deRiskFrames;
|
|
4330
|
+
await runWorkerEncodePipelineLoop(
|
|
4331
|
+
session,
|
|
4332
|
+
totalFrames,
|
|
4345
4333
|
job,
|
|
4346
|
-
|
|
4347
|
-
|
|
4334
|
+
currentEncoder,
|
|
4335
|
+
reorderBuffer,
|
|
4336
|
+
assertNotAborted,
|
|
4348
4337
|
onProgress,
|
|
4349
|
-
|
|
4350
|
-
|
|
4338
|
+
log,
|
|
4339
|
+
deDrainStats,
|
|
4340
|
+
abortSignal,
|
|
4341
|
+
resume,
|
|
4342
|
+
riskFrames && riskFrames.size > 0 ? {
|
|
4343
|
+
frames: riskFrames,
|
|
4344
|
+
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4345
|
+
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4346
|
+
open: async () => {
|
|
4347
|
+
riskSession ??= await openRiskSession();
|
|
4348
|
+
},
|
|
4349
|
+
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4350
|
+
} : void 0
|
|
4351
4351
|
);
|
|
4352
|
+
} else {
|
|
4353
|
+
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4354
|
+
let lastProgressAt = Date.now();
|
|
4355
|
+
for (let i = resume.from; i < totalFrames; i++) {
|
|
4356
|
+
assertNotAborted();
|
|
4357
|
+
const time = i * job.config.fps.den / job.config.fps.num;
|
|
4358
|
+
const budget = stallBudgetMs(
|
|
4359
|
+
stallTimeoutMs,
|
|
4360
|
+
i,
|
|
4361
|
+
session.captureMode === "beginframe",
|
|
4362
|
+
cfg
|
|
4363
|
+
);
|
|
4364
|
+
const { buffer } = await raceAgainstStall(
|
|
4365
|
+
captureFrameToBuffer(session, i, time),
|
|
4366
|
+
budget - (Date.now() - lastProgressAt),
|
|
4367
|
+
{ session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
|
|
4368
|
+
abortSignal
|
|
4369
|
+
);
|
|
4370
|
+
await reorderBuffer.waitForFrame(i);
|
|
4371
|
+
ensureFrameWritten(await currentEncoder.writeFrame(buffer), i, currentEncoder);
|
|
4372
|
+
reorderBuffer.advanceTo(i + 1);
|
|
4373
|
+
job.framesRendered = i + 1;
|
|
4374
|
+
resume.written = i + 1;
|
|
4375
|
+
lastProgressAt = Date.now();
|
|
4376
|
+
const frameProgress = (i + 1) / totalFrames;
|
|
4377
|
+
const progress = 25 + frameProgress * 55;
|
|
4378
|
+
reportFrameProgress(
|
|
4379
|
+
job,
|
|
4380
|
+
`Streaming frame ${i + 1}/${totalFrames}`,
|
|
4381
|
+
Math.round(progress),
|
|
4382
|
+
onProgress,
|
|
4383
|
+
i + 1,
|
|
4384
|
+
totalFrames
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
dedupPerfs.push(getCapturePerfSummary(session));
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4391
|
+
if (!resumesAfterStall(error, abortSignal, resume, resumes)) {
|
|
4392
|
+
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
4352
4393
|
}
|
|
4394
|
+
log.warn("[Render] Sequential capture stalled; resuming from the first unwritten frame", {
|
|
4395
|
+
resumeFrom: resume.written,
|
|
4396
|
+
totalFrames
|
|
4397
|
+
});
|
|
4398
|
+
resume.from = resume.written;
|
|
4399
|
+
resumes++;
|
|
4400
|
+
resumed = true;
|
|
4401
|
+
} finally {
|
|
4402
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4403
|
+
await closeCaptureSession(session);
|
|
4404
|
+
if (riskSession) await closeCaptureSession(riskSession);
|
|
4353
4405
|
}
|
|
4354
|
-
|
|
4355
|
-
} catch (error) {
|
|
4356
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4357
|
-
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
4358
|
-
} finally {
|
|
4359
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4360
|
-
await closeCaptureSession(session);
|
|
4361
|
-
if (riskSession) await closeCaptureSession(riskSession);
|
|
4406
|
+
if (!resumed) break;
|
|
4362
4407
|
}
|
|
4363
4408
|
}
|
|
4364
4409
|
const encodeFrom = job.progress;
|
|
@@ -5569,35 +5614,33 @@ function watchStall(phase, rule) {
|
|
|
5569
5614
|
const detail = rule.detail ?? (() => void 0);
|
|
5570
5615
|
let last = rule.progress();
|
|
5571
5616
|
let budgetMs = rule.firstProgressMs;
|
|
5572
|
-
|
|
5617
|
+
const silence = createStallClock();
|
|
5618
|
+
const pollMs = Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs);
|
|
5573
5619
|
let attempt;
|
|
5574
5620
|
let timer;
|
|
5575
5621
|
let inTail = false;
|
|
5576
5622
|
const restartWindow = (ms) => {
|
|
5577
5623
|
last = rule.progress();
|
|
5578
5624
|
budgetMs = ms;
|
|
5579
|
-
|
|
5625
|
+
silence.reset();
|
|
5580
5626
|
};
|
|
5581
5627
|
const tripped = new Promise((_, reject) => {
|
|
5582
|
-
timer = setInterval(
|
|
5583
|
-
()
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
},
|
|
5599
|
-
Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs)
|
|
5600
|
-
);
|
|
5628
|
+
timer = setInterval(() => {
|
|
5629
|
+
const current = rule.progress();
|
|
5630
|
+
if (rule.complete?.() && !inTail) {
|
|
5631
|
+
inTail = true;
|
|
5632
|
+
return restartWindow(rule.tailMs ?? rule.noProgressMs);
|
|
5633
|
+
}
|
|
5634
|
+
if (current !== last) return restartWindow(rule.noProgressMs);
|
|
5635
|
+
if (silence.idle(pollMs) < budgetMs) return;
|
|
5636
|
+
const error = new RenderStageStalledError(phase, budgetMs, current, detail());
|
|
5637
|
+
if (attempt && !attempt.signal.aborted) {
|
|
5638
|
+
attempt.abort(error);
|
|
5639
|
+
return restartWindow(rule.noProgressMs);
|
|
5640
|
+
}
|
|
5641
|
+
clearInterval(timer);
|
|
5642
|
+
reject(error);
|
|
5643
|
+
}, pollMs);
|
|
5601
5644
|
});
|
|
5602
5645
|
const openAttempt = (parent) => {
|
|
5603
5646
|
attempt = new AbortController();
|
package/dist/public-server.js
CHANGED
|
@@ -3734,6 +3734,27 @@ import {
|
|
|
3734
3734
|
prepareCaptureSessionForReuse,
|
|
3735
3735
|
spawnStreamingEncoder
|
|
3736
3736
|
} from "@hyperframes/engine";
|
|
3737
|
+
|
|
3738
|
+
// src/services/render/stallClock.ts
|
|
3739
|
+
var SUSPEND_GAP_MS = 15e3;
|
|
3740
|
+
function createStallClock(now = () => Date.now()) {
|
|
3741
|
+
let since = now();
|
|
3742
|
+
let seen = since;
|
|
3743
|
+
return {
|
|
3744
|
+
reset() {
|
|
3745
|
+
since = seen = now();
|
|
3746
|
+
},
|
|
3747
|
+
idle(expectedMs) {
|
|
3748
|
+
const at = now();
|
|
3749
|
+
const gap = at - seen;
|
|
3750
|
+
seen = at;
|
|
3751
|
+
if (gap > expectedMs + SUSPEND_GAP_MS) since += gap;
|
|
3752
|
+
return at - since;
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
// src/services/render/stages/captureStreamingStage.ts
|
|
3737
3758
|
var DEFAULT_CAPTURE_STALL_MS = 6e4;
|
|
3738
3759
|
var DE_STALL_POLL_MS = 5e3;
|
|
3739
3760
|
var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
|
|
@@ -3778,21 +3799,27 @@ var SequentialCaptureStallError = class extends Error {
|
|
|
3778
3799
|
};
|
|
3779
3800
|
function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
3780
3801
|
return new Promise((resolve11, reject) => {
|
|
3781
|
-
const
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
)
|
|
3792
|
-
);
|
|
3793
|
-
},
|
|
3794
|
-
Math.max(0, deadlineMs)
|
|
3802
|
+
const clock = createStallClock();
|
|
3803
|
+
const trip = () => reject(
|
|
3804
|
+
signal?.aborted ? new Error(
|
|
3805
|
+
`[Render] Sequential ${captureModeLabel(input.session.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
|
|
3806
|
+
) : new SequentialCaptureStallError(
|
|
3807
|
+
input.session,
|
|
3808
|
+
input.stallTimeoutMs,
|
|
3809
|
+
input.frameIndex,
|
|
3810
|
+
input.totalFrames
|
|
3811
|
+
)
|
|
3795
3812
|
);
|
|
3813
|
+
const deadline = Math.max(0, deadlineMs);
|
|
3814
|
+
let wait = Math.min(deadline, DE_STALL_POLL_MS);
|
|
3815
|
+
let timer;
|
|
3816
|
+
const look = () => {
|
|
3817
|
+
const idle = clock.idle(wait);
|
|
3818
|
+
if (idle >= deadline) return trip();
|
|
3819
|
+
wait = Math.min(deadline - idle, DE_STALL_POLL_MS);
|
|
3820
|
+
timer = setTimeout(look, wait);
|
|
3821
|
+
};
|
|
3822
|
+
timer = setTimeout(look, wait);
|
|
3796
3823
|
promise.then(
|
|
3797
3824
|
(value) => {
|
|
3798
3825
|
clearTimeout(timer);
|
|
@@ -3902,26 +3929,26 @@ function createDrainFrameGuard(args) {
|
|
|
3902
3929
|
return buf;
|
|
3903
3930
|
};
|
|
3904
3931
|
}
|
|
3905
|
-
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
|
|
3932
|
+
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, resume, risk) {
|
|
3906
3933
|
let prev = null;
|
|
3907
3934
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
3908
3935
|
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
|
3909
3936
|
const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
|
|
3910
3937
|
const captureRisk = async (idx) => {
|
|
3911
3938
|
await risk.open();
|
|
3912
|
-
|
|
3939
|
+
quiet.reset();
|
|
3913
3940
|
return raceAgainstStall(
|
|
3914
3941
|
risk.capture(idx, frameTime(idx)),
|
|
3915
|
-
stallTimeoutMs -
|
|
3942
|
+
stallTimeoutMs - quiet.idle(0),
|
|
3916
3943
|
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3917
3944
|
abortSignal
|
|
3918
3945
|
);
|
|
3919
3946
|
};
|
|
3920
3947
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
3921
|
-
|
|
3948
|
+
const quiet = createStallClock();
|
|
3922
3949
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
3923
3950
|
promise,
|
|
3924
|
-
stallTimeoutMs -
|
|
3951
|
+
stallTimeoutMs - quiet.idle(0),
|
|
3925
3952
|
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3926
3953
|
abortSignal
|
|
3927
3954
|
);
|
|
@@ -3933,7 +3960,8 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3933
3960
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
|
|
3934
3961
|
reorderBuffer.advanceTo(prev.idx + 1);
|
|
3935
3962
|
job.framesRendered = prev.idx + 1;
|
|
3936
|
-
|
|
3963
|
+
resume.written = prev.idx + 1;
|
|
3964
|
+
quiet.reset();
|
|
3937
3965
|
reportFrameProgress(
|
|
3938
3966
|
job,
|
|
3939
3967
|
`Streaming frame ${prev.idx + 1}/${totalFrames}`,
|
|
@@ -3953,7 +3981,8 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3953
3981
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
|
|
3954
3982
|
reorderBuffer.advanceTo(item.idx + 1);
|
|
3955
3983
|
job.framesRendered = item.idx + 1;
|
|
3956
|
-
|
|
3984
|
+
resume.written = item.idx + 1;
|
|
3985
|
+
quiet.reset();
|
|
3957
3986
|
reportFrameProgress(
|
|
3958
3987
|
job,
|
|
3959
3988
|
`Streaming frame ${item.idx + 1}/${totalFrames}`,
|
|
@@ -3968,7 +3997,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3968
3997
|
const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
|
|
3969
3998
|
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
|
|
3970
3999
|
let prevBatch = [];
|
|
3971
|
-
let i =
|
|
4000
|
+
let i = resume.from;
|
|
3972
4001
|
while (i < totalFrames) {
|
|
3973
4002
|
assertNotAborted();
|
|
3974
4003
|
if (batchable(i)) {
|
|
@@ -4001,7 +4030,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
4001
4030
|
await drainBatch(prevBatch);
|
|
4002
4031
|
return;
|
|
4003
4032
|
}
|
|
4004
|
-
for (let i =
|
|
4033
|
+
for (let i = resume.from; i < totalFrames; i++) {
|
|
4005
4034
|
assertNotAborted();
|
|
4006
4035
|
if (risk?.frames.has(i)) {
|
|
4007
4036
|
const buffer = await captureRisk(i);
|
|
@@ -4019,6 +4048,10 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
4019
4048
|
}
|
|
4020
4049
|
await drainPrev();
|
|
4021
4050
|
}
|
|
4051
|
+
var STALL_RESUMES_BEFORE_FALLBACK = 3;
|
|
4052
|
+
function resumesAfterStall(error, abortSignal, resume, resumes) {
|
|
4053
|
+
return error instanceof SequentialCaptureStallError && abortSignal?.aborted !== true && resume.written > resume.from && resumes < STALL_RESUMES_BEFORE_FALLBACK;
|
|
4054
|
+
}
|
|
4022
4055
|
async function runCaptureStreamingStage(input) {
|
|
4023
4056
|
const {
|
|
4024
4057
|
fileServer,
|
|
@@ -4092,7 +4125,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4092
4125
|
let parallelGuardRan = false;
|
|
4093
4126
|
let writtenFrames = 0;
|
|
4094
4127
|
let resumeFrom = 0;
|
|
4095
|
-
let
|
|
4128
|
+
let resumes = 0;
|
|
4096
4129
|
let workerResults;
|
|
4097
4130
|
for (; ; ) {
|
|
4098
4131
|
const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
|
|
@@ -4127,7 +4160,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4127
4160
|
}
|
|
4128
4161
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4129
4162
|
let lastCapturedFrames = 0;
|
|
4130
|
-
|
|
4163
|
+
const quiet = createStallClock();
|
|
4131
4164
|
const workerPhases = /* @__PURE__ */ new Map();
|
|
4132
4165
|
const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
|
|
4133
4166
|
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
@@ -4138,23 +4171,21 @@ async function runCaptureStreamingStage(input) {
|
|
|
4138
4171
|
captureLabel === "BeginFrame",
|
|
4139
4172
|
cfg
|
|
4140
4173
|
);
|
|
4141
|
-
const
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
Math.min(budget(), DE_STALL_POLL_MS)
|
|
4157
|
-
);
|
|
4174
|
+
const pollMs = Math.min(budget(), DE_STALL_POLL_MS);
|
|
4175
|
+
const stallTimer = setInterval(() => {
|
|
4176
|
+
if (!watchdogArmed || quiet.idle(pollMs) <= budget()) return;
|
|
4177
|
+
stalled = true;
|
|
4178
|
+
const stallErr = new ParallelCaptureStallError({
|
|
4179
|
+
variant: "drain",
|
|
4180
|
+
captureLabel,
|
|
4181
|
+
stallTimeoutMs: budget(),
|
|
4182
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4183
|
+
totalFrames,
|
|
4184
|
+
phaseSummary: phaseSummary()
|
|
4185
|
+
});
|
|
4186
|
+
parallelReorder.abort(stallErr);
|
|
4187
|
+
stallController.abort();
|
|
4188
|
+
}, pollMs);
|
|
4158
4189
|
try {
|
|
4159
4190
|
workerResults = await executeParallelCapture(
|
|
4160
4191
|
fileServer.url,
|
|
@@ -4169,7 +4200,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4169
4200
|
workerPhases.set(phase.workerId, phaseDetail(phase));
|
|
4170
4201
|
if (phase.phase === "frame_capture" && !watchdogArmed) {
|
|
4171
4202
|
watchdogArmed = true;
|
|
4172
|
-
|
|
4203
|
+
quiet.reset();
|
|
4173
4204
|
}
|
|
4174
4205
|
log.info("[Render] Parallel capture worker phase", {
|
|
4175
4206
|
workerId: phase.workerId,
|
|
@@ -4185,7 +4216,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4185
4216
|
}
|
|
4186
4217
|
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4187
4218
|
lastCapturedFrames = progress.capturedFrames;
|
|
4188
|
-
|
|
4219
|
+
quiet.reset();
|
|
4189
4220
|
watchdogArmed = true;
|
|
4190
4221
|
}
|
|
4191
4222
|
const doneFrames = resumeFrom + progress.capturedFrames;
|
|
@@ -4216,17 +4247,14 @@ async function runCaptureStreamingStage(input) {
|
|
|
4216
4247
|
}
|
|
4217
4248
|
);
|
|
4218
4249
|
} catch (err) {
|
|
4219
|
-
if (stalled && abortSignal?.aborted !== true &&
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
totalFrames,
|
|
4226
|
-
workerPhases: phaseSummary()
|
|
4227
|
-
}
|
|
4228
|
-
);
|
|
4250
|
+
if (stalled && abortSignal?.aborted !== true && writtenFrames > resumeFrom && resumes < STALL_RESUMES_BEFORE_FALLBACK) {
|
|
4251
|
+
log.warn("[Render] Parallel capture stalled; resuming from the first unwritten frame", {
|
|
4252
|
+
resumeFrom: writtenFrames,
|
|
4253
|
+
totalFrames,
|
|
4254
|
+
workerPhases: phaseSummary()
|
|
4255
|
+
});
|
|
4229
4256
|
resumeFrom = writtenFrames;
|
|
4257
|
+
resumes++;
|
|
4230
4258
|
continue;
|
|
4231
4259
|
}
|
|
4232
4260
|
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
@@ -4262,116 +4290,133 @@ async function runCaptureStreamingStage(input) {
|
|
|
4262
4290
|
}
|
|
4263
4291
|
lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
|
|
4264
4292
|
} else {
|
|
4265
|
-
const
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
videoInjector,
|
|
4271
|
-
captureCfg
|
|
4272
|
-
);
|
|
4273
|
-
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
4274
|
-
if (probeSession) {
|
|
4275
|
-
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
4276
|
-
probeSession = null;
|
|
4277
|
-
}
|
|
4278
|
-
let riskSession = null;
|
|
4279
|
-
const openRiskSession = async () => {
|
|
4280
|
-
const opened = await createCaptureSession(
|
|
4293
|
+
const resume = { from: 0, written: 0 };
|
|
4294
|
+
let resumes = 0;
|
|
4295
|
+
for (; ; ) {
|
|
4296
|
+
const videoInjector = createRenderVideoFrameInjector();
|
|
4297
|
+
const session = probeSession ?? await createCaptureSession(
|
|
4281
4298
|
fileServer.url,
|
|
4282
4299
|
framesDir,
|
|
4283
4300
|
buildCaptureOptions(),
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
...captureCfg,
|
|
4287
|
-
useDrawElement: false,
|
|
4288
|
-
staticFrameDedup: false,
|
|
4289
|
-
enableBrowserPool: false
|
|
4290
|
-
}
|
|
4301
|
+
videoInjector,
|
|
4302
|
+
captureCfg
|
|
4291
4303
|
);
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
try {
|
|
4297
|
-
if (!session.isInitialized) {
|
|
4298
|
-
await initializeSession(session);
|
|
4304
|
+
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
4305
|
+
if (probeSession) {
|
|
4306
|
+
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
4307
|
+
probeSession = null;
|
|
4299
4308
|
}
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
totalFrames,
|
|
4314
|
-
job,
|
|
4315
|
-
currentEncoder,
|
|
4316
|
-
reorderBuffer,
|
|
4317
|
-
assertNotAborted,
|
|
4318
|
-
onProgress,
|
|
4319
|
-
log,
|
|
4320
|
-
deDrainStats,
|
|
4321
|
-
abortSignal,
|
|
4322
|
-
riskFrames && riskFrames.size > 0 ? {
|
|
4323
|
-
frames: riskFrames,
|
|
4324
|
-
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4325
|
-
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4326
|
-
open: async () => {
|
|
4327
|
-
riskSession ??= await openRiskSession();
|
|
4328
|
-
},
|
|
4329
|
-
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4330
|
-
} : void 0
|
|
4309
|
+
let riskSession = null;
|
|
4310
|
+
const openRiskSession = async () => {
|
|
4311
|
+
const opened = await createCaptureSession(
|
|
4312
|
+
fileServer.url,
|
|
4313
|
+
framesDir,
|
|
4314
|
+
buildCaptureOptions(),
|
|
4315
|
+
createRenderVideoFrameInjector(),
|
|
4316
|
+
{
|
|
4317
|
+
...captureCfg,
|
|
4318
|
+
useDrawElement: false,
|
|
4319
|
+
staticFrameDedup: false,
|
|
4320
|
+
enableBrowserPool: false
|
|
4321
|
+
}
|
|
4331
4322
|
);
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
const frameProgress = (i + 1) / totalFrames;
|
|
4356
|
-
const progress = 25 + frameProgress * 55;
|
|
4357
|
-
reportFrameProgress(
|
|
4323
|
+
riskSession = opened;
|
|
4324
|
+
await initializeSession(opened);
|
|
4325
|
+
return opened;
|
|
4326
|
+
};
|
|
4327
|
+
let resumed = false;
|
|
4328
|
+
try {
|
|
4329
|
+
if (!session.isInitialized) {
|
|
4330
|
+
await initializeSession(session);
|
|
4331
|
+
}
|
|
4332
|
+
await completeDeferredDrawElementInit(session);
|
|
4333
|
+
assertNotAborted();
|
|
4334
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4335
|
+
if (session.workerEncodeEnabled) {
|
|
4336
|
+
deDrainStats ??= {
|
|
4337
|
+
verifyChecked: 0,
|
|
4338
|
+
blankSuspects: 0,
|
|
4339
|
+
blankDeterministicAccepts: 0,
|
|
4340
|
+
blankRecaptures: 0
|
|
4341
|
+
};
|
|
4342
|
+
const riskFrames = session.deRiskFrames;
|
|
4343
|
+
await runWorkerEncodePipelineLoop(
|
|
4344
|
+
session,
|
|
4345
|
+
totalFrames,
|
|
4358
4346
|
job,
|
|
4359
|
-
|
|
4360
|
-
|
|
4347
|
+
currentEncoder,
|
|
4348
|
+
reorderBuffer,
|
|
4349
|
+
assertNotAborted,
|
|
4361
4350
|
onProgress,
|
|
4362
|
-
|
|
4363
|
-
|
|
4351
|
+
log,
|
|
4352
|
+
deDrainStats,
|
|
4353
|
+
abortSignal,
|
|
4354
|
+
resume,
|
|
4355
|
+
riskFrames && riskFrames.size > 0 ? {
|
|
4356
|
+
frames: riskFrames,
|
|
4357
|
+
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4358
|
+
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4359
|
+
open: async () => {
|
|
4360
|
+
riskSession ??= await openRiskSession();
|
|
4361
|
+
},
|
|
4362
|
+
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4363
|
+
} : void 0
|
|
4364
4364
|
);
|
|
4365
|
+
} else {
|
|
4366
|
+
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4367
|
+
let lastProgressAt = Date.now();
|
|
4368
|
+
for (let i = resume.from; i < totalFrames; i++) {
|
|
4369
|
+
assertNotAborted();
|
|
4370
|
+
const time = i * job.config.fps.den / job.config.fps.num;
|
|
4371
|
+
const budget = stallBudgetMs(
|
|
4372
|
+
stallTimeoutMs,
|
|
4373
|
+
i,
|
|
4374
|
+
session.captureMode === "beginframe",
|
|
4375
|
+
cfg
|
|
4376
|
+
);
|
|
4377
|
+
const { buffer } = await raceAgainstStall(
|
|
4378
|
+
captureFrameToBuffer(session, i, time),
|
|
4379
|
+
budget - (Date.now() - lastProgressAt),
|
|
4380
|
+
{ session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
|
|
4381
|
+
abortSignal
|
|
4382
|
+
);
|
|
4383
|
+
await reorderBuffer.waitForFrame(i);
|
|
4384
|
+
ensureFrameWritten(await currentEncoder.writeFrame(buffer), i, currentEncoder);
|
|
4385
|
+
reorderBuffer.advanceTo(i + 1);
|
|
4386
|
+
job.framesRendered = i + 1;
|
|
4387
|
+
resume.written = i + 1;
|
|
4388
|
+
lastProgressAt = Date.now();
|
|
4389
|
+
const frameProgress = (i + 1) / totalFrames;
|
|
4390
|
+
const progress = 25 + frameProgress * 55;
|
|
4391
|
+
reportFrameProgress(
|
|
4392
|
+
job,
|
|
4393
|
+
`Streaming frame ${i + 1}/${totalFrames}`,
|
|
4394
|
+
Math.round(progress),
|
|
4395
|
+
onProgress,
|
|
4396
|
+
i + 1,
|
|
4397
|
+
totalFrames
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
dedupPerfs.push(getCapturePerfSummary(session));
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4404
|
+
if (!resumesAfterStall(error, abortSignal, resume, resumes)) {
|
|
4405
|
+
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
4365
4406
|
}
|
|
4407
|
+
log.warn("[Render] Sequential capture stalled; resuming from the first unwritten frame", {
|
|
4408
|
+
resumeFrom: resume.written,
|
|
4409
|
+
totalFrames
|
|
4410
|
+
});
|
|
4411
|
+
resume.from = resume.written;
|
|
4412
|
+
resumes++;
|
|
4413
|
+
resumed = true;
|
|
4414
|
+
} finally {
|
|
4415
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4416
|
+
await closeCaptureSession(session);
|
|
4417
|
+
if (riskSession) await closeCaptureSession(riskSession);
|
|
4366
4418
|
}
|
|
4367
|
-
|
|
4368
|
-
} catch (error) {
|
|
4369
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4370
|
-
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
4371
|
-
} finally {
|
|
4372
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4373
|
-
await closeCaptureSession(session);
|
|
4374
|
-
if (riskSession) await closeCaptureSession(riskSession);
|
|
4419
|
+
if (!resumed) break;
|
|
4375
4420
|
}
|
|
4376
4421
|
}
|
|
4377
4422
|
const encodeFrom = job.progress;
|
|
@@ -5582,35 +5627,33 @@ function watchStall(phase, rule) {
|
|
|
5582
5627
|
const detail = rule.detail ?? (() => void 0);
|
|
5583
5628
|
let last = rule.progress();
|
|
5584
5629
|
let budgetMs = rule.firstProgressMs;
|
|
5585
|
-
|
|
5630
|
+
const silence = createStallClock();
|
|
5631
|
+
const pollMs = Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs);
|
|
5586
5632
|
let attempt;
|
|
5587
5633
|
let timer;
|
|
5588
5634
|
let inTail = false;
|
|
5589
5635
|
const restartWindow = (ms) => {
|
|
5590
5636
|
last = rule.progress();
|
|
5591
5637
|
budgetMs = ms;
|
|
5592
|
-
|
|
5638
|
+
silence.reset();
|
|
5593
5639
|
};
|
|
5594
5640
|
const tripped = new Promise((_, reject) => {
|
|
5595
|
-
timer = setInterval(
|
|
5596
|
-
()
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
},
|
|
5612
|
-
Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs)
|
|
5613
|
-
);
|
|
5641
|
+
timer = setInterval(() => {
|
|
5642
|
+
const current = rule.progress();
|
|
5643
|
+
if (rule.complete?.() && !inTail) {
|
|
5644
|
+
inTail = true;
|
|
5645
|
+
return restartWindow(rule.tailMs ?? rule.noProgressMs);
|
|
5646
|
+
}
|
|
5647
|
+
if (current !== last) return restartWindow(rule.noProgressMs);
|
|
5648
|
+
if (silence.idle(pollMs) < budgetMs) return;
|
|
5649
|
+
const error = new RenderStageStalledError(phase, budgetMs, current, detail());
|
|
5650
|
+
if (attempt && !attempt.signal.aborted) {
|
|
5651
|
+
attempt.abort(error);
|
|
5652
|
+
return restartWindow(rule.noProgressMs);
|
|
5653
|
+
}
|
|
5654
|
+
clearInterval(timer);
|
|
5655
|
+
reject(error);
|
|
5656
|
+
}, pollMs);
|
|
5614
5657
|
});
|
|
5615
5658
|
const openAttempt = (parent) => {
|
|
5616
5659
|
attempt = new AbortController();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface StallClock {
|
|
2
|
+
/** Progress landed: the silence starts again. */
|
|
3
|
+
reset(): void;
|
|
4
|
+
/** Silent ms, not counting a suspended stretch; `expectedMs` is how long after the last look this one was due. */
|
|
5
|
+
idle(expectedMs: number): number;
|
|
6
|
+
}
|
|
7
|
+
export declare function createStallClock(now?: () => number): StallClock;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
4
4
|
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"postcss": "^8.4.0",
|
|
36
36
|
"puppeteer": "~25.11.0",
|
|
37
37
|
"puppeteer-core": "^25.11.0",
|
|
38
|
-
"@hyperframes/core": "0.9.
|
|
39
|
-
"@hyperframes/engine": "0.9.
|
|
40
|
-
"@hyperframes/lint": "0.9.
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/core": "0.9.11",
|
|
39
|
+
"@hyperframes/engine": "0.9.11",
|
|
40
|
+
"@hyperframes/lint": "0.9.11",
|
|
41
|
+
"@hyperframes/parsers": "0.9.11",
|
|
42
|
+
"@hyperframes/studio-server": "0.9.11"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^25.0.10",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"tsx": "^4.21.0",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^4.1.11",
|
|
51
|
-
"@hyperframes/aws-lambda": "0.9.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.9.11"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|