---
title: pool
---

<div class="python-reference">
<a id="fastvideo.eval.pool-classes"></a>
<a id="fastvideo.eval.pool.VideoPool-functions"></a>
<a id="fastvideo.eval.pool"></a>
<p>Async path-→-tensor prefetcher for the Evaluator.</p>
<p>Hides video-decode latency behind metric compute by running a small
thread pool of decoders that fill a bounded queue. One :class:<code>VideoPool</code>
is owned by the Evaluator per <code>evaluate(samples=...)</code> call; workers
consume via :meth:<code>VideoPool.get</code>. Decode order is non-deterministic;
each yielded item carries its original input index so consumers can
write back into a result list in input order.</p>
<p>Pool sizing: <code>max_size = prefetch_factor * num_workers</code>.</p>
<h2 id="fastvideo.eval.pool.Video"><code>fastvideo.eval.pool.Video</code></h2>
<pre class="doc-signature"><code>Video(source: Any, fps: float | None = None, frames: Any = None, audio: Any = None, audio_sr: int | None = None)</code></pre>
<p>Path-backed media handle. The :class:<code>VideoPool</code> populates
<code>frames</code> (and optionally <code>audio</code>) before the metric loop sees
the sample.</p>

<h3 id="fastvideo.eval.pool.Video.has_frames"><code>fastvideo.eval.pool.Video.has_frames</code></h3>
<pre class="doc-signature"><code>has_frames() -&gt; bool</code></pre>

<h3 id="fastvideo.eval.pool.Video.has_audio"><code>fastvideo.eval.pool.Video.has_audio</code></h3>
<pre class="doc-signature"><code>has_audio() -&gt; bool</code></pre>

<h2 id="fastvideo.eval.pool.VideoPool"><code>fastvideo.eval.pool.VideoPool</code></h2>
<pre class="doc-signature"><code>VideoPool(samples: list&#91;dict&#93;, *, loader_threads: int = 1, max_size: int = 4)</code></pre>
<p>Bounded prefetch queue feeding decoded samples to consumers.</p>
<p>Use as a context manager so loader threads are always cleaned up::</p>
<pre><code>with VideoPool(samples, loader_threads=1, max_size=4) as pool:
    while True:
        item = pool.get()
        if item is None:
            break
        idx, decoded = item
        results[idx] = worker.evaluate(**decoded)
</code></pre>

<details><summary>Source</summary><pre><code>class VideoPool:
    &quot;&quot;&quot;Bounded prefetch queue feeding decoded samples to consumers.

    Use as a context manager so loader threads are always cleaned up::

        with VideoPool(samples, loader_threads=1, max_size=4) as pool:
            while True:
                item = pool.get()
                if item is None:
                    break
                idx, decoded = item
                results&#91;idx&#93; = worker.evaluate(**decoded)
    &quot;&quot;&quot;

    def __init__(
        self,
        samples: list&#91;dict&#93;,
        *,
        loader_threads: int = 1,
        max_size: int = 4,
    ) -&gt; None:
        if loader_threads &lt; 1:
            raise ValueError(&quot;loader_threads must be &gt;= 1&quot;)
        self._samples = samples
        self._loader_threads_n = loader_threads
        self._max_size = max(max_size, 1)

        self._task_q: queue.Queue = queue.Queue()
        self._ready_q: queue.Queue = queue.Queue(maxsize=self._max_size)
        self._loaders: list&#91;threading.Thread&#93; = &#91;&#93;
        self._stop = threading.Event()

        self._consumed = 0
        self._consume_lock = threading.Lock()

    def __enter__(self) -&gt; VideoPool:
        for idx, sample in enumerate(self._samples):
            self._task_q.put((idx, sample))
        for _ in range(self._loader_threads_n):
            self._task_q.put(_SENTINEL)
        for _ in range(self._loader_threads_n):
            t = threading.Thread(target=self._loader_loop, daemon=True)
            t.start()
            self._loaders.append(t)
        return self

    def __exit__(self, *_exc: Any) -&gt; None:
        self._stop.set()
        # Drain ready queue so any blocked-on-put loader unblocks.
        while True:
            try:
                self._ready_q.get_nowait()
            except queue.Empty:
                break
        for t in self._loaders:
            t.join(timeout=5.0)

    def get(self) -&gt; tuple&#91;int, dict&#93; | None:
        &quot;&quot;&quot;Pop the next decoded ``(idx, sample)``.

        Returns ``None`` when all input samples have been consumed.
        Polls in 0.1 s slices so extra consumer threads (when
        ``len(samples) &lt; num_workers``) wake up periodically to
        re-check ``_consumed`` and exit cleanly — without the poll,
        a blocking ``_ready_q.get(timeout=None)`` deadlocks because
        the loaders are already done. Re-raises any exception caught
        in a loader thread on the consumer&#x27;s stack so callers don&#x27;t
        hang on a dead loader. Thread-safe: multiple consumer threads
        may share one pool.
        &quot;&quot;&quot;
        while True:
            with self._consume_lock:
                if self._consumed &gt;= len(self._samples):
                    return None
            try:
                item = self._ready_q.get(timeout=0.1)
            except queue.Empty:
                continue
            with self._consume_lock:
                self._consumed += 1
            idx, payload = item
            if isinstance(payload, _DecodeError):
                raise payload.exc
            return item

    def _loader_loop(self) -&gt; None:
        while not self._stop.is_set():
            item = self._task_q.get()
            if item is _SENTINEL:
                return
            idx, sample = item
            try:
                decoded: Any = self._decode(sample)
            except BaseException as exc:  # noqa: BLE001 — forward to consumer
                self._ready_q.put((idx, _DecodeError(exc)))
                continue
            # Blocking put: under normal flow the consumer drains the
            # queue; under shutdown ``__exit__`` drains it for us. A
            # timeout would silently drop samples and hang the consumer.
            self._ready_q.put((idx, decoded))

    def _decode(self, sample: dict) -&gt; dict:
        &quot;&quot;&quot;Materialize :class:`Video` instances by populating ``.frames``.

        Any value that is a ``Video`` with a source path and no frames
        yet gets decoded.  Every other value passes through unchanged —
        wrap paths in :class:`Video` (or use
        :func:`fastvideo.eval.io.as_video` / :func:`samples_from`) to
        make them decode-eligible.
        &quot;&quot;&quot;
        from fastvideo.eval.io.video import load_video, probe_video_fps

        out = dict(sample)
        for key, val in sample.items():
            if isinstance(val, Video) and val.frames is None and val.source is not None:
                if key == &quot;video&quot; and &quot;fps&quot; not in out:
                    if val.fps is None:
                        val.fps = probe_video_fps(val.source)
                    if val.fps is not None:
                        out&#91;&quot;fps&quot;&#93; = float(val.fps)
                val.frames = load_video(val.source)
                out&#91;key&#93; = val
        return out</code></pre></details>
<h3 id="fastvideo.eval.pool.VideoPool.get"><code>fastvideo.eval.pool.VideoPool.get</code></h3>
<pre class="doc-signature"><code>get() -&gt; tuple&#91;int, dict&#93; | None</code></pre>
<p>Pop the next decoded <code>(idx, sample)</code>.</p>
<p>Returns <code>None</code> when all input samples have been consumed.
Polls in 0.1 s slices so extra consumer threads (when
<code>len(samples) &lt; num_workers</code>) wake up periodically to
re-check <code>_consumed</code> and exit cleanly — without the poll,
a blocking <code>_ready_q.get(timeout=None)</code> deadlocks because
the loaders are already done. Re-raises any exception caught
in a loader thread on the consumer's stack so callers don't
hang on a dead loader. Thread-safe: multiple consumer threads
may share one pool.</p>
<details><summary>Source</summary><pre><code>def get(self) -&gt; tuple&#91;int, dict&#93; | None:
    &quot;&quot;&quot;Pop the next decoded ``(idx, sample)``.

    Returns ``None`` when all input samples have been consumed.
    Polls in 0.1 s slices so extra consumer threads (when
    ``len(samples) &lt; num_workers``) wake up periodically to
    re-check ``_consumed`` and exit cleanly — without the poll,
    a blocking ``_ready_q.get(timeout=None)`` deadlocks because
    the loaders are already done. Re-raises any exception caught
    in a loader thread on the consumer&#x27;s stack so callers don&#x27;t
    hang on a dead loader. Thread-safe: multiple consumer threads
    may share one pool.
    &quot;&quot;&quot;
    while True:
        with self._consume_lock:
            if self._consumed &gt;= len(self._samples):
                return None
        try:
            item = self._ready_q.get(timeout=0.1)
        except queue.Empty:
            continue
        with self._consume_lock:
            self._consumed += 1
        idx, payload = item
        if isinstance(payload, _DecodeError):
            raise payload.exc
        return item</code></pre></details>
</div>
