use std::fs;
use std::path::Path;
use std::time::SystemTime;
use crate::core::NormalizedPath;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use super::event_log::{format_timestamp, open_append};
mod derive;
mod journal_thread;
mod outcome;
#[cfg(test)]
mod tests;
pub use derive::{derive_crate_name, derive_crate_type, derive_output_ext};
pub use outcome::extract_outcome;
use journal_thread::{journal_thread, JournalMessage};
pub mod miss_reason {
pub const CONTEXT_NOT_FOUND: &str = "context_not_found";
pub const INPUT_FINGERPRINT_MISMATCH: &str = "input_fingerprint_mismatch";
pub const NO_ARTIFACT_FOR_KEY: &str = "no_artifact_for_key";
pub const VERSION_SKEW: &str = "version_skew";
pub const UNCACHEABLE_INPUT: &str = "uncacheable_input";
pub const UNKNOWN: &str = "unknown";
pub const ALL: &[&str] = &[
CONTEXT_NOT_FOUND,
INPUT_FINGERPRINT_MISMATCH,
NO_ARTIFACT_FOR_KEY,
VERSION_SKEW,
UNCACHEABLE_INPUT,
UNKNOWN,
];
}
#[derive(Debug, Serialize)]
pub struct JournalEntry {
pub ts: String,
pub outcome: &'static str,
pub compiler: String,
pub args: Vec<String>,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<Vec<(String, String)>>,
pub exit_code: i32,
pub session_id: Option<String>,
pub latency_ns: u128,
#[serde(skip_serializing_if = "Option::is_none")]
pub crate_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub crate_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_ext: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub miss_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub miss_diff: Option<MissDiff>,
#[serde(skip_serializing_if = "Option::is_none")]
pub self_profile_ns: Option<SelfProfileNs>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MissDiff {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changed_files: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changed_flags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changed_deps: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SelfProfileNs {
pub hash_inputs: u128,
pub lookup: u128,
pub decompress: u128,
pub store: u128,
}
pub struct JournalContext {
pub compiler: String,
pub args: Vec<String>,
pub cwd: String,
pub env: Option<Vec<(String, String)>>,
pub session_id: Option<String>,
}
impl JournalEntry {
pub fn new(
ctx: JournalContext,
outcome: &'static str,
exit_code: i32,
latency_ns: u128,
miss_reason: Option<&'static str>,
) -> Self {
Self {
ts: format_timestamp(SystemTime::now()),
outcome,
compiler: ctx.compiler,
args: ctx.args,
cwd: ctx.cwd,
env: ctx.env,
exit_code,
session_id: ctx.session_id,
latency_ns,
crate_name: None,
crate_type: None,
output_ext: None,
miss_reason: miss_reason.map(str::to_string),
miss_diff: None,
self_profile_ns: None,
}
}
#[must_use]
pub fn with_profile_fields(mut self, spans: Option<SelfProfileSpans>) -> Self {
let derived_name = derive_crate_name(&self.args);
let derived_type = derive_crate_type(&self.args);
self.output_ext = derive_output_ext(derived_type).map(str::to_string);
self.crate_type = derived_type.map(str::to_string);
self.crate_name = derived_name;
self.self_profile_ns = spans.map(SelfProfileSpans::finish);
if matches!(self.outcome, "miss" | "link_miss") {
self.miss_diff = Some(MissDiff::default());
}
self
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SelfProfileSpans {
pub hash_inputs_ns: u128,
pub lookup_ns: u128,
pub decompress_ns: u128,
pub store_ns: u128,
}
impl SelfProfileSpans {
#[must_use]
pub fn finish(self) -> SelfProfileNs {
SelfProfileNs {
hash_inputs: self.hash_inputs_ns,
lookup: self.lookup_ns,
decompress: self.decompress_ns,
store: self.store_ns,
}
}
pub fn add_hash_inputs_ns(&mut self, ns: u128) {
self.hash_inputs_ns = self.hash_inputs_ns.saturating_add(ns);
}
pub fn add_lookup_ns(&mut self, ns: u128) {
self.lookup_ns = self.lookup_ns.saturating_add(ns);
}
pub fn add_decompress_ns(&mut self, ns: u128) {
self.decompress_ns = self.decompress_ns.saturating_add(ns);
}
pub fn add_store_ns(&mut self, ns: u128) {
self.store_ns = self.store_ns.saturating_add(ns);
}
}
pub struct CompileJournal {
sender: Option<mpsc::UnboundedSender<JournalMessage>>,
}
impl CompileJournal {
pub fn new(log_dir: NormalizedPath) -> Self {
match Self::try_new(log_dir) {
Ok(journal) => journal,
Err(e) => {
tracing::warn!("compile journal init failed: {e} — running without journal");
Self::noop()
}
}
}
fn try_new(log_dir: NormalizedPath) -> std::io::Result<Self> {
fs::create_dir_all(&log_dir)?;
let path = log_dir.join("compile_journal.jsonl");
let file = open_append(&path)?;
let (tx, rx) = mpsc::unbounded_channel();
std::thread::Builder::new()
.name("zccache-journal".into())
.spawn(move || journal_thread(rx, path, file))
.map_err(std::io::Error::other)?;
Ok(Self { sender: Some(tx) })
}
#[must_use]
pub fn noop() -> Self {
Self { sender: None }
}
pub fn log(&self, entry: &JournalEntry, session_path: Option<&Path>) {
if let Some(tx) = &self.sender {
match serde_json::to_string(entry) {
Ok(line) => {
let _ = tx.send(JournalMessage::Entry {
line,
session_path: session_path.map(Into::into),
});
}
Err(e) => {
tracing::debug!("journal serialize error: {e}");
}
}
}
}
pub fn close_session(&self, path: &Path) {
if let Some(tx) = &self.sender {
let _ = tx.send(JournalMessage::CloseSession { path: path.into() });
}
}
}