zirium 0.2.0

Lossless textual MLIR syntax parsing and semantic processing
Documentation
use std::env;
use std::hint::black_box;
use std::process::Command;
use std::time::{Duration, Instant};

use crate::dialect::DialectRegistry;
use crate::parser::ParsedFile;

use super::{AnalysisCaches, AttributeSpec, Document, DocumentEditor, LoweringMode};

const SEED: u64 = 0x5a49_5249_554d_0015;

#[derive(Clone, Copy, Default)]
struct Stages {
    opening_validation: Duration,
    copy: Duration,
    edit: Duration,
    commit_validation: Duration,
    verifier: Duration,
    total: Duration,
    peak_bytes: usize,
}

impl std::ops::AddAssign for Stages {
    fn add_assign(&mut self, other: Self) {
        self.opening_validation += other.opening_validation;
        self.copy += other.copy;
        self.edit += other.edit;
        self.commit_validation += other.commit_validation;
        self.verifier += other.verifier;
        self.total += other.total;
        self.peak_bytes = self.peak_bytes.max(other.peak_bytes);
    }
}

fn fixture(operation_count: usize) -> Document {
    let mut source = String::with_capacity(operation_count * 32);
    for _ in 0..operation_count {
        source.push_str("\"bench.op\"() : () -> ()\n");
    }
    let parsed = ParsedFile::parse(source.as_bytes()).expect("benchmark fixture parses");
    let result =
        super::lower_with_dialect_registry(&parsed, LoweringMode::Strict, &DialectRegistry::EMPTY);
    assert!(result.diagnostics.is_empty());
    let document = result.document.expect("benchmark fixture lowers");
    document
        .validate_structure()
        .expect("fixture is structurally valid");
    document
        .verify_semantics(&DialectRegistry::EMPTY)
        .expect("fixture verifies");
    document
}

fn transaction(document: &mut Document, targets: &[super::OperationId]) -> Stages {
    let total_started = Instant::now();
    let started = Instant::now();
    document
        .validate_structure()
        .expect("transaction input stays structurally valid");
    let opening_validation = started.elapsed();

    let started = Instant::now();
    let working = document.edit_snapshot();
    let copy = started.elapsed();
    let mut editor = DocumentEditor {
        working,
        original: document,
        registry: &DialectRegistry::EMPTY,
        last_string: None,
        last_type: None,
        last_attribute: None,
    };

    let started = Instant::now();
    for operation in targets {
        editor
            .set_attribute(*operation, AttributeSpec::string("bench.tag", "edited"))
            .expect("benchmark edit succeeds");
    }
    let edit = started.elapsed();

    let started = Instant::now();
    editor
        .working
        .validate_structure()
        .expect("edited document stays structurally valid");
    let commit_validation = started.elapsed();

    let started = Instant::now();
    editor
        .working
        .verify_semantics_only(editor.registry)
        .expect("edited document verifies");
    let verifier = started.elapsed();

    editor.working.revision = editor.original.revision.wrapping_add(1);
    *editor
        .working
        .analyses
        .0
        .write()
        .expect("analysis cache lock is not poisoned") = AnalysisCaches::default();
    *editor.original = editor.working;

    Stages {
        opening_validation,
        copy,
        edit,
        commit_validation,
        verifier,
        total: total_started.elapsed(),
        peak_bytes: 0,
    }
}

fn measure(operation_count: usize, edit_count: usize, batched: bool) -> Stages {
    let mut document = fixture(operation_count);
    let targets = document.root_operations()[..edit_count].to_vec();
    let baseline = crate::benchmark_allocator::begin();

    let mut stages = Stages::default();
    if batched {
        stages += transaction(&mut document, &targets);
    } else {
        for target in &targets {
            stages += transaction(&mut document, std::slice::from_ref(target));
        }
    }
    stages.peak_bytes = crate::benchmark_allocator::finish(baseline);

    document
        .validate_structure()
        .expect("measured result validates");
    document
        .verify_semantics(&DialectRegistry::EMPTY)
        .expect("measured result verifies");
    assert!(
        targets
            .iter()
            .all(|target| document.attribute_id(*target, "bench.tag").is_some())
    );
    assert_eq!(
        document.revision(),
        if batched { 1 } else { edit_count as u64 }
    );
    black_box(document);
    stages
}

fn parse_list(name: &str, default: &[usize]) -> Vec<usize> {
    env::var(name).map_or_else(
        |_| default.to_vec(),
        |value| {
            value
                .split(',')
                .map(|part| part.parse().expect("benchmark dimensions are integers"))
                .collect()
        },
    )
}

fn command(program: &str, args: &[&str]) -> String {
    Command::new(program)
        .args(args)
        .output()
        .ok()
        .filter(|output| output.status.success())
        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
        .unwrap_or_else(|| "unknown".into())
}

fn median(values: &mut [u128]) -> u128 {
    values.sort_unstable();
    values[values.len() / 2]
}

fn field(samples: &[Stages], select: impl Fn(Stages) -> Duration) -> (u128, u128) {
    let mut values = samples
        .iter()
        .copied()
        .map(|sample| select(sample).as_nanos())
        .collect::<Vec<_>>();
    let min = *values.iter().min().unwrap();
    let max = *values.iter().max().unwrap();
    (median(&mut values), max - min)
}

#[cfg(debug_assertions)]
fn require_release() {
    panic!("run this benchmark with --release");
}

#[cfg(not(debug_assertions))]
fn require_release() {}

#[test]
#[ignore = "release-only edit transaction measurement; see processing-benchmarks.md"]
fn measure_edit_transaction_costs() {
    require_release();
    let smoke = env::var_os("ZIRIUM_EDIT_BENCH_SMOKE").is_some();
    let operations = parse_list(
        "ZIRIUM_EDIT_BENCH_OPERATIONS",
        if smoke { &[64] } else { &[1_000, 10_000] },
    );
    let edits = parse_list(
        "ZIRIUM_EDIT_BENCH_EDITS",
        if smoke { &[4] } else { &[1, 10, 100] },
    );
    let warmups = env::var("ZIRIUM_EDIT_BENCH_WARMUPS")
        .ok()
        .map_or(if smoke { 0 } else { 1 }, |value| value.parse().unwrap());
    let runs = env::var("ZIRIUM_EDIT_BENCH_RUNS")
        .ok()
        .map_or(if smoke { 1 } else { 3 }, |value| value.parse().unwrap());
    assert!(runs > 0);

    println!(
        "benchmark=edit-transactions profile=release seed=0x{SEED:016x} warmups={warmups} measured_runs={runs} rustc={} target={} os={}",
        command("rustc", &["-V"]),
        command("rustc", &["-vV"])
            .lines()
            .find_map(|line| line.strip_prefix("host: "))
            .unwrap_or("unknown"),
        command("uname", &["-srv"]),
    );
    println!(
        "operations,edits,mode,opening_validation_ns,opening_validation_spread_ns,copy_ns,copy_spread_ns,edit_ns,edit_spread_ns,commit_validation_ns,commit_validation_spread_ns,verifier_ns,verifier_spread_ns,total_ns,total_spread_ns,peak_allocated_bytes,peak_allocated_spread_bytes"
    );

    for operation_count in operations {
        for &edit_count in &edits {
            assert!(edit_count > 0 && edit_count <= operation_count);
            for (mode, batched) in [("batch", true), ("many", false)] {
                for _ in 0..warmups {
                    black_box(measure(operation_count, edit_count, batched));
                }
                let samples = (0..runs)
                    .map(|_| measure(operation_count, edit_count, batched))
                    .collect::<Vec<_>>();
                let opening = field(&samples, |sample| sample.opening_validation);
                let copy = field(&samples, |sample| sample.copy);
                let edit = field(&samples, |sample| sample.edit);
                let validation = field(&samples, |sample| sample.commit_validation);
                let verifier = field(&samples, |sample| sample.verifier);
                let total = field(&samples, |sample| sample.total);
                let mut peaks = samples
                    .iter()
                    .map(|sample| sample.peak_bytes as u128)
                    .collect::<Vec<_>>();
                let peak_min = *peaks.iter().min().unwrap();
                let peak_max = *peaks.iter().max().unwrap();
                let peak = median(&mut peaks);
                println!(
                    "{operation_count},{edit_count},{mode},{},{},{},{},{},{},{},{},{},{},{},{},{peak},{}",
                    opening.0,
                    opening.1,
                    copy.0,
                    copy.1,
                    edit.0,
                    edit.1,
                    validation.0,
                    validation.1,
                    verifier.0,
                    verifier.1,
                    total.0,
                    total.1,
                    peak_max - peak_min,
                );
            }
        }
    }
}