Skip to content

Studio: VideoFrameThumbnail enters an infinite error → cleanup loop — one CPU core pinned + unbounded React update allocation (OOM in long-running embeds) #4036

Description

@CasbaL

Summary

VideoFrameThumbnail's error handler calls cleanup(), which sets video.src = "" and calls video.load(). Assigning an empty/invalid src itself fires a new error event on the element, which re-enters the same handler — an event-loop-speed infinite loop. Every pass allocates a React update object via setFailed(true) (dispatchSetState).

The same loop also starts on the success path: the seeked handler calls cleanup() after extracting the frame, and the resulting synthetic error re-enters the handler.

Every VideoFrameThumbnail ends up in this loop, whether the video loads or not.

Affected versions

Verified in the shipped studio bundles of 0.8.40 and 0.8.45 (current latest)assets/index-*.js still contains const c=()=>{a.src="",a.load()} with a.addEventListener("error",()=>{s(!0),c()}).

Source: packages/studio/src/components/ui/VideoFrameThumbnail.tsx

Minimal repro

<script>
  const video = document.createElement("video");
  video.addEventListener("error", () => { video.src = ""; video.load(); }); // ← re-enters on its own synthetic error
  video.src = "/missing.mp4";
  video.load();
</script>

Open DevTools → Performance: the error handler spins forever. Instrumenting the src setter counted ~475,000 assignments/second in a real session.

Measured impact (real project, embedded studio)

  • Allocation sampling: ~8.6 MB/s of React update objects ({lane, revertLane, gesture, action, hasEagerState, eagerState, next}) allocated inside dispatchSetState, called from this component's setFailed(true).
  • A heap snapshot after ~40 minutes showed 24.6M update objects / 983 MB (93% of heap) and a renderer pinned at ~100% CPU; in our embedded (Electron) use the JS heap grew to 2.8 GB at ~7 MB/s until the page died.

Suggested fix

Remove the listener before clearing src, and ignore the synthetic error cleanup() itself provokes:

const onError = () => {
  if (!video.getAttribute("src")) return; // synthetic error from cleanup below
  setFailed(true);
  cleanup();
};

const cleanup = () => {
  video.removeEventListener("error", onError);
  video.src = "";
  video.load();
};

// ... video.addEventListener("error", onError); instead of the inline arrow

Happy to open a PR if helpful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions