Skip to content

perf: avoid materializing full camera channel in Episode.frames() when only timestamps are needed #687

Closed

Description

@Kaileshwar16

What happened

Episode.frames() appears to materialize the full camera channel even though it only needs the channel timestamps to associate extracted frames with their source log times.

The video generation path already processes camera data in batches, which keeps memory usage bounded. However, after frame extraction, frames() calls the equivalent of:

self.channel(topic).timestamps.tolist()

self.channel(topic) constructs a complete ChannelData object and accumulates the channel payload across batches. For camera channels, this can mean retaining a large amount of encoded video data in memory solely to retrieve timestamps.

frames_at_indices() appears to use the same full-channel path.

Why this matters

For long camera episodes, the encoded video payload can be much larger than the timestamp array itself.

For example, a 3-hour 30 FPS stream contains about 324,000 timestamps. At 8 bytes per timestamp, the timestamp data is only around 2.6 MB, while the corresponding encoded camera payload may be hundreds of megabytes.

This means frame extraction may use significantly more peak memory than necessary even though the underlying video path is otherwise streamed.

Expected behavior

Frame-related APIs that only need camera timestamps should read only the timestamp/log-time column instead of materializing the complete channel payload.

Possible approach

Add an internal timestamp-only helper that iterates over channel batches and collects only log_times, for example:

def _channel_log_times(self, channel_id):
    parts = []

    for batch in self._reader.iter_batches(channel_ids=[channel_id]):
        parts.append(batch.log_times)

    if not parts:
        return np.empty(0, dtype=np.int64)

    return np.concatenate(parts)

Then use this path in frames() / frames_at_indices() instead of calling self.channel(topic).

It may also be worth allowing the timestamp-mapping code to operate directly on a NumPy array or other sequence to avoid an additional .tolist() allocation.

Benchmark

Before and after the change, benchmark a reasonably long camera episode using:

episode.frames(fps=1)

and:

episode.frames_at_indices(...)

Measure:

  • peak RSS / memory usage
  • wall-clock execution time
  • returned frame count
  • returned log_time_ns values

The optimized implementation should produce identical timestamps and frames while reducing peak memory usage.

Scope

This should be a performance-only change with no change to the public behavior of Episode.frames() or Episode.frames_at_indices().

Activity

  1. kstonekuan commented on Oct 4, 2026

    @kstonekuan
    Contributor

    Confirmed. frames() (src/hflow/episode.py:772) and frames_at_indices() (:813) call self.channel(topic) only for timestamps. That buffers every encoded payload, and channel() caches the result, so it stays in memory until the episode closes. A timestamp-only helper like the one you sketched is the right fix. Reuse a cached ChannelData when one already exists. A test that the frame log times are unchanged and that frames() leaves nothing in the channel cache is enough; no benchmark needed.

  2. added a commit that references this issue on Oct 6, 2026
    6050356
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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions