use crate::analyses::import_graph::{build_import_graph, graph_metrics};
use crate::calibration::{load_active_artifact, raw_percentile};
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ArchitectureMetricRow {
pub metric: String,
pub value: String,
}
const CORE_DOMINANCE: f64 = 0.6;
#[tracing::instrument(name = "architecture-metrics", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_architecture_metrics(
db: &FactsDb,
opts: &Options,
) -> Result<Vec<ArchitectureMetricRow>> {
let graph = build_import_graph(db)?;
let m = graph_metrics(&graph);
let resolution_rows = import_resolution_rows(db)?;
if m.n == 0 {
return Ok(resolution_rows);
}
let n_f = f64::from(u32::try_from(m.n).unwrap_or(u32::MAX));
let acd = m.ccd / n_f;
let ccd_btree = (n_f + 1.0) * (n_f + 1.0).log2() - n_f;
let nccd = if ccd_btree > 0.0 {
m.ccd / ccd_btree
} else {
0.0
};
let largest_f = f64::from(m.largest_cycle);
let cyclic_f = f64::from(m.cyclic_nodes);
let arch_type = if m.cycle_count == 0 {
"hierarchical"
} else if cyclic_f > 0.0 && largest_f / cyclic_f >= CORE_DOMINANCE {
"core-periphery"
} else {
"multi-core"
};
let mut rows = vec![
row("propagation_cost", format!("{:.4}", m.propagation_cost)),
row("acd", format!("{acd:.2}")),
row("nccd", format!("{nccd:.2}")),
row("dependency_cycles", m.cycle_count.to_string()),
row("largest_cycle", m.largest_cycle.to_string()),
row("files", m.n.to_string()),
row("architecture_type", arch_type.to_owned()),
];
rows.extend(resolution_rows);
if let Some(artifact) = load_active_artifact(opts)?
&& let Some(repo_metrics) = artifact.repo_metrics.as_ref()
{
let cycle_file_share = cyclic_f / n_f;
let mut corpus_n: Option<usize> = None;
if let Some(pool) = repo_metrics.values.get("propagation_cost")
&& !pool.is_empty()
&& let Some(p) = raw_percentile(pool, m.propagation_cost)
{
rows.push(row("corpus_percentile:propagation_cost", format!("{p:.2}")));
let (lo, hi) = crate::stats::wilson_ci_from_proportion(p, pool_sample_size(pool));
rows.push(row(
"corpus_percentile:propagation_cost:ci_low",
format!("{lo:.2}"),
));
rows.push(row(
"corpus_percentile:propagation_cost:ci_high",
format!("{hi:.2}"),
));
corpus_n = Some(pool.len());
}
if let Some(pool) = repo_metrics.values.get("cycle_file_share")
&& !pool.is_empty()
&& let Some(p) = raw_percentile(pool, cycle_file_share)
{
rows.push(row("corpus_percentile:cycle_file_share", format!("{p:.2}")));
let (lo, hi) = crate::stats::wilson_ci_from_proportion(p, pool_sample_size(pool));
rows.push(row(
"corpus_percentile:cycle_file_share:ci_low",
format!("{lo:.2}"),
));
rows.push(row(
"corpus_percentile:cycle_file_share:ci_high",
format!("{hi:.2}"),
));
corpus_n.get_or_insert(pool.len());
}
if let Some(n) = corpus_n {
rows.push(row("corpus_n", n.to_string()));
}
}
Ok(rows)
}
fn import_resolution_rows(db: &FactsDb) -> Result<Vec<ArchitectureMetricRow>> {
let counts: Vec<(f64, i64, i64, i64)> = crate::analyses::query::query_map_collect(
db,
"SELECT \
COALESCE(COUNT(*) FILTER (WHERE target_path IS NOT NULL) * 1.0 \
/ NULLIF(COUNT(*), 0), 0.0), \
COUNT(*) FILTER (WHERE target_path IS NOT NULL), \
COUNT(*) FILTER (WHERE target_path IS NOT NULL OR kind = 'relative'), \
COUNT(*) \
FROM imports",
[],
"import-resolution-rate",
|r| {
Ok((
r.get::<_, f64>(0)?,
r.get::<_, i64>(1)?,
r.get::<_, i64>(2)?,
r.get::<_, i64>(3)?,
))
},
)?;
let Some(&(rate, resolved, first_party, total)) = counts.first() else {
return Ok(Vec::new());
};
if total == 0 {
return Ok(Vec::new());
}
let as_f64 = |n: i64| f64::from(u32::try_from(n).unwrap_or(u32::MAX));
let (resolved_f, first_party_f, total_f) =
(as_f64(resolved), as_f64(first_party), as_f64(total));
let mut rows = vec![
row("import_resolution_rate", format!("{rate:.4}")),
row(
"first_party_import_share",
format!("{:.4}", first_party_f / total_f),
),
];
if first_party > 0 {
rows.push(row(
"resolution_rate_first_party",
format!("{:.4}", resolved_f / first_party_f),
));
}
Ok(rows)
}
fn pool_sample_size(pool: &[f64]) -> u32 {
u32::try_from(pool.len()).unwrap_or(u32::MAX)
}
fn row(metric: &str, value: String) -> ArchitectureMetricRow {
ArchitectureMetricRow {
metric: metric.to_owned(),
value,
}
}