zccache 1.11.8

Local-first compiler cache for C/C++/Rust/Emscripten
Documentation
//! Shared cached-hit materialization for compile cache branches.

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: PathBuf,
    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) 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,
        compile_start,
        hit_label,
        cached_error_label,
        record_compilation,
        downgrade_output_metadata,
        phases,
    } = request;

    // Issue #460: collapse clock reads on the warm-hit path. Previously this
    // function did 9 `Instant::now()` reads per hit (4 explicit + 5 implicit
    // via `.elapsed()`); each costs ~50ns on Linux and ~500ns on Windows
    // (QueryPerformanceCounter). The phase split below now uses 4 clock
    // reads (`t0..t3`) and derives every `_ns` value by arithmetic — plus
    // drops the `cached_ref.last_used = Instant::now()` write which was a
    // dead store (no readers anywhere in the workspace).
    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 {
                secondary_output_dir.join(&names[i]).into()
            };
            let cache_file = state.artifact_dir.join(format!("{artifact_key_hex}_{i}"));
            (out, cache_file)
        })
        .collect();
    if !write_payloads_par(&targets, &payloads) {
        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();
    }
    // `latency_ns` is the cache-hit response latency excluding bookkeeping
    // (record_hit / record_session_stat / write_session_log). Same boundary
    // as before — derived from `t2` instead of a fresh clock read.
    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_keep_cache_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().to_path_buf(),
            compile_start: Instant::now(),
            hit_label: "HIT_TEST",
            cached_error_label: "CACHED_ERROR_TEST",
            record_compilation: true,
            downgrade_output_metadata: true,
            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());
        assert_eq!(
            file_time(&output_path).unix_seconds(),
            old_time.unix_seconds()
        );
        assert_eq!(state.stats.snapshot().compilations, 1);
        assert_eq!(state.stats.snapshot().hits, 1);
    }

    /// Issue #460: warm-hit materialization should stay under budget — the
    /// fix collapsed 9 clock reads per hit to 4. A future regression that
    /// reintroduces a syscall-per-phase pattern (or worse, a synchronous I/O
    /// call) on the hit path would bust this budget. 100 iterations / 1 s
    /// gives ~50× headroom on Linux Docker and ~5× on Windows CI (Defender +
    /// shared-runner jitter typically lands warm-hit timings around 2 ms each
    /// on those runners; native Windows hosts measure ~150–250 µs/hit).
    #[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().to_path_buf(),
                compile_start: Instant::now(),
                hit_label: "HIT_TEST",
                cached_error_label: "CACHED_ERROR_TEST",
                record_compilation: true,
                downgrade_output_metadata: false,
                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);
    }
}