Performance

Zero-copy guarantees, allocation budgets, benchmark suite, and tuning guidance for high-throughput EDIFACT processing.

On this page
  1. Zero-copy parsing
  2. SmallVec inline storage
  3. from_reader — O(1) memory streaming
  4. edifact_deserialize_owned — zero-batch typed extraction
  5. WriterEmitter — zero-alloc output
  6. Parsing modes and memory comparison
  7. Benchmarks
    1. Benchmark groups (Criterion)
  8. Fuzz testing
  9. Profiling tips
    1. flamegraph
    2. perf stat (Linux)
    3. Memory usage with heaptrack
  10. Tuning ReaderConfig
  11. Memory budget summary
  12. Next steps

edifact-rs is designed for high-throughput, low-allocation EDIFACT processing. This guide explains the key design decisions, how to measure them, and how to tune for production workloads.


Zero-copy parsing

from_bytes(input: &[u8]) is the fastest entry point. It yields Segment<'a> values that borrow directly from the input slice:

let input: &[u8] = b"BGM+220+PO-4711+9'";
// No heap allocations for the segment itself
let seg = edifact_rs::from_bytes(input).next().unwrap().unwrap();
assert_eq!(seg.tag, "BGM");
  • Segment<'a> borrows the tag, element values, and component slices from input
  • Cow::Borrowed is returned for components that contain no release characters — zero copy
  • Cow::Owned is returned only when a component contains escape sequences (e.g. ?++)

For a 1 MB interchange, from_bytes typically completes in < 5 ms on a modern CPU.


SmallVec inline storage

Elements and components use SmallVec<[T; 4]> (with the union feature enabled). This means segments with ≤ 4 components per element avoid all heap allocations entirely — which covers the large majority of real-world EDIFACT segments.


from_reader — O(1) memory streaming

from_reader(reader) is the reader-based API with minimal memory overhead:

use edifact_rs::from_reader;

let f = std::fs::File::open("large.edi")?;
for seg in from_reader(f) {
    let seg = seg?;  // OwnedSegment (segment-sized allocation only)
    // process and drop — O(1) peak memory
}
# Ok::<(), edifact_rs::EdifactError>(())

It holds at most one OwnedSegment in memory at a time. Use from_reader instead of from_reader_collect when processing interchanges that are larger than available RAM.


edifact_deserialize_owned — zero-batch typed extraction

deserialize_messages_from_reader::<T, R> never materializes a Vec<Segment<'_>> for the whole interchange. It uses edifact_deserialize_owned internally to deserialize each UNH..UNT window as a typed value, then immediately drops the window segments:

use edifact_rs::{deserialize_messages_from_reader, EdifactDeserialize};

# #[derive(Debug, EdifactDeserialize)]
# struct OrderMessage {}
let f = std::fs::File::open("interchange.edi")?;
for order in deserialize_messages_from_reader::<OrderMessage, _>(f) {
    let order = order?;
    // the window's raw segments have already been freed
}
# Ok::<(), edifact_rs::EdifactError>(())

This achieves O(1) peak memory even when an interchange contains thousands of messages.


WriterEmitter — zero-alloc output

WriterEmitter<W> writes EDIFACT directly to any std::io::Write without accumulating a Vec<u8>. It implements the EventEmitter trait, whose single emit method takes an EdifactEvent; each event is written out immediately:

use edifact_rs::{EdifactEvent, EventEmitter, WriterEmitter};

let mut out = Vec::<u8>::new();
let mut emitter = WriterEmitter::new(&mut out);
emitter.emit(EdifactEvent::StartSegment { tag: "BGM" })?;
emitter.emit(EdifactEvent::Element { value: "220" })?;
emitter.emit(EdifactEvent::Element { value: "PO-4711" })?;
emitter.emit(EdifactEvent::EndSegment)?;
assert_eq!(out, b"BGM+220+PO-4711'");
# Ok::<(), edifact_rs::EdifactError>(())

Use WriterEmitter over to_bytes or Writer when generating large interchanges that you want to pipe directly to a file or socket.


Parsing modes and memory comparison

APIInputPeak memoryNotes
from_bytes&[u8]O(1) — zero copyFastest; requires full buffer
from_reader_collectimpl ReadO(n) segmentsEagerly collects all segments into Vec
from_readerimpl ReadO(1)Lazy iterator — one segment at a time
from_bytes_windows&[u8]O(window)One UNH..UNT window at a time
message_windows_from_readerimpl ReadO(window)Reader-based windows
deserialize_messages_from_readerimpl ReadO(1) typedZero raw-segment buffer

Benchmarks

The benchmark suite uses both divan (micro-benchmarks) and Criterion (statistical):

# Divan micro-benchmarks
cargo bench -p edifact-rs --bench bench_core

# Criterion statistical benchmarks (with HTML reports)
cargo bench -p edifact-rs --bench bench_criterion

Criterion outputs are saved to target/criterion/. Open target/criterion/report/index.html in a browser for a full comparison dashboard.

Benchmark groups (Criterion)

BenchmarkMeasures
tokenizer/smallTokenization throughput on a single message
tokenizer/1mbTokenization throughput on a 1 MB interchange
parser/smallParse + collect on a single message
parser/1mbParse + collect on 1 MB
reader/1mbfrom_reader_collect on 1 MB (reader overhead)
reader/parse_reader_chunkedReader path across read-buffer boundaries
writer/sample_messageSerialize a message back to wire format
validation/validate_structure_ordersDirectory structure validation
validation/validate_profile_custom_packA single ProfileRulePack
validation/validate_profile_composed_packsComposed packs (extend_from)
validation/parse_large_messageParse cost on a large message, for contrast
validation/validate_large_messageValidation cost on the same message

Fuzz testing

The crate integrates bolero for fuzz/property testing:

# Run the bolero harness (requires cargo-bolero)
cargo bolero test -p edifact-rs bolero_harness

# With libFuzzer (requires nightly + cargo-fuzz)
cargo +nightly bolero test -p edifact-rs bolero_harness --engine libfuzzer

The harness feeds arbitrary bytes to from_bytes and asserts that the parser never panics — only returns Ok or Err(EdifactError).


Profiling tips

flamegraph

cargo flamegraph -p edifact-rs --bench bench_criterion -- \
  --bench --profile-time=10 parser/1mb

perf stat (Linux)

cargo build --release -p edifact-rs --example bench_large_message_memory
perf stat ./target/release/examples/bench_large_message_memory

Memory usage with heaptrack

cargo build --release -p edifact-rs --example bench_large_message_memory
heaptrack ./target/release/examples/bench_large_message_memory
heaptrack_gui heaptrack.*.gz

Tuning ReaderConfig

The reader-based APIs accept a ReaderConfig that controls the DOS guard:

use edifact_rs::{from_bufread_stream_with_config, ReaderConfig};

// The builder methods leave every budget you do not name at its default.
let config = ReaderConfig::default().max_segment_bytes(256 * 1024);
let cursor = std::io::Cursor::new(b"...");
let _iter = from_bufread_stream_with_config(cursor, config);

Setting max_segment_bytes too low will cause E020 SegmentTooLong on large but legitimate segments (e.g. free-text FTX segments). The default is 64 KiB, which comfortably covers real-world EDIFACT; raise it only if your trading partners genuinely send larger segments.

The whole-input budgets — max_segments, max_messages, and max_input_bytes — are unset by default and raise E036 LimitExceeded when tripped. See Parsing → DoS hardening.


Memory budget summary

ScenarioTypical peak allocation
Parse 1 MB interchange, collect all~3–4× input size (segments + SmallVec inlining)
Parse 1 MB, from_reader (streaming)~4 KB (one segment buffer)
Typed streaming, deserialize_messages_from_reader~4 KB + sizeof(T)
Write 1 MB interchange via WriterEmitter~8 KB (internal buffer)
Write 1 MB interchange via ser::to_bytes~1× output size

Next steps