Performance
Zero-copy guarantees, allocation budgets, benchmark suite, and tuning guidance for high-throughput EDIFACT processing.
On this page
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 frominputCow::Borrowedis returned for components that contain no release characters — zero copyCow::Ownedis 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
| API | Input | Peak memory | Notes |
|---|---|---|---|
from_bytes | &[u8] | O(1) — zero copy | Fastest; requires full buffer |
from_reader_collect | impl Read | O(n) segments | Eagerly collects all segments into Vec |
from_reader | impl Read | O(1) | Lazy iterator — one segment at a time |
from_bytes_windows | &[u8] | O(window) | One UNH..UNT window at a time |
message_windows_from_reader | impl Read | O(window) | Reader-based windows |
deserialize_messages_from_reader | impl Read | O(1) typed | Zero 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)
| Benchmark | Measures |
|---|---|
tokenizer/small | Tokenization throughput on a single message |
tokenizer/1mb | Tokenization throughput on a 1 MB interchange |
parser/small | Parse + collect on a single message |
parser/1mb | Parse + collect on 1 MB |
reader/1mb | from_reader_collect on 1 MB (reader overhead) |
reader/parse_reader_chunked | Reader path across read-buffer boundaries |
writer/sample_message | Serialize a message back to wire format |
validation/validate_structure_orders | Directory structure validation |
validation/validate_profile_custom_pack | A single ProfileRulePack |
validation/validate_profile_composed_packs | Composed packs (extend_from) |
validation/parse_large_message | Parse cost on a large message, for contrast |
validation/validate_large_message | Validation 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/1mbperf stat (Linux)
cargo build --release -p edifact-rs --example bench_large_message_memory
perf stat ./target/release/examples/bench_large_message_memoryMemory 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
| Scenario | Typical 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
- Streaming — memory-efficient streaming APIs
- Async Integration —
spawn_blockingpatterns - Error Reference —
E020 SegmentTooLongandReaderConfig