fix(studio): stop VideoFrameThumbnail error→cleanup infinite loop - #4037
Conversation
The error handler called cleanup(), which sets video.src = "" and calls load(). Clearing src itself fires another error event, re-entering the handler at event-loop speed — every pass allocating a React update via setFailed(true). The seeked path triggers the same loop: its cleanup provokes the synthetic error right after a successful frame extract. Detach the error listener inside cleanup() and ignore the synthetic empty-src error. Instrumentation on a real session measured ~475k src assignments/second and ~8.6 MB/s of update allocations before the fix. Fixes heygen-com#4036
63e4954 to
348c9fc
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Note: @miguel-heygen already APPROVED at this exact head (348c9fc6) — this review is confirmatory, not a second gate.
Strengths
VideoFrameThumbnail.tsx:31-41— the fix closes the loop by construction, not by luck.cleanup()(reached from unmount,src-change, and the direct error path — React always runs the prior effect's cleanup before a new one) removes theerrorlistener as its first statement, beforevideo.src=""/load()runs. No ref is used, so there's no stale-element-identity risk onsrcchange either.- The new
onErrorearly-return guard (!video.getAttribute("src")) is real defense-in-depth, not decoration. I mutation-tested three ways: removing onlyremoveEventListener(guard intact) → 4/4 still pass; removing only the guard (removeEventListenerintact) → 4/4 still pass; removing both → 2/4 go red withloadCalls5 and 4 instead of 2 (VideoFrameThumbnail.test.tsx:138,161). Each mechanism independently breaks the loop; only removing both reproduces the original bug. Confirms this is genuinely belt-and-braces, not one dead guard dressed up as two. - The OOM's two contributing factors (infinite trigger + unbounded per-iteration allocation) collapse to one fix here — blocking the first re-entry attempt bounds
setFailed(true)to at most once per real error, by construction, not probabilistically.
Notes (non-blocking)
- PR body's perf numbers (~475k src-assignments/sec, 24.6M update objects/983MB heap) are from the reporter's own production session — can't verify from this repo, not contradicted either.
- Unrelated to this PR: this repo's default shell
NODE_ENV=productionbreaks the wholeVideoFrameThumbnailsuite withTypeError: act is not a function(React resolves the production build, which dropsact). Not this author's fault, just a footgun for anyone re-running these tests with default env.
Verdict: COMMENT (no blockers, no important findings — deferring to the standing human approval on this one; happy to flip to a formal approve if you want a second stamp on record).
Reasoning: Independently mutation-tested the loop-breaking mechanism myself (not just re-run the PR's own tests) and it holds under every partial-revert I tried; nothing else in the diff (10 net production lines) introduces new risk.
— Miga
|
Thanks for the fix. Verified before merge with an undecodable video in a fresh test project, counting video.load() calls (one per error, cleanup, retry cycle) after opening the Assets tab:
The loop is gone and the thumbnail settles after its one real load. |
Fixes #4036
Problem
VideoFrameThumbnail's error handler calledcleanup(), which setsvideo.src = ""and callsvideo.load(). Clearingsrcitself fires anothererrorevent on the element, re-entering the handler — an event-loop-speed infinite loop. Every pass allocates a React update object viasetFailed(true).The
seekedpath triggers the same loop: itscleanup()call provokes the syntheticerrorright after a successful frame extraction, so every thumbnail ends up spinning, whether the video loads or not.Evidence (real project, embedded studio)
HTMLMediaElement.srcsetter: ~475,000 assignments/second from theerror → cleanup → errorcycle.{lane, revertLane, gesture, action, hasEagerState, eagerState, next}) insidedispatchSetState, attributed to this component'ssetFailed(true).Fix
cleanup()before clearingsrc.!video.getAttribute("src")) as a belt-and-braces guard.Test
New
VideoFrameThumbnail.test.tsxcovers: fallback label on error, staying inert after repeated post-cleanup synthetic errors, keeping the extracted frame when the post-seek cleanup error fires, and re-arming a fresh video element whensrcchanges.