Skip to content
All work
04Media processing and binary formatsInterra Systems·2022 — 2024

Reading less of the file

A multicore analysis pipeline was reading entire multi-gigabyte media files to compute information the container format already stored.

ProRes 2160p, 17.6 GB

22.3% / 28.6%

ProRes 2160p, 17.6 GB

ProRes 1080p, 2.5 GB

14.7% / 24.4%

ProRes 1080p, 2.5 GB

DNxHD 1080p, 20.3 GB

21.1% / 21.5%

DNxHD 1080p, 20.3 GB

Percentages are gain against the disabled build and against the previous release respectively.

C++MultithreadingMXF / KLVMOV / MP4ProResDNxHDAVC-IntraJPEG 2000

My contribution

I designed and implemented the metadata-driven chunk mapping for MOV/MP4 and the index-table-driven equivalent for MXF, including the buffer sizing work that made the MXF path viable.

Context

The analysis engine runs multicore. A dedicated reader thread pulls essence data off the file into per-track buffers; separate threads build random access points from that data; each access point is then handed to whichever CPU is free. It is a clean producer/consumer arrangement and it scales with cores.

To build access points, the pipeline needed a map of where each chunk of essence lives in the file: its offset and its length. That map was constructed by the reader thread as it went, which meant building the map required reading the file.

The problem

Chunk offsets and lengths are not derived from the essence. They are recorded in the container. The pipeline was reading tens of gigabytes of video to recover information the file header already knew.

Locally that cost was mostly hidden by the page cache. Over object storage there is no page cache, and every unnecessary byte is a byte pulled across a network.

01

Building the chunk map from metadata

For MOV and MP4, the sample tables in the container describe exactly this: chunk offsets, sample sizes, the mapping from samples to chunks, and the edit list. I added a step that runs before the reader thread starts and constructs the chunk map directly from those tables, after which the tracks it covers can be dropped from the read list entirely.

For ProRes and DNxHD, that removes the essence read completely: the access points are built from the map, and the data itself is never touched at this stage.

  • Chunk offset tables, for where each chunk begins
  • Sample size and sample-to-chunk tables, for how long each chunk runs
  • Edit list, for how the samples map onto the presentation timeline
Building the chunk map from container metadataBefore: the reader thread reads essence data from the file in order to build a chunk map, which is then used to construct random access points. After: the chunk map is built directly from the container sample tables before reading begins, and tracks it covers are removed from the read list.BEFOREMedia filetens of GBReader threadChunk mapoffset + lengthAccess pointstasked to CPUsThe map is built by reading essence: gigabytes of video, to recover what the header already stored.AFTERContainer tablesalready in headerChunk mapbuilt from metadataAccess pointstasked to CPUsNo essence is read to build the map.ProRes / DNxHDtrack dropped from the readlist entirelyAVC-Intraparameter sets live in thebitstream, so the read isbounded to the first 5000bytes of each sampleChunk offsets and lengths are recorded in the container.They are not derived from the essence, so they never required reading it.

Before: build the chunk map by reading essence. After: build it from the container tables, and skip the read for codecs that do not need it.

02

The codec that would not cooperate

AVC-Intra could not be handled the same way. Building access points for it needs the sequence and picture parameter sets, and those are properties of the bitstream, not the container, and no amount of metadata reading recovers them.

But the H.264 specification places both at the start of each sample. So instead of skipping the read, I bounded it: the reader fetches only the first 5000 bytes of each sample and zero-fills the remainder of the buffer. The parameter sets are always inside that window, and the rest of every sample stays where it is.

03

MXF, where interleaving breaks the approach

MXF stores its essence interleaved across partitions, which defeats the MOV strategy: even if you know exactly which bytes you want, reading around them pulls in the neighbouring tracks you were trying to avoid.

The way through is the index table, together with the random index pack that locates each partition. From a partition pack you can derive where essence begins, accounting for the header and index byte counts, the partition offset, and the alignment grid the format uses to pad KLV elements onto byte boundaries. From the index table you can then locate an individual picture item within an edit unit, whether edit units are fixed-size or variable, and derive its length by computing the offset of the next element and subtracting.

Each picture item and its trailing filler becomes one chunk. A format-specific step then skips the filler when populating track buffers, so the padding that makes the arithmetic clean never reaches the analysis code.

Deriving essence position in MXFAn MXF file is a sequence of partitions located by the random index pack. Within a partition, essence offset is derived from the partition pack fields, and the position of an individual picture item inside an edit unit comes from the index table.RANDOM INDEX PACK — LOCATES EVERY PARTITIONPARTITION 1packhdridxessoffset · lengthsalignment gridPARTITION 2packhdridxessoffset · lengthsalignment gridPARTITION 3packhdridxessoffset · lengthsalignment gridESSENCE OFFSET — DERIVED, NOT SEARCHEDheader byte count + index byte count + partition offset, aligned to the format'sbyte-boundary grid. A partition carries essence only when its body identifier is non-zero.PICTURE POSITION WITHIN AN EDIT UNIT — FROM THE INDEX TABLEFixed-size edit units index arithmetically; variable-size ones use stream and slice offsets.Length comes from the offset of the next element. Each picture plus its filler is one chunk.

Essence offset is derived from the partition pack; picture position within an edit unit comes from the index table. Neither requires reading essence.

04

The buffer that had to shrink last

MXF analysis used a 256 KB internal read buffer. With interleaved data, a buffer that large reads neighbouring tracks by accident, so the obvious move was to shrink it.

I tried 10 KB, 50 KB and 100 KB. 50 KB was the best of the three and all of them were slower than the original 256 KB. The buffer was not the problem: while the pipeline still had to read essence speculatively to build chunk information, a large buffer was doing useful work by amortising round trips, and shrinking it just added round trips.

Only once chunk construction came from the index table did a small buffer make sense: with reads now precisely targeted, 64 KB at both the storage and format layers stopped pulling in data nothing would use. The ordering mattered: the same change that failed on its own succeeded once the reads around it became precise.

The hard part

Every codec needed a different answer

There was no single optimization here. ProRes and DNxHD in MOV could skip the read entirely. AVC-Intra could not, and needed a bounded read shaped by where H.264 puts its parameter sets. MXF could not use the MOV approach at all because of interleaving, and needed an index-driven derivation plus a filler-skipping read path plus a buffer change that only worked in that order.

The general principle — build the map from metadata, not from data — held throughout. Every concrete application of it was different, and each one had to be verified against real files rather than assumed from the specification.

Measured result

Measured on a 4-core, 16 GB cloud instance. Each row compares the optimization enabled against the same build with it disabled, and against the previous release.

  • Previous release
  • Optimization off
  • Optimization on

ProRes 2160p

17.6 GB · 3 min content

Previous release
796 s
Optimization off
731 s
Optimization on
568 s

ProRes 1080p

2.5 GB · 2 min 5 s content

Previous release
131 s
Optimization off
116 s
Optimization on
99 s

DNxHD 1080p

20.3 GB · 14 min 31 s content

Previous release
1301 s
Optimization off
1294 s
Optimization on
1021 s

Lower is faster. Each group is scaled independently, because the three files differ by an order of magnitude in duration.

Analysis time with the optimization enabled, against the same build with it disabled and against the previous release.

Internal benchmark · 4-core, 16 GB cloud instance

ProRes 2160p, 17.6 GB
796 s previous release, 731 s disabled, 568 s enabled
22.3% / 28.6%
ProRes 1080p, 2.5 GB
131 s previous release, 116 s disabled, 99 s enabled
14.7% / 24.4%
DNxHD 1080p, 20.3 GB
1301 s previous release, 1294 s disabled, 1021 s enabled
21.1% / 21.5%

Percentages are gain against the disabled build and against the previous release respectively.

Engineering takeaway

The fastest read is the one that never happens. Most of the win came from noticing the data was already described somewhere cheaper to read.