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,
}
#[doc(hidden)]
pub struct DiagnosticSession {
output: Option<(PathBuf, File)>,
}
impl DiagnosticSession {
#[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 })
}
#[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);
}
}
#[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);
}