cargo-rail 0.19.0

Graph-aware CI, dependency unification, changesets-style releases, and crate split/sync for Rust workspaces
Documentation
//! Explicit, out-of-band diagnostic counters for performance workloads.

use std::fs::File;
use std::io::Write as _;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};

use serde::Serialize;

use crate::error::{RailError, RailResult};

const SCHEMA_VERSION: u32 = 5;

static COUNTERS: OnceLock<Counters> = OnceLock::new();

struct Counters {
  snapshot_id: OnceLock<String>,
  cargo_metadata_loads: AtomicU64,
  cargo_metadata_cache_hits: AtomicU64,
  target_view_loads: AtomicU64,
  hash_operations: AtomicU64,
  hash_input_bytes: AtomicU64,
  hashed_file_bytes_read: AtomicU64,
  git_subprocesses: AtomicU64,
  git_object_reads: AtomicU64,
  git_object_read_batches: AtomicU64,
  git_path_change_reads: AtomicU64,
  git_path_change_batches: AtomicU64,
  graph_traversals: AtomicU64,
  graph_node_visits: AtomicU64,
  graph_edge_visits: AtomicU64,
  cas_objects_written: AtomicU64,
  cas_bytes_written: AtomicU64,
  cas_bytes_read: AtomicU64,
  cas_bytes_restored: AtomicU64,
  hermetic_cargo_executions: AtomicU64,
  hermetic_compiler_units: AtomicU64,
  hermetic_fetch_executions: AtomicU64,
  hermetic_cargo_probes: AtomicU64,
  hermetic_rustc_probes: AtomicU64,
  hermetic_rustdoc_probes: AtomicU64,
  hermetic_platform_probes: AtomicU64,
}

impl Counters {
  const fn new() -> Self {
    Self {
      snapshot_id: OnceLock::new(),
      cargo_metadata_loads: AtomicU64::new(0),
      cargo_metadata_cache_hits: AtomicU64::new(0),
      target_view_loads: AtomicU64::new(0),
      hash_operations: AtomicU64::new(0),
      hash_input_bytes: AtomicU64::new(0),
      hashed_file_bytes_read: AtomicU64::new(0),
      git_subprocesses: AtomicU64::new(0),
      git_object_reads: AtomicU64::new(0),
      git_object_read_batches: AtomicU64::new(0),
      git_path_change_reads: AtomicU64::new(0),
      git_path_change_batches: AtomicU64::new(0),
      graph_traversals: AtomicU64::new(0),
      graph_node_visits: AtomicU64::new(0),
      graph_edge_visits: AtomicU64::new(0),
      cas_objects_written: AtomicU64::new(0),
      cas_bytes_written: AtomicU64::new(0),
      cas_bytes_read: AtomicU64::new(0),
      cas_bytes_restored: AtomicU64::new(0),
      hermetic_cargo_executions: AtomicU64::new(0),
      hermetic_compiler_units: AtomicU64::new(0),
      hermetic_fetch_executions: AtomicU64::new(0),
      hermetic_cargo_probes: AtomicU64::new(0),
      hermetic_rustc_probes: AtomicU64::new(0),
      hermetic_rustdoc_probes: AtomicU64::new(0),
      hermetic_platform_probes: AtomicU64::new(0),
    }
  }

  fn snapshot(&self) -> CounterSnapshot {
    CounterSnapshot {
      schema_version: SCHEMA_VERSION,
      snapshot_id: self.snapshot_id.get().cloned(),
      cargo_metadata_loads: self.cargo_metadata_loads.load(Ordering::Relaxed),
      cargo_metadata_cache_hits: self.cargo_metadata_cache_hits.load(Ordering::Relaxed),
      target_view_loads: self.target_view_loads.load(Ordering::Relaxed),
      hash_operations: self.hash_operations.load(Ordering::Relaxed),
      hash_input_bytes: self.hash_input_bytes.load(Ordering::Relaxed),
      hashed_file_bytes_read: self.hashed_file_bytes_read.load(Ordering::Relaxed),
      git_subprocesses: self.git_subprocesses.load(Ordering::Relaxed),
      git_object_reads: self.git_object_reads.load(Ordering::Relaxed),
      git_object_read_batches: self.git_object_read_batches.load(Ordering::Relaxed),
      git_path_change_reads: self.git_path_change_reads.load(Ordering::Relaxed),
      git_path_change_batches: self.git_path_change_batches.load(Ordering::Relaxed),
      graph_traversals: self.graph_traversals.load(Ordering::Relaxed),
      graph_node_visits: self.graph_node_visits.load(Ordering::Relaxed),
      graph_edge_visits: self.graph_edge_visits.load(Ordering::Relaxed),
      cas_objects_written: self.cas_objects_written.load(Ordering::Relaxed),
      cas_bytes_written: self.cas_bytes_written.load(Ordering::Relaxed),
      cas_bytes_read: self.cas_bytes_read.load(Ordering::Relaxed),
      cas_bytes_restored: self.cas_bytes_restored.load(Ordering::Relaxed),
      hermetic_cargo_executions: self.hermetic_cargo_executions.load(Ordering::Relaxed),
      hermetic_compiler_units: self.hermetic_compiler_units.load(Ordering::Relaxed),
      hermetic_fetch_executions: self.hermetic_fetch_executions.load(Ordering::Relaxed),
      hermetic_cargo_probes: self.hermetic_cargo_probes.load(Ordering::Relaxed),
      hermetic_rustc_probes: self.hermetic_rustc_probes.load(Ordering::Relaxed),
      hermetic_rustdoc_probes: self.hermetic_rustdoc_probes.load(Ordering::Relaxed),
      hermetic_platform_probes: self.hermetic_platform_probes.load(Ordering::Relaxed),
    }
  }
}

#[derive(Serialize)]
struct CounterSnapshot {
  schema_version: u32,
  snapshot_id: Option<String>,
  cargo_metadata_loads: u64,
  cargo_metadata_cache_hits: u64,
  target_view_loads: u64,
  hash_operations: u64,
  hash_input_bytes: u64,
  hashed_file_bytes_read: u64,
  git_subprocesses: u64,
  git_object_reads: u64,
  git_object_read_batches: u64,
  git_path_change_reads: u64,
  git_path_change_batches: u64,
  graph_traversals: u64,
  graph_node_visits: u64,
  graph_edge_visits: u64,
  cas_objects_written: u64,
  cas_bytes_written: u64,
  cas_bytes_read: u64,
  cas_bytes_restored: u64,
  hermetic_cargo_executions: u64,
  hermetic_compiler_units: u64,
  hermetic_fetch_executions: u64,
  hermetic_cargo_probes: u64,
  hermetic_rustc_probes: u64,
  hermetic_rustdoc_probes: u64,
  hermetic_platform_probes: u64,
}

/// Active diagnostic session for one cargo-rail process.
#[doc(hidden)]
pub struct DiagnosticSession {
  output: Option<(PathBuf, File)>,
}

impl DiagnosticSession {
  /// Enable counters when an explicit output path was supplied.
  #[doc(hidden)]
  pub fn start(output: Option<PathBuf>) -> RailResult<Self> {
    let output = match output {
      Some(path) => {
        let file = File::options()
          .write(true)
          .create_new(true)
          .open(&path)
          .map_err(|error| {
            RailError::message(format!(
              "failed to reserve diagnostic counter file '{}': {error}",
              path.display()
            ))
          })?;
        let _ = COUNTERS.get_or_init(Counters::new);
        Some((path, file))
      }
      None => None,
    };
    Ok(Self { output })
  }

  /// Write the final counter snapshot without touching normal stdout or stderr.
  #[doc(hidden)]
  pub fn finish(self) -> RailResult<()> {
    let Some((output, mut file)) = self.output else {
      return Ok(());
    };
    let Some(counters) = COUNTERS.get() else {
      return Ok(());
    };

    let mut encoded = serde_json::to_vec_pretty(&counters.snapshot())
      .map_err(|error| RailError::message(format!("failed to serialize diagnostic counters: {error}")))?;
    encoded.push(b'\n');
    file.write_all(&encoded).map_err(|error| {
      RailError::message(format!(
        "failed to write diagnostic counters to '{}': {error}",
        output.display()
      ))
    })?;
    file.sync_all().map_err(|error| {
      RailError::message(format!(
        "failed to sync diagnostic counters to '{}': {error}",
        output.display()
      ))
    })
  }
}

fn amount(value: usize) -> u64 {
  u64::try_from(value).unwrap_or(u64::MAX)
}

fn add(counter: fn(&Counters) -> &AtomicU64, value: u64) {
  if let Some(counters) = COUNTERS.get() {
    counter(counters).fetch_add(value, Ordering::Relaxed);
  }
}

pub(crate) fn record_cargo_metadata_load(target_view: bool) {
  add(|counters| &counters.cargo_metadata_loads, 1);
  if target_view {
    add(|counters| &counters.target_view_loads, 1);
  }
}

/// Record the authoritative workspace identity for diagnostic-only inspection.
#[doc(hidden)]
pub fn record_snapshot_id(snapshot_id: String) {
  if let Some(counters) = COUNTERS.get() {
    let _ = counters.snapshot_id.set(snapshot_id);
  }
}

pub(crate) fn record_cargo_metadata_cache_hit() {
  add(|counters| &counters.cargo_metadata_cache_hits, 1);
}

pub(crate) fn record_hash(input_bytes: usize) {
  record_hash_operation();
  record_hash_input_bytes(input_bytes);
}

pub(crate) fn record_hash_operation() {
  add(|counters| &counters.hash_operations, 1);
}

pub(crate) fn record_hash_input_bytes(input_bytes: usize) {
  add(|counters| &counters.hash_input_bytes, amount(input_bytes));
}

pub(crate) fn record_hashed_file_bytes_read(bytes: usize) {
  add(|counters| &counters.hashed_file_bytes_read, amount(bytes));
}

pub(crate) fn record_git_subprocess() {
  add(|counters| &counters.git_subprocesses, 1);
}

pub(crate) fn record_git_object_read_batch(objects: usize) {
  add(|counters| &counters.git_object_reads, amount(objects));
  add(|counters| &counters.git_object_read_batches, 1);
}

pub(crate) fn record_git_path_change_batch(commits: usize) {
  add(|counters| &counters.git_path_change_reads, amount(commits));
  add(|counters| &counters.git_path_change_batches, 1);
}

pub(crate) fn record_graph_traversal(node_visits: usize, edge_visits: usize) {
  add(|counters| &counters.graph_traversals, 1);
  add(|counters| &counters.graph_node_visits, amount(node_visits));
  add(|counters| &counters.graph_edge_visits, amount(edge_visits));
}

pub(crate) fn record_cas_write(bytes: u64, objects: u64) {
  add(|counters| &counters.cas_bytes_written, bytes);
  add(|counters| &counters.cas_objects_written, objects);
}

pub(crate) fn record_cas_read(bytes: u64) {
  add(|counters| &counters.cas_bytes_read, bytes);
}

#[cfg(any(unix, windows, test))]
pub(crate) fn record_cas_restore(bytes: u64) {
  add(|counters| &counters.cas_bytes_restored, bytes);
}

pub(crate) fn record_hermetic_cargo_execution() {
  add(|counters| &counters.hermetic_cargo_executions, 1);
}

pub(crate) fn record_hermetic_compiler_units(units: usize) {
  add(|counters| &counters.hermetic_compiler_units, amount(units));
}

pub(crate) fn record_hermetic_fetch_execution() {
  add(|counters| &counters.hermetic_fetch_executions, 1);
}

pub(crate) fn record_hermetic_toolchain_probe(program: &std::ffi::OsStr) {
  let name = std::path::Path::new(program)
    .file_stem()
    .and_then(std::ffi::OsStr::to_str)
    .unwrap_or_default();
  match name {
    "cargo" => add(|counters| &counters.hermetic_cargo_probes, 1),
    "rustc" => add(|counters| &counters.hermetic_rustc_probes, 1),
    "rustdoc" => add(|counters| &counters.hermetic_rustdoc_probes, 1),
    _ => {}
  }
}

#[cfg(target_os = "macos")]
pub(crate) fn record_hermetic_platform_probe() {
  add(|counters| &counters.hermetic_platform_probes, 1);
}