inkferro-core 0.1.0

Layout, text measurement, ANSI render, and frame-diff engine for inkferro — a Rust-backed, byte-for-byte drop-in for the ink terminal UI library.
Documentation
//! Criterion engine microbenches for `inkferro-core`.
//!
//! Three benches, each paired by id with a JS baseline in
//! `bench/js-baseline/` (the 5x gate, `bench/check-5x.sh`, pairs by exact id):
//!
//! - `pipeline`    — full render path: `apply(ops) -> fresh Arena ->
//!   render_to_string` on the shared 14-node styled frame (see `fixture.rs`).
//! - `wrap_ansi`   — `text::wrap_ansi::wrap_ansi` on a long styled string.
//! - `input_parse` — `input::Parser::feed` on a mixed key/kitty/paste byte
//!   script.
//!
//! Fixtures are read from `bench/fixtures/` ONCE, outside every timed loop. The
//! `pipeline` bench asserts its render byte-equals `pipeline_golden.txt` before
//! timing, so any drift between this fixture and the JS tree is caught here
//! rather than silently benching different work (see docs/perf-backlog.md).

use std::hint::black_box;

use criterion::{Criterion, criterion_group, criterion_main};
use inkferro_core::dom::{Arena, apply};
use inkferro_core::input::Parser;
use inkferro_core::render::render_to_string;
use inkferro_core::text::wrap_ansi::wrap_ansi;

#[path = "fixture.rs"]
mod fixture;

/// Absolute path to a file under `bench/fixtures/`, resolved from this crate's
/// manifest dir (`crates/inkferro-core` -> repo root -> `bench/fixtures`).
fn fixture_path(name: &str) -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../bench/fixtures")
        .join(name)
}

fn read_fixture_string(name: &str) -> String {
    std::fs::read_to_string(fixture_path(name))
        .unwrap_or_else(|e| panic!("read fixture {name}: {e}"))
}

fn read_fixture_bytes(name: &str) -> Vec<u8> {
    std::fs::read(fixture_path(name)).unwrap_or_else(|e| panic!("read fixture {name}: {e}"))
}

fn bench_pipeline(c: &mut Criterion) {
    // Precompute the op stream and golden OUTSIDE the timed loop.
    let ops = fixture::build_ops();
    let golden = read_fixture_string("pipeline_golden.txt");

    // Golden-frame guard: the same work, byte-for-byte, that the JS baseline
    // asserts. A drift here fails the bench before any timing is reported.
    {
        let mut arena = Arena::new();
        apply(&mut arena, &ops);
        let frame = render_to_string(&arena, fixture::FIXTURE_ROOT, fixture::FIXTURE_WIDTH);
        assert_eq!(frame, golden, "pipeline fixture drifted from golden frame");
    }

    c.bench_function("pipeline", |b| {
        b.iter(|| {
            // Fresh arena each iteration — mirrors ink's per-call reconciler
            // setup, which the JS `renderToString` baseline also pays.
            let mut arena = Arena::new();
            apply(&mut arena, black_box(&ops));
            let frame = render_to_string(&arena, fixture::FIXTURE_ROOT, fixture::FIXTURE_WIDTH);
            black_box(frame)
        });
    });
}

fn bench_wrap_ansi(c: &mut Criterion) {
    let input = read_fixture_string("wrap_input.txt");
    // 30 columns forces many wrap boundaries through the SGR state machine.
    let columns = 30usize;

    c.bench_function("wrap_ansi", |b| {
        b.iter(|| black_box(wrap_ansi(black_box(&input), black_box(columns))));
    });
}

fn bench_input_parse(c: &mut Criterion) {
    let script = read_fixture_bytes("input_script.bin");

    c.bench_function("input_parse", |b| {
        b.iter(|| {
            // Fresh parser per iteration so the segmenter starts cold; feed the
            // whole script in one chunk.
            let mut parser = Parser::new();
            black_box(parser.feed(black_box(&script)))
        });
    });
}

criterion_group!(benches, bench_pipeline, bench_wrap_ansi, bench_input_parse);
criterion_main!(benches);