use super::super::*;
pub(super) struct CachedHitPhases {
pub(super) parse_args_ns: u64,
pub(super) build_context_ns: u64,
pub(super) hash_source_ns: u64,
pub(super) hash_headers_ns: u64,
pub(super) depgraph_check_ns: u64,
pub(super) request_cache_lookup_ns: u64,
pub(super) cross_root_validate_ns: u64,
}
impl CachedHitPhases {
pub(super) fn request_cache(request_cache_lookup_ns: u64, cross_root_validate_ns: u64) -> Self {
Self {
parse_args_ns: 0,
build_context_ns: 0,
hash_source_ns: 0,
hash_headers_ns: 0,
depgraph_check_ns: 0,
request_cache_lookup_ns,
cross_root_validate_ns,
}
}
}
pub(super) struct CachedHitMaterializeRequest<'a> {
pub(super) state: &'a SharedState,
pub(super) sid: &'a SessionId,
pub(super) artifact_key_hex: &'a str,
pub(super) source_path: &'a NormalizedPath,
pub(super) output_path: &'a NormalizedPath,
pub(super) secondary_output_dir: NormalizedPath,
pub(super) current_depfile_dest: Option<NormalizedPath>,
pub(super) compile_start: Instant,
pub(super) hit_label: &'static str,
pub(super) cached_error_label: &'static str,
pub(super) record_compilation: bool,
pub(super) downgrade_output_metadata: bool,
pub(super) mtime_floor_paths: Vec<NormalizedPath>,
pub(super) phases: CachedHitPhases,
}
pub(super) fn materialize_cached_compile_hit(
request: CachedHitMaterializeRequest<'_>,
) -> Option<Response> {
let CachedHitMaterializeRequest {
state,
sid,
artifact_key_hex,
source_path,
output_path,
secondary_output_dir,
current_depfile_dest,
compile_start,
hit_label,
cached_error_label,
record_compilation,
downgrade_output_metadata,
mtime_floor_paths,
phases,
} = request;
let t0 = Instant::now();
let mut cached_ref = lookup_artifact_with_disk_fallback(state, artifact_key_hex)?;
ensure_payloads(&mut cached_ref, &state.artifact_dir, artifact_key_hex)?;
let t1 = Instant::now();
let artifact_lookup_ns = (t1 - t0).as_nanos() as u64;
let payloads = Arc::clone(cached_ref.payloads.as_ref().unwrap());
let names = Arc::clone(&cached_ref.meta.output_names);
let exit_code = cached_ref.meta.exit_code;
let stdout = cached_ref.stdout.clone();
let stderr = cached_ref.stderr.clone();
let artifact_bytes = cached_ref.meta.total_size;
drop(cached_ref);
let targets: Vec<(NormalizedPath, NormalizedPath)> = (0..payloads.len())
.map(|i| {
let out: NormalizedPath = if i == 0 {
output_path.clone()
} else if i == 1 && payloads.len() == 2 {
current_depfile_dest
.clone()
.unwrap_or_else(|| secondary_output_dir.join(&names[i]))
} else {
secondary_output_dir.join(&names[i])
};
let cache_file = state.artifact_dir.join(format!("{artifact_key_hex}_{i}"));
(out, cache_file)
})
.collect();
if !write_payloads_par_with_mtime_floor(&targets, &payloads, &mtime_floor_paths) {
return None;
}
let t2 = Instant::now();
let write_output_ns = (t2 - t1).as_nanos() as u64;
let cached_error = exit_code != 0;
if !cached_error && downgrade_output_metadata {
state.cache_system.metadata().downgrade(output_path);
}
if record_compilation {
state.stats.record_compilation();
}
let latency_ns = (t2 - compile_start).as_nanos() as u64;
if cached_error {
state.stats.record_cached_error();
record_session_stat(&state.sessions, sid, |t| {
t.record_cached_error();
});
} else {
state.stats.record_hit(latency_ns, artifact_bytes);
let src = source_path.clone();
record_session_stat(&state.sessions, sid, move |t| {
t.record_hit(src, latency_ns, artifact_bytes);
});
}
write_session_log(
&state.sessions,
sid,
&format!(
"[{}] {} -> {}",
if cached_error {
cached_error_label
} else {
hit_label
},
source_path.display(),
output_path.display()
),
);
let t3 = Instant::now();
let bookkeeping_ns = (t3 - t2).as_nanos() as u64;
let total_ns = (t3 - compile_start).as_nanos() as u64;
if !cached_error {
state.profiler.record_hit(&HitPhases {
parse_args_ns: phases.parse_args_ns,
build_context_ns: phases.build_context_ns,
hash_source_ns: phases.hash_source_ns,
hash_headers_ns: phases.hash_headers_ns,
depgraph_check_ns: phases.depgraph_check_ns,
request_cache_lookup_ns: phases.request_cache_lookup_ns,
cross_root_validate_ns: phases.cross_root_validate_ns,
artifact_lookup_ns,
write_output_ns,
bookkeeping_ns,
total_ns,
});
}
Some(Response::CompileResult {
exit_code,
stdout,
stderr,
cached: true,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn file_time(path: &Path) -> filetime::FileTime {
filetime::FileTime::from_last_modification_time(&std::fs::metadata(path).unwrap())
}
#[tokio::test(flavor = "current_thread")]
async fn target_paths_get_fresh_mtime_through_shared_materializer() {
let dir = tempfile::tempdir().unwrap();
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let state = server.state.as_ref();
let cache_dir = state.artifact_dir.clone();
let source_path: NormalizedPath = dir.path().join("source.cc").into();
let output_path: NormalizedPath = dir.path().join("output.o").into();
let cache_path = cache_dir.join("artifact-key_0");
let payload = Arc::new(b"compiled object".to_vec());
std::fs::write(&cache_path, payload.as_slice()).unwrap();
let old_time = filetime::FileTime::from_unix_time(1_000_000_000, 0);
filetime::set_file_mtime(&cache_path, old_time).unwrap();
let sid = state.sessions.create(crate::depgraph::SessionConfig {
client_pid: std::process::id(),
working_dir: dir.path().into(),
log_file: None,
track_stats: true,
journal_path: None,
profile: false,
private_env: Vec::new(),
owner_pids: Vec::new(),
});
let meta = ArtifactIndex::new(
vec!["output.o".to_string()],
vec![payload.len() as u64],
Arc::new(Vec::new()),
Arc::new(Vec::new()),
0,
);
state.artifacts.insert(
"artifact-key".to_string(),
CachedArtifact::from_file_payloads(meta, vec![cache_path]),
);
let response = materialize_cached_compile_hit(CachedHitMaterializeRequest {
state,
sid: &sid,
artifact_key_hex: "artifact-key",
source_path: &source_path,
output_path: &output_path,
secondary_output_dir: dir.path().into(),
current_depfile_dest: None,
compile_start: Instant::now(),
hit_label: "HIT_TEST",
cached_error_label: "CACHED_ERROR_TEST",
record_compilation: true,
downgrade_output_metadata: true,
mtime_floor_paths: Vec::new(),
phases: CachedHitPhases::request_cache(0, 0),
})
.unwrap();
assert!(matches!(
response,
Response::CompileResult {
cached: true,
exit_code: 0,
..
}
));
assert_eq!(std::fs::read(&output_path).unwrap(), payload.as_slice());
let output_time = file_time(&output_path);
assert!(
output_time.unix_seconds() > old_time.unix_seconds(),
"compile-hit output must be fresher than stale cache artifact; \
output={output_time:?}, cache={old_time:?}",
);
assert_eq!(state.stats.snapshot().compilations, 1);
assert_eq!(state.stats.snapshot().hits, 1);
}
#[tokio::test(flavor = "current_thread")]
async fn cached_hit_restores_user_depfile_alongside_object() {
let dir = tempfile::tempdir().unwrap();
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let state = server.state.as_ref();
let cache_dir = state.artifact_dir.clone();
let source_path: NormalizedPath = dir.path().join("source.cc").into();
let output_path: NormalizedPath = dir.path().join("source.o").into();
let depfile_dest: NormalizedPath = dir.path().join("build/out/source.o.d").into();
let obj_payload = Arc::new(b"compiled object bytes".to_vec());
let dep_payload = Arc::new(
b"source.o: source.cc header_a.h header_b.h\n\nheader_a.h:\n\nheader_b.h:\n".to_vec(),
);
let obj_cache_path = cache_dir.join("depfile-key_0");
let dep_cache_path = cache_dir.join("depfile-key_1");
std::fs::write(&obj_cache_path, obj_payload.as_slice()).unwrap();
std::fs::write(&dep_cache_path, dep_payload.as_slice()).unwrap();
let sid = state.sessions.create(crate::depgraph::SessionConfig {
client_pid: std::process::id(),
working_dir: dir.path().into(),
log_file: None,
track_stats: true,
journal_path: None,
profile: false,
private_env: Vec::new(),
owner_pids: Vec::new(),
});
let meta = ArtifactIndex::new(
vec!["source.o".to_string(), "source.o.d".to_string()],
vec![obj_payload.len() as u64, dep_payload.len() as u64],
Arc::new(Vec::new()),
Arc::new(Vec::new()),
0,
);
state.artifacts.insert(
"depfile-key".to_string(),
CachedArtifact::from_file_payloads(meta, vec![obj_cache_path, dep_cache_path]),
);
let response = materialize_cached_compile_hit(CachedHitMaterializeRequest {
state,
sid: &sid,
artifact_key_hex: "depfile-key",
source_path: &source_path,
output_path: &output_path,
secondary_output_dir: dir.path().into(),
current_depfile_dest: Some(depfile_dest.clone()),
compile_start: Instant::now(),
hit_label: "HIT_TEST",
cached_error_label: "CACHED_ERROR_TEST",
record_compilation: true,
downgrade_output_metadata: true,
mtime_floor_paths: Vec::new(),
phases: CachedHitPhases::request_cache(0, 0),
})
.expect("materialize_cached_compile_hit must succeed");
assert!(matches!(
response,
Response::CompileResult {
cached: true,
exit_code: 0,
..
}
));
assert_eq!(
std::fs::read(&output_path).unwrap(),
obj_payload.as_slice(),
"cache hit must restore the object at its destination",
);
assert!(
depfile_dest.as_path().exists(),
"cache hit must restore the depfile at the *current* build's -MF \
destination ({}), not the cached basename — this is the #643 \
stale-incremental-build fix",
depfile_dest.display(),
);
assert_eq!(
std::fs::read(depfile_dest.as_path()).unwrap(),
dep_payload.as_slice(),
"restored depfile bytes must match the cached payload",
);
}
#[tokio::test(flavor = "current_thread")]
async fn cached_hit_object_only_artifact_ignores_depfile_dest() {
let dir = tempfile::tempdir().unwrap();
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let state = server.state.as_ref();
let cache_dir = state.artifact_dir.clone();
let source_path: NormalizedPath = dir.path().join("source.cc").into();
let output_path: NormalizedPath = dir.path().join("source.o").into();
let depfile_dest: NormalizedPath = dir.path().join("source.o.d").into();
let obj_payload = Arc::new(b"object only".to_vec());
let cache_path = cache_dir.join("legacy-key_0");
std::fs::write(&cache_path, obj_payload.as_slice()).unwrap();
let sid = state.sessions.create(crate::depgraph::SessionConfig {
client_pid: std::process::id(),
working_dir: dir.path().into(),
log_file: None,
track_stats: true,
journal_path: None,
profile: false,
private_env: Vec::new(),
owner_pids: Vec::new(),
});
let meta = ArtifactIndex::new(
vec!["source.o".to_string()],
vec![obj_payload.len() as u64],
Arc::new(Vec::new()),
Arc::new(Vec::new()),
0,
);
state.artifacts.insert(
"legacy-key".to_string(),
CachedArtifact::from_file_payloads(meta, vec![cache_path]),
);
let response = materialize_cached_compile_hit(CachedHitMaterializeRequest {
state,
sid: &sid,
artifact_key_hex: "legacy-key",
source_path: &source_path,
output_path: &output_path,
secondary_output_dir: dir.path().into(),
current_depfile_dest: Some(depfile_dest.clone()),
compile_start: Instant::now(),
hit_label: "HIT_TEST",
cached_error_label: "CACHED_ERROR_TEST",
record_compilation: true,
downgrade_output_metadata: false,
mtime_floor_paths: Vec::new(),
phases: CachedHitPhases::request_cache(0, 0),
})
.expect("legacy single-output hit must still succeed");
assert!(matches!(
response,
Response::CompileResult {
cached: true,
exit_code: 0,
..
}
));
assert_eq!(std::fs::read(&output_path).unwrap(), obj_payload.as_slice());
assert!(
!depfile_dest.as_path().exists(),
"legacy single-output artifact must NOT manufacture a depfile",
);
}
#[tokio::test(flavor = "current_thread")]
async fn warm_hit_materialization_under_budget() {
let dir = tempfile::tempdir().unwrap();
let server = DaemonServer::bind(&crate::ipc::unique_test_endpoint()).unwrap();
let state = server.state.as_ref();
let cache_dir = state.artifact_dir.clone();
let source_path: NormalizedPath = dir.path().join("source.cc").into();
let cache_path = cache_dir.join("budget-key_0");
let payload = Arc::new(b"compiled object".to_vec());
std::fs::write(&cache_path, payload.as_slice()).unwrap();
let sid = state.sessions.create(crate::depgraph::SessionConfig {
client_pid: std::process::id(),
working_dir: dir.path().into(),
log_file: None,
track_stats: true,
journal_path: None,
profile: false,
private_env: Vec::new(),
owner_pids: Vec::new(),
});
let meta = ArtifactIndex::new(
vec!["output.o".to_string()],
vec![payload.len() as u64],
Arc::new(Vec::new()),
Arc::new(Vec::new()),
0,
);
state.artifacts.insert(
"budget-key".to_string(),
CachedArtifact::from_file_payloads(meta, vec![cache_path]),
);
const ITERATIONS: u32 = 100;
let start = Instant::now();
for i in 0..ITERATIONS {
let output_path: NormalizedPath = dir.path().join(format!("out-{i}.o")).into();
let response = materialize_cached_compile_hit(CachedHitMaterializeRequest {
state,
sid: &sid,
artifact_key_hex: "budget-key",
source_path: &source_path,
output_path: &output_path,
secondary_output_dir: dir.path().into(),
current_depfile_dest: None,
compile_start: Instant::now(),
hit_label: "HIT_TEST",
cached_error_label: "CACHED_ERROR_TEST",
record_compilation: true,
downgrade_output_metadata: false,
mtime_floor_paths: Vec::new(),
phases: CachedHitPhases::request_cache(0, 0),
})
.expect("materialize_cached_compile_hit must succeed");
assert!(matches!(
response,
Response::CompileResult {
cached: true,
exit_code: 0,
..
}
));
}
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(1),
"warm-hit materialization regressed: {ITERATIONS} hits took {elapsed:?} \
(budget: 1 s; avg {:?}/hit)",
elapsed / ITERATIONS
);
assert_eq!(state.stats.snapshot().hits as u32, ITERATIONS);
}
}