test(producer): add parallel capture regression test (#1088)

* test(producer): add parallel capture regression test

Add a regression fixture that forces workers: 2, ensuring the parallel
capture code path (browser-per-worker in BeginFrame mode) is exercised
in CI. All existing fixtures pin workers: 1, so this is the first test
that would catch a regression in the multi-worker pool isolation fix
from PR #1087.

The composition is 5s @ 30fps (150 frames), which exceeds both
MIN_FRAMES_PER_WORKER * 2 (60) and minParallelFrames (120), so the
parallel coordinator will always split work across workers.

Baseline output/output.mp4 must be generated inside Dockerfile.test
before the fixture can run in CI.

* test(producer): bump parallel capture test to 4 workers

Matches realistic auto-mode worker counts (4-6 on typical machines),
not just the minimum (2) that triggers the bug.

* test(producer): add golden baseline for parallel capture regression

Generated inside Dockerfile.test on amd64 Linux (Docker image
hyperframes-producer:test) to match the CI rendering environment.

* test(producer): address review feedback on parallel capture test

- Add fixture to shard-5 in regression.yml so CI actually runs it
- Reframe description: multi-worker path coverage (frame distribution,
  reorder buffer, per-worker browser lifecycle), not GPU-specific crash
  guard — SwiftShader CI can't reproduce the hardware compositor race
- Remove dead @keyframes count-up (content doesn't apply to div)
- Remove unused CSS animation reference on .counter
- Regenerate golden baseline with cleaned-up HTML

* fix(producer): replace rAF + CSS keyframes with GSAP in parallel-capture test

The composition used requestAnimationFrame for a frame counter and CSS
@keyframes for animations, which triggered screenshot capture mode
(non-deterministic across workers) and caused 29 PSNR failures in CI.
All animations now use the GSAP timeline, keeping the render in
deterministic BeginFrame mode. Baseline regenerated in Docker.
This commit is contained in:
Miguel Ángel
2026-05-26 23:10:49 -04:00
committed by GitHub
parent 2d0acb3494
commit 3cd6cd6a1c
5 changed files with 305 additions and 1 deletions
@@ -0,0 +1,13 @@
{
"name": "parallel-capture-regression",
"description": "Multi-worker parallel capture path coverage. Exercises frame distribution, reorder buffer, and per-worker browser lifecycle at workers > 1. Guards against regressions in executeParallelCapture and the streaming encoder's ordered-write path. Note: the GPU-specific BeginFrame compositor race (#1087) only reproduces on hardware-GPU hosts, not in SwiftShader CI.",
"tags": ["regression", "parallel", "render-compat"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 4
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b84069be35430d72eced09c9b1e6cfd2c3a750c1a42f4332b99dfa54c516ddf
size 1147085
@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
body {
margin: 0;
background: #0d1117;
width: 1920px;
height: 1080px;
font-family: system-ui, -apple-system, sans-serif;
color: #e6edf3;
overflow: hidden;
}
.stage {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 48px;
}
.title {
font-size: 48px;
font-weight: 700;
letter-spacing: 0.04em;
}
.track {
width: 960px;
height: 40px;
background: rgba(255, 255, 255, 0.08);
border-radius: 20px;
overflow: hidden;
position: relative;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0%;
background: linear-gradient(90deg, #58a6ff, #3fb950);
border-radius: 20px;
}
.counter {
font-size: 160px;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #58a6ff, #bc8cff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pulse-ring {
width: 280px;
height: 280px;
border: 4px solid rgba(88, 166, 255, 0.3);
border-radius: 50%;
opacity: 0.3;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1);
pointer-events: none;
}
.stripe-bg {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 80px,
rgba(88, 166, 255, 0.03) 80px,
rgba(88, 166, 255, 0.03) 160px
);
}
</style>
</head>
<body>
<div id="root"
data-composition-id="parallel-capture-regression"
data-width="1920"
data-height="1080"
data-start="0"
data-duration="5">
<div class="stage clip" data-start="0" data-duration="5">
<div class="stripe-bg" id="stripes"></div>
<div class="pulse-ring" id="pulse"></div>
<div class="title">Parallel Capture</div>
<div class="counter" id="counter">0</div>
<div class="track">
<div class="bar" id="bar"></div>
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
var tl = gsap.timeline({ paused: true });
var duration = 5;
tl.to("#bar", { width: "100%", duration: duration, ease: "none" }, 0);
tl.to("#pulse", {
opacity: 0.8,
scale: 1.15,
duration: 0.5,
ease: "sine.inOut",
yoyo: true,
repeat: Math.ceil(duration / 1) - 1,
repeatDelay: 0,
}, 0);
tl.to("#stripes", {
backgroundPosition: "226px 0",
duration: 2,
ease: "none",
repeat: Math.ceil(duration / 2) - 1,
}, 0);
var counterObj = { val: 0 };
tl.to(counterObj, {
val: 150,
duration: duration,
ease: "none",
onUpdate: function () {
document.getElementById("counter").textContent = Math.floor(counterObj.val);
},
}, 0);
window.__timelines["parallel-capture-regression"] = tl;
</script>
</body>
</html>