Skip to content

macOS export: a dedicated encode thread buys nothing — measured, and why #587

Description

@EtienneLescot

Negative result, written down so nobody spends the day I spent on it.

After #583 fixed the decode choice, the macOS export profile was two waits and almost nothing else:

enc.send_frame   7.658 s   40.6 %   CPU waiting on VideoToolbox
gpu.wait         7.501 s   39.8 %   CPU waiting on Metal

Two different engines, one serial loop. The obvious move — and the one that was worth −30 % on the Linux path — is to put the encoder on its own thread so those two waits overlap. It was implemented (bounded queue, backpressure, error propagation, av_frame_free on the consumer side) and measured.

It changes nothing. Three cycles, one ffmpeg floor per cycle, closing drift 1.0003, machine 84–85 % idle, Mac mini M1:

median MAD cost
serial 23 039 ms 1 ms 1.308× floor
encode thread 22 995 ms 2 ms 1.305× floor

+0.2 %. Output byte-identical either way.

Why

A probe on the producer's blocking (enc.queue_wait) answers it exactly:

serial threaded
enc.send_frame (consumer side) 7.658 s 17.258 s
enc.queue_wait (producer side) 7.631 s
gpu.wait 7.501 s 7.498 s
walk wall clock 18.847 s 18.823 s

The thread moved the wait, it did not remove it. Blocking on a full queue (7.631 s) equals the old blocking inside avcodec_send_frame (7.658 s) to within 0.4 %.

The export is bound by VideoToolbox's encode throughput, not by CPU serialisation. The main thread's own work is 9.64 s across 3600 frames (2.68 ms/frame — it could feed 373 fps); the encoder absorbs about 191. No scheduling changes that.

The ceiling, measured separately

Feeding h264_videotoolbox from system memory on the same machine and content, 3600 frames of 1080p60 at 8 Mbps:

decode only      1 864 ms   1931 fps
encode only     15 763 ms    228 fps   <- the ceiling
export walk     18 823 ms    191 fps

So there is still ~19 % of headroom in the walk, but the walk can never go below ~15.8 s on this hardware.

What is NOT established: the 228 fps ceiling was measured feeding frames from system memory, while the export feeds VideoToolbox frames from a hw_frames_ctx pool that Metal has just written. Whether the remaining 3 s is the zero-copy feed path costing encoder throughput, or simply an unsaturated pipeline at queue depth 4, was not separated. That is the next thing worth measuring, and it is the only reason this issue is not simply "closed, at the hardware ceiling".

Recommendation

Do not ship the thread as it stands — it adds concurrency, a queue, a second error path and a Drop that must join, for a measured 0.2 %.

It could pay on hardware where the encoder is not the constraint: an M-series Pro/Max/Ultra has more encode blocks, and there the producer would become the limit. That is a hypothesis, not a measurement, and shipping unmeasured concurrency on the strength of it is exactly the mistake #583 documents.

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