use std::io::Cursor;
use std::path::Path;
use sha2::{Digest, Sha256};
use crate::hash::{base64_encode, HashAlgorithm, NixHash};
use crate::nar::{NarError, NarWriter};
use crate::store_path::compute_fixed_output_hash;
mod perf_trace {
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::Duration;
static ENABLED: AtomicBool = AtomicBool::new(false);
static INIT: std::sync::Once = std::sync::Once::new();
#[derive(Default)]
pub struct Stat {
pub calls: u64,
pub bytes: u64,
pub elapsed: Duration,
}
static PER_PATH: Mutex<Option<HashMap<String, Stat>>> = Mutex::new(None);
#[inline]
pub fn enabled() -> bool {
INIT.call_once(|| {
if std::env::var("SUI_PERF_TRACE").ok().as_deref() == Some("1") {
ENABLED.store(true, Ordering::Relaxed);
*PER_PATH.lock().unwrap() = Some(HashMap::new());
}
});
ENABLED.load(Ordering::Relaxed)
}
pub fn record(path: &str, bytes: u64, elapsed: Duration) {
if let Ok(mut g) = PER_PATH.lock() {
if let Some(map) = g.as_mut() {
let s = map.entry(path.to_string()).or_default();
s.calls += 1;
s.bytes += bytes;
s.elapsed += elapsed;
let total_calls: u64 = map.values().map(|s| s.calls).sum();
if total_calls % 200 == 0 {
let total_bytes: u64 = map.values().map(|s| s.bytes).sum();
let total_elapsed: Duration = map.values().map(|s| s.elapsed).sum();
let redundant: u64 =
map.values().map(|s| s.calls.saturating_sub(1)).sum();
eprintln!(
"[nar-trace] calls:{total_calls} distinct:{} redundant:{redundant} bytes:{:.0}MiB nar_time:{:.1}s",
map.len(),
total_bytes as f64 / 1_048_576.0,
total_elapsed.as_secs_f64(),
);
let mut rows: Vec<(&String, &Stat)> = map.iter().collect();
rows.sort_by(|a, b| b.1.elapsed.cmp(&a.1.elapsed));
for (p, s) in rows.iter().take(5) {
eprintln!(
" [{:>4} calls, {:>7.1}s, {:>6.1}MiB/call] {}",
s.calls,
s.elapsed.as_secs_f64(),
s.bytes as f64 / 1_048_576.0 / s.calls as f64,
p
);
}
}
}
}
}
pub fn dump() {
let g = match PER_PATH.lock() {
Ok(g) => g,
Err(_) => return,
};
let Some(map) = g.as_ref() else { return };
let mut total_calls = 0u64;
let mut total_bytes = 0u64;
let mut total_elapsed = Duration::ZERO;
let mut repeated = 0u64;
let mut rows: Vec<(&String, &Stat)> = map.iter().collect();
for (_p, s) in &rows {
total_calls += s.calls;
total_bytes += s.bytes;
total_elapsed += s.elapsed;
if s.calls > 1 {
repeated += s.calls - 1;
}
}
rows.sort_by(|a, b| b.1.elapsed.cmp(&a.1.elapsed));
eprintln!("\n=== nar_hash_source_tree trace ===");
eprintln!("distinct trees: {}", map.len());
eprintln!("total calls: {total_calls}");
eprintln!(
"redundant calls (same tree >1x): {repeated} ({:.1}%)",
if total_calls > 0 {
repeated as f64 / total_calls as f64 * 100.0
} else {
0.0
}
);
eprintln!("total bytes: {total_bytes} ({:.1} MiB)", total_bytes as f64 / 1_048_576.0);
eprintln!("total elapsed: {:.2}s", total_elapsed.as_secs_f64());
eprintln!("--- top 15 trees by elapsed ---");
for (p, s) in rows.iter().take(15) {
eprintln!(
" {:>6} calls {:>8.1} MiB {:>7.2}s {}",
s.calls,
s.bytes as f64 / 1_048_576.0,
s.elapsed.as_secs_f64(),
p
);
}
eprintln!("==================================\n");
}
}
pub fn nar_hash_dump() {
if perf_trace::enabled() {
perf_trace::dump();
}
}
mod nar_memo {
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
static MEMO: Mutex<Option<HashMap<(String, String), (String, String)>>> =
Mutex::new(None);
static DISABLED: AtomicBool = AtomicBool::new(false);
static INIT: std::sync::Once = std::sync::Once::new();
fn active() -> bool {
INIT.call_once(|| {
if std::env::var("SUI_NO_NAR_MEMO").ok().as_deref() == Some("1") {
DISABLED.store(true, Ordering::Relaxed);
}
});
!DISABLED.load(Ordering::Relaxed)
}
pub fn get(dir: &str, name: &str) -> Option<(String, String)> {
if !active() {
return None;
}
let g = MEMO.lock().ok()?;
g.as_ref()?
.get(&(dir.to_string(), name.to_string()))
.cloned()
}
pub fn put(dir: &str, name: &str, store_path: String, nar_hash_sri: String) {
if !active() {
return;
}
if let Ok(mut g) = MEMO.lock() {
g.get_or_insert_with(HashMap::new)
.insert((dir.to_string(), name.to_string()), (store_path, nar_hash_sri));
}
}
}
#[derive(Debug, Clone)]
pub struct SourceHash {
pub store_path: String,
pub nar_hash_sri: String,
pub nar_bytes: Vec<u8>,
}
pub fn nar_hash_source_tree(dir: &Path, name: &str) -> Result<SourceHash, NarError> {
let dir_key = dir.to_string_lossy();
if let Some((store_path, nar_hash_sri)) = nar_memo::get(&dir_key, name) {
return Ok(SourceHash {
store_path,
nar_hash_sri,
nar_bytes: Vec::new(),
});
}
let sh = nar_hash_source_tree_uncached(dir, name)?;
nar_memo::put(
&dir_key,
name,
sh.store_path.clone(),
sh.nar_hash_sri.clone(),
);
Ok(sh)
}
pub fn nar_hash_source_tree_uncached(dir: &Path, name: &str) -> Result<SourceHash, NarError> {
let trace = perf_trace::enabled();
let t0 = if trace { Some(std::time::Instant::now()) } else { None };
let mut nar_bytes = Vec::new();
{
let mut cursor = Cursor::new(&mut nar_bytes);
NarWriter::write_path(&mut cursor, dir)?;
}
if let Some(t0) = t0 {
perf_trace::record(&dir.to_string_lossy(), nar_bytes.len() as u64, t0.elapsed());
}
let digest = Sha256::digest(&nar_bytes);
let digest_bytes = digest.to_vec();
let hex: String = digest_bytes.iter().map(|b| format!("{b:02x}")).collect();
let store_path = compute_fixed_output_hash("sha256", &hex, true, name);
let nar_hash = NixHash::new(HashAlgorithm::Sha256, digest_bytes.clone());
let nar_hash_sri = nar_hash.to_sri();
Ok(SourceHash {
store_path,
nar_hash_sri,
nar_bytes,
})
}
#[must_use]
pub fn base64_sha256(bytes: &[u8]) -> String {
base64_encode(&Sha256::digest(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn mk_flake_dir() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let flake_nix = dir.path().join("flake.nix");
let mut f = std::fs::File::create(&flake_nix).unwrap();
write!(f, "{{ outputs = {{ self }}: {{ value = 42; }}; }}\n").unwrap();
dir
}
#[test]
fn source_tree_produces_a_store_path_and_an_sri_hash() {
let dir = mk_flake_dir();
let sh = nar_hash_source_tree(dir.path(), "source").expect("nar hash");
assert!(sh.store_path.starts_with("/nix/store/"));
assert!(sh.store_path.ends_with("-source"));
assert!(sh.nar_hash_sri.starts_with("sha256-"));
assert!(!sh.nar_bytes.is_empty());
assert!(sh.nar_bytes.starts_with(b"\r\x00\x00\x00\x00\x00\x00\x00nix-archive-1"),
"NAR must begin with the magic header — got {:?}",
&sh.nar_bytes[..16]);
}
#[test]
fn memo_returns_byte_identical_hashes_on_repeat() {
let dir = mk_flake_dir();
let first = nar_hash_source_tree(dir.path(), "source").expect("first");
let second = nar_hash_source_tree(dir.path(), "source").expect("second");
assert_eq!(first.store_path, second.store_path, "memo changed store_path");
assert_eq!(first.nar_hash_sri, second.nar_hash_sri, "memo changed narHash");
let uncached = nar_hash_source_tree_uncached(dir.path(), "source").expect("uncached");
assert_eq!(first.store_path, uncached.store_path);
assert_eq!(first.nar_hash_sri, uncached.nar_hash_sri);
assert!(!uncached.nar_bytes.is_empty(), "uncached always yields NAR bytes");
}
#[test]
fn memo_keys_on_name_not_just_dir() {
let dir = mk_flake_dir();
let a = nar_hash_source_tree(dir.path(), "source").expect("a");
let b = nar_hash_source_tree(dir.path(), "other").expect("b");
assert_ne!(a.store_path, b.store_path, "name must be part of the memo key");
assert_eq!(a.nar_hash_sri, b.nar_hash_sri);
}
}