mdstream 0.3.0

Streaming-first Markdown middleware for LLM output (committed + pending blocks, render-agnostic).
Documentation
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use mdstream::{MdStream, Options};
use std::hint::black_box;

const BASIC_MANY_BLOCKS: &str =
    include_str!("../tests/fixtures/streamdown_bench/basic_many_blocks_100.md");
const LARGE_TABLE: &str =
    include_str!("../tests/fixtures/streamdown_bench/table_large_100_rows.md");
const MIXED_CONTENT: &str =
    include_str!("../tests/fixtures/streamdown_bench/mixed_content_realistic.md");

fn large_code_fence() -> String {
    let mut text = String::from("```rust\n");
    for i in 0..1_000 {
        text.push_str("fn generated_");
        text.push_str(&i.to_string());
        text.push_str("() { println!(\"line ");
        text.push_str(&i.to_string());
        text.push_str("\"); }\n");
    }
    text.push_str("```\n");
    text
}

fn chunk_whole(text: &str) -> Vec<String> {
    vec![text.to_string()]
}

fn chunk_lines(text: &str) -> Vec<String> {
    text.split_inclusive('\n').map(str::to_string).collect()
}

fn chunk_chars(text: &str) -> Vec<String> {
    text.chars().map(String::from).collect()
}

fn fnv1a64(s: &str) -> u64 {
    let mut h: u64 = 0xcbf29ce484222325;
    for &b in s.as_bytes() {
        h ^= u64::from(b);
        h = h.wrapping_mul(0x100000001b3);
    }
    h
}

fn xorshift64(state: &mut u64) -> u64 {
    let mut x = *state;
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    *state = x;
    x
}

fn chunk_pseudo_random(text: &str, seed_label: &str, max_bytes: usize) -> Vec<String> {
    assert!(max_bytes > 0);

    let mut state = fnv1a64(seed_label);
    let mut chunks = Vec::new();
    let mut start = 0usize;

    while start < text.len() {
        let want = (xorshift64(&mut state) as usize % max_bytes) + 1;
        let mut end = (start + want).min(text.len());
        while end < text.len() && !text.is_char_boundary(end) {
            end += 1;
        }
        chunks.push(text[start..end].to_string());
        start = end;
    }

    chunks
}

fn run_owned(chunks: &[String]) -> usize {
    let mut stream = MdStream::new(Options::default());
    let mut observed = 0usize;

    for chunk in chunks {
        let update = stream.append(chunk);
        observed = observed.wrapping_add(update.committed.len());
        if let Some(pending) = update.pending {
            observed = observed.wrapping_add(pending.display_or_raw().len());
        }
        observed = observed.wrapping_add(update.invalidated.len());
    }

    let update = stream.finalize();
    observed = observed.wrapping_add(update.committed.len());
    observed = observed.wrapping_add(update.invalidated.len());
    observed
}

fn run_borrowed(chunks: &[String]) -> usize {
    let mut stream = MdStream::new(Options::default());
    let mut observed = 0usize;

    for chunk in chunks {
        let update = stream.append_ref(chunk);
        observed = observed.wrapping_add(update.committed.len());
        if let Some(pending) = update.pending {
            observed = observed.wrapping_add(pending.display_or_raw().len());
        }
        observed = observed.wrapping_add(update.invalidated.len());
    }

    let update = stream.finalize_ref();
    observed = observed.wrapping_add(update.committed.len());
    observed = observed.wrapping_add(update.invalidated.len());
    observed
}

fn streaming_benchmarks(c: &mut Criterion) {
    let scenarios = vec![
        ("many_blocks_whole", chunk_whole(BASIC_MANY_BLOCKS)),
        ("many_blocks_lines", chunk_lines(BASIC_MANY_BLOCKS)),
        (
            "mixed_random_chunks",
            chunk_pseudo_random(MIXED_CONTENT, "mixed_content", 40),
        ),
        ("large_table_lines", chunk_lines(LARGE_TABLE)),
        ("large_table_chars", chunk_chars(LARGE_TABLE)),
        {
            let text = large_code_fence();
            ("large_code_fence_lines", chunk_lines(&text))
        },
        {
            let text = large_code_fence();
            (
                "large_code_fence_random",
                chunk_pseudo_random(&text, "large_code_fence", 80),
            )
        },
    ];

    let mut group = c.benchmark_group("streaming");

    for (name, chunks) in scenarios {
        let bytes: u64 = chunks.iter().map(|chunk| chunk.len() as u64).sum();
        group.throughput(Throughput::Bytes(bytes));

        group.bench_with_input(
            BenchmarkId::new("append_owned", name),
            &chunks,
            |b, chunks| {
                b.iter_batched(
                    || chunks.clone(),
                    |chunks| black_box(run_owned(&chunks)),
                    BatchSize::SmallInput,
                );
            },
        );

        group.bench_with_input(
            BenchmarkId::new("append_borrowed", name),
            &chunks,
            |b, chunks| {
                b.iter_batched(
                    || chunks.clone(),
                    |chunks| black_box(run_borrowed(&chunks)),
                    BatchSize::SmallInput,
                );
            },
        );
    }

    group.finish();
}

criterion_group!(benches, streaming_benchmarks);
criterion_main!(benches);