use super::note;
use crate::config::{Config, SummaryStyle};
use crate::util::{format_duration, write_atomic};
use bytesize::ByteSize;
use eyre::Result;
use log::warn;
use mbx_cache_core::AgentStats;
use serde::Serialize;
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Serialize)]
pub(super) struct StatsReport {
version: u8,
session_duration_ns: u64,
lookups: u64,
hits: u64,
misses: u64,
unconsulted: u64,
compiler_invocations_avoided: u64,
estimated_compiler_duration_avoided_ns: u64,
compiler: BTreeMap<String, CompilerStatsReport>,
slow_compilations: Vec<SlowCompilationReport>,
verifications: u64,
divergences: u64,
prefetched_actions: u64,
predictions_loaded: u64,
prefetch_runs: u64,
bypasses: BTreeMap<String, u64>,
downloaded_bytes: u64,
uploaded_bytes: u64,
background_uploads: u64,
background_upload_failures: u64,
remote_blob_pack_uploads: u64,
remote_blob_pack_upload_blobs: u64,
upload_drain_duration_ns: u64,
stored_bytes: u64,
restored_output_files: u64,
restored_output_bytes: u64,
reflinked_output_files: u64,
reflinked_output_bytes: u64,
copied_output_files: u64,
copied_output_bytes: u64,
reused_output_files: u64,
reused_output_bytes: u64,
remote_failures: u64,
remote_manifest_lookups: u64,
remote_action_lookups: u64,
remote_blob_requests: u64,
remote_blob_pack_requests: u64,
remote_blob_pack_blobs: u64,
remote_manifest_lookup_duration_ns: u64,
remote_action_lookup_duration_ns: u64,
remote_blob_transfer_duration_ns: u64,
local_cas_write_duration_ns: u64,
prefetch_duration_ns: u64,
materialization_duration_ns: u64,
}
#[derive(Serialize)]
struct CompilerStatsReport {
invocations: u64,
duration_ns: u64,
}
#[derive(Serialize)]
struct SlowCompilationReport {
crate_name: String,
duration_ns: u64,
}
impl From<&AgentStats> for StatsReport {
fn from(stats: &AgentStats) -> Self {
Self {
version: 4,
session_duration_ns: stats.session_duration_ns,
lookups: stats.lookups,
hits: stats.hits,
misses: cache_misses(stats),
unconsulted: stats.unconsulted,
compiler_invocations_avoided: stats.hits,
estimated_compiler_duration_avoided_ns: stats.avoided_compiler_duration_ns,
compiler: stats
.compiler
.iter()
.map(|(outcome, stats)| {
(
outcome.clone(),
CompilerStatsReport {
invocations: stats.invocations,
duration_ns: stats.duration_ns,
},
)
})
.collect(),
slow_compilations: slow_compilations(stats)
.into_iter()
.map(|(crate_name, duration_ns)| SlowCompilationReport {
crate_name: crate_name.clone(),
duration_ns: *duration_ns,
})
.collect(),
verifications: stats.verifications,
divergences: stats.divergences,
prefetched_actions: stats.prefetched_actions,
predictions_loaded: stats.predictions_loaded,
prefetch_runs: stats.prefetch_runs,
bypasses: stats.bypasses.clone(),
downloaded_bytes: stats.downloaded_bytes,
uploaded_bytes: stats.uploaded_bytes,
background_uploads: stats.background_uploads,
background_upload_failures: stats.background_upload_failures,
remote_blob_pack_uploads: stats.remote_blob_pack_uploads,
remote_blob_pack_upload_blobs: stats.remote_blob_pack_upload_blobs,
upload_drain_duration_ns: stats.upload_drain_duration_ns,
stored_bytes: stats.stored_bytes,
restored_output_files: stats.restored_output_files,
restored_output_bytes: stats.restored_output_bytes,
reflinked_output_files: stats.reflinked_output_files,
reflinked_output_bytes: stats.reflinked_output_bytes,
copied_output_files: stats.copied_output_files,
copied_output_bytes: stats.copied_output_bytes,
reused_output_files: stats.reused_output_files,
reused_output_bytes: stats.reused_output_bytes,
remote_failures: stats.remote_failures,
remote_manifest_lookups: stats.remote_manifest_lookups,
remote_action_lookups: stats.remote_action_lookups,
remote_blob_requests: stats.remote_blob_requests,
remote_blob_pack_requests: stats.remote_blob_pack_requests,
remote_blob_pack_blobs: stats.remote_blob_pack_blobs,
remote_manifest_lookup_duration_ns: stats.remote_manifest_lookup_duration_ns,
remote_action_lookup_duration_ns: stats.remote_action_lookup_duration_ns,
remote_blob_transfer_duration_ns: stats.remote_blob_transfer_duration_ns,
local_cas_write_duration_ns: stats.local_cas_write_duration_ns,
prefetch_duration_ns: stats.prefetch_duration_ns,
materialization_duration_ns: stats.materialization_duration_ns,
}
}
}
pub(crate) fn display_stats(stats: &AgentStats, config: &Config, style: SummaryStyle) {
if let Some(path) = &config.stats_report
&& let Err(error) = write_stats_report(path, stats)
{
warn!(
"the statistics report could not be written to {}: {error}",
path.display()
);
}
match style {
SummaryStyle::Off => return,
SummaryStyle::Short => {
if should_display_short_stats(stats) {
note(&short_summary(stats));
}
return;
}
SummaryStyle::Full => {}
}
if !should_display_stats(stats) {
return;
}
note(&format!(
"mbx[cache]: {} hits, {} misses, {} prefetched; {} downloaded, {} uploaded, {} stored locally",
stats.hits,
cache_misses(stats),
stats.prefetched_actions,
ByteSize::b(stats.downloaded_bytes).display().iec(),
ByteSize::b(stats.uploaded_bytes).display().iec(),
ByteSize::b(stats.stored_bytes).display().iec(),
));
if stats.remote_failures > 0 {
note(&format!(
"mbx[cache]: the remote cache failed {} of its requests; this build ran without what it could not reach, and the warnings above say why",
stats.remote_failures,
));
}
if stats.unconsulted > 0 {
note(&format!(
"mbx[cache]: could not look up {} compilations: no usable dep-info from an earlier build and no prediction to derive an action key from",
stats.unconsulted,
));
if let Some(explanation) = stale_manifest_note(stats) {
note(&explanation);
}
}
if !stats.bypasses.is_empty() {
let total: u64 = stats.bypasses.values().sum();
let mut reasons: Vec<_> = stats.bypasses.iter().collect();
reasons.sort_by(|left, right| right.1.cmp(left.1).then(left.0.cmp(right.0)));
let detail = reasons
.iter()
.map(|(kind, count)| format!("{count} {kind}"))
.collect::<Vec<_>>()
.join(", ");
note(&format!(
"mbx[cache]: bypassed {total} compilations: {detail}"
));
}
if !stats.compiler.is_empty() || stats.avoided_compiler_duration_ns > 0 {
let spent = stats.compiler.values().fold(0_u64, |total, compiler| {
total.saturating_add(compiler.duration_ns)
});
let detail = stats
.compiler
.iter()
.map(|(outcome, compiler)| {
format!(
"{} {} in {}",
compiler.invocations,
outcome,
format_nanos(compiler.duration_ns)
)
})
.collect::<Vec<_>>()
.join(", ");
note(&format!(
"mbx[cache]: compiler time: {} estimated avoided; {} spent ({detail})",
format_nanos(stats.avoided_compiler_duration_ns),
format_nanos(spent),
));
if let Some(compiler) = stats.compiler.get("incremental") {
note(&format!(
"mbx[cache]: {} compilations kept their own incremental state, so they were not stored",
compiler.invocations
));
}
let slow = slow_compilations(stats);
if !slow.is_empty() {
note(&format!(
"mbx[cache]: slowest uncached crates: {}",
slow.into_iter()
.map(|(name, duration)| format!("{name} {}", format_nanos(*duration)))
.collect::<Vec<_>>()
.join(", ")
));
}
}
let remote_lookup_duration_ns = stats
.remote_manifest_lookup_duration_ns
.saturating_add(stats.remote_action_lookup_duration_ns);
note(&format!(
"mbx[cache]: timing: {} session, {} prefetch; cumulative {} remote lookup, {} blob transfer, {} CAS write, {} materialization",
format_nanos(stats.session_duration_ns),
format_nanos(stats.prefetch_duration_ns),
format_nanos(remote_lookup_duration_ns),
format_nanos(stats.remote_blob_transfer_duration_ns),
format_nanos(stats.local_cas_write_duration_ns),
format_nanos(stats.materialization_duration_ns),
));
if stats.background_uploads > 0 || stats.background_upload_failures > 0 {
let packed = if stats.remote_blob_pack_uploads > 0 {
format!(
" ({} of them in {} packs)",
stats.remote_blob_pack_upload_blobs, stats.remote_blob_pack_uploads
)
} else {
String::new()
};
note(&format!(
"mbx[cache]: uploads: {} published{packed}, {} not published; {} waited for after the build",
stats.background_uploads,
stats.background_upload_failures,
format_nanos(stats.upload_drain_duration_ns),
));
}
if stats.restored_output_files > 0 {
note(&format!(
"mbx[cache]: materialization: {} outputs ({}) reflinked, {} outputs ({}) copied, {} outputs ({}) already in place",
stats.reflinked_output_files,
ByteSize::b(stats.reflinked_output_bytes).display().iec(),
stats.copied_output_files,
ByteSize::b(stats.copied_output_bytes).display().iec(),
stats.reused_output_files,
ByteSize::b(stats.reused_output_bytes).display().iec(),
));
}
if stats.verifications > 0 {
note(&format!(
"mbx[cache]: qualification: {} verified, {} diverged",
stats.verifications, stats.divergences,
));
}
}
pub(super) fn short_summary(stats: &AgentStats) -> String {
let unexpected_bypasses = unexpected_bypasses(stats);
let mut outcomes = vec![
format!("{} hits", stats.hits),
format!("{} misses", cache_misses(stats)),
];
if stats.unconsulted > 0 {
outcomes.push(format!("{} not looked up", stats.unconsulted));
}
if stats.prefetched_actions > 0 {
outcomes.push(format!("{} prefetched", stats.prefetched_actions));
}
if unexpected_bypasses > 0 {
outcomes.push(format!("{unexpected_bypasses} bypassed"));
}
if stats.verifications > 0 {
outcomes.push(format!(
"{} verified, {} diverged",
stats.verifications, stats.divergences
));
}
if stats.remote_failures > 0 {
outcomes.push(format!("{} remote failures", stats.remote_failures));
}
format!(
"mbx[cache]: {}; {} downloaded, {} uploaded, {} stored locally",
outcomes.join(", "),
ByteSize::b(stats.downloaded_bytes).display().iec(),
ByteSize::b(stats.uploaded_bytes).display().iec(),
ByteSize::b(stats.stored_bytes).display().iec(),
)
}
fn unexpected_bypasses(stats: &AgentStats) -> u64 {
stats
.bypasses
.iter()
.filter(|(kind, _)| {
!matches!(
kind.as_str(),
"compiler-query" | "standard-input" | "cc-compiler-query" | "cc-standard-input"
)
})
.map(|(_, count)| count)
.sum()
}
pub(super) fn should_display_short_stats(stats: &AgentStats) -> bool {
stats.lookups > 0
|| stats.unconsulted > 0
|| stats.prefetched_actions > 0
|| stats.stores > 0
|| stats.verifications > 0
|| stats.downloaded_bytes > 0
|| stats.uploaded_bytes > 0
|| stats.background_uploads > 0
|| unexpected_bypasses(stats) > 0
|| stats.avoided_compiler_duration_ns > 0
|| stats.remote_failures > 0
}
pub(super) fn stale_manifest_note(stats: &AgentStats) -> Option<String> {
(stats.unconsulted > 0 && stats.lookups == 0 && stats.predictions_loaded > 0).then(|| {
format!(
"mbx[cache]: a manifest predicting {} compilations was loaded, but none matched this build; the compiler or its flags changed since they were recorded (a toolchain update does this)",
stats.predictions_loaded,
)
})
}
pub(crate) fn session_was_active(stats: &AgentStats) -> bool {
should_display_stats(stats)
}
pub(super) fn should_display_stats(stats: &AgentStats) -> bool {
stats.lookups > 0
|| stats.unconsulted > 0
|| stats.stores > 0
|| stats.verifications > 0
|| stats.downloaded_bytes > 0
|| stats.uploaded_bytes > 0
|| stats.background_uploads > 0
|| !stats.bypasses.is_empty()
|| !stats.compiler.is_empty()
|| stats.avoided_compiler_duration_ns > 0
|| stats.remote_failures > 0
}
pub(super) fn write_stats_report(path: &Path, stats: &AgentStats) -> Result<()> {
let mut report = serde_json::to_vec_pretty(&StatsReport::from(stats))?;
report.push(b'\n');
write_atomic(path, &report)
}
fn format_nanos(nanoseconds: u64) -> String {
format_duration(std::time::Duration::from_nanos(nanoseconds))
}
fn slow_compilations(stats: &AgentStats) -> Vec<(&String, &u64)> {
let mut slow = stats.slow_compilations.iter().collect::<Vec<_>>();
slow.sort_by(|left, right| right.1.cmp(left.1).then(left.0.cmp(right.0)));
slow.truncate(5);
slow
}
pub(super) fn cache_misses(stats: &AgentStats) -> u64 {
stats
.lookups
.saturating_sub(stats.hits)
.saturating_sub(stats.verifications)
}