Skip to content

VideoFormat ↔ VideoFormatComponent reference cycle prevents gc from freeing decoded frames #2206

@jacob-aegis

Description

@jacob-aegis

Summary

VideoFormat and VideoFormatComponent form a reference cycle that CPython's reference counting cannot break. Every decoded frame creates these objects, and they accumulate until gc.collect() is explicitly called. This is a significant memory issue for long-running video processing applications.

The cycle

VideoFormat.components (tuple) → VideoFormatComponent.format → VideoFormat

In format.pyx / format.py:

  • VideoFormat._init() eagerly creates: self.components = tuple(VideoFormatComponent(self, i) for ...)
  • VideoFormatComponent.__cinit__() stores: self.format = format (strong back-reference)

Both fields are cdef — cannot be modified or broken from Python.

Minimal reproducer

import gc

gc.collect()
gc.set_debug(gc.DEBUG_SAVEALL)
gc.disable()

import av
fmt = av.VideoFormat('bgr24', 1920, 1080)
del fmt

n = gc.collect()
print(f"gc.collect() freed: {n}")
print(f"gc.garbage: {len(gc.garbage)} objects")
for obj in gc.garbage:
    print(f"  {type(obj).__name__}: {repr(obj)[:80]}")
gc.garbage.clear()
gc.set_debug(0)

Output:

gc.collect() freed: 5
gc.garbage: 5 objects
  VideoFormat: <av.VideoFormat bgr24, 1920x1080>
  VideoFormatComponent: <av.video.format.VideoFormatComponent object at 0x...>
  VideoFormatComponent: <av.video.format.VideoFormatComponent object at 0x...>
  VideoFormatComponent: <av.video.format.VideoFormatComponent object at 0x...>
  tuple: (<av.video.format.VideoFormatComponent object at 0x...>, ...

Every VideoFormat that goes out of scope leaks 5 objects (1 format + 3 components + 1 tuple) until the cyclic GC runs.

Real-world impact

We run a long-lived video processing pipeline decoding 64 concurrent RTSP streams (12× 4K HEVC). Without periodic gc.collect() calls, memory grows from 7 GB to 16+ GB due to accumulated VideoFormat / VideoFormatComponent cycles from decoded frames.

We instrumented gc.collect() with DEBUG_SAVEALL during a live run and the dominant garbage types are:

Type Count per collection
av.video.format.VideoFormatComponent 105
av.video.format.VideoFormat 35
tuple (component tuples) 104
av.video.frame.VideoFrame ~5
av.sidedata.sidedata.SideDataContainer ~5

Our workaround is a dedicated gc.collect() daemon thread every 15 seconds, which keeps memory stable but acquires the GIL for the full sweep — causing latency spikes in our inference pipeline.

Suggested fix

Make components lazy (computed on first access) instead of eagerly stored, similar to how #517 / PR #516 made planes lazy to fix the VideoFrame cycle. Alternatively, use a weakref for VideoFormatComponent.format.

Versions

  • PyAV 13.1.0 (our production version)
  • Also confirmed on PyAV 17.0.0 (main branch) — format.py and format.pxd still have the same eager components tuple and strong cdef VideoFormat format back-reference.

Related

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