Recursive AI

Allocations, constant memory and I/O, 0.8

All onemedia docs

Measured on the x64 worker (AMD Ryzen 9 7950X3D, .NET 10.0.13); method in bench.md and harness.md. The speed rows are in 0.8-x64.md and 0.8-arm64.md.

Allocation gate: zero bytes per packet on the pull path

bench pullalloc FILE...: after a full first pass the packet loop TryReadPacket(out PacketView) runs again (from the start, or over the second half of the stream when the source cannot seek) with GC.GetAllocatedBytesForCurrentThread read around it. The gate is 0 bytes; all 41 files, one or more per reader, pass. The owned path (TryReadOwnedPacket with the shared MemoryPool<byte>, each packet disposed at once) is reported: 24 bytes per packet is the IMemoryOwner wrapper; the AVI, Ogg and RF64 figures above 24 are the pool growing for packets larger than any before.

FileReaderPacketsPull path B/packetOwned path B/packet
blender/BigBuckBunny_320x180.mp4ISO BMFF422640.024.0
blender/big_buck_bunny_480p_h264.movISO BMFF (QuickTime)422760.024.0
generated/editlist/audio-aac-priming.m4aISO BMFF (audio)1890.024.0
generated/3gp/h263-amrnb.3gpISO BMFF (3GP)2610.024.0
blender/BigBuckBunny_640x360.m4vISO BMFF400040.024.0
generated/mkv/av1-vorbis.mkvMatroska3090.024.0
generated/mkv-live/vp9-opus-unknown-size.webmWebM (unknown-size clusters)3210.024.0
generated/mka/audio-aac.mkaMatroska audio1890.024.0
blender/big_buck_bunny_480p_stereo.aviAVI391450.037.5
generated/asf/wmv1-wmav1.wmvASF2140.024.0
generated/asf/mp3-tagged-unicode.wmaASF1680.024.0
generated/asf/msmpeg4v3-mp3.asfASF2880.024.0
generated/mpeg/large/ts-2gb-seed.tsMPEG-TS67500.024.0
generated/mpeg/m2ts/h264-aac.m2tsM2TS2890.024.0
samples/avchd/dean00007.mtsAVCHD M2TS12190.024.0
generated/mpeg/ps/dvd-ac3-ntsc.vobMPEG-PS2630.024.0
generated/mpeg/ps/h264-mp2.mpgMPEG-PS2670.024.0
generated/xiph-flv/flv/aac-mono-22k.flvFLV880.024.0
blender/big_buck_bunny_480p_stereo.oggOgg502900.032.8
generated/ogg/audio-opus.opusOgg Opus2010.024.0
generated/xiph-flv/ogg/theora.ogvOgg Theora1200.024.0
generated/xiph-flv/flac/192k-24bit.flacFLAC470.024.0
generated/pcm/rf64-pcm_s16le-stereo48k.wavRF64/WAV360.024.0
generated/pcm/w64-pcm_f32le-stereo48k.w64Wave64360.024.0
generated/pcm/aiff-pcm_f32be-stereo48k.aiffAIFF2820.024.0
generated/pcm/caf-alac-mono44k.cafCAF170.024.0
generated/audio-es/aac/lc-32k-stereo.aacADTS950.024.0
generated/audio-es/ac3/ac3-32k-stereo-96k.ac3AC-3630.024.0
generated/audio-es/ac3/eac3-32k-stereo-96k.eac3E-AC-3630.024.0
samples/mpeg/H264_artifacts_motion.h264Annex B H.2646000.024.0
generated/mpeg/es/av1-1.ivfIVF720.024.0
generated/mpeg/es/av1-1.obuAV1 OBU720.024.0
generated/dv/dv25-ntsc.dvDV890.024.0
generated/audio-es/mp3/long-60s.mp3MP322980.024.0
generated/mpeg/ts/mpeg2-interlaced.tsMPEG-TS2670.024.0
generated/bench/frag-bbb-1080p.mp4ISO BMFF422760.024.0
generated/fmp4/audio-aac.mp4ISO BMFF1890.024.0
generated/audio-es/amr/nb-10200.amrAMR1510.024.0
generated/mp4/audio-aac-51.mp4ISO BMFF1890.024.0
generated/asf/wmv1-wmav1.wmvASF2140.024.0
generated/asf/wmv2-wmav2.wmvASF2140.024.0

Fixed on the way: the ASF reader allocated a reassembly buffer for most video objects of wmv1-wmav1.wmv (77 B per packet) because its buffer pool threw away smaller buffers when a larger one was asked for; it now hands out the smallest buffer that fits and rounds new ones up, and the file reads at 0.

Constant working set

bench memcheck AREA FILE OUTDIR runs one operation on one file in a fresh process and prints the process's peak resident set (bench/memory.sh runs the matrix; each output, up to 4 GiB, is deleted after its run). The files past 4 GiB come from tools/corpus/generate.sh on the worker and are never committed. The resident set before any work is 28.9 MiB. Remux goes to Matroska on disk, packaging to HLS (CMAF) on disk, repair rebuilds the index into a new file, and the tag edit is SaveAs with four common fields changed.

AreaInputInput sizeOutput sizePeak MiBSeconds
demuxvideo.mp426.6 MiB0.0 MiB35.50.0
demuxover4gb-moov-at-end.mp44173.2 MiB0.0 MiB37.60.3
demuxh264-aac.mkv0.2 MiB0.0 MiB36.10.0
demuxover4gb.mkv4239.9 MiB0.0 MiB41.00.5
demuxtagged-mpeg4-mp3.avi0.4 MiB0.0 MiB35.70.0
demuxodml-4p6gb.avi4431.7 MiB0.0 MiB37.60.4
demuxwav-pcm_s16le-mono44k.wav0.3 MiB0.0 MiB35.00.0
demuxover4gb-rf64.wav4211.4 MiB0.0 MiB35.911.8
demuxmpeg2-interlaced.ts1.1 MiB0.0 MiB35.80.0
demuxts-2gb.ts2262.1 MiB0.0 MiB46.70.8
remuxvideo.mp426.6 MiB25.8 MiB48.50.1
remuxover4gb-moov-at-end.mp44173.2 MiB4172.9 MiB67.81.5
remuxh264-aac.mkv0.2 MiB0.2 MiB49.50.1
remuxover4gb.mkv4239.9 MiB4240.1 MiB84.45.4
remuxtagged-mpeg4-mp3.avi0.4 MiB0.4 MiB50.40.1
remuxodml-4p6gb.avi4431.7 MiB4431.6 MiB64.55.2
packagevideo.mp426.6 MiB25.8 MiB46.90.1
packageover4gb-moov-at-end.mp44173.2 MiB4172.8 MiB112.32.5
packageh264-aac.mkv0.2 MiB0.2 MiB44.10.1
packageover4gb.mkv4239.9 MiB4240.2 MiB130.32.8
repairvideo.mp426.6 MiB26.6 MiB37.80.0
repairover4gb-moov-at-end.mp44173.2 MiB4173.2 MiB50.71.1
repairh264-aac.mkv0.2 MiB0.2 MiB41.70.0
repairover4gb.mkv4239.9 MiB4239.9 MiB57.01.3
repairtagged-mpeg4-mp3.avi0.4 MiB0.4 MiB34.80.0
repairodml-4p6gb.avi4431.7 MiB4431.7 MiB42.71.3
tagvideo.mp426.6 MiB26.6 MiB38.70.1
tagover4gb-moov-at-end.mp44173.2 MiB4173.2 MiB39.23.3
tagh264-aac.mkv0.2 MiB0.2 MiB40.90.1
tagover4gb.mkv4239.9 MiB4239.9 MiB44.23.7
tagwav-pcm_s16le-mono44k.wav0.3 MiB0.3 MiB37.70.0
tagover4gb-rf64.wav4211.4 MiB4211.4 MiB39.13.6

Demux, repair and tag edits peak within 10 MiB of their small-file figure at 4 GiB. Remux grows from about 50 to 65-85 MiB. Packaging follows the segment, not the input (u76; the first run of the table above had 173 and 189 MiB for the two 4 GiB inputs and 50 for the 27 MB file). The packager holds one segment per track, and a segment is as large as the bit rate times the segment duration: these two test files run at 43 Mbit/s, so a 6 s segment is 32 MiB. The cause of the extra memory was copies of that segment, not the segments written so far: the muxer kept the open fragment in one array that grew by doubling through the shared pool, and the segment buffer doubled beside it, leaving a chain of dead arrays. The fragment is now kept in reusable 256 KiB chunks and the segment buffer is sized once from the muxer's byte count and reused, so the 4 GiB inputs peak at 112 and 130 MiB (two runs each, within 0.3 MiB) and the 27 MB file at 47. The 4.2 GB MKV at other segment lengths: 2 s (10.7 MiB segments) 92 MiB, 6 s 130, 12 s 192, so the peak is about 29 MiB plus 2.5 times the largest segment; a file of any length with the same bit rate peaks the same. The buffer is bounded by MediaLimits.MaxSegmentBytes (256 MiB default; a larger segment raises MediaLimitExceededException) and documented in packaging.md. The live-mode soak is below.

Structural-only reads (probe and metadata)

io-results.txt lists every file: bytes read and calls through strace on the file, and bytes / HTTP range requests through the local range server, next to ffprobe 7.0.2, MediaInfo 26.10 and ExifTool 13.60 on the same files (1,000+ files, 13 families). "Ours" opens the file and reads its tags and basic facts. Summary of the ours/http column against the two tools that can read over HTTP:

FamilyFilesOurs bytes median / maxOurs requests median / maxffprobe bytes median / maxMediaInfo bytes median / max
isobmff1318,143 / 1,634,1888 / 11234,829 / 70,064,401334,634 / 185,319,424
matroska9437,163 / 4,705,63212 / 16180,224 / 5,210,112238,726 / 194,398,837
asf5536,864 / 36,8643 / 3180,224 / 5,128,192379,050 / 6,750,914
mpa9253,046 / 282,6243 / 449,353 / 360,57648,950 / 196,768
adts1141,342 / 88,9353 / 3111,738 / 254,51737,406 / 84,999
ac31464,433 / 244,7363 / 360,337 / 180,22460,337 / 180,224
amr178,166 / 13,2553 / 34,083 / 9,1594,083 / 9,159
xiph7269,666 / 143,3604 / 567,301 / 638,74755,832 / 409,600
flv3069,632 / 69,6473 / 5180,224 / 2,293,760215,071 / 5,866,523
pcm22520,480 / 36,8643 / 495,276 / 5,095,42440,936 / 425,984
avi70114,509 / 273,7169 / 13404,606 / 5,357,568572,786 / 12,648,448
dv1469,632 / 69,6323 / 34,816,896 / 5,226,496114,688 / 393,376
mpeg151266,240 / 7,080,5363 / 26368,640 / 14,294,576227,292 / 118,964,404

Ours is structural: the median probe reads 8 KB (ISO BMFF, AMR) to 266 KB (MPEG), in 3 to 12 requests, on files from 0.2 MB to 4.4 GB, and a movie box at the end of a 4.4 GB file costs one seek (1,069,403 bytes in 7 requests). Where the figure is above 1 MiB the format forces a scan; each case:

FamilyFileFile sizeOurs bytesRequestsWhy
mpegfoxksaz.ts30,527,4887,080,53626front 5 MB plus the 1 MiB tail; the search for program tables may run to 6 MB (docs/mpeg.md), no range is read twice
mpegvid_0x80.ts8,400,0006,818,38825front 5 MB plus the 1 MiB tail; the search for program tables may run to 6 MB (docs/mpeg.md), no range is read twice
mpegHD-h264.ts126,511,0286,294,27223front 5 MB plus the 1 MiB tail; the search for program tables may run to 6 MB (docs/mpeg.md), no range is read twice
mpegtest-ebu-422.40000.pakets.ts7,520,0007,080,48826front 5 MB plus the 1 MiB tail; the search for program tables may run to 6 MB (docs/mpeg.md), no range is read twice
matroskaaac-sbr-ffaac-slow.mkv15,728,6404,705,63216ffprobe reads the same 4.7 MB of aac-sbr-ffaac-slow.mkv (MediaInfo reads the whole file): the file needs the cluster headers to describe its AAC track
mpeglibmpeg2bug.ts5,242,8802,363,1288front 5 MB plus the 1 MiB tail; the search for program tables may run to 6 MB (docs/mpeg.md), no range is read twice
matroskaticket-a_aac.mkv6,580,8132,124,32716ffprobe reads the same 4.7 MB of aac-sbr-ffaac-slow.mkv (MediaInfo reads the whole file): the file needs the cluster headers to describe its AAC track
mpegres_change_ffmpeg_aspect.ts1,678,6691,576,9935front 5 MB plus the 1 MiB tail; the search for program tables may run to 6 MB (docs/mpeg.md), no range is read twice
isobmffquicktime.mov280,986,3951,634,1886the movie box itself is 1.0-1.6 MB (ffprobe reads 2.0 MB of quicktime.mov): one range, read once
mpegts-2gb-seed.ts182,561,7241,576,9765no index in the format: docs/mpeg.md bounds the probe to 5,000,000 bytes from the front plus 1 MiB at the tail
mpegts-2gb.ts2,371,987,5401,576,9765no index in the format: docs/mpeg.md bounds the probe to 5,000,000 bytes from the front plus 1 MiB at the tail
mpegmpeg2-svcd.mpg1,264,2561,575,1045no index in the format: docs/mpeg.md bounds the probe to 5,000,000 bytes from the front plus 1 MiB at the tail
mpegdvd-ac3-ntsc.vob2,308,0961,572,8785no index in the format: docs/mpeg.md bounds the probe to 5,000,000 bytes from the front plus 1 MiB at the tail
mpegdvd-pcm.vob1,368,0641,572,8785no index in the format: docs/mpeg.md bounds the probe to 5,000,000 bytes from the front plus 1 MiB at the tail

36 of the 1,000+ files read more than 1 MiB. The ISO BMFF and Matroska cases read about as much as ffprobe does. The MPEG cases below the documented bound are the front and tail windows; The four transport streams that read past the documented bound in the first run are fixed (u76). Per stream, ours over HTTP before and after (bytes / requests), next to ffprobe 7.0.2 and MediaInfo 26.10 over HTTP:

Stream (size)BeforeAfterffprobe 7.0.2MediaInfo 26.10What it was
foxksaz.ts (30,527,488)25,425,648 / 967,080,536 / 269,818,256 / 430,527,488 / 1no tables in the file: the search for them ran 24 MB; it now stops at 6 MB and says so (ts.tables-not-found)
vid_0x80.ts (8,400,000)12,059,900 / 456,818,388 / 2514,294,576 / 58,400,000 / 1its one PAT is at 5.57 MB; the 5.77 MB front was read, then read again
HD-h264.ts (126,511,028)11,535,784 / 436,294,272 / 236,049,936 / 4118,866,100 / 2tables after the first payloads: the 5.2 MB front was read twice
test-ebu-422.40000.pakets.ts (7,520,000)9,096,976 / 347,080,488 / 2610,686,608 / 47,520,000 / 1no tables: the whole file was read, then a rewind read the front again

The cause was the rewind: after the tables are found, or after the streams are guessed, the probe goes back to the start of the stream so each stream's first frame describes it, and that second pass went to the source again. It is now answered from a copy of the first pass (at most 6.5 MB, held only while the stream opens), so no range is requested twice, and the table search is bounded at 6 MB. What remains above 5 MB + 1 MiB is exactly the table search: vid_0x80.ts needs 5.77 MB to reach its PAT and PMT, and a stream without tables costs the full 6 MB before it is guessed (docs/mpeg.md). The whole MPEG family (151 files) was measured again: only these four, libmpeg2bug.ts and res_change_ffmpeg_aspect.ts (both a little lower) changed; the medians are the same (ours / ffprobe / MediaInfo / ExifTool, bytes and reads: transport streams 476,352 B in 3 / 470,524 B in 23 / 240,264 B in 4 / 58,864 B in 9; M2TS 836,352 in 3 / 850,760 in 34 / 582,144 in 9 / 91,888 in 13; program streams 1,472,526 in 4 / 1,198,224 in 46 / 1,316,160 in 20 / 284,400 in 36; elementary streams 143,531 in 2 / 65,536 in 2 / 110,562 in 2). The full table is io-results.txt.

AVI changed in this stage: the header list is read through a 32 KiB front cache and a read-ahead window. Counted as read-class system calls on the file made by the avi-probe bench cell (open, track list, duration, tags; bench/iocount.sh, strace), a probe makes 7 to 9 reads of the file instead of 85 to 94: the 4.6 GB OpenDML file odml-4p6gb.avi 94 before (commit 72741b0) and 7 after (172,032 bytes), tagged-mpeg4-mp3.avi 89 and 9, GoneNutty.avi 85 before. The 0.8.0-preview.1 tag and the current code give the same 7 and 9; the "50" and "48" first written for the 4.6 GB file in this file and in the 0.8 CHANGELOG entry do not reproduce and were wrong. The io table above counts something else: ours/strace in io-results.txt is the number of reads our reader asks of the file source while it opens the file and reads its tags and basic facts (io --metadata: 10 for the 4.6 GB file, 8 without the metadata), and ours/http the HTTP range requests of the same run (9 requests, 172,044 bytes). The avi-probe row went from 1.95x to 0.52x of the fastest comparator. Over HTTP the median probe is 114,509 B in 9 requests (before: 4,559 B in 32 requests; the whole table is above). It trades bytes for round trips; an 8 KiB window gave 45 KB in 10 requests and is left as a follow-up because the speed rows were taken with this build.