Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/bin/cairo-metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ description = "Walltime benchmark harness for the Cairo compiler."
anyhow.workspace = true
cairo-lang-compiler = { path = "../../cairo-lang-compiler", version = "=2.15.0" }
cairo-lang-lowering = { path = "../../cairo-lang-lowering", version = "=2.15.0" }
cairo-lang-sierra = { path = "../../cairo-lang-sierra", version = "=2.15.0" }
cairo-lang-sierra-to-casm = { path = "../../cairo-lang-sierra-to-casm", version = "=2.15.0" }
cairo-lang-sierra-type-size = { path = "../../cairo-lang-sierra-type-size", version = "=2.15.0" }
cairo-lang-utils = { path = "../../cairo-lang-utils", version = "=2.15.0", features = ["tracing"] }
clap.workspace = true
crossterm.workspace = true
Expand Down
20 changes: 18 additions & 2 deletions crates/bin/cairo-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A benchmark harness for the Cairo compiler.

## Features

- Walltime benchmarks using `cairo-compile`
- Multi-phase compilation benchmarks (diagnostics, sierra, casm, full)
- Robust statistics (median, MAD) for noisy CI environments
- SQLite storage for historical tracking
- Baseline comparison with TUI
Expand All @@ -19,6 +19,9 @@ cargo run -p cairo-metrics -- run
# Run specific benchmarks
cargo run -p cairo-metrics -- run --include corelib

# Run specific phases (default: all)
cargo run -p cairo-metrics -- run --phases sierra,casm

# Force builtin engine (for CI without hyperfine)
cargo run -p cairo-metrics -- run --engine builtin

Expand All @@ -29,7 +32,7 @@ cargo run -p cairo-metrics -- compare baseline-sha current-sha
cargo run -p cairo-metrics -- list

# Show history for a benchmark
cargo run -p cairo-metrics -- history corelib-clean-full-walltime
cargo run -p cairo-metrics -- history corelib-clean-sierra-walltime
```

## Adding Benchmarks
Expand Down Expand Up @@ -59,6 +62,19 @@ library = true
| `path` | yes | Path to Cairo project directory |
| `library` | no | Mark as library project (optional) |

## Compilation Phases

The `--phases` flag controls which compilation phases to benchmark:

| Phase | Description |
| ------------- | ------------------------------------------------- |
| `diagnostics` | Parse + semantic analysis + lowering (no codegen) |
| `sierra` | Cairo to Sierra IR generation |
| `casm` | Sierra to CASM (sierra compilation untimed) |
| `full` | Full pipeline: Cairo to Sierra to CASM |

All phases are run by default.

## Timing Engines

Two timing backends are available:
Expand Down
95 changes: 89 additions & 6 deletions crates/bin/cairo-metrics/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ use std::process::Command;
use std::time::{Duration, Instant};

use anyhow::Result;
use cairo_lang_compiler::db::RootDatabase;
use cairo_lang_compiler::diagnostics::DiagnosticsReporter;
use cairo_lang_compiler::project::setup_project;
use cairo_lang_compiler::{CompilerConfig, compile_cairo_project_at_path};
use cairo_lang_lowering::optimizations::config::Optimizations;
use cairo_lang_lowering::utils::InliningStrategy;
use cairo_lang_sierra::program::Program;
use cairo_lang_sierra_to_casm::compiler::SierraToCasmConfig;
use cairo_lang_sierra_to_casm::metadata::calc_metadata;
use cairo_lang_sierra_type_size::ProgramRegistryInfo;
use time::OffsetDateTime;
use tracing::info;

Expand Down Expand Up @@ -136,7 +144,7 @@ pub fn get_git_head() -> String {
}

/// Runs a single clean-build benchmark using the selected engine.
/// Returns None if the benchmark should be skipped.
/// Returns None if the benchmark should be skipped (e.g., Casm for library projects).
fn run_single(
config: &MetricsConfig,
bench: &Benchmark,
Expand All @@ -161,7 +169,7 @@ fn run_single(
}

/// Runs a single incremental-build benchmark using the selected engine.
/// Returns None if the benchmark should be skipped.
/// Returns None if the benchmark should be skipped (e.g., Casm for library projects).
fn run_single_patched(
config: &MetricsConfig,
bench: &Benchmark,
Expand Down Expand Up @@ -331,25 +339,30 @@ impl BuiltinBench {
)
}

/// Copies source to temp dir and runs the compilation.
/// Copies source to temp dir and runs the phase-appropriate compilation.
/// Returns None if the phase should be skipped.
fn run_in_temp(&self) -> Result<Option<Duration>> {
let temp_dir_handle = TempDir::new(&self.benchmark)?;
copy_dir(&self.path, temp_dir_handle.path())?;

match self.phase {
Phase::Diagnostics => self.run_diagnostics(temp_dir_handle.path()).map(Some),
Phase::Sierra => self.run_sierra(temp_dir_handle.path()).map(Some),
Phase::Casm => self.run_casm(temp_dir_handle.path()),
Phase::Full => self.run_full(temp_dir_handle.path()).map(Some),
}
}

/// Simulates incremental compilation: copy source, build to populate cache, apply patch,
/// then time the rebuild. Only the final rebuild is timed.
/// Returns None if the phase should be skipped (e.g., Casm for library projects).
fn run_in_temp_with_patch(&self, patch: &Patch) -> Result<Option<Duration>> {
let temp_dir_handle = TempDir::new(&self.benchmark)?;
copy_dir(&self.path, temp_dir_handle.path())?;
let temp_path = temp_dir_handle.path();

match self.phase {
Phase::Full => {
Phase::Diagnostics | Phase::Sierra | Phase::Casm | Phase::Full => {
// Initial build to populate cache.
compile_cairo_project_at_path(
temp_path,
Expand All @@ -366,8 +379,28 @@ impl BuiltinBench {
}
}

/// Runs full compilation: source to Sierra via library call.
fn run_full(&self, temp_path: &Path) -> Result<Duration> {
/// Runs diagnostics only: parse, semantic analysis, lowering diagnostics.
/// Does NOT generate Sierra IR.
fn run_diagnostics(&self, temp_path: &Path) -> Result<Duration> {
let start = Instant::now();

let mut db = RootDatabase::builder()
.with_optimizations(Optimizations::enabled_with_default_movable_functions(
InliningStrategy::Default,
))
.detect_corelib()
.build()?;

setup_project(&mut db, temp_path)?;

let mut reporter = DiagnosticsReporter::stderr();
reporter.ensure(&db)?;

Ok(start.elapsed())
}

/// Runs Sierra compilation: Cairo source to Sierra IR.
fn run_sierra(&self, temp_path: &Path) -> Result<Duration> {
let start = Instant::now();
compile_cairo_project_at_path(
temp_path,
Expand All @@ -377,6 +410,56 @@ impl BuiltinBench {
Ok(start.elapsed())
}

/// Runs CASM phase: compile Sierra first (untimed), then convert to CASM (timed).
/// Returns None if no Sierra program was produced.
fn run_casm(&self, temp_path: &Path) -> Result<Option<Duration>> {
// Compile to Sierra (untimed).
let sierra_program = compile_cairo_project_at_path(
temp_path,
CompilerConfig::default(),
InliningStrategy::Default,
)?;

// Convert Sierra to CASM (timed).
let duration = self.compile_sierra_to_casm(&sierra_program)?;
Ok(Some(duration))
}

/// Runs full pipeline: Cairo source to Sierra to CASM, all timed together.
fn run_full(&self, temp_path: &Path) -> Result<Duration> {
let start = Instant::now();

// Cairo to Sierra.
let sierra_program = compile_cairo_project_at_path(
temp_path,
CompilerConfig::default(),
InliningStrategy::Default,
)?;

// Sierra to CASM.
self.compile_sierra_to_casm(&sierra_program)?;

Ok(start.elapsed())
}

/// Compiles a Sierra program to CASM and returns the time taken.
fn compile_sierra_to_casm(&self, sierra_program: &Program) -> Result<Duration> {
let start = Instant::now();

let program_info = ProgramRegistryInfo::new(sierra_program)?;

let metadata = calc_metadata(sierra_program, &program_info, Default::default())?;

cairo_lang_sierra_to_casm::compiler::compile(
sierra_program,
&program_info,
&metadata,
SierraToCasmConfig { gas_usage_check: true, max_bytecode_size: usize::MAX },
)?;

Ok(start.elapsed())
}

/// Computes statistics and returns the benchmark result.
fn compute_result(&self, times: Vec<u64>) -> Result<BenchmarkResult> {
let timing = compute_stats(&times, self.runs, self.warmup)?;
Expand Down
67 changes: 58 additions & 9 deletions crates/bin/cairo-metrics/src/hyperfine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use tracing::info;
use crate::config::{Benchmark, MetricsConfig, Patch};
use crate::format::log_timing_stats;
use crate::model::{BenchmarkResult, TimingStats, format_display_name};
use crate::runner::cairo_compile_path;
use crate::stats::compute_mad;
use crate::{Metric, Phase, Scenario};

Expand Down Expand Up @@ -143,26 +142,76 @@ impl Bench {
})
}

/// Returns the cairo-compile command string.
/// Returns the cairo-compile command that outputs to stdout.
fn cairo_compile_cmd(&self) -> String {
format!("{} {}", cairo_compile_path().display(), self.temp_dir.display())
format!(
// TODO: Figure out how to use cargo run -p here without incurring cargo overhead.
"target/debug/cairo-compile {}",
self.temp_dir.display()
)
}

/// Returns the cairo-compile command that outputs Sierra to a file.
fn cairo_compile_to_file_cmd(&self) -> String {
format!(
// TODO: Figure out how to use cargo run -p here without incurring cargo overhead.
"target/debug/cairo-compile {} {}",
self.temp_dir.display(),
self.sierra_output_path().display()
)
}

/// Returns the timed command for full compilation.
/// Returns the sierra-compile command using pre-built binary.
fn sierra_compile_cmd(&self) -> String {
format!(
// TODO: Figure out how to use cargo run -p here without incurring cargo overhead.
"target/debug/sierra-compile {} {}",
self.sierra_output_path().display(),
self.casm_output_path().display()
)
}

fn sierra_output_path(&self) -> PathBuf {
self.temp_dir.join("output.sierra")
}

fn casm_output_path(&self) -> PathBuf {
self.temp_dir.join("output.casm")
}

/// Returns the timed command for the given phase.
fn phase_command(&self) -> String {
match self.phase {
Phase::Full => self.cairo_compile_cmd(),
Phase::Diagnostics => todo!("diagnostics-only not supported via shell"),
Phase::Sierra => self.cairo_compile_cmd(),
Phase::Casm => self.sierra_compile_cmd(),
Phase::Full => format!("{} && {}", self.cairo_compile_to_file_cmd(), self.sierra_compile_cmd()),
}
}

fn build_clean_config(&self) -> Config {
let temp = self.temp_dir.display();
let src = self.path.display();

// Copy source to temp directory.
let setup = format!("mkdir -p {0} && cp -r {1}/* {0}/", temp, src);
// Remove any previous output before each run.
let prepare = format!("rm -rf {0}/*.sierra", temp);
// Build required binaries in setup.
let setup = match self.phase {
Phase::Diagnostics => todo!("diagnostics-only not supported via shell"),
Phase::Sierra => format!(
"cargo build --bin cairo-compile && mkdir -p {0} && cp -r {1}/* {0}/",
temp, src
),
Phase::Casm | Phase::Full => format!(
"cargo build --bin cairo-compile --bin sierra-compile && mkdir -p {0} && cp -r {1}/* {0}/",
temp, src
),
};

// For Casm phase: prepare generates Sierra (untimed), command times sierra-compile.
let prepare = match self.phase {
Phase::Casm => self.cairo_compile_to_file_cmd(),
_ => "true".to_string(),
};

let command = self.phase_command();

Config { runs: self.runs, warmup: self.warmup, setup, prepare, command }
Expand Down
20 changes: 17 additions & 3 deletions crates/bin/cairo-metrics/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,32 @@ impl Metric {
/// Compilation phase to measure.
#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Hash)]
pub enum Phase {
/// Full compilation pipeline: source to Sierra (cairo-compile).
/// Diagnostics validation (currently same as Sierra with library calls).
Diagnostics,
/// Cairo to Sierra IR generation.
#[default]
Sierra,
/// Sierra to CASM code generation.
Casm,
/// Full compilation pipeline: source to Sierra to CASM.
Full,
}

impl Phase {
/// Returns the phase name as a string.
pub fn as_str(&self) -> &str {
match self {
Phase::Diagnostics => "diagnostics",
Phase::Sierra => "sierra",
Phase::Casm => "casm",
Phase::Full => "full",
}
}

/// Returns all available phases.
pub fn all() -> Vec<Phase> {
vec![Phase::Diagnostics, Phase::Sierra, Phase::Casm, Phase::Full]
}
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -299,8 +313,8 @@ enum Commands {
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Scenario::defaults())]
scenarios: Vec<Scenario>,

/// Compilation phases to measure.
#[arg(long, value_enum, value_delimiter = ',', default_value = "full")]
/// Compilation phases to measure: diagnostics, sierra, casm, or full.
#[arg(long, value_enum, value_delimiter = ',', default_values_t = Phase::all())]
phases: Vec<Phase>,

/// Metrics to measure.
Expand Down
23 changes: 0 additions & 23 deletions crates/bin/cairo-metrics/src/runner.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,8 @@
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::{env, fs};

use anyhow::{Context, Result};

static CAIRO_COMPILE_PATH: OnceLock<PathBuf> = OnceLock::new();

/// Returns the path to cairo-compile binary.
/// First checks for sibling binary (same dir as cairo-metrics), then falls back to PATH.
pub fn cairo_compile_path() -> &'static Path {
CAIRO_COMPILE_PATH.get_or_init(|| {
// Try sibling binary first (for local development).
if let Ok(exe) = env::current_exe()
&& let Some(dir) = exe.parent()
{
let sibling = dir.join("cairo-compile");
if sibling.exists() {
tracing::debug!("Using sibling cairo-compile: {}", sibling.display());
return sibling;
}
}
// Fall back to PATH lookup.
tracing::debug!("Using cairo-compile from PATH");
PathBuf::from("cairo-compile")
})
}

/// Temporary directory with automatic cleanup.
pub struct TempDir(PathBuf);

Expand Down
Loading