Build BlockGroups in memory so the hot path never seeks
Every BlockGroup frame back-patched its element size via ebml::end_master, which
does two stream_position() calls and two real seeks. BufWriter does not override
Seek::stream_position, so each position query is seek(Current(0)) = flush_buf +
lseek — and each of those flushed the 4 MiB BufWriter while every position-moving
seek reset WritebackPipeline::last_flush_pos. The buffer never got to do its job.
An MPEG-2 title takes this path for EVERY frame (the parser stamps a per-frame
duration, so I, P and B all become BlockGroups) — roughly 350,000 per feature.
A BlockGroup's size is knowable before writing, so there is no need to back-patch
at all. New seek-free twins start_master_buf / end_master_buf patch a placeholder
by buffer INDEX instead of file offset, and build_block_group assembles the whole
element into a persistent buffer that write_block_group and its MVC sibling take,
fill, write once, and hand back — including on the error path — so the allocation
is made once rather than per frame.
Measured with a counting writer that, like BufWriter, does not override
stream_position, over 200 frames:
seek calls position-moving
plain before 912 451
plain after 112 51
MVC before 2516 1253
MVC after 116 53
Per-frame cost is now zero; the residual is the header, the per-cluster
back-patch and Cues. For 350k BlockGroups that is 1.4M seek calls removed on the
plain path, 4.2M on an MVC title.
end_master is deliberately NOT changed for its other callers. The Cluster master
genuinely streams — frames are appended to an open cluster over time, so its body
cannot be buffered without holding a whole cluster in memory — and the rest
(EBML header, Tracks, Info, Chapters, Cues) run once, not per frame.
Byte-identity is the safety property, and it is structural: end_master always
patches a FIXED-width 8-byte VINT, and the buffered pair writes and patches
exactly that same placeholder, so the encodings cannot differ. Verified by
capture-then-compare over 200 frames (17 keyframes / 183 non-keyframes, multiple
clusters, BlockDuration present and absent, both reference branches) — output
byte-for-byte identical.
Three tests now pin it permanently: buffered vs seeking output byte-for-byte for
empty/tiny/multi-byte bodies, the same for NESTED masters (the MVC path nests
BlockAdditions > BlockMore, where an index-arithmetic slip would surface), and
end_master_buf erroring rather than panicking on a position outside the buffer.
All three verified red against a deliberately divergent placeholder width.
A note on verifying this kind of change: comparing emitted MKV across two
worktrees at different commits shows a spurious 14-byte difference, because
MuxingApp/WritingApp embed the build's git SHA twice. Compare at the same base.
This commit is contained in:
+85
-41
@@ -617,6 +617,20 @@ pub struct MkvMuxer<W: Write + Seek> {
|
||||
cluster_pos: u64,
|
||||
cluster_size_pos: u64,
|
||||
cluster_ts_ticks: i64,
|
||||
/// Reusable scratch buffer for assembling ONE BlockGroup before it is
|
||||
/// written with a single `write_all`.
|
||||
///
|
||||
/// The BlockGroup path is the hot one — the MPEG-2 parser stamps a per-frame
|
||||
/// duration, so every I/P/B frame lands there, plus AC-3 audio and PGS
|
||||
/// subtitles (~350k BlockGroups for a 90-minute feature). Building the
|
||||
/// element in memory means its size is known before it reaches the file, so
|
||||
/// no `start_master`/`end_master` back-patch is needed: that removes the
|
||||
/// per-frame `stream_position()` + two `seek()`s, each of which flushed the
|
||||
/// 4 MiB `BufWriter` (`BufWriter` does not override `stream_position`, so a
|
||||
/// position query is `seek(Current(0))` = flush + lseek) and reset the
|
||||
/// writeback pipeline's `last_flush_pos`. Kept on the muxer so the
|
||||
/// allocation is made once, not per frame.
|
||||
block_group_buf: Vec<u8>,
|
||||
base_pts_ticks: Option<i64>,
|
||||
/// Last block timecode (TimestampScale ticks, relative to base_pts) written
|
||||
/// PER TRACK, to enforce strictly-monotonic per-track timestamps —
|
||||
@@ -1237,6 +1251,7 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
cluster_open: false,
|
||||
cluster_pos: 0,
|
||||
cluster_size_pos: 0,
|
||||
block_group_buf: Vec::new(),
|
||||
cluster_ts_ticks: 0,
|
||||
base_pts_ticks: None,
|
||||
last_pts_ticks: std::collections::HashMap::new(),
|
||||
@@ -1851,24 +1866,69 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
data: &[u8],
|
||||
reference: Option<i64>,
|
||||
duration_ticks: u64,
|
||||
) -> io::Result<()> {
|
||||
let mut buf = std::mem::take(&mut self.block_group_buf);
|
||||
buf.clear();
|
||||
let res = Self::build_block_group(
|
||||
&mut buf,
|
||||
track_num,
|
||||
relative_ts,
|
||||
data,
|
||||
reference,
|
||||
Some(duration_ticks),
|
||||
None,
|
||||
)
|
||||
.and_then(|()| self.writer.write_all(&buf));
|
||||
// Hand the (now grown) scratch buffer back so the next frame reuses the
|
||||
// allocation, on the error path too.
|
||||
self.block_group_buf = buf;
|
||||
res
|
||||
}
|
||||
|
||||
/// Assemble a complete BlockGroup into `buf` — one `write_all` at the call
|
||||
/// site, no `stream_position` and no `seek`.
|
||||
///
|
||||
/// `additional` is `Some(dependent AU)` for the MVC path, which appends
|
||||
/// `BlockAdditions > BlockMore { BlockAddID=2, BlockAdditional }`.
|
||||
fn build_block_group(
|
||||
buf: &mut Vec<u8>,
|
||||
track_num: usize,
|
||||
relative_ts: i16,
|
||||
data: &[u8],
|
||||
reference: Option<i64>,
|
||||
duration_ticks: Option<u64>,
|
||||
additional: Option<&[u8]>,
|
||||
) -> io::Result<()> {
|
||||
let (tv, tv_len) = track_vint(track_num);
|
||||
let track_vint = &tv[..tv_len];
|
||||
// The 0x80 Keyframe flag is SimpleBlock-only; inside a BlockGroup Block
|
||||
// it is reserved and MUST be 0 — keyframe-ness is signalled by the
|
||||
// presence/absence of ReferenceBlock.
|
||||
let flags: u8 = 0x00;
|
||||
let block_size = track_vint.len() + 2 + 1 + data.len();
|
||||
|
||||
let bg_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_GROUP)?;
|
||||
ebml::write_id(&mut self.writer, ebml::BLOCK)?;
|
||||
ebml::write_size(&mut self.writer, block_size as u64)?;
|
||||
self.writer.write_all(track_vint)?;
|
||||
self.writer.write_all(&relative_ts.to_be_bytes())?;
|
||||
self.writer.write_all(&[flags])?;
|
||||
self.writer.write_all(data)?;
|
||||
ebml::write_uint(&mut self.writer, ebml::BLOCK_DURATION, duration_ticks)?;
|
||||
if let Some(ref_off) = reference {
|
||||
ebml::write_int(&mut self.writer, ebml::REFERENCE_BLOCK, ref_off)?;
|
||||
let bg_pos = ebml::start_master_buf(buf, ebml::BLOCK_GROUP)?;
|
||||
ebml::write_id(buf, ebml::BLOCK)?;
|
||||
ebml::write_size(buf, block_size as u64)?;
|
||||
buf.extend_from_slice(track_vint);
|
||||
buf.extend_from_slice(&relative_ts.to_be_bytes());
|
||||
buf.push(flags);
|
||||
buf.extend_from_slice(data);
|
||||
if let Some(dt) = duration_ticks {
|
||||
ebml::write_uint(buf, ebml::BLOCK_DURATION, dt)?;
|
||||
}
|
||||
ebml::end_master(&mut self.writer, bg_pos)?;
|
||||
if let Some(ref_off) = reference {
|
||||
ebml::write_int(buf, ebml::REFERENCE_BLOCK, ref_off)?;
|
||||
}
|
||||
if let Some(additional) = additional {
|
||||
let adds_pos = ebml::start_master_buf(buf, ebml::BLOCK_ADDITIONS)?;
|
||||
let more_pos = ebml::start_master_buf(buf, ebml::BLOCK_MORE)?;
|
||||
ebml::write_uint(buf, ebml::BLOCK_ADD_ID, BLOCK_ADD_ID_VALUE_MVC)?;
|
||||
ebml::write_binary(buf, ebml::BLOCK_ADDITIONAL, additional)?;
|
||||
ebml::end_master_buf(buf, more_pos)?;
|
||||
ebml::end_master_buf(buf, adds_pos)?;
|
||||
}
|
||||
ebml::end_master_buf(buf, bg_pos)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1886,36 +1946,20 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
reference: Option<i64>,
|
||||
duration_ticks: Option<u64>,
|
||||
) -> io::Result<()> {
|
||||
let (tv, tv_len) = track_vint(track_num);
|
||||
let track_vint = &tv[..tv_len];
|
||||
// The 0x80 Keyframe flag is SimpleBlock-only; inside a BlockGroup Block
|
||||
// it is reserved and MUST be 0 — keyframe-ness is signalled by the
|
||||
// presence/absence of ReferenceBlock.
|
||||
let flags: u8 = 0x00;
|
||||
let block_size = track_vint.len() + 2 + 1 + data.len();
|
||||
|
||||
let bg_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_GROUP)?;
|
||||
ebml::write_id(&mut self.writer, ebml::BLOCK)?;
|
||||
ebml::write_size(&mut self.writer, block_size as u64)?;
|
||||
self.writer.write_all(track_vint)?;
|
||||
self.writer.write_all(&relative_ts.to_be_bytes())?;
|
||||
self.writer.write_all(&[flags])?;
|
||||
self.writer.write_all(data)?;
|
||||
if let Some(dt) = duration_ticks {
|
||||
ebml::write_uint(&mut self.writer, ebml::BLOCK_DURATION, dt)?;
|
||||
}
|
||||
if let Some(ref_off) = reference {
|
||||
ebml::write_int(&mut self.writer, ebml::REFERENCE_BLOCK, ref_off)?;
|
||||
}
|
||||
// BlockAdditions → BlockMore { BlockAddID=2, BlockAdditional=dependent AU }.
|
||||
let adds_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_ADDITIONS)?;
|
||||
let more_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_MORE)?;
|
||||
ebml::write_uint(&mut self.writer, ebml::BLOCK_ADD_ID, BLOCK_ADD_ID_VALUE_MVC)?;
|
||||
ebml::write_binary(&mut self.writer, ebml::BLOCK_ADDITIONAL, additional)?;
|
||||
ebml::end_master(&mut self.writer, more_pos)?;
|
||||
ebml::end_master(&mut self.writer, adds_pos)?;
|
||||
ebml::end_master(&mut self.writer, bg_pos)?;
|
||||
Ok(())
|
||||
let mut buf = std::mem::take(&mut self.block_group_buf);
|
||||
buf.clear();
|
||||
let res = Self::build_block_group(
|
||||
&mut buf,
|
||||
track_num,
|
||||
relative_ts,
|
||||
data,
|
||||
reference,
|
||||
duration_ticks,
|
||||
Some(additional),
|
||||
)
|
||||
.and_then(|()| self.writer.write_all(&buf));
|
||||
self.block_group_buf = buf;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user