Skip to content

perf: add a benchmark, then investigate the Generate SEI copy and the TS double sweep #50

Description

@nitsuga

There is no benchmark target in this repo, so none of the performance
observations below are measured. Filing them together, because the first thing
worth doing is not any of the individual fixes — it is having a number to
compare against.

Step 0: a benchmark worth trusting

Something small: decode+re-encode a fixture packet N times, and run a fixed
synthetic .ts through extract_ts_klv and through the gst extractor. The
test/fixtures/ generators already produce suitable inputs.

Without it, every item below is speculation about where the time goes.

A. Generate SEI path copies each whole access unit

src/gst/gst_video.cpp:415-450

With sei_0604 = Sei0604::Generate, every H.264 access unit gets a fresh
gst_buffer_new_allocate(new_size) and the whole frame is memcpy'd in, to
splice ~30 bytes of SEI and strip the source one. At 1080p30 that is a
full-frame allocation and copy 30 times a second.

The tempting fix is to stop flattening: assemble the output from
gst_buffer_copy_region sub-buffers (sharing the parent's memory) plus one
small wrapped SEI memory, giving a multi-memory GstBuffer with no payload
copy.

Measure before building this. mpegtsmux downstream maps the buffer for
reading, and gst_buffer_map on a multi-memory buffer merges it into a single
allocation — which is the same copy, one element later. The optimization only
pays if the downstream consumer walks memories individually. It could easily be
a no-op or a regression.

Also note the gst_h264_parser_identify_nalu walk (:383-397) is a second full
pass over each frame and runs even when nothing is injected. That one looks
inherent: the strip set is not knowable without scanning.

Scope note: the default Preserve path does none of this work. This only
affects callers who opted into Generate.

B. extract_ts_klv sweeps the whole buffer twice

src/ts.cpp:99 (earliest_pts_90k) then src/ts.cpp:171 (main loop)

The pre-pass reads the header of every 188-byte packet, then the main loop
walks all of them again. Header-only, but it still touches a cache line per
packet, so it is effectively two memory sweeps of the input.

No cheap fix is apparent, and this is filed as a known cost rather than a
proposed change. The origin must be the minimum PTS across the whole buffer —
the comment at :70-74 is right that first-in-file-order breaks on reordered
video — and on_packet fires during the second pass, so resolving the origin
lazily at the end would change the streaming callback contract. Anyone
attacking this should come with a design, not a patch.

C. One heap allocation per KLV packet in KlvStream

src/gst/stream.cpp:19

The extract handler allocates a std::vector<std::byte> per packet to carry
bytes across the ownership boundary. (The subsequent Message::adopt(std::move(...))
is already copy-free — that part is right and should stay.) A recycled buffer
pool would remove the malloc. At ~30 Hz KLV this is almost certainly noise;
it only matters at high packet rates.

D. Message::encode() is O(edits x items)

src/message.cpp:97-98

in_source(tag) linear-scans every source item, inside the loop over edits_.
With ~80 items and a handful of edits that is a few hundred integer compares —
irrelevant unless a caller edits many tags per message. Filed for completeness.

Deliberately not listed

Things that read as already correct and should not be "optimized":
Message::adopt and the push(std::vector&&) overload (one copy at the
ownership boundary, which is the minimum), PtsMarks (amortized O(1), pruned
on both paths), reassembly.erase (shifts only the unconsumed tail),
pes[pid].assign() (reuses capacity), and Registry::find (binary search over
<= 66 entries).


Filed by Claude Opus 5 from a read-only performance pass. Nothing here was profiled.

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

    enhancementNew feature or requestpriority: lowBacklog: message quality, diagnostics, minor test gaps

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions