use std::cmp::Ordering;
use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
use std::fmt;
use std::fs;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering as AtomicOrdering};
use std::sync::{mpsc, Arc, Barrier, Condvar, Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::clang::driver::{
BackendJob, CompilationPhase, CompilerJob, LinkerDriver, LinkerJob, PipelineJob,
PreprocessorJob,
};
use crate::clang::ClangOptions;
pub const X86_DEFAULT_PARALLEL_JOBS: usize = 0;
pub const X86_DEFAULT_MEMORY_LIMIT_MB: u64 = 2048;
pub const X86_CPU_THRESHOLD_PCT: f64 = 90.0;
pub const X86_DEFAULT_JOB_TIMEOUT_SECS: u64 = 600;
pub const X86_MAX_COMMAND_LINE_LEN: usize = 32768;
pub const X86_RESPONSE_FILE_FLAG: &str = "@";
pub const X86_DISTCC_DEFAULT_PORT: u16 = 3632;
pub const X86_ICECC_DEFAULT_PORT: u16 = 10245;
pub const X86_DISTCC_PROTOCOL_VERSION: u32 = 3;
pub const X86_DEFAULT_CACHE_SIZE_MB: u64 = 5120;
pub const X86_CACHE_MAX_AGE_SECS: u64 = 604800;
pub const X86_DISTCC_RESPONSE_PREFIX: &str = "DISTCC_HOSTS";
#[derive(Debug)]
pub struct X86DriverJobs {
pub scheduler: Arc<Mutex<X86JobScheduler>>,
pub pipeline: X86JobPipeline,
pub distributed: Option<X86DistributedCompilation>,
pub farm: Arc<X86CompilationFarm>,
pub cache: Arc<X86JobCache>,
total_submitted: AtomicU64,
total_completed: AtomicU64,
start_time: Instant,
pub verbose: bool,
pub dry_run: bool,
}
impl X86DriverJobs {
pub fn new(verbose: bool, dry_run: bool) -> Self {
let scheduler_config = X86SchedulerConfig::default();
Self {
scheduler: Arc::new(Mutex::new(X86JobScheduler::new(scheduler_config))),
pipeline: X86JobPipeline::new(),
distributed: None,
farm: Arc::new(X86CompilationFarm::new()),
cache: Arc::new(X86JobCache::new()),
total_submitted: AtomicU64::new(0),
total_completed: AtomicU64::new(0),
start_time: Instant::now(),
verbose,
dry_run,
}
}
pub fn with_parallel_jobs(jobs: usize, verbose: bool, dry_run: bool) -> Self {
let mut config = X86SchedulerConfig::default();
config.max_parallel = jobs;
Self {
scheduler: Arc::new(Mutex::new(X86JobScheduler::new(config))),
pipeline: X86JobPipeline::new(),
distributed: None,
farm: Arc::new(X86CompilationFarm::new()),
cache: Arc::new(X86JobCache::new()),
total_submitted: AtomicU64::new(0),
total_completed: AtomicU64::new(0),
start_time: Instant::now(),
verbose,
dry_run,
}
}
pub fn enable_distcc(&mut self, hosts: Vec<String>) -> &mut Self {
let dist = X86DistributedCompilation::new_distcc(hosts);
self.distributed = Some(dist);
self
}
pub fn enable_icecream(&mut self, scheduler_addr: SocketAddr) -> &mut Self {
let dist = X86DistributedCompilation::new_icecream(scheduler_addr);
self.distributed = Some(dist);
self
}
pub fn add_source(&mut self, path: &Path, options: &ClangOptions) -> usize {
self.pipeline.add_source(path, options)
}
pub fn add_sources(&mut self, paths: &[&Path], options: &ClangOptions) -> Vec<usize> {
paths
.iter()
.map(|p| self.pipeline.add_source(p, options))
.collect()
}
pub fn build_jobs(&mut self) -> X86JobGraphStats {
let stats = self.pipeline.build(&self.scheduler, &self.cache);
self.total_submitted
.store(stats.total_jobs as u64, AtomicOrdering::SeqCst);
stats
}
pub fn execute_sequential(&self) -> Vec<X86JobResult> {
if self.dry_run {
self.dry_run_report();
return Vec::new();
}
self.scheduler.lock().unwrap().execute_sequential()
}
pub fn execute_parallel(&self) -> Vec<X86JobResult> {
if self.dry_run {
self.dry_run_report();
return Vec::new();
}
self.scheduler.lock().unwrap().execute_parallel()
}
pub fn execute_distributed(&self) -> Vec<X86JobResult> {
if self.dry_run {
self.dry_run_report();
return Vec::new();
}
if let Some(ref dist) = self.distributed {
if dist.is_available() {
return self.execute_with_distributed(dist);
}
}
self.execute_parallel()
}
fn execute_with_distributed(&self, dist: &X86DistributedCompilation) -> Vec<X86JobResult> {
let mut results = Vec::new();
let mut scheduler = self.scheduler.lock().unwrap();
let ready = scheduler.get_ready_jobs();
for (job_id, job) in ready {
match dist.dispatch_job(&job) {
Ok(result) => {
scheduler.mark_completed(job_id, result.status.clone());
results.push(result);
}
Err(_) => {
let result = scheduler.execute_single(job_id);
results.push(result);
}
}
}
results
}
pub fn add_link_job(&self, objects: &[PathBuf], output: &Path) -> usize {
let mut scheduler = self.scheduler.lock().unwrap();
let link_job = X86DriverJob {
kind: X86JobKind::Link(X86LinkJob {
inputs: objects.to_vec(),
output: output.to_path_buf(),
libraries: Vec::new(),
library_paths: Vec::new(),
link_flags: Vec::new(),
}),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: X86_DEFAULT_JOB_TIMEOUT_SECS,
estimated_memory_mb: 512,
use_response_file: false,
};
scheduler.add_job(link_job, X86JobPriority::Critical)
}
pub fn statistics(&self) -> X86BuildStatistics {
let scheduler = self.scheduler.lock().unwrap();
let sched_stats = scheduler.statistics();
X86BuildStatistics {
total_jobs: self.total_submitted.load(AtomicOrdering::SeqCst) as usize,
completed: self.total_completed.load(AtomicOrdering::SeqCst) as usize,
failed: sched_stats.failed,
skipped: sched_stats.skipped,
elapsed_ms: self.start_time.elapsed().as_millis() as u64,
cache_hits: self.cache.stats().hits,
cache_misses: self.cache.stats().misses,
distributed_jobs: if self.distributed.is_some() {
sched_stats.total_jobs
} else {
0
},
}
}
fn dry_run_report(&self) {
let scheduler = self.scheduler.lock().unwrap();
let job_list = scheduler.all_jobs();
println!("[X86 DRY RUN] Would execute {} jobs:", job_list.len());
for (id, job) in &job_list {
println!(" [{}] {} — {:?}", id, job.description(), job.kind);
}
}
pub fn has_failures(&self) -> bool {
let stats = self.statistics();
stats.failed > 0
}
}
impl Default for X86DriverJobs {
fn default() -> Self {
Self::new(false, false)
}
}
#[derive(Debug, Clone, Copy)]
pub struct X86BuildStatistics {
pub total_jobs: usize,
pub completed: usize,
pub failed: usize,
pub skipped: usize,
pub elapsed_ms: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub distributed_jobs: usize,
}
impl fmt::Display for X86BuildStatistics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"╔══════════════════════════════════════════════════════╗"
)?;
writeln!(f, "║ X86 Build Statistics ║")?;
writeln!(
f,
"╠══════════════════════════════════════════════════════╣"
)?;
writeln!(
f,
"║ Total jobs: {:5} ║",
self.total_jobs
)?;
writeln!(
f,
"║ Completed: {:5} ║",
self.completed
)?;
writeln!(
f,
"║ Failed: {:5} ║",
self.failed
)?;
writeln!(
f,
"║ Skipped (cache): {:5} ║",
self.skipped
)?;
writeln!(
f,
"║ Cache hits: {:5} ║",
self.cache_hits
)?;
writeln!(
f,
"║ Cache misses: {:5} ║",
self.cache_misses
)?;
writeln!(
f,
"║ Distributed: {:5} ║",
self.distributed_jobs
)?;
writeln!(
f,
"║ Elapsed: {:5} ms ║",
self.elapsed_ms
)?;
writeln!(
f,
"╚══════════════════════════════════════════════════════╝"
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct X86JobGraphStats {
pub total_jobs: usize,
pub compile_jobs: usize,
pub link_jobs: usize,
pub archive_jobs: usize,
pub lto_jobs: usize,
pub preprocess_jobs: usize,
pub dependency_edges: usize,
}
impl fmt::Display for X86JobGraphStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"jobs={} (C={}, L={}, A={}, LTO={}, PP={}) edges={}",
self.total_jobs,
self.compile_jobs,
self.link_jobs,
self.archive_jobs,
self.lto_jobs,
self.preprocess_jobs,
self.dependency_edges
)
}
}
#[derive(Debug, Clone)]
pub struct X86SchedulerConfig {
pub max_parallel: usize,
pub fail_fast: bool,
pub use_priority: bool,
pub show_progress: bool,
pub verbose: bool,
pub memory_limit_mb: u64,
pub cpu_threshold_pct: f64,
pub batch_jobs: bool,
pub max_batch_size: usize,
pub incremental: bool,
}
impl Default for X86SchedulerConfig {
fn default() -> Self {
Self {
max_parallel: detect_cpu_count(),
fail_fast: false,
use_priority: true,
show_progress: true,
verbose: false,
memory_limit_mb: X86_DEFAULT_MEMORY_LIMIT_MB,
cpu_threshold_pct: X86_CPU_THRESHOLD_PCT,
batch_jobs: false,
max_batch_size: 32,
incremental: true,
}
}
}
#[derive(Debug)]
pub struct X86JobScheduler {
config: X86SchedulerConfig,
jobs: HashMap<usize, X86DriverJob>,
dependencies: HashMap<usize, Vec<usize>>,
reverse_deps: HashMap<usize, Vec<usize>>,
ready_queue: BinaryHeap<X86JobQueueEntry>,
results: HashMap<usize, X86JobResult>,
running: HashSet<usize>,
failed: HashSet<usize>,
completed_count: usize,
next_job_id: usize,
topo_order: Option<Vec<usize>>,
temp_files: X86TempFileManager,
start_time: Option<Instant>,
resource_tracker: X86ResourceTracker,
}
impl X86JobScheduler {
pub fn new(config: X86SchedulerConfig) -> Self {
Self {
config,
jobs: HashMap::new(),
dependencies: HashMap::new(),
reverse_deps: HashMap::new(),
ready_queue: BinaryHeap::new(),
results: HashMap::new(),
running: HashSet::new(),
failed: HashSet::new(),
completed_count: 0,
next_job_id: 0,
topo_order: None,
temp_files: X86TempFileManager::new("x86_job"),
start_time: None,
resource_tracker: X86ResourceTracker::new(),
}
}
pub fn add_job(&mut self, job: X86DriverJob, priority: X86JobPriority) -> usize {
let id = self.next_job_id;
self.next_job_id += 1;
self.jobs.insert(id, job);
self.dependencies.entry(id).or_default();
self.reverse_deps.entry(id).or_default();
self.topo_order = None; id
}
pub fn add_dependency(&mut self, job: usize, depends_on: usize) {
self.dependencies.entry(job).or_default().push(depends_on);
self.reverse_deps.entry(depends_on).or_default().push(job);
self.topo_order = None;
}
pub fn job_count(&self) -> usize {
self.jobs.len()
}
pub fn dependencies_of(&self, job: usize) -> Option<&[usize]> {
self.dependencies.get(&job).map(|v| v.as_slice())
}
pub fn dependents_of(&self, job: usize) -> Option<&[usize]> {
self.reverse_deps.get(&job).map(|v| v.as_slice())
}
pub fn topological_order(&self) -> Vec<usize> {
let mut in_degree: HashMap<usize, usize> = HashMap::new();
for id in self.jobs.keys() {
in_degree.entry(*id).or_insert(0);
}
for (job_id, deps) in &self.dependencies {
for dep in deps {
*in_degree.entry(*dep).or_insert(0) += 0; *in_degree.entry(*job_id).or_insert(0) += 1; }
}
let mut indeg: HashMap<usize, usize> = HashMap::new();
for id in self.jobs.keys() {
indeg.insert(*id, 0);
}
for (job_id, deps) in &self.dependencies {
for dep in deps {
let entry = indeg.entry(*job_id).or_insert(0);
*entry += 1;
}
}
let mut queue: VecDeque<usize> = indeg
.iter()
.filter(|&(_, °)| deg == 0)
.map(|(&id, _)| id)
.collect();
let mut order = Vec::new();
while let Some(node) = queue.pop_front() {
order.push(node);
if let Some(dependents) = self.reverse_deps.get(&node) {
for &dep in dependents {
if let Some(deg) = indeg.get_mut(&dep) {
*deg -= 1;
if *deg == 0 {
queue.push_back(dep);
}
}
}
}
}
order
}
pub fn get_ready_jobs(&mut self) -> Vec<(usize, X86DriverJob)> {
let mut ready = Vec::new();
for (&id, job) in &self.jobs {
if self.results.contains_key(&id) || self.running.contains(&id) {
continue;
}
let deps = self
.dependencies
.get(&id)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let all_deps_done = deps.iter().all(|d| {
self.results
.get(d)
.map(|r| {
r.status == X86JobStatus::Completed || r.status == X86JobStatus::Skipped
})
.unwrap_or(false)
});
if all_deps_done {
ready.push((id, job.clone()));
}
}
ready
}
pub fn enqueue_ready(&mut self, id: usize, priority: X86JobPriority) {
self.ready_queue.push(X86JobQueueEntry { id, priority });
}
pub fn build_ready_queue(&mut self) {
for (&id, _job) in &self.jobs {
if self.results.contains_key(&id) || self.running.contains(&id) {
continue;
}
let deps = self
.dependencies
.get(&id)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let all_done = deps.iter().all(|d| {
self.results
.get(d)
.map(|r| {
r.status == X86JobStatus::Completed || r.status == X86JobStatus::Skipped
})
.unwrap_or(false)
});
if all_done {
let priority = self
.jobs
.get(&id)
.map(|j| j.priority)
.unwrap_or(X86JobPriority::Normal);
self.ready_queue.push(X86JobQueueEntry { id, priority });
}
}
}
pub fn mark_running(&mut self, job_id: usize) {
self.running.insert(job_id);
}
pub fn mark_completed(&mut self, job_id: usize, status: X86JobStatus) {
self.running.remove(&job_id);
let result = X86JobResult {
job_id,
status: status.clone(),
duration: Duration::from_secs(0),
stdout: String::new(),
stderr: String::new(),
exit_code: if status == X86JobStatus::Completed {
0
} else {
1
},
cached: false,
};
self.results.insert(job_id, result);
if status.is_failed() {
self.failed.insert(job_id);
} else {
self.completed_count += 1;
}
if let Some(dependents) = self.reverse_deps.get(&job_id).cloned() {
for dep_id in dependents {
if self.results.contains_key(&dep_id) || self.running.contains(&dep_id) {
continue;
}
let all_done = self
.dependencies
.get(&dep_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
.iter()
.all(|d| self.results.contains_key(d));
if all_done {
let priority = self
.jobs
.get(&dep_id)
.map(|j| j.priority)
.unwrap_or(X86JobPriority::Normal);
self.ready_queue.push(X86JobQueueEntry {
id: dep_id,
priority,
});
}
}
}
}
pub fn execute_single(&mut self, job_id: usize) -> X86JobResult {
let job = match self.jobs.get(&job_id) {
Some(j) => j.clone(),
None => {
return X86JobResult {
job_id,
status: X86JobStatus::Failed(format!("Job {} not found", job_id)),
duration: Duration::from_secs(0),
stdout: String::new(),
stderr: String::new(),
exit_code: 1,
cached: false,
};
}
};
let start = Instant::now();
let result = self.simulate_execute(&job);
let elapsed = start.elapsed();
let mut final_result = result;
final_result.job_id = job_id;
final_result.duration = elapsed;
self.results.insert(job_id, final_result.clone());
if final_result.status.is_completed() || final_result.status.is_skipped() {
self.completed_count += 1;
}
if final_result.status.is_failed() {
self.failed.insert(job_id);
}
final_result
}
fn simulate_execute(&self, job: &X86DriverJob) -> X86JobResult {
let output = format!(
"[simulated] Executing {} job: {}",
job.kind.name(),
job.description()
);
X86JobResult {
job_id: 0,
status: X86JobStatus::Completed,
duration: Duration::from_millis(1),
stdout: output,
stderr: String::new(),
exit_code: 0,
cached: false,
}
}
pub fn execute_sequential(&mut self) -> Vec<X86JobResult> {
self.start_time = Some(Instant::now());
let order = self.topological_order();
let mut results = Vec::new();
for &id in &order {
if self.failed.contains(&id) && self.config.fail_fast {
break;
}
let result = self.execute_single(id);
results.push(result);
}
results
}
pub fn execute_parallel(&mut self) -> Vec<X86JobResult> {
self.start_time = Some(Instant::now());
let mut results = Vec::new();
let total = self.jobs.len();
self.build_ready_queue();
while results.len() < total {
if self.config.fail_fast && !self.failed.is_empty() {
for (&id, _job) in &self.jobs {
if !self.results.contains_key(&id) {
self.results.insert(
id,
X86JobResult {
job_id: id,
status: X86JobStatus::Skipped,
duration: Duration::from_secs(0),
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
cached: false,
},
);
}
}
break;
}
let mut batch = Vec::new();
while batch.len() < self.config.max_parallel {
if let Some(entry) = self.ready_queue.pop() {
batch.push(entry.id);
} else {
break;
}
}
if batch.is_empty() {
if self.running.is_empty() {
break;
}
let running_ids: Vec<usize> = self.running.iter().copied().collect();
for rid in running_ids {
self.mark_completed(rid, X86JobStatus::Completed);
}
continue;
}
for job_id in batch {
let result = self.execute_single(job_id);
self.mark_completed(job_id, result.status.clone());
results.push(result);
}
}
for (&id, _) in &self.jobs {
if !self.results.contains_key(&id) {
self.results.insert(
id,
X86JobResult {
job_id: id,
status: X86JobStatus::Skipped,
duration: Duration::from_secs(0),
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
cached: false,
},
);
}
}
results
}
pub fn execute_threaded(&mut self) -> Vec<X86JobResult> {
let num_threads = self.config.max_parallel.max(1);
let fail_fast = self.config.fail_fast;
let (tx, rx) = mpsc::channel::<X86JobResult>();
let job_count = Arc::new(AtomicUsize::new(self.jobs.len()));
let failed_flag = Arc::new(AtomicBool::new(false));
let jobs_arc = Arc::new(self.jobs.clone());
let deps_arc = Arc::new(self.dependencies.clone());
let results_arc = Arc::new(Mutex::new(HashMap::new()));
let running_arc = Arc::new(Mutex::new(HashSet::new()));
let ready_arc = Arc::new(Mutex::new(BinaryHeap::<X86JobQueueEntry>::new()));
{
let mut ready = ready_arc.lock().unwrap();
for (&id, _) in jobs_arc.iter() {
let deps = deps_arc.get(&id).map(|v| v.as_slice()).unwrap_or(&[]);
if deps.is_empty() {
ready.push(X86JobQueueEntry {
id,
priority: X86JobPriority::Normal,
});
}
}
}
let barrier = Arc::new(Barrier::new(num_threads + 1));
for _ in 0..num_threads {
let tx = tx.clone();
let jobs = Arc::clone(&jobs_arc);
let deps = Arc::clone(&deps_arc);
let results = Arc::clone(&results_arc);
let running = Arc::clone(&running_arc);
let ready = Arc::clone(&ready_arc);
let job_count = Arc::clone(&job_count);
let failed_flag = Arc::clone(&failed_flag);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
loop {
if failed_flag.load(AtomicOrdering::SeqCst) && fail_fast {
break;
}
let job_id = {
let mut r = ready.lock().unwrap();
r.pop().map(|e| e.id)
};
let job_id = match job_id {
Some(id) => id,
None => {
let running_count = running.lock().unwrap().len();
if running_count == 0 {
break;
}
thread::sleep(Duration::from_millis(1));
continue;
}
};
{
let mut run = running.lock().unwrap();
run.insert(job_id);
}
let job = jobs.get(&job_id).cloned();
let result = if let Some(j) = job {
X86JobResult {
job_id,
status: X86JobStatus::Completed,
duration: Duration::from_millis(1),
stdout: format!("[thread] {}", j.description()),
stderr: String::new(),
exit_code: 0,
cached: false,
}
} else {
X86JobResult {
job_id,
status: X86JobStatus::Failed("Job not found".into()),
duration: Duration::from_secs(0),
stdout: String::new(),
stderr: String::new(),
exit_code: 1,
cached: false,
}
};
{
let mut run = running.lock().unwrap();
run.remove(&job_id);
}
if result.status.is_failed() {
failed_flag.store(true, AtomicOrdering::SeqCst);
}
{
let mut res = results.lock().unwrap();
res.insert(job_id, result.status.clone());
}
job_count.fetch_sub(1, AtomicOrdering::SeqCst);
let rev = reverse_deps_global(&deps);
if let Some(dependents) = rev.get(&job_id) {
let mut r = ready.lock().unwrap();
for &dep_id in dependents {
let dep_deps = deps.get(&dep_id).map(|v| v.as_slice()).unwrap_or(&[]);
let all_ready = dep_deps
.iter()
.all(|d| results.lock().unwrap().contains_key(d));
if all_ready {
let prio = jobs
.get(&dep_id)
.map(|j| j.priority)
.unwrap_or(X86JobPriority::Normal);
r.push(X86JobQueueEntry {
id: dep_id,
priority: prio,
});
}
}
}
let _ = tx.send(result);
}
});
}
barrier.wait();
drop(tx);
let mut collected = Vec::new();
for r in rx {
collected.push(r);
}
collected
}
pub fn statistics(&self) -> X86SchedulerStats {
let total = self.jobs.len();
let completed = self.results.len();
let failed = self.failed.len();
let skipped = total.saturating_sub(completed);
let elapsed = self.start_time.map(|t| t.elapsed()).unwrap_or_default();
X86SchedulerStats {
total_jobs: total,
completed,
failed,
skipped,
elapsed,
cache_hits: 0,
peak_memory_mb: 0,
distributed_count: 0,
}
}
pub fn all_jobs(&self) -> Vec<(usize, &X86DriverJob)> {
let mut jobs: Vec<_> = self.jobs.iter().map(|(k, v)| (*k, v)).collect();
jobs.sort_by_key(|(id, _)| *id);
jobs
}
pub fn clear(&mut self) {
self.jobs.clear();
self.dependencies.clear();
self.reverse_deps.clear();
self.ready_queue.clear();
self.results.clear();
self.running.clear();
self.failed.clear();
self.completed_count = 0;
self.topo_order = None;
self.start_time = None;
}
}
fn reverse_deps_global(deps: &HashMap<usize, Vec<usize>>) -> HashMap<usize, Vec<usize>> {
let mut rev: HashMap<usize, Vec<usize>> = HashMap::new();
for (&job, dep_list) in deps {
rev.entry(job).or_default();
for &d in dep_list {
rev.entry(d).or_default().push(job);
}
}
rev
}
#[derive(Debug, Clone, Eq)]
struct X86JobQueueEntry {
id: usize,
priority: X86JobPriority,
}
impl PartialEq for X86JobQueueEntry {
fn eq(&self, other: &Self) -> bool {
self.priority == other.priority && self.id == other.id
}
}
impl PartialOrd for X86JobQueueEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for X86JobQueueEntry {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.cmp(&other.priority)
.then_with(|| self.id.cmp(&other.id).reverse())
}
}
#[derive(Debug, Clone)]
pub struct X86SchedulerStats {
pub total_jobs: usize,
pub completed: usize,
pub failed: usize,
pub skipped: usize,
pub elapsed: Duration,
pub cache_hits: u64,
pub peak_memory_mb: u64,
pub distributed_count: usize,
}
#[derive(Debug)]
struct X86ResourceTracker {
current_memory_mb: u64,
peak_memory_mb: u64,
}
impl X86ResourceTracker {
fn new() -> Self {
Self {
current_memory_mb: 0,
peak_memory_mb: 0,
}
}
fn reserve(&mut self, mb: u64) -> bool {
self.current_memory_mb += mb;
self.peak_memory_mb = self.peak_memory_mb.max(self.current_memory_mb);
true
}
fn release(&mut self, mb: u64) {
self.current_memory_mb = self.current_memory_mb.saturating_sub(mb);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum X86JobPriority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum X86JobStatus {
Pending,
Running,
Completed,
Failed(String),
Skipped,
}
impl X86JobStatus {
pub fn is_failed(&self) -> bool {
matches!(self, X86JobStatus::Failed(_))
}
pub fn is_completed(&self) -> bool {
matches!(self, X86JobStatus::Completed)
}
pub fn is_skipped(&self) -> bool {
matches!(self, X86JobStatus::Skipped)
}
pub fn is_done(&self) -> bool {
matches!(
self,
X86JobStatus::Completed | X86JobStatus::Failed(_) | X86JobStatus::Skipped
)
}
}
#[derive(Debug, Clone)]
pub struct X86JobResult {
pub job_id: usize,
pub status: X86JobStatus,
pub duration: Duration,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub cached: bool,
}
#[derive(Debug, Clone)]
pub enum X86JobKind {
Preprocess(X86PreprocessJob),
Compile(X86CompileJob),
Backend(X86BackendJob),
Assemble(X86AssembleJob),
Link(X86LinkJob),
Archive(X86ArchiveJob),
LTO(X86LTOJob),
Custom(X86CustomJob),
}
impl X86JobKind {
pub fn name(&self) -> &'static str {
match self {
X86JobKind::Preprocess(_) => "preprocess",
X86JobKind::Compile(_) => "compile",
X86JobKind::Backend(_) => "backend",
X86JobKind::Assemble(_) => "assemble",
X86JobKind::Link(_) => "link",
X86JobKind::Archive(_) => "archive",
X86JobKind::LTO(_) => "lto",
X86JobKind::Custom(_) => "custom",
}
}
pub fn input_extension(&self) -> &'static str {
match self {
X86JobKind::Preprocess(_) => ".c",
X86JobKind::Compile(_) => ".i",
X86JobKind::Backend(_) => ".ll",
X86JobKind::Assemble(_) => ".s",
X86JobKind::Link(_) => ".o",
X86JobKind::Archive(_) => ".o",
X86JobKind::LTO(_) => ".o",
X86JobKind::Custom(_) => "",
}
}
pub fn output_extension(&self) -> &'static str {
match self {
X86JobKind::Preprocess(_) => ".i",
X86JobKind::Compile(_) => ".o",
X86JobKind::Backend(_) => ".o",
X86JobKind::Assemble(_) => ".o",
X86JobKind::Link(_) => "",
X86JobKind::Archive(_) => ".a",
X86JobKind::LTO(_) => "",
X86JobKind::Custom(_) => "",
}
}
pub fn is_distributable(&self) -> bool {
matches!(
self,
X86JobKind::Compile(_) | X86JobKind::Preprocess(_) | X86JobKind::Assemble(_)
)
}
}
#[derive(Debug, Clone)]
pub struct X86DriverJob {
pub kind: X86JobKind,
pub priority: X86JobPriority,
pub working_dir: Option<PathBuf>,
pub timeout_secs: u64,
pub estimated_memory_mb: u64,
pub use_response_file: bool,
}
impl X86DriverJob {
pub fn description(&self) -> String {
match &self.kind {
X86JobKind::Preprocess(j) => {
format!("Preprocess {}", j.input.display())
}
X86JobKind::Compile(j) => {
format!("Compile {}", j.input.display())
}
X86JobKind::Backend(j) => {
format!("Backend {}", j.input.display())
}
X86JobKind::Assemble(j) => {
format!("Assemble {}", j.input.display())
}
X86JobKind::Link(j) => {
format!("Link → {}", j.output.display())
}
X86JobKind::Archive(j) => {
format!("Archive → {}", j.output.display())
}
X86JobKind::LTO(j) => {
format!("LTO → {}", j.output.display())
}
X86JobKind::Custom(j) => {
format!("Custom: {}", j.description)
}
}
}
pub fn to_command_line(&self) -> String {
match &self.kind {
X86JobKind::Preprocess(j) => {
format!("clang -E {} -o {}", j.input.display(), j.output.display())
}
X86JobKind::Compile(j) => {
format!(
"clang -c {} -o {} {}",
j.input.display(),
j.output.display(),
j.flags.join(" ")
)
}
X86JobKind::Backend(j) => {
format!(
"llc {} -o {} {}",
j.input.display(),
j.output.display(),
j.flags.join(" ")
)
}
X86JobKind::Assemble(j) => {
format!("as {} -o {}", j.input.display(), j.output.display())
}
X86JobKind::Link(j) => {
let inputs = j
.inputs
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" ");
format!("clang {} -o {}", inputs, j.output.display())
}
X86JobKind::Archive(j) => {
let members = j
.members
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" ");
format!("ar rcs {} {}", j.output.display(), members)
}
X86JobKind::LTO(j) => {
format!(
"clang -flto {} -o {}",
j.inputs
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" "),
j.output.display()
)
}
X86JobKind::Custom(j) => j.command.clone(),
}
}
pub fn maybe_write_response_file(&self, base_dir: &Path) -> Option<PathBuf> {
let cmd = self.to_command_line();
if cmd.len() <= X86_MAX_COMMAND_LINE_LEN {
return None;
}
let rsp_path = base_dir.join(format!("job_{}.rsp", self.description().replace(' ', "_")));
if let Ok(mut f) = fs::File::create(&rsp_path) {
let _ = f.write_all(cmd.as_bytes());
}
Some(rsp_path)
}
}
#[derive(Debug, Clone)]
pub struct X86PreprocessJob {
pub input: PathBuf,
pub output: PathBuf,
pub defines: Vec<(String, Option<String>)>,
pub includes: Vec<PathBuf>,
pub undefines: Vec<String>,
pub line_markers: bool,
pub keep_comments: bool,
}
impl X86PreprocessJob {
pub fn new(input: PathBuf, output: PathBuf) -> Self {
Self {
input,
output,
defines: Vec::new(),
includes: Vec::new(),
undefines: Vec::new(),
line_markers: true,
keep_comments: false,
}
}
}
#[derive(Debug, Clone)]
pub struct X86CompileJob {
pub input: PathBuf,
pub output: PathBuf,
pub flags: Vec<String>,
pub opt_level: String,
pub standard: Option<String>,
pub debug_info: bool,
pub target_triple: String,
pub pic: bool,
pub includes: Vec<PathBuf>,
pub defines: Vec<(String, Option<String>)>,
}
impl X86CompileJob {
pub fn new(input: PathBuf, output: PathBuf) -> Self {
Self {
input,
output,
flags: Vec::new(),
opt_level: "-O2".to_string(),
standard: None,
debug_info: false,
target_triple: "x86_64-unknown-linux-gnu".to_string(),
pic: false,
includes: Vec::new(),
defines: Vec::new(),
}
}
pub fn all_flags(&self) -> Vec<String> {
let mut flags = self.flags.clone();
flags.push(self.opt_level.clone());
if let Some(ref std) = self.standard {
flags.push(format!("-std={}", std));
}
if self.debug_info {
flags.push("-g".to_string());
}
flags.push(format!("-target"));
flags.push(self.target_triple.clone());
if self.pic {
flags.push("-fPIC".to_string());
}
for inc in &self.includes {
flags.push(format!("-I{}", inc.display()));
}
for (name, value) in &self.defines {
if let Some(val) = value {
flags.push(format!("-D{}={}", name, val));
} else {
flags.push(format!("-D{}", name));
}
}
flags
}
}
#[derive(Debug, Clone)]
pub struct X86BackendJob {
pub input: PathBuf,
pub output: PathBuf,
pub flags: Vec<String>,
pub target_triple: String,
pub mcpu: Option<String>,
pub mattr: Vec<String>,
}
impl X86BackendJob {
pub fn new(input: PathBuf, output: PathBuf) -> Self {
Self {
input,
output,
flags: Vec::new(),
target_triple: "x86_64-unknown-linux-gnu".to_string(),
mcpu: None,
mattr: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct X86AssembleJob {
pub input: PathBuf,
pub output: PathBuf,
pub target_triple: String,
}
impl X86AssembleJob {
pub fn new(input: PathBuf, output: PathBuf) -> Self {
Self {
input,
output,
target_triple: "x86_64-unknown-linux-gnu".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct X86LinkJob {
pub inputs: Vec<PathBuf>,
pub output: PathBuf,
pub libraries: Vec<String>,
pub library_paths: Vec<PathBuf>,
pub link_flags: Vec<String>,
}
impl X86LinkJob {
pub fn new(inputs: Vec<PathBuf>, output: PathBuf) -> Self {
Self {
inputs,
output,
libraries: Vec::new(),
library_paths: Vec::new(),
link_flags: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct X86ArchiveJob {
pub members: Vec<PathBuf>,
pub output: PathBuf,
}
impl X86ArchiveJob {
pub fn new(members: Vec<PathBuf>, output: PathBuf) -> Self {
Self { members, output }
}
}
#[derive(Debug, Clone)]
pub struct X86LTOJob {
pub inputs: Vec<PathBuf>,
pub output: PathBuf,
pub thin: bool,
pub lto_jobs: usize,
pub cache_dir: Option<PathBuf>,
pub opt_level: String,
}
impl X86LTOJob {
pub fn new(inputs: Vec<PathBuf>, output: PathBuf) -> Self {
Self {
inputs,
output,
thin: true,
lto_jobs: detect_cpu_count(),
cache_dir: None,
opt_level: "-O2".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct X86CustomJob {
pub command: String,
pub description: String,
pub inputs: Vec<PathBuf>,
pub outputs: Vec<PathBuf>,
}
impl X86CustomJob {
pub fn new(command: &str, description: &str) -> Self {
Self {
command: command.to_string(),
description: description.to_string(),
inputs: Vec::new(),
outputs: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct X86JobPipeline {
sources: Vec<X86PipelineSource>,
output_dir: PathBuf,
generate_deps: bool,
dep_dir: Option<PathBuf>,
use_response_files: bool,
incremental: bool,
compile_db: Vec<X86CompileCommand>,
generate_compile_db: bool,
compile_db_path: Option<PathBuf>,
}
impl X86JobPipeline {
pub fn new() -> Self {
Self {
sources: Vec::new(),
output_dir: PathBuf::from("."),
generate_deps: false,
dep_dir: None,
use_response_files: true,
incremental: true,
compile_db: Vec::new(),
generate_compile_db: false,
compile_db_path: None,
}
}
pub fn set_output_dir(&mut self, dir: &Path) -> &mut Self {
self.output_dir = dir.to_path_buf();
self
}
pub fn enable_dep_files(&mut self, dep_dir: Option<&Path>) -> &mut Self {
self.generate_deps = true;
self.dep_dir = dep_dir.map(|p| p.to_path_buf());
self
}
pub fn enable_compile_db(&mut self, path: &Path) -> &mut Self {
self.generate_compile_db = true;
self.compile_db_path = Some(path.to_path_buf());
self
}
pub fn force_rebuild(&mut self) -> &mut Self {
self.incremental = false;
self
}
pub fn add_source(&mut self, path: &Path, options: &ClangOptions) -> usize {
let id = self.sources.len();
self.sources.push(X86PipelineSource {
path: path.to_path_buf(),
options: options.clone(),
last_modified: Self::get_mtime(path),
});
id
}
pub fn build(
&mut self,
scheduler: &Arc<Mutex<X86JobScheduler>>,
cache: &Arc<X86JobCache>,
) -> X86JobGraphStats {
let mut stats = X86JobGraphStats {
total_jobs: 0,
compile_jobs: 0,
link_jobs: 0,
archive_jobs: 0,
lto_jobs: 0,
preprocess_jobs: 0,
dependency_edges: 0,
};
let mut scheduler = scheduler.lock().unwrap();
let mut obj_ids: Vec<usize> = Vec::new();
let mut all_objects: Vec<PathBuf> = Vec::new();
for source in &self.sources {
let cache_key = X86CacheKey::from_source(&source.path, &source.options);
if self.incremental {
if let Some(cached) = cache.lookup(&cache_key) {
if cached.is_fresh(source.last_modified) {
stats.total_jobs += 1;
let job_id = scheduler.add_job(
X86DriverJob {
kind: X86JobKind::Custom(X86CustomJob::new("cached", "cached")),
priority: X86JobPriority::Low,
working_dir: None,
timeout_secs: 0,
estimated_memory_mb: 0,
use_response_file: false,
},
X86JobPriority::Low,
);
scheduler.mark_completed(job_id, X86JobStatus::Skipped);
all_objects.push(cached.output_path.clone());
obj_ids.push(job_id);
continue;
}
}
}
let pp_output = self.output_dir.join(format!(
"{}.i",
source.path.file_stem().unwrap().to_string_lossy()
));
let pp_job = X86DriverJob {
kind: X86JobKind::Preprocess(X86PreprocessJob::new(
source.path.clone(),
pp_output.clone(),
)),
priority: X86JobPriority::High,
working_dir: None,
timeout_secs: X86_DEFAULT_JOB_TIMEOUT_SECS,
estimated_memory_mb: 256,
use_response_file: false,
};
let pp_id = scheduler.add_job(pp_job, X86JobPriority::High);
stats.preprocess_jobs += 1;
let obj_output = self.output_dir.join(format!(
"{}.o",
source.path.file_stem().unwrap().to_string_lossy()
));
let mut compile = X86CompileJob::new(pp_output.clone(), obj_output.clone());
compile.target_triple = source.options.target_triple.clone();
compile.debug_info = source.options.debug_info;
compile.opt_level = if source.options.optimize {
"-O2".to_string()
} else {
"-O0".to_string()
};
for inc in &source.options.includes {
compile.includes.push(PathBuf::from(inc));
}
compile.defines = source.options.defines.clone();
if self.generate_deps {
let dep_file = self
.dep_dir
.clone()
.unwrap_or_else(|| self.output_dir.clone())
.join(format!(
"{}.d",
source.path.file_stem().unwrap().to_string_lossy()
));
compile.flags.push(format!("-MF{}", dep_file.display()));
compile.flags.push("-MD".to_string());
}
let compile_job = X86DriverJob {
kind: X86JobKind::Compile(compile),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: X86_DEFAULT_JOB_TIMEOUT_SECS,
estimated_memory_mb: 512,
use_response_file: self.use_response_files,
};
let compile_id = scheduler.add_job(compile_job, X86JobPriority::Normal);
scheduler.add_dependency(compile_id, pp_id);
stats.dependency_edges += 1;
stats.compile_jobs += 1;
all_objects.push(obj_output.clone());
obj_ids.push(compile_id);
if self.generate_compile_db {
self.compile_db.push(X86CompileCommand {
directory: std::env::current_dir().unwrap_or_default(),
file: source.path.clone(),
arguments: vec![
"clang".to_string(),
"-c".to_string(),
source.path.display().to_string(),
"-o".to_string(),
obj_output.display().to_string(),
],
output: Some(obj_output.display().to_string()),
});
}
}
if !all_objects.is_empty() {
let exe_output = self.output_dir.join("a.out");
let link = X86LinkJob::new(all_objects, exe_output);
let link_job = X86DriverJob {
kind: X86JobKind::Link(link),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: X86_DEFAULT_JOB_TIMEOUT_SECS,
estimated_memory_mb: 1024,
use_response_file: true,
};
let link_id = scheduler.add_job(link_job, X86JobPriority::Critical);
for obj_id in &obj_ids {
scheduler.add_dependency(link_id, *obj_id);
stats.dependency_edges += 1;
}
stats.link_jobs += 1;
}
stats.total_jobs = scheduler.job_count();
if self.generate_compile_db {
if let Some(ref db_path) = self.compile_db_path {
if let Ok(json) = serde_json::to_string_pretty(&self.compile_db) {
let _ = fs::write(db_path, json);
}
}
}
stats
}
pub fn load_compile_commands(&mut self, path: &Path) -> Result<usize, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
let commands: Vec<X86CompileCommand> = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;
let count = commands.len();
self.compile_db = commands;
Ok(count)
}
pub fn write_depfile(output: &Path, target: &Path, dependencies: &[PathBuf]) -> io::Result<()> {
let mut f = fs::File::create(output)?;
writeln!(
f,
"{}: {}",
Self::escape_path(target),
dependencies
.iter()
.map(|p| Self::escape_path(p))
.collect::<Vec<_>>()
.join(" \\\n ")
)?;
for dep in dependencies {
writeln!(f)?;
writeln!(f, "{}:", Self::escape_path(dep))?;
}
Ok(())
}
fn escape_path(p: &Path) -> String {
let s = p.display().to_string();
if s.contains(' ') || s.contains('#') {
s.replace(' ', "\\ ").replace('#', "\\#")
} else {
s
}
}
fn get_mtime(path: &Path) -> Option<SystemTime> {
fs::metadata(path).ok().and_then(|m| m.modified().ok())
}
}
impl Default for X86JobPipeline {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
struct X86PipelineSource {
path: PathBuf,
options: ClangOptions,
last_modified: Option<SystemTime>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct X86CompileCommand {
pub directory: PathBuf,
pub file: PathBuf,
pub arguments: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum X86DistributedProtocol {
Distcc,
Icecream,
}
#[derive(Debug)]
pub struct X86DistributedCompilation {
pub protocol: X86DistributedProtocol,
pub distcc_hosts: Vec<String>,
pub icecream_scheduler: Option<SocketAddr>,
pub local_preprocess: bool,
pub compress: bool,
pub compression_level: u32,
pub fallback_local: bool,
pub max_distributed_jobs: usize,
pub remote_timeout_secs: u64,
pub verify_results: bool,
available: AtomicBool,
}
impl Clone for X86DistributedCompilation {
fn clone(&self) -> Self {
Self {
protocol: self.protocol.clone(),
distcc_hosts: self.distcc_hosts.clone(),
icecream_scheduler: self.icecream_scheduler,
local_preprocess: self.local_preprocess,
compress: self.compress,
compression_level: self.compression_level,
fallback_local: self.fallback_local,
max_distributed_jobs: self.max_distributed_jobs,
remote_timeout_secs: self.remote_timeout_secs,
verify_results: self.verify_results,
available: AtomicBool::new(self.available.load(std::sync::atomic::Ordering::SeqCst)),
}
}
}
impl X86DistributedCompilation {
pub fn new_distcc(hosts: Vec<String>) -> Self {
Self {
protocol: X86DistributedProtocol::Distcc,
distcc_hosts: hosts,
icecream_scheduler: None,
local_preprocess: true,
compress: true,
compression_level: 3,
fallback_local: true,
max_distributed_jobs: detect_cpu_count() * 2,
remote_timeout_secs: 120,
verify_results: true,
available: AtomicBool::new(true),
}
}
pub fn new_icecream(scheduler_addr: SocketAddr) -> Self {
Self {
protocol: X86DistributedProtocol::Icecream,
distcc_hosts: Vec::new(),
icecream_scheduler: Some(scheduler_addr),
local_preprocess: true,
compress: true,
compression_level: 3,
fallback_local: true,
max_distributed_jobs: detect_cpu_count() * 4,
remote_timeout_secs: 120,
verify_results: true,
available: AtomicBool::new(true),
}
}
pub fn is_available(&self) -> bool {
self.available.load(AtomicOrdering::SeqCst)
}
pub fn set_available(&self, available: bool) {
self.available.store(available, AtomicOrdering::SeqCst);
}
pub fn dispatch_job(&self, job: &X86DriverJob) -> Result<X86JobResult, String> {
if !job.kind.is_distributable() {
return Err("Job kind is not distributable".to_string());
}
match self.protocol {
X86DistributedProtocol::Distcc => self.dispatch_distcc(job),
X86DistributedProtocol::Icecream => self.dispatch_icecream(job),
}
}
fn dispatch_distcc(&self, job: &X86DriverJob) -> Result<X86JobResult, String> {
let start = Instant::now();
let mut args = vec!["distcc".to_string()];
for host in &self.distcc_hosts {
args.push(format!("DISTCC_HOSTS={}", host));
}
if self.compress {
args.push("--compress".to_string());
}
args.push(job.to_command_line());
let result = X86JobResult {
job_id: 0,
status: X86JobStatus::Completed,
duration: start.elapsed(),
stdout: format!("[distcc] compiled on remote: {}", job.description()),
stderr: String::new(),
exit_code: 0,
cached: false,
};
Ok(result)
}
fn dispatch_icecream(&self, job: &X86DriverJob) -> Result<X86JobResult, String> {
let start = Instant::now();
let scheduler = self
.icecream_scheduler
.ok_or("No icecream scheduler configured")?;
let _ = scheduler;
let result = X86JobResult {
job_id: 0,
status: X86JobStatus::Completed,
duration: start.elapsed(),
stdout: format!("[icecream] compiled on remote: {}", job.description()),
stderr: String::new(),
exit_code: 0,
cached: false,
};
Ok(result)
}
pub fn local_preprocess_source(&self, source: &Path, output: &Path) -> Result<PathBuf, String> {
let pp_output = output.with_extension("i");
let _ = fs::write(
&pp_output,
format!(
"/* Preprocessed from {} */\nint main() {{ return 0; }}",
source.display()
),
);
Ok(pp_output)
}
fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, String> {
if !self.compress {
return Ok(data.to_vec());
}
Ok(data.to_vec())
}
fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, String> {
if !self.compress {
return Ok(data.to_vec());
}
Ok(data.to_vec())
}
pub fn verify_result(&self, remote: &Path, local: &Path) -> bool {
if !self.verify_results {
return true;
}
match (fs::metadata(remote), fs::metadata(local)) {
(Ok(rm), Ok(lm)) => {
rm.len() == lm.len()
}
_ => false,
}
}
pub fn distcc_hosts_string(&self) -> String {
self.distcc_hosts.join(" ")
}
pub fn parse_distcc_hosts(hosts_str: &str) -> Vec<String> {
hosts_str
.split_whitespace()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
}
#[derive(Debug)]
struct DistccFrame {
frame_type: u32,
payload_len: u32,
payload: Vec<u8>,
}
impl DistccFrame {
fn new(frame_type: u32, payload: Vec<u8>) -> Self {
Self {
frame_type,
payload_len: payload.len() as u32,
payload,
}
}
fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(8 + self.payload.len());
buf.extend_from_slice(&self.frame_type.to_be_bytes());
buf.extend_from_slice(&self.payload_len.to_be_bytes());
buf.extend_from_slice(&self.payload);
buf
}
}
pub struct X86CompilationFarm {
nodes: RwLock<HashMap<String, X86WorkerNode>>,
balancer: RwLock<X86LoadBalancer>,
object_cache: RwLock<HashMap<String, X86SharedObjectCacheEntry>>,
health_interval: Duration,
shutdown: AtomicBool,
}
impl X86CompilationFarm {
pub fn new() -> Self {
Self {
nodes: RwLock::new(HashMap::new()),
balancer: RwLock::new(X86LoadBalancer::new()),
object_cache: RwLock::new(HashMap::new()),
health_interval: Duration::from_secs(30),
shutdown: AtomicBool::new(false),
}
}
pub fn add_node(&self, host: &str, port: u16, slots: usize) {
let addr = SocketAddr::new(
host.parse()
.unwrap_or_else(|_| "127.0.0.1".parse().unwrap()),
port,
);
let node = X86WorkerNode {
addr,
slots,
available_slots: slots,
healthy: true,
last_health_check: Instant::now(),
total_jobs_completed: 0,
total_jobs_failed: 0,
average_job_time_ms: 0,
};
self.nodes.write().unwrap().insert(host.to_string(), node);
}
pub fn remove_node(&self, host: &str) -> bool {
self.nodes.write().unwrap().remove(host).is_some()
}
pub fn node_addresses(&self) -> Vec<SocketAddr> {
self.nodes
.read()
.unwrap()
.values()
.map(|n| n.addr)
.collect()
}
pub fn healthy_count(&self) -> usize {
self.nodes
.read()
.unwrap()
.values()
.filter(|n| n.healthy)
.count()
}
pub fn available_slots(&self) -> usize {
self.nodes
.read()
.unwrap()
.values()
.filter(|n| n.healthy)
.map(|n| n.available_slots)
.sum()
}
pub fn select_node(&self) -> Option<SocketAddr> {
let nodes = self.nodes.read().unwrap();
let addr = self.balancer.write().unwrap().select(&nodes);
addr
}
pub fn job_started(&self, host: &str) {
if let Some(node) = self.nodes.write().unwrap().get_mut(host) {
if node.available_slots > 0 {
node.available_slots -= 1;
}
}
}
pub fn job_completed(&self, host: &str, success: bool, duration_ms: u64) {
if let Some(node) = self.nodes.write().unwrap().get_mut(host) {
node.available_slots += 1;
if success {
node.total_jobs_completed += 1;
} else {
node.total_jobs_failed += 1;
}
node.average_job_time_ms = if node.average_job_time_ms == 0 {
duration_ms
} else {
(node.average_job_time_ms * 7 + duration_ms) / 8
};
}
}
pub fn health_check(&self) -> X86FarmHealthReport {
let mut report = X86FarmHealthReport::default();
let nodes = self.nodes.read().unwrap();
for (name, node) in nodes.iter() {
if node.is_healthy() {
report.healthy.push(name.clone());
} else {
report.unhealthy.push(name.clone());
}
}
report.total = nodes.len();
report
}
pub fn start_health_monitor(self: &Arc<Self>) {
let farm = Arc::clone(self);
thread::spawn(move || {
while !farm.shutdown.load(AtomicOrdering::SeqCst) {
thread::sleep(Duration::from_secs(30));
let _report = farm.health_check();
}
});
}
pub fn stop_health_monitor(&self) {
self.shutdown.store(true, AtomicOrdering::SeqCst);
}
pub fn cache_lookup(&self, key: &str) -> Option<X86SharedObjectCacheEntry> {
self.object_cache.read().unwrap().get(key).cloned()
}
pub fn cache_store(&self, key: &str, path: PathBuf, size: u64) {
let entry = X86SharedObjectCacheEntry {
path,
size,
timestamp: SystemTime::now(),
hit_count: 0,
};
self.object_cache
.write()
.unwrap()
.insert(key.to_string(), entry);
}
}
impl fmt::Debug for X86CompilationFarm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let nodes = self.nodes.read().unwrap();
f.debug_struct("X86CompilationFarm")
.field("node_count", &nodes.len())
.field("healthy", &nodes.values().filter(|n| n.healthy).count())
.finish()
}
}
impl Default for X86CompilationFarm {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86WorkerNode {
pub addr: SocketAddr,
pub slots: usize,
pub available_slots: usize,
pub healthy: bool,
pub last_health_check: Instant,
pub total_jobs_completed: u64,
pub total_jobs_failed: u64,
pub average_job_time_ms: u64,
}
impl X86WorkerNode {
pub fn is_healthy(&self) -> bool {
self.healthy && self.last_health_check.elapsed() < Duration::from_secs(120)
}
pub fn load_factor(&self) -> f64 {
if self.slots == 0 {
return 1.0;
}
(self.slots - self.available_slots) as f64 / self.slots as f64
}
}
pub struct X86LoadBalancer {
next_index: usize,
}
impl X86LoadBalancer {
fn new() -> Self {
Self { next_index: 0 }
}
fn select(&mut self, nodes: &HashMap<String, X86WorkerNode>) -> Option<SocketAddr> {
let healthy: Vec<&X86WorkerNode> = nodes
.values()
.filter(|n| n.is_healthy() && n.available_slots > 0)
.collect();
if healthy.is_empty() {
return None;
}
let best = healthy
.iter()
.min_by(|a, b| {
a.load_factor()
.partial_cmp(&b.load_factor())
.unwrap_or(Ordering::Equal)
})
.copied()?;
Some(best.addr)
}
fn round_robin(&mut self, nodes: &HashMap<String, X86WorkerNode>) -> Option<SocketAddr> {
let healthy: Vec<&X86WorkerNode> = nodes.values().filter(|n| n.is_healthy()).collect();
if healthy.is_empty() {
return None;
}
let idx = self.next_index % healthy.len();
self.next_index += 1;
Some(healthy[idx].addr)
}
}
#[derive(Debug, Clone, Default)]
pub struct X86FarmHealthReport {
pub total: usize,
pub healthy: Vec<String>,
pub unhealthy: Vec<String>,
}
impl fmt::Display for X86FarmHealthReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"Compilation Farm Health: {}/{} healthy",
self.healthy.len(),
self.total
)?;
for h in &self.healthy {
writeln!(f, " ✓ {}", h)?;
}
for u in &self.unhealthy {
writeln!(f, " ✗ {}", u)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct X86SharedObjectCacheEntry {
pub path: PathBuf,
pub size: u64,
pub timestamp: SystemTime,
pub hit_count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct X86CacheKey {
source_hash: u64,
flags_hash: u64,
deps_hash: u64,
combined_hash: u64,
}
impl X86CacheKey {
pub fn from_source(path: &Path, options: &ClangOptions) -> Self {
let source_hash = Self::hash_path(path);
let flags_hash = Self::hash_options(options);
let deps_hash = 0u64; let combined_hash = source_hash ^ flags_hash ^ deps_hash;
Self {
source_hash,
flags_hash,
deps_hash,
combined_hash,
}
}
pub fn with_deps(path: &Path, options: &ClangOptions, deps: &[PathBuf]) -> Self {
let mut deps_hash = 0u64;
for dep in deps {
deps_hash ^= Self::hash_path(dep);
}
let source_hash = Self::hash_path(path);
let flags_hash = Self::hash_options(options);
Self {
source_hash,
flags_hash,
deps_hash,
combined_hash: source_hash ^ flags_hash ^ deps_hash,
}
}
fn hash_path(path: &Path) -> u64 {
Self::hash_bytes(path.to_string_lossy().as_bytes())
}
fn hash_options(options: &ClangOptions) -> u64 {
let mut h: u64 = 0;
h ^= Self::hash_bytes(options.target_triple.as_bytes());
h ^= (options.optimize as u64) << 32;
h ^= (options.debug_info as u64) << 16;
h ^= (options.standard.as_str().as_bytes().len() as u64) << 8;
for inc in &options.includes {
h ^= Self::hash_bytes(inc.as_bytes());
}
for (name, val) in &options.defines {
h ^= Self::hash_bytes(name.as_bytes());
if let Some(v) = val {
h ^= Self::hash_bytes(v.as_bytes());
}
}
h
}
fn hash_bytes(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
pub fn combined(&self) -> u64 {
self.combined_hash
}
}
#[derive(Debug)]
pub struct X86CachedEntry {
pub key: X86CacheKey,
pub output_path: PathBuf,
pub created_at: SystemTime,
pub size_bytes: u64,
pub hit_count: AtomicU64,
pub source_mtime: Option<SystemTime>,
}
impl Clone for X86CachedEntry {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
output_path: self.output_path.clone(),
created_at: self.created_at,
size_bytes: self.size_bytes,
hit_count: AtomicU64::new(self.hit_count.load(AtomicOrdering::SeqCst)),
source_mtime: self.source_mtime,
}
}
}
impl X86CachedEntry {
pub fn is_fresh(&self, current_mtime: Option<SystemTime>) -> bool {
match (self.source_mtime, current_mtime) {
(Some(cached), Some(current)) => cached >= current,
(None, _) | (_, None) => true, }
}
pub fn record_hit(&self) {
self.hit_count.fetch_add(1, AtomicOrdering::Relaxed);
}
}
pub struct X86JobCache {
memory_cache: RwLock<HashMap<u64, X86CachedEntry>>,
disk_cache_dir: RwLock<Option<PathBuf>>,
http_cache_url: RwLock<Option<String>>,
redis_addr: RwLock<Option<SocketAddr>>,
s3_bucket: RwLock<Option<String>>,
s3_prefix: RwLock<Option<String>>,
stats: RwLock<X86CacheStats>,
max_memory_entries: usize,
max_disk_size_bytes: u64,
current_disk_size: AtomicU64,
}
impl X86JobCache {
pub fn new() -> Self {
Self {
memory_cache: RwLock::new(HashMap::new()),
disk_cache_dir: RwLock::new(None),
http_cache_url: RwLock::new(None),
redis_addr: RwLock::new(None),
s3_bucket: RwLock::new(None),
s3_prefix: RwLock::new(None),
stats: RwLock::new(X86CacheStats::default()),
max_memory_entries: 1024,
max_disk_size_bytes: X86_DEFAULT_CACHE_SIZE_MB * 1024 * 1024,
current_disk_size: AtomicU64::new(0),
}
}
pub fn set_disk_cache(&self, dir: &Path) {
*self.disk_cache_dir.write().unwrap() = Some(dir.to_path_buf());
let _ = fs::create_dir_all(dir);
}
pub fn set_http_cache(&self, url: &str) {
*self.http_cache_url.write().unwrap() = Some(url.to_string());
}
pub fn set_redis_cache(&self, addr: SocketAddr) {
*self.redis_addr.write().unwrap() = Some(addr);
}
pub fn set_s3_cache(&self, bucket: &str, prefix: Option<&str>) {
*self.s3_bucket.write().unwrap() = Some(bucket.to_string());
*self.s3_prefix.write().unwrap() = prefix.map(|s| s.to_string());
}
pub fn lookup(&self, key: &X86CacheKey) -> Option<X86CachedEntry> {
let combined = key.combined();
{
let mem = self.memory_cache.read().unwrap();
if let Some(entry) = mem.get(&combined) {
entry.record_hit();
self.stats.write().unwrap().hits += 1;
return Some(entry.clone());
}
}
if let Some(ref dir) = *self.disk_cache_dir.read().unwrap() {
let cache_file = dir.join(format!("{:016x}.cache", combined));
if cache_file.exists() {
if let Ok(entry) = Self::deserialize_entry(&cache_file) {
self.memory_cache
.write()
.unwrap()
.insert(combined, entry.clone());
entry.record_hit();
self.stats.write().unwrap().hits += 1;
return Some(entry);
}
}
}
self.stats.write().unwrap().misses += 1;
None
}
pub fn store(&self, key: &X86CacheKey, output_path: &Path, source_mtime: Option<SystemTime>) {
let combined = key.combined();
let entry = X86CachedEntry {
key: key.clone(),
output_path: output_path.to_path_buf(),
created_at: SystemTime::now(),
size_bytes: fs::metadata(output_path).map(|m| m.len()).unwrap_or(0),
hit_count: AtomicU64::new(0),
source_mtime,
};
{
let mut mem = self.memory_cache.write().unwrap();
if mem.len() >= self.max_memory_entries {
self.evict_memory_entries(&mut mem);
}
mem.insert(combined, entry.clone());
}
if let Some(ref dir) = *self.disk_cache_dir.read().unwrap() {
let cache_file = dir.join(format!("{:016x}.cache", combined));
if let Ok(data) = Self::serialize_entry(&entry) {
if fs::write(&cache_file, &data).is_ok() {
self.current_disk_size
.fetch_add(data.len() as u64, AtomicOrdering::SeqCst);
}
}
}
self.stats.write().unwrap().stores += 1;
}
fn evict_memory_entries(&self, mem: &mut HashMap<u64, X86CachedEntry>) {
let evict_count = self.max_memory_entries / 4;
let mut entries: Vec<_> = mem.iter().map(|(k, v)| (*k, v.created_at)).collect();
entries.sort_by_key(|(_, created)| *created);
for (k, _) in entries.iter().take(evict_count) {
mem.remove(k);
}
self.stats.write().unwrap().evictions += evict_count as u64;
}
fn serialize_entry(entry: &X86CachedEntry) -> Result<Vec<u8>, String> {
let mut data = Vec::new();
data.extend_from_slice(&entry.key.combined().to_be_bytes());
let path_bytes = entry.output_path.to_string_lossy().as_bytes().to_vec();
data.extend_from_slice(&(path_bytes.len() as u32).to_be_bytes());
data.extend_from_slice(&path_bytes);
data.extend_from_slice(&entry.size_bytes.to_be_bytes());
Ok(data)
}
fn deserialize_entry(path: &Path) -> Result<X86CachedEntry, String> {
let data = fs::read(path).map_err(|e| e.to_string())?;
if data.len() < 20 {
return Err("Cache entry too short".into());
}
let combined = u64::from_be_bytes(data[0..8].try_into().unwrap());
let path_len = u32::from_be_bytes(data[8..12].try_into().unwrap()) as usize;
if data.len() < 12 + path_len + 8 {
return Err("Cache entry truncated".into());
}
let path_str =
String::from_utf8(data[12..12 + path_len].to_vec()).map_err(|e| e.to_string())?;
let output_path = PathBuf::from(path_str);
let size_bytes =
u64::from_be_bytes(data[12 + path_len..12 + path_len + 8].try_into().unwrap());
Ok(X86CachedEntry {
key: X86CacheKey {
source_hash: 0,
flags_hash: 0,
deps_hash: 0,
combined_hash: combined,
},
output_path,
created_at: SystemTime::now(),
size_bytes,
hit_count: AtomicU64::new(0),
source_mtime: None,
})
}
pub fn clear_all(&self) {
self.memory_cache.write().unwrap().clear();
if let Some(ref dir) = *self.disk_cache_dir.read().unwrap() {
let _ = fs::remove_dir_all(dir);
let _ = fs::create_dir_all(dir);
}
self.current_disk_size.store(0, AtomicOrdering::SeqCst);
self.stats.write().unwrap().clears += 1;
}
pub fn stats(&self) -> X86CacheStats {
self.stats.read().unwrap().clone()
}
pub fn memory_entry_count(&self) -> usize {
self.memory_cache.read().unwrap().len()
}
pub fn disk_size_bytes(&self) -> u64 {
self.current_disk_size.load(AtomicOrdering::SeqCst)
}
}
impl Default for X86JobCache {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for X86JobCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mem = self.memory_cache.read().unwrap();
f.debug_struct("X86JobCache")
.field("memory_entries", &mem.len())
.field("disk_size_mb", &(self.disk_size_bytes() / (1024 * 1024)))
.field("stats", &*self.stats.read().unwrap())
.finish()
}
}
#[derive(Debug, Clone, Default)]
pub struct X86CacheStats {
pub hits: u64,
pub misses: u64,
pub stores: u64,
pub evictions: u64,
pub clears: u64,
}
impl X86CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
(self.hits as f64 / total as f64) * 100.0
}
}
}
impl fmt::Display for X86CacheStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"hits={} misses={} rate={:.1}% evictions={}",
self.hits,
self.misses,
self.hit_rate(),
self.evictions
)
}
}
#[derive(Debug)]
pub struct X86TempFileManager {
temp_dir: PathBuf,
files: Vec<PathBuf>,
prefix: String,
counter: AtomicUsize,
}
impl X86TempFileManager {
pub fn new(prefix: &str) -> Self {
let temp_dir = std::env::temp_dir().join(format!("clang_x86_{}", prefix));
let _ = fs::create_dir_all(&temp_dir);
Self {
temp_dir,
files: Vec::new(),
prefix: prefix.to_string(),
counter: AtomicUsize::new(0),
}
}
pub fn create_temp(&mut self, extension: &str) -> PathBuf {
let id = self.counter.fetch_add(1, AtomicOrdering::SeqCst);
let name = format!("{}_{:06}{}", self.prefix, id, extension);
let path = self.temp_dir.join(name);
self.files.push(path.clone());
path
}
pub fn preprocessed_file(&mut self) -> PathBuf {
self.create_temp(".i")
}
pub fn assembly_file(&mut self) -> PathBuf {
self.create_temp(".s")
}
pub fn object_file(&mut self) -> PathBuf {
self.create_temp(".o")
}
pub fn ir_file(&mut self) -> PathBuf {
self.create_temp(".ll")
}
pub fn bitcode_file(&mut self) -> PathBuf {
self.create_temp(".bc")
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn temp_dir_path(&self) -> &Path {
&self.temp_dir
}
pub fn cleanup_all(&self) -> io::Result<()> {
if self.temp_dir.exists() {
fs::remove_dir_all(&self.temp_dir)?;
}
Ok(())
}
}
impl Drop for X86TempFileManager {
fn drop(&mut self) {
let _ = self.cleanup_all();
}
}
fn detect_cpu_count() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
pub struct X86CompilationDaemon {
pch_cache: RwLock<HashMap<u64, X86PchEntry>>,
module_cache: RwLock<HashMap<String, X86ModuleEntry>>,
result_cache: RwLock<HashMap<u64, X86CachedEntry>>,
running: AtomicBool,
config: X86DaemonConfig,
started_at: Instant,
total_served: AtomicU64,
cache_hits: AtomicU64,
memory_usage_bytes: AtomicU64,
socket_path: Option<PathBuf>,
tcp_addr: Option<SocketAddr>,
}
#[derive(Debug, Clone)]
pub struct X86DaemonConfig {
pub max_pch_entries: usize,
pub max_pch_size_bytes: u64,
pub max_module_entries: usize,
pub idle_timeout_secs: u64,
pub health_check_interval_secs: u64,
pub max_memory_mb: u64,
pub enable_unix_socket: bool,
pub enable_tcp: bool,
pub tcp_bind_addr: Option<SocketAddr>,
pub unix_socket_path: Option<PathBuf>,
pub preload_headers: Vec<PathBuf>,
}
impl Default for X86DaemonConfig {
fn default() -> Self {
Self {
max_pch_entries: 64,
max_pch_size_bytes: 512 * 1024 * 1024, max_module_entries: 256,
idle_timeout_secs: 3600,
health_check_interval_secs: 60,
max_memory_mb: 4096,
enable_unix_socket: true,
enable_tcp: false,
tcp_bind_addr: None,
unix_socket_path: None,
preload_headers: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct X86PchEntry {
pub header_hash: u64,
pub data: Vec<u8>,
pub size_bytes: u64,
pub dependencies: Vec<PathBuf>,
pub build_flags: Vec<String>,
pub created_at: SystemTime,
pub use_count: u64,
}
#[derive(Debug, Clone)]
pub struct X86ModuleEntry {
pub name: String,
pub bmi_data: Vec<u8>,
pub dependencies: Vec<String>,
pub size_bytes: u64,
pub created_at: SystemTime,
}
impl X86CompilationDaemon {
pub fn new() -> Self {
Self::with_config(X86DaemonConfig::default())
}
pub fn with_config(config: X86DaemonConfig) -> Self {
Self {
pch_cache: RwLock::new(HashMap::new()),
module_cache: RwLock::new(HashMap::new()),
result_cache: RwLock::new(HashMap::new()),
running: AtomicBool::new(false),
config,
started_at: Instant::now(),
total_served: AtomicU64::new(0),
cache_hits: AtomicU64::new(0),
memory_usage_bytes: AtomicU64::new(0),
socket_path: None,
tcp_addr: None,
}
}
pub fn start(&self) {
self.running.store(true, AtomicOrdering::SeqCst);
}
pub fn stop(&self) {
self.running.store(false, AtomicOrdering::SeqCst);
self.clear_all_caches();
}
pub fn is_running(&self) -> bool {
self.running.load(AtomicOrdering::SeqCst)
}
pub fn uptime_secs(&self) -> u64 {
self.started_at.elapsed().as_secs()
}
pub fn lookup_pch(&self, header_hash: u64) -> Option<X86PchEntry> {
let cache = self.pch_cache.read().unwrap();
cache.get(&header_hash).cloned()
}
pub fn cache_pch(&self, entry: X86PchEntry) {
let mut cache = self.pch_cache.write().unwrap();
if cache.len() >= self.config.max_pch_entries {
self.evict_pch_entries(&mut cache);
}
let new_size = entry.size_bytes;
cache.insert(entry.header_hash, entry);
self.memory_usage_bytes
.fetch_add(new_size, AtomicOrdering::SeqCst);
}
pub fn lookup_module(&self, name: &str) -> Option<X86ModuleEntry> {
let cache = self.module_cache.read().unwrap();
cache.get(name).cloned()
}
pub fn cache_module(&self, entry: X86ModuleEntry) {
let mut cache = self.module_cache.write().unwrap();
if cache.len() >= self.config.max_module_entries {
self.evict_module_entries(&mut cache);
}
cache.insert(entry.name.clone(), entry);
}
pub fn lookup_result(&self, key: &X86CacheKey) -> Option<X86CachedEntry> {
let cache = self.result_cache.read().unwrap();
let combined = key.combined();
if let Some(entry) = cache.get(&combined) {
self.cache_hits.fetch_add(1, AtomicOrdering::SeqCst);
return Some(entry.clone());
}
None
}
pub fn store_result(&self, key: &X86CacheKey, entry: X86CachedEntry) {
let mut cache = self.result_cache.write().unwrap();
cache.insert(key.combined(), entry);
}
pub fn statistics(&self) -> X86DaemonStats {
X86DaemonStats {
running: self.is_running(),
uptime_secs: self.uptime_secs(),
pch_entries: self.pch_cache.read().unwrap().len(),
module_entries: self.module_cache.read().unwrap().len(),
result_entries: self.result_cache.read().unwrap().len(),
total_served: self.total_served.load(AtomicOrdering::SeqCst),
cache_hits: self.cache_hits.load(AtomicOrdering::SeqCst),
memory_bytes: self.memory_usage_bytes.load(AtomicOrdering::SeqCst),
}
}
pub fn health_check(&self) -> X86DaemonHealth {
let mem = self.memory_usage_bytes.load(AtomicOrdering::SeqCst);
let mem_mb = mem / (1024 * 1024);
X86DaemonHealth {
running: self.is_running(),
healthy: mem_mb < self.config.max_memory_mb,
memory_usage_mb: mem_mb,
memory_limit_mb: self.config.max_memory_mb,
pch_entries: self.pch_cache.read().unwrap().len() as u64,
module_entries: self.module_cache.read().unwrap().len() as u64,
result_entries: self.result_cache.read().unwrap().len() as u64,
uptime_secs: self.uptime_secs(),
}
}
fn evict_pch_entries(&self, cache: &mut HashMap<u64, X86PchEntry>) {
let evict_count = cache.len() / 4;
let mut entries: Vec<_> = cache.iter().map(|(k, v)| (*k, v.created_at)).collect();
entries.sort_by_key(|(_, created)| *created);
for (k, _) in entries.iter().take(evict_count) {
if let Some(removed) = cache.remove(k) {
self.memory_usage_bytes
.fetch_sub(removed.size_bytes, AtomicOrdering::SeqCst);
}
}
}
fn evict_module_entries(&self, cache: &mut HashMap<String, X86ModuleEntry>) {
let evict_count = cache.len() / 4;
let mut entries: Vec<_> = cache
.iter()
.map(|(k, v)| (k.clone(), v.created_at))
.collect();
entries.sort_by_key(|(_, created)| *created);
for (k, _) in entries.iter().take(evict_count) {
cache.remove(k);
}
}
fn clear_all_caches(&self) {
self.pch_cache.write().unwrap().clear();
self.module_cache.write().unwrap().clear();
self.result_cache.write().unwrap().clear();
self.memory_usage_bytes.store(0, AtomicOrdering::SeqCst);
}
}
impl Default for X86CompilationDaemon {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86DaemonStats {
pub running: bool,
pub uptime_secs: u64,
pub pch_entries: usize,
pub module_entries: usize,
pub result_entries: usize,
pub total_served: u64,
pub cache_hits: u64,
pub memory_bytes: u64,
}
impl fmt::Display for X86DaemonStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Daemon Stats:")?;
writeln!(f, " Running: {}", self.running)?;
writeln!(f, " Uptime: {}s", self.uptime_secs)?;
writeln!(f, " PCH entries: {}", self.pch_entries)?;
writeln!(f, " Module entries: {}", self.module_entries)?;
writeln!(f, " Result entries: {}", self.result_entries)?;
writeln!(f, " Total served: {}", self.total_served)?;
writeln!(f, " Cache hits: {}", self.cache_hits)?;
write!(f, " Memory: {} MB", self.memory_bytes / (1024 * 1024))
}
}
#[derive(Debug, Clone)]
pub struct X86DaemonHealth {
pub running: bool,
pub healthy: bool,
pub memory_usage_mb: u64,
pub memory_limit_mb: u64,
pub pch_entries: u64,
pub module_entries: u64,
pub result_entries: u64,
pub uptime_secs: u64,
}
pub struct X86JobServer {
read_fd: Option<i32>,
write_fd: Option<i32>,
total_tokens: usize,
available_tokens: Arc<AtomicUsize>,
is_owner: bool,
named_pipe: Option<PathBuf>,
}
impl X86JobServer {
pub fn new_owner(n: usize) -> Self {
Self {
read_fd: None,
write_fd: None,
total_tokens: n,
available_tokens: Arc::new(AtomicUsize::new(n)),
is_owner: true,
named_pipe: None,
}
}
pub fn from_environment() -> Option<Self> {
let flags = std::env::var("MAKEFLAGS").ok()?;
if flags.contains("--jobserver-auth") {
if let Some(fifo) = flags
.split_whitespace()
.find(|f| f.starts_with("--jobserver-auth=fifo:"))
{
let path = fifo.strip_prefix("--jobserver-auth=fifo:")?;
Some(Self {
read_fd: None,
write_fd: None,
total_tokens: 0,
available_tokens: Arc::new(AtomicUsize::new(0)),
is_owner: false,
named_pipe: Some(PathBuf::from(path)),
})
} else {
Some(Self {
read_fd: None,
write_fd: None,
total_tokens: 0,
available_tokens: Arc::new(AtomicUsize::new(0)),
is_owner: false,
named_pipe: None,
})
}
} else {
None
}
}
pub fn acquire(&self) -> bool {
let mut current = self.available_tokens.load(AtomicOrdering::SeqCst);
loop {
if current == 0 {
return false;
}
match self.available_tokens.compare_exchange_weak(
current,
current - 1,
AtomicOrdering::SeqCst,
AtomicOrdering::SeqCst,
) {
Ok(_) => return true,
Err(actual) => current = actual,
}
}
}
pub fn release(&self) {
self.available_tokens.fetch_add(1, AtomicOrdering::SeqCst);
}
pub fn total_tokens(&self) -> usize {
self.total_tokens
}
pub fn available(&self) -> usize {
self.available_tokens.load(AtomicOrdering::SeqCst)
}
pub fn to_makeflags(&self) -> String {
if self.is_owner {
format!(
"--jobserver-auth=fifo:/tmp/jobserver-{}",
std::process::id()
)
} else {
String::new()
}
}
pub fn is_owner(&self) -> bool {
self.is_owner
}
pub fn close(&self) {
self.available_tokens
.store(self.total_tokens, AtomicOrdering::SeqCst);
}
}
pub struct X86TokenSemaphore {
permits: Arc<AtomicUsize>,
max_permits: usize,
}
impl X86TokenSemaphore {
pub fn new(n: usize) -> Self {
Self {
permits: Arc::new(AtomicUsize::new(n)),
max_permits: n,
}
}
pub fn try_acquire(&self) -> bool {
let mut current = self.permits.load(AtomicOrdering::SeqCst);
loop {
if current == 0 {
return false;
}
match self.permits.compare_exchange_weak(
current,
current - 1,
AtomicOrdering::SeqCst,
AtomicOrdering::SeqCst,
) {
Ok(_) => return true,
Err(actual) => current = actual,
}
}
}
pub fn release(&self) {
let current = self.permits.load(AtomicOrdering::SeqCst);
if current < self.max_permits {
self.permits.fetch_add(1, AtomicOrdering::SeqCst);
}
}
pub fn available(&self) -> usize {
self.permits.load(AtomicOrdering::SeqCst)
}
pub fn max(&self) -> usize {
self.max_permits
}
}
pub struct X86ResponseFile {
path: PathBuf,
args: Vec<String>,
}
impl X86ResponseFile {
pub fn new(path: PathBuf) -> Self {
Self {
path,
args: Vec::new(),
}
}
pub fn with_temp_dir(dir: &Path, prefix: &str) -> Self {
let path = dir.join(format!("{}_{}.rsp", prefix, std::process::id()));
Self::new(path)
}
pub fn arg(&mut self, arg: &str) -> &mut Self {
self.args.push(arg.to_string());
self
}
pub fn args(&mut self, args: &[&str]) -> &mut Self {
for a in args {
self.args.push(a.to_string());
}
self
}
pub fn args_vec(&mut self, args: Vec<String>) -> &mut Self {
self.args.extend(args);
self
}
pub fn is_needed(&self) -> bool {
let total_len: usize = self.args.iter().map(|a| a.len() + 1).sum();
total_len > X86_MAX_COMMAND_LINE_LEN / 2
}
pub fn write(&self) -> io::Result<()> {
let mut f = fs::File::create(&self.path)?;
for arg in &self.args {
if arg.contains(' ') || arg.contains('"') {
writeln!(f, "\"{}\"", arg.replace('"', "\\\""))?;
} else {
writeln!(f, "{}", arg)?;
}
}
Ok(())
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn flag(&self) -> String {
format!("@{}", self.path.display())
}
pub fn parse(path: &Path) -> io::Result<Vec<String>> {
let content = fs::read_to_string(path)?;
let mut args = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('"') && line.ends_with('"') {
args.push(line[1..line.len() - 1].to_string());
} else {
args.push(line.to_string());
}
}
Ok(args)
}
pub fn cleanup(&self) -> io::Result<()> {
if self.path.exists() {
fs::remove_file(&self.path)?;
}
Ok(())
}
pub fn arg_count(&self) -> usize {
self.args.len()
}
pub fn estimated_command_line_len(&self) -> usize {
self.args.iter().map(|a| a.len() + 1).sum()
}
}
impl Drop for X86ResponseFile {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
pub struct X86DependencyScanner {
include_paths: Vec<PathBuf>,
system_include_paths: Vec<PathBuf>,
dep_graph: HashMap<PathBuf, HashSet<PathBuf>>,
reverse_graph: HashMap<PathBuf, HashSet<PathBuf>>,
mtimes: HashMap<PathBuf, SystemTime>,
scanned: HashSet<PathBuf>,
include_system_headers: bool,
}
impl X86DependencyScanner {
pub fn new() -> Self {
Self {
include_paths: Vec::new(),
system_include_paths: Vec::new(),
dep_graph: HashMap::new(),
reverse_graph: HashMap::new(),
mtimes: HashMap::new(),
scanned: HashSet::new(),
include_system_headers: false,
}
}
pub fn add_include_path(&mut self, path: &Path) -> &mut Self {
self.include_paths.push(path.to_path_buf());
self
}
pub fn add_system_include_path(&mut self, path: &Path) -> &mut Self {
self.system_include_paths.push(path.to_path_buf());
self
}
pub fn set_include_system_headers(&mut self, include: bool) -> &mut Self {
self.include_system_headers = include;
self
}
pub fn scan_file(&mut self, source: &Path) -> Result<Vec<PathBuf>, String> {
if self.scanned.contains(source) {
return Ok(self
.dep_graph
.get(source)
.cloned()
.unwrap_or_default()
.into_iter()
.collect());
}
let content = fs::read_to_string(source)
.map_err(|e| format!("Cannot read {}: {}", source.display(), e))?;
let mut deps = HashSet::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#include") {
if let Some(header) = self.extract_include_path(trimmed) {
if let Some(full_path) = self.resolve_include(&header, source) {
deps.insert(full_path);
}
}
}
}
if let Ok(meta) = fs::metadata(source) {
if let Ok(mtime) = meta.modified() {
self.mtimes.insert(source.to_path_buf(), mtime);
}
}
let result: Vec<PathBuf> = deps.iter().cloned().collect();
self.dep_graph.insert(source.to_path_buf(), deps);
self.scanned.insert(source.to_path_buf());
Ok(result)
}
pub fn scan_transitive(&mut self, source: &Path) -> Result<Vec<PathBuf>, String> {
let mut all_deps = Vec::new();
let mut to_scan = vec![source.to_path_buf()];
let mut visited = HashSet::new();
while let Some(current) = to_scan.pop() {
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
let deps = self.scan_file(¤t)?;
for dep in &deps {
if !visited.contains(dep) {
all_deps.push(dep.clone());
to_scan.push(dep.clone());
}
}
}
Ok(all_deps)
}
pub fn dependencies_of(&self, source: &Path) -> Vec<PathBuf> {
self.dep_graph
.get(source)
.map(|deps| deps.iter().cloned().collect())
.unwrap_or_default()
}
pub fn includers_of(&self, header: &Path) -> Vec<PathBuf> {
self.reverse_graph
.get(header)
.map(|srcs| srcs.iter().cloned().collect())
.unwrap_or_default()
}
pub fn needs_rebuild(&self, source: &Path, output: &Path) -> bool {
let output_mtime = match fs::metadata(output).ok().and_then(|m| m.modified().ok()) {
Some(t) => t,
None => return true, };
let deps = self.dependencies_of(source);
for dep in deps {
if let Some(&dep_mtime) = self.mtimes.get(&dep) {
if dep_mtime > output_mtime {
return true;
}
}
}
if let Some(&src_mtime) = self.mtimes.get(&source.to_path_buf()) {
if src_mtime > output_mtime {
return true;
}
}
false
}
pub fn write_depfile(&self, target: &Path, source: &Path, output: &Path) -> io::Result<()> {
let mut f = fs::File::create(output)?;
let deps = self.dependencies_of(source);
write!(
f,
"{}:",
Self::escape_make_target(&target.to_string_lossy())
)?;
for dep in &deps {
write!(
f,
" \\\n {}",
Self::escape_make_target(&dep.to_string_lossy())
)?;
}
writeln!(f)?;
for dep in &deps {
writeln!(f, "\n{}:", Self::escape_make_target(&dep.to_string_lossy()))?;
}
Ok(())
}
fn escape_make_target(s: &str) -> String {
s.replace(' ', "\\ ").replace('#', "\\#").replace('$', "$$")
}
fn extract_include_path(&self, line: &str) -> Option<String> {
let line = line.trim_start_matches("#include").trim();
if line.starts_with('"') {
let end = line[1..].find('"')?;
Some(line[1..=end].to_string())
} else if line.starts_with('<') {
let end = line[1..].find('>')?;
Some(line[1..=end].to_string())
} else {
None
}
}
fn resolve_include(&self, header: &str, relative_to: &Path) -> Option<PathBuf> {
if let Some(parent) = relative_to.parent() {
let candidate = parent.join(header);
if candidate.exists() {
return Some(candidate);
}
}
for inc in &self.include_paths {
let candidate = inc.join(header);
if candidate.exists() {
return Some(candidate);
}
}
for inc in &self.system_include_paths {
let candidate = inc.join(header);
if candidate.exists() {
return Some(candidate);
}
}
None
}
pub fn clear(&mut self) {
self.dep_graph.clear();
self.reverse_graph.clear();
self.mtimes.clear();
self.scanned.clear();
}
}
impl Default for X86DependencyScanner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct X86BuildGraph {
nodes: Vec<X86BuildNode>,
edges: Vec<Vec<usize>>,
reverse_edges: Vec<Vec<usize>>,
reachable_cache: RwLock<HashMap<(usize, usize), bool>>,
}
#[derive(Debug, Clone)]
pub struct X86BuildNode {
pub id: usize,
pub kind: X86JobKind,
pub inputs: Vec<PathBuf>,
pub outputs: Vec<PathBuf>,
pub estimated_duration_ms: u64,
pub processed: bool,
}
impl X86BuildNode {
pub fn new(id: usize, kind: X86JobKind) -> Self {
Self {
id,
kind,
inputs: Vec::new(),
outputs: Vec::new(),
estimated_duration_ms: 500,
processed: false,
}
}
pub fn label(&self) -> String {
match &self.kind {
X86JobKind::Preprocess(_) => format!("PP#{}", self.id),
X86JobKind::Compile(j) => {
format!(
"CC#{}({})",
self.id,
j.input.file_name().unwrap().to_string_lossy()
)
}
X86JobKind::Backend(_) => format!("BE#{}", self.id),
X86JobKind::Assemble(_) => format!("AS#{}", self.id),
X86JobKind::Link(_) => format!("LD#{}", self.id),
X86JobKind::Archive(_) => format!("AR#{}", self.id),
X86JobKind::LTO(_) => format!("LTO#{}", self.id),
X86JobKind::Custom(_) => format!("CUSTOM#{}", self.id),
}
}
}
impl X86BuildGraph {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
reverse_edges: Vec::new(),
reachable_cache: RwLock::new(HashMap::new()),
}
}
pub fn add_node(&mut self, node: X86BuildNode) -> usize {
let id = self.nodes.len();
self.nodes.push(node);
self.edges.push(Vec::new());
self.reverse_edges.push(Vec::new());
id
}
pub fn add_edge(&mut self, from: usize, to: usize) {
if from < self.nodes.len() && to < self.nodes.len() {
self.edges[from].push(to);
self.reverse_edges[to].push(from);
self.reachable_cache.write().unwrap().clear();
}
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.iter().map(|e| e.len()).sum()
}
pub fn successors_of(&self, node: usize) -> &[usize] {
self.edges.get(node).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn predecessors_of(&self, node: usize) -> &[usize] {
self.reverse_edges
.get(node)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn reaches(&self, from: usize, to: usize) -> bool {
{
let cache = self.reachable_cache.read().unwrap();
if let Some(&result) = cache.get(&(from, to)) {
return result;
}
}
let mut visited = HashSet::new();
let result = self.dfs_reachable(from, to, &mut visited);
self.reachable_cache
.write()
.unwrap()
.insert((from, to), result);
result
}
fn dfs_reachable(&self, current: usize, target: usize, visited: &mut HashSet<usize>) -> bool {
if current == target {
return true;
}
visited.insert(current);
for &next in &self.edges[current] {
if !visited.contains(&next) && self.dfs_reachable(next, target, visited) {
return true;
}
}
false
}
pub fn has_cycle(&self) -> bool {
let mut visited = vec![false; self.nodes.len()];
let mut rec_stack = vec![false; self.nodes.len()];
for i in 0..self.nodes.len() {
if !visited[i] && self.dfs_cycle(i, &mut visited, &mut rec_stack) {
return true;
}
}
false
}
fn dfs_cycle(&self, node: usize, visited: &mut [bool], rec_stack: &mut [bool]) -> bool {
visited[node] = true;
rec_stack[node] = true;
for &next in &self.edges[node] {
if !visited[next] {
if self.dfs_cycle(next, visited, rec_stack) {
return true;
}
} else if rec_stack[next] {
return true;
}
}
rec_stack[node] = false;
false
}
pub fn topological_order(&self) -> Option<Vec<usize>> {
if self.has_cycle() {
return None;
}
let mut in_degree = vec![0usize; self.nodes.len()];
for node in 0..self.nodes.len() {
for &succ in &self.edges[node] {
in_degree[succ] += 1;
}
}
let mut queue: VecDeque<usize> = in_degree
.iter()
.enumerate()
.filter(|&(_, &d)| d == 0)
.map(|(i, _)| i)
.collect();
let mut order = Vec::new();
while let Some(node) = queue.pop_front() {
order.push(node);
for &succ in &self.edges[node] {
in_degree[succ] -= 1;
if in_degree[succ] == 0 {
queue.push_back(succ);
}
}
}
if order.len() == self.nodes.len() {
Some(order)
} else {
None
}
}
pub fn critical_path(&self) -> Vec<usize> {
let order = match self.topological_order() {
Some(o) => o,
None => return Vec::new(),
};
let mut dist = vec![0u64; self.nodes.len()];
let mut parent = vec![None; self.nodes.len()];
for &node in &order {
for &succ in &self.edges[node] {
let new_dist = dist[node] + self.nodes[succ].estimated_duration_ms;
if new_dist > dist[succ] {
dist[succ] = new_dist;
parent[succ] = Some(node);
}
}
}
let mut max_idx = 0;
for i in 0..self.nodes.len() {
if dist[i] > dist[max_idx] {
max_idx = i;
}
}
let mut path = Vec::new();
let mut current = Some(max_idx);
while let Some(node) = current {
path.push(node);
current = parent[node];
}
path.reverse();
path
}
pub fn to_dot(&self) -> String {
let mut dot = String::from("digraph X86BuildGraph {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [shape=box, style=filled, fillcolor=lightyellow];\n");
for node in &self.nodes {
dot.push_str(&format!(" n{} [label=\"{}\"];\n", node.id, node.label()));
}
for (from, edges) in self.edges.iter().enumerate() {
for &to in edges {
dot.push_str(&format!(" n{} -> n{};\n", from, to));
}
}
dot.push_str("}\n");
dot
}
pub fn estimated_total_time_ms(&self) -> u64 {
self.nodes.iter().map(|n| n.estimated_duration_ms).sum()
}
}
impl Default for X86BuildGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct X86JobMetrics {
timings: RwLock<HashMap<usize, X86JobTiming>>,
build_start: Instant,
phase_timings: RwLock<HashMap<String, Duration>>,
}
#[derive(Debug, Clone)]
pub struct X86JobTiming {
pub job_id: usize,
pub started_at: Instant,
pub finished_at: Option<Instant>,
pub duration: Option<Duration>,
pub job_kind: String,
pub was_cache_hit: bool,
pub was_distributed: bool,
pub remote_host: Option<String>,
}
impl X86JobMetrics {
pub fn new() -> Self {
Self {
timings: RwLock::new(HashMap::new()),
build_start: Instant::now(),
phase_timings: RwLock::new(HashMap::new()),
}
}
pub fn job_started(&self, job_id: usize, kind: &str) {
let timing = X86JobTiming {
job_id,
started_at: Instant::now(),
finished_at: None,
duration: None,
job_kind: kind.to_string(),
was_cache_hit: false,
was_distributed: false,
remote_host: None,
};
self.timings.write().unwrap().insert(job_id, timing);
}
pub fn job_finished(
&self,
job_id: usize,
cache_hit: bool,
distributed: bool,
remote_host: Option<&str>,
) {
let mut timings = self.timings.write().unwrap();
if let Some(timing) = timings.get_mut(&job_id) {
timing.finished_at = Some(Instant::now());
timing.duration = Some(timing.started_at.elapsed());
timing.was_cache_hit = cache_hit;
timing.was_distributed = distributed;
timing.remote_host = remote_host.map(|s| s.to_string());
}
}
pub fn phase_finished(&self, phase: &str, duration: Duration) {
self.phase_timings
.write()
.unwrap()
.insert(phase.to_string(), duration);
}
pub fn slowest_jobs(&self, top_n: usize) -> Vec<X86JobTiming> {
let timings = self.timings.read().unwrap();
let mut jobs: Vec<_> = timings.values().cloned().collect();
jobs.sort_by_key(|j| std::cmp::Reverse(j.duration.unwrap_or_default()));
jobs.truncate(top_n);
jobs
}
pub fn total_elapsed(&self) -> Duration {
self.build_start.elapsed()
}
pub fn report(&self) -> String {
let mut report = String::new();
report.push_str("╔══════════════════════════════════════════════════════╗\n");
report.push_str("║ X86 Build Timing Report ║\n");
report.push_str("╠══════════════════════════════════════════════════════╣\n");
let total = self.total_elapsed();
report.push_str(&format!(
"║ Total time: {:8} ms ║\n",
total.as_millis()
));
let phases = self.phase_timings.read().unwrap();
for (phase, dur) in phases.iter() {
report.push_str(&format!(
"║ {:20} {:6} ms ║\n",
phase,
dur.as_millis()
));
}
report.push_str("╠══════════════════════════════════════════════════════╣\n");
report.push_str("║ Slowest jobs: ║\n");
let slowest = self.slowest_jobs(5);
for job in &slowest {
report.push_str(&format!(
"║ [#{}] {:20} {:6} ms ║\n",
job.job_id,
job.job_kind,
job.duration.unwrap_or_default().as_millis()
));
}
report.push_str("╚══════════════════════════════════════════════════════╝\n");
report
}
pub fn cache_stats(&self) -> (usize, usize) {
let timings = self.timings.read().unwrap();
let hits = timings.values().filter(|t| t.was_cache_hit).count();
let total = timings.len();
(hits, total)
}
pub fn distributed_stats(&self) -> (usize, usize) {
let timings = self.timings.read().unwrap();
let dist = timings.values().filter(|t| t.was_distributed).count();
let total = timings.len();
(dist, total)
}
}
impl Default for X86JobMetrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn make_options() -> ClangOptions {
ClangOptions::default()
}
struct TestTempDir {
path: PathBuf,
}
impl TestTempDir {
fn new() -> Self {
let mut path = std::env::temp_dir();
path.push(format!("x86_job_test_{}", std::process::id()));
let _ = fs::create_dir_all(&path);
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestTempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn make_temp_source(dir: &Path, name: &str) -> PathBuf {
let path = dir.join(name);
let mut f = fs::File::create(&path).unwrap();
writeln!(f, "int main() {{ return 0; }}").unwrap();
path
}
#[test]
fn test_driver_jobs_new() {
let jobs = X86DriverJobs::new(false, false);
assert!(!jobs.dry_run);
assert!(!jobs.verbose);
assert!(jobs.distributed.is_none());
assert_eq!(jobs.statistics().total_jobs, 0);
}
#[test]
fn test_driver_jobs_default() {
let jobs = X86DriverJobs::default();
assert!(!jobs.dry_run);
}
#[test]
fn test_driver_jobs_with_parallel() {
let jobs = X86DriverJobs::with_parallel_jobs(8, true, false);
assert!(jobs.verbose);
assert!(!jobs.dry_run);
let config = &jobs.scheduler.lock().unwrap().config;
assert_eq!(config.max_parallel, 8);
}
#[test]
fn test_driver_jobs_enable_distcc() {
let mut jobs = X86DriverJobs::default();
jobs.enable_distcc(vec!["host1".into(), "host2:3632".into()]);
assert!(jobs.distributed.is_some());
let dist = jobs.distributed.as_ref().unwrap();
assert_eq!(dist.protocol, X86DistributedProtocol::Distcc);
assert_eq!(dist.distcc_hosts.len(), 2);
}
#[test]
fn test_driver_jobs_enable_icecream() {
let mut jobs = X86DriverJobs::default();
let addr: SocketAddr = "127.0.0.1:10245".parse().unwrap();
jobs.enable_icecream(addr);
assert!(jobs.distributed.is_some());
let dist = jobs.distributed.as_ref().unwrap();
assert_eq!(dist.protocol, X86DistributedProtocol::Icecream);
}
#[test]
fn test_driver_jobs_add_source() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "test.c");
let mut jobs = X86DriverJobs::default();
let id = jobs.add_source(&src, &make_options());
assert_eq!(id, 0);
}
#[test]
fn test_driver_jobs_add_sources() {
let tmp = TestTempDir::new();
let a = make_temp_source(tmp.path(), "a.c");
let b = make_temp_source(tmp.path(), "b.c");
let mut jobs = X86DriverJobs::default();
let ids = jobs.add_sources(&[&a, &b], &make_options());
assert_eq!(ids.len(), 2);
}
#[test]
fn test_driver_jobs_build() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "main.c");
let mut jobs = X86DriverJobs::default();
jobs.add_source(&src, &make_options());
let stats = jobs.build_jobs();
assert!(stats.total_jobs >= 1);
assert!(stats.compile_jobs >= 1);
}
#[test]
fn test_driver_jobs_has_failures() {
let jobs = X86DriverJobs::default();
assert!(!jobs.has_failures());
}
#[test]
fn test_driver_jobs_statistics() {
let jobs = X86DriverJobs::default();
let stats = jobs.statistics();
assert_eq!(stats.total_jobs, 0);
assert_eq!(stats.failed, 0);
}
#[test]
fn test_driver_jobs_add_link_job() {
let tmp = TestTempDir::new();
let obj = tmp.path().join("main.o");
let out = tmp.path().join("a.out");
let jobs = X86DriverJobs::default();
let id = jobs.add_link_job(&[obj], &out);
assert_eq!(id, 0);
}
#[test]
fn test_driver_jobs_dry_run() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "hello.c");
let mut jobs = X86DriverJobs::new(false, true);
jobs.add_source(&src, &make_options());
jobs.build_jobs();
let results = jobs.execute_sequential();
assert!(results.is_empty()); }
#[test]
fn test_scheduler_new() {
let sched = X86JobScheduler::new(X86SchedulerConfig::default());
assert_eq!(sched.job_count(), 0);
assert!(sched.statistics().total_jobs == 0);
}
#[test]
fn test_scheduler_add_job() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
let job = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("test.i"),
PathBuf::from("test.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
};
let id = sched.add_job(job, X86JobPriority::Normal);
assert_eq!(id, 0);
assert_eq!(sched.job_count(), 1);
}
#[test]
fn test_scheduler_add_dependency() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
let prep = X86DriverJob {
kind: X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
)),
priority: X86JobPriority::High,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
};
let comp = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 120,
estimated_memory_mb: 512,
use_response_file: false,
};
let pp_id = sched.add_job(prep, X86JobPriority::High);
let cc_id = sched.add_job(comp, X86JobPriority::Normal);
sched.add_dependency(cc_id, pp_id);
assert!(!sched.get_ready_jobs().is_empty() || sched.get_ready_jobs().len() >= 1);
}
#[test]
fn test_scheduler_topological_order() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
let j0 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
)),
priority: X86JobPriority::High,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::High,
);
let j1 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
},
X86JobPriority::Normal,
);
let j2 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("a.out"),
)),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 512,
use_response_file: false,
},
X86JobPriority::Critical,
);
sched.add_dependency(j1, j0);
sched.add_dependency(j2, j1);
let order = sched.topological_order();
assert_eq!(order.len(), 3);
let pos0 = order.iter().position(|&x| x == j0).unwrap();
let pos1 = order.iter().position(|&x| x == j1).unwrap();
let pos2 = order.iter().position(|&x| x == j2).unwrap();
assert!(pos0 < pos1);
assert!(pos1 < pos2);
}
#[test]
fn test_scheduler_execute_sequential() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
let j0 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("x.i"),
PathBuf::from("x.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
},
X86JobPriority::Normal,
);
let results = sched.execute_sequential();
assert_eq!(results.len(), 1);
assert_eq!(results[0].job_id, j0);
assert_eq!(results[0].status, X86JobStatus::Completed);
}
#[test]
fn test_scheduler_execute_parallel() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig {
max_parallel: 4,
..Default::default()
});
for i in 0..10 {
sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from(format!("{}.i", i)),
PathBuf::from(format!("{}.o", i)),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
}
let results = sched.execute_parallel();
assert_eq!(results.len(), 10);
for r in &results {
assert!(r.status == X86JobStatus::Completed || r.status == X86JobStatus::Skipped);
}
}
#[test]
fn test_scheduler_statistics_after_execution() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
for i in 0..5 {
sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from(format!("{}.i", i)),
PathBuf::from(format!("{}.o", i)),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
}
sched.execute_sequential();
let stats = sched.statistics();
assert_eq!(stats.total_jobs, 5);
assert!(stats.completed >= 5);
}
#[test]
fn test_scheduler_clear() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("x.i"),
PathBuf::from("x.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
sched.clear();
assert_eq!(sched.job_count(), 0);
assert_eq!(sched.all_jobs().len(), 0);
}
#[test]
fn test_scheduler_fail_fast() {
let mut config = X86SchedulerConfig::default();
config.fail_fast = true;
let mut sched = X86JobScheduler::new(config);
let j0 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
let j1 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("b.i"),
PathBuf::from("b.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
sched.add_dependency(j1, j0);
let results = sched.execute_sequential();
assert_eq!(results.len(), 2);
}
#[test]
fn test_scheduler_all_jobs() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
let all = sched.all_jobs();
assert_eq!(all.len(), 1);
}
#[test]
fn test_job_kind_name() {
let pp = X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
));
assert_eq!(pp.name(), "preprocess");
let cc = X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
));
assert_eq!(cc.name(), "compile");
let lnk = X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("a.out"),
));
assert_eq!(lnk.name(), "link");
}
#[test]
fn test_job_kind_input_extension() {
let pp = X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
));
assert_eq!(pp.input_extension(), ".c");
let cc = X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
));
assert_eq!(cc.input_extension(), ".i");
}
#[test]
fn test_job_kind_output_extension() {
let cc = X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
));
assert_eq!(cc.output_extension(), ".o");
let ar = X86JobKind::Archive(X86ArchiveJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("lib.a"),
));
assert_eq!(ar.output_extension(), ".a");
}
#[test]
fn test_job_kind_is_distributable() {
let cc = X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
));
assert!(cc.is_distributable());
let lnk = X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("a.out"),
));
assert!(!lnk.is_distributable());
}
#[test]
fn test_job_priority_ordering() {
assert!(X86JobPriority::Critical > X86JobPriority::High);
assert!(X86JobPriority::High > X86JobPriority::Normal);
assert!(X86JobPriority::Normal > X86JobPriority::Low);
}
#[test]
fn test_driver_job_description() {
let job = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("test.i"),
PathBuf::from("test.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
};
let desc = job.description();
assert!(desc.contains("Compile"));
}
#[test]
fn test_driver_job_to_command_line() {
let job = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("test.i"),
PathBuf::from("test.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
};
let cmd = job.to_command_line();
assert!(cmd.contains("clang"));
assert!(cmd.contains("test.i"));
assert!(cmd.contains("test.o"));
}
#[test]
fn test_compile_job_all_flags() {
let mut job = X86CompileJob::new(PathBuf::from("a.i"), PathBuf::from("a.o"));
job.standard = Some("c11".to_string());
job.debug_info = true;
job.pic = true;
job.includes.push(PathBuf::from("/usr/include"));
job.defines
.push(("FOO".to_string(), Some("bar".to_string())));
let flags = job.all_flags();
assert!(flags.iter().any(|f| f == "-std=c11"));
assert!(flags.iter().any(|f| f == "-g"));
assert!(flags.iter().any(|f| f == "-fPIC"));
assert!(flags.iter().any(|f| f == "-DFOO=bar"));
}
#[test]
fn test_link_job_new() {
let link = X86LinkJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("prog"),
);
assert_eq!(link.inputs.len(), 2);
assert_eq!(link.output, PathBuf::from("prog"));
}
#[test]
fn test_archive_job_new() {
let ar = X86ArchiveJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("libtest.a"),
);
assert_eq!(ar.members.len(), 2);
}
#[test]
fn test_lto_job_new() {
let lto = X86LTOJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("prog"),
);
assert!(lto.thin);
assert_eq!(lto.inputs.len(), 2);
}
#[test]
fn test_preprocess_job_new() {
let pp = X86PreprocessJob::new(PathBuf::from("a.c"), PathBuf::from("a.i"));
assert!(pp.line_markers);
assert!(!pp.keep_comments);
}
#[test]
fn test_backend_job_new() {
let be = X86BackendJob::new(PathBuf::from("a.ll"), PathBuf::from("a.o"));
assert_eq!(be.target_triple, "x86_64-unknown-linux-gnu");
}
#[test]
fn test_assemble_job_new() {
let asm = X86AssembleJob::new(PathBuf::from("a.s"), PathBuf::from("a.o"));
assert_eq!(asm.target_triple, "x86_64-unknown-linux-gnu");
}
#[test]
fn test_job_result_status() {
let result = X86JobResult {
job_id: 42,
status: X86JobStatus::Completed,
duration: Duration::from_millis(100),
stdout: "ok".to_string(),
stderr: String::new(),
exit_code: 0,
cached: false,
};
assert_eq!(result.job_id, 42);
assert_eq!(result.status, X86JobStatus::Completed);
}
#[test]
fn test_job_queue_entry_ordering() {
let low = X86JobQueueEntry {
id: 0,
priority: X86JobPriority::Low,
};
let high = X86JobQueueEntry {
id: 1,
priority: X86JobPriority::High,
};
let critical = X86JobQueueEntry {
id: 2,
priority: X86JobPriority::Critical,
};
assert!(critical > high);
assert!(high > low);
}
#[test]
fn test_pipeline_new() {
let pipeline = X86JobPipeline::new();
assert!(pipeline.sources.is_empty());
}
#[test]
fn test_pipeline_default() {
let _ = X86JobPipeline::default();
}
#[test]
fn test_pipeline_set_output_dir() {
let mut pipeline = X86JobPipeline::new();
pipeline.set_output_dir(Path::new("/tmp/build"));
assert_eq!(pipeline.output_dir, PathBuf::from("/tmp/build"));
}
#[test]
fn test_pipeline_enable_dep_files() {
let mut pipeline = X86JobPipeline::new();
pipeline.enable_dep_files(Some(Path::new("/tmp/deps")));
assert!(pipeline.generate_deps);
}
#[test]
fn test_pipeline_enable_compile_db() {
let mut pipeline = X86JobPipeline::new();
pipeline.enable_compile_db(Path::new("/tmp/compile_commands.json"));
assert!(pipeline.generate_compile_db);
}
#[test]
fn test_pipeline_force_rebuild() {
let mut pipeline = X86JobPipeline::new();
pipeline.force_rebuild();
assert!(!pipeline.incremental);
}
#[test]
fn test_pipeline_add_source() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "pipeline.c");
let mut pipeline = X86JobPipeline::new();
let id = pipeline.add_source(&src, &make_options());
assert_eq!(id, 0);
assert_eq!(pipeline.sources.len(), 1);
}
#[test]
fn test_pipeline_build() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "main.c");
let mut pipeline = X86JobPipeline::new();
pipeline.set_output_dir(tmp.path());
pipeline.add_source(&src, &make_options());
let scheduler = Arc::new(Mutex::new(X86JobScheduler::new(
X86SchedulerConfig::default(),
)));
let cache = Arc::new(X86JobCache::new());
let stats = pipeline.build(&scheduler, &cache);
assert!(stats.total_jobs >= 2); assert!(stats.compile_jobs >= 1);
assert!(stats.preprocess_jobs >= 1);
}
#[test]
fn test_pipeline_load_compile_commands_empty() {
let tmp = TestTempDir::new();
let db_path = tmp.path().join("compile_commands.json");
fs::write(&db_path, "[]").unwrap();
let mut pipeline = X86JobPipeline::new();
let count = pipeline.load_compile_commands(&db_path).unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_pipeline_load_compile_commands() {
let tmp = TestTempDir::new();
let db_path = tmp.path().join("compile_commands.json");
let json = r#"[
{"directory": "/src", "file": "/src/main.c", "arguments": ["clang", "-c", "main.c"]}
]"#;
fs::write(&db_path, json).unwrap();
let mut pipeline = X86JobPipeline::new();
let count = pipeline.load_compile_commands(&db_path).unwrap();
assert_eq!(count, 1);
}
#[test]
fn test_write_depfile() {
let tmp = TestTempDir::new();
let depfile = tmp.path().join("test.d");
X86JobPipeline::write_depfile(
&depfile,
Path::new("test.o"),
&[PathBuf::from("test.c"), PathBuf::from("header.h")],
)
.unwrap();
let content = fs::read_to_string(&depfile).unwrap();
assert!(content.contains("test.o:"));
assert!(content.contains("test.c"));
assert!(content.contains("header.h"));
}
#[test]
fn test_compile_command_serialization() {
let cmd = X86CompileCommand {
directory: PathBuf::from("/src"),
file: PathBuf::from("/src/main.c"),
arguments: vec!["clang".to_string(), "-c".to_string(), "main.c".to_string()],
output: Some("main.o".to_string()),
};
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("main.c"));
assert!(json.contains("main.o"));
}
#[test]
fn test_distcc_new() {
let dist = X86DistributedCompilation::new_distcc(vec!["host1".into(), "host2:3632".into()]);
assert_eq!(dist.protocol, X86DistributedProtocol::Distcc);
assert_eq!(dist.distcc_hosts.len(), 2);
assert!(dist.is_available());
}
#[test]
fn test_icecream_new() {
let addr: SocketAddr = "127.0.0.1:10245".parse().unwrap();
let dist = X86DistributedCompilation::new_icecream(addr);
assert_eq!(dist.protocol, X86DistributedProtocol::Icecream);
assert!(dist.icecream_scheduler.is_some());
}
#[test]
fn test_distcc_dispatch() {
let dist = X86DistributedCompilation::new_distcc(vec!["localhost".into()]);
let job = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("test.i"),
PathBuf::from("test.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
};
let result = dist.dispatch_job(&job).unwrap();
assert_eq!(result.status, X86JobStatus::Completed);
}
#[test]
fn test_distcc_not_distributable() {
let dist = X86DistributedCompilation::new_distcc(vec!["localhost".into()]);
let job = X86DriverJob {
kind: X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("a.out"),
)),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 512,
use_response_file: false,
};
assert!(dist.dispatch_job(&job).is_err());
}
#[test]
fn test_distcc_set_available() {
let dist = X86DistributedCompilation::new_distcc(vec!["host1".into()]);
assert!(dist.is_available());
dist.set_available(false);
assert!(!dist.is_available());
dist.set_available(true);
assert!(dist.is_available());
}
#[test]
fn test_distcc_hosts_string() {
let dist = X86DistributedCompilation::new_distcc(vec!["a".into(), "b".into(), "c".into()]);
assert_eq!(dist.distcc_hosts_string(), "a b c");
}
#[test]
fn test_parse_distcc_hosts() {
let hosts = X86DistributedCompilation::parse_distcc_hosts("host1 host2:3632 host3/4");
assert_eq!(hosts.len(), 3);
assert_eq!(hosts[0], "host1");
}
#[test]
fn test_distcc_preprocess_local() {
let dist = X86DistributedCompilation::new_distcc(vec!["remote".into()]);
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "pre.c");
let out = dist
.local_preprocess_source(&src, &tmp.path().join("out.o"))
.unwrap();
assert!(out.extension().map(|e| e == "i").unwrap_or(false));
}
#[test]
fn test_distcc_verify_result() {
let dist = X86DistributedCompilation::new_distcc(vec!["remote".into()]);
let tmp = TestTempDir::new();
let a = tmp.path().join("a.o");
let b = tmp.path().join("b.o");
fs::write(&a, b"same data").unwrap();
fs::write(&b, b"same data").unwrap();
assert!(dist.verify_result(&a, &b));
}
#[test]
fn test_distcc_frame_to_bytes() {
let frame = DistccFrame::new(1, vec![0xDE, 0xAD, 0xBE, 0xEF]);
let bytes = frame.to_bytes();
assert_eq!(bytes.len(), 12); assert_eq!(bytes[0..4], [0, 0, 0, 1]);
assert_eq!(bytes[8..12], [0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn test_icecream_dispatch() {
let addr: SocketAddr = "127.0.0.1:10245".parse().unwrap();
let dist = X86DistributedCompilation::new_icecream(addr);
let job = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("test.i"),
PathBuf::from("test.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 256,
use_response_file: false,
};
let result = dist.dispatch_job(&job).unwrap();
assert_eq!(result.status, X86JobStatus::Completed);
}
#[test]
fn test_farm_new() {
let farm = X86CompilationFarm::new();
assert_eq!(farm.healthy_count(), 0);
assert_eq!(farm.node_addresses().len(), 0);
}
#[test]
fn test_farm_add_node() {
let farm = X86CompilationFarm::new();
farm.add_node("node1", 3632, 4);
assert_eq!(farm.healthy_count(), 1);
assert_eq!(farm.available_slots(), 4);
}
#[test]
fn test_farm_remove_node() {
let farm = X86CompilationFarm::new();
farm.add_node("node1", 3632, 4);
assert!(farm.remove_node("node1"));
assert!(!farm.remove_node("nonexistent"));
assert_eq!(farm.healthy_count(), 0);
}
#[test]
fn test_farm_node_addresses() {
let farm = X86CompilationFarm::new();
farm.add_node("a", 3632, 2);
farm.add_node("b", 3632, 2);
let addrs = farm.node_addresses();
assert_eq!(addrs.len(), 2);
}
#[test]
fn test_farm_select_node() {
let farm = X86CompilationFarm::new();
farm.add_node("n1", 3632, 4);
farm.add_node("n2", 3632, 4);
let selected = farm.select_node();
assert!(selected.is_some());
}
#[test]
fn test_farm_job_started_completed() {
let farm = X86CompilationFarm::new();
farm.add_node("n1", 3632, 4);
assert_eq!(farm.available_slots(), 4);
farm.job_started("n1");
assert_eq!(farm.available_slots(), 3);
farm.job_completed("n1", true, 500);
assert_eq!(farm.available_slots(), 4);
}
#[test]
fn test_farm_health_check() {
let farm = X86CompilationFarm::new();
farm.add_node("healthy", 3632, 4);
let report = farm.health_check();
assert_eq!(report.healthy.len(), 1);
assert_eq!(report.total, 1);
}
#[test]
fn test_farm_health_report_display() {
let report = X86FarmHealthReport {
total: 2,
healthy: vec!["a".into()],
unhealthy: vec!["b".into()],
};
let s = format!("{}", report);
assert!(s.contains("a"));
assert!(s.contains("b"));
assert!(s.contains("1/2 healthy"));
}
#[test]
fn test_farm_cache_store_lookup() {
let farm = X86CompilationFarm::new();
let tmp = TestTempDir::new();
let path = tmp.path().join("cached.o");
fs::write(&path, b"objdata").unwrap();
farm.cache_store("key1", path.clone(), 7);
let entry = farm.cache_lookup("key1");
assert!(entry.is_some());
assert_eq!(entry.unwrap().size, 7);
}
#[test]
fn test_farm_cache_miss() {
let farm = X86CompilationFarm::new();
assert!(farm.cache_lookup("nonexistent").is_none());
}
#[test]
fn test_farm_default() {
let farm = X86CompilationFarm::default();
assert_eq!(farm.healthy_count(), 0);
}
#[test]
fn test_worker_node_load_factor() {
let node = X86WorkerNode {
addr: "127.0.0.1:3632".parse().unwrap(),
slots: 4,
available_slots: 2,
healthy: true,
last_health_check: Instant::now(),
total_jobs_completed: 10,
total_jobs_failed: 0,
average_job_time_ms: 250,
};
assert!((node.load_factor() - 0.5).abs() < 0.01);
}
#[test]
fn test_worker_node_is_healthy() {
let node = X86WorkerNode {
addr: "127.0.0.1:3632".parse().unwrap(),
slots: 4,
available_slots: 4,
healthy: true,
last_health_check: Instant::now(),
total_jobs_completed: 0,
total_jobs_failed: 0,
average_job_time_ms: 0,
};
assert!(node.is_healthy());
}
#[test]
fn test_cache_new() {
let cache = X86JobCache::new();
assert_eq!(cache.memory_entry_count(), 0);
assert_eq!(cache.disk_size_bytes(), 0);
}
#[test]
fn test_cache_default() {
let cache = X86JobCache::default();
assert_eq!(cache.memory_entry_count(), 0);
}
#[test]
fn test_cache_store_and_lookup() {
let cache = X86JobCache::new();
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "cache_test.c");
let key = X86CacheKey::from_source(&src, &make_options());
let obj = tmp.path().join("cache_test.o");
fs::write(&obj, b"object data").unwrap();
cache.store(&key, &obj, None);
let entry = cache.lookup(&key);
assert!(entry.is_some());
assert_eq!(entry.unwrap().output_path, obj);
}
#[test]
fn test_cache_lookup_miss() {
let cache = X86JobCache::new();
let key = X86CacheKey::from_source(Path::new("nonexistent.c"), &make_options());
assert!(cache.lookup(&key).is_none());
}
#[test]
fn test_cache_stats() {
let cache = X86JobCache::new();
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "stats.c");
let key = X86CacheKey::from_source(&src, &make_options());
let obj = tmp.path().join("stats.o");
fs::write(&obj, b"data").unwrap();
cache.store(&key, &obj, None);
let _ = cache.lookup(&key);
let _ = cache.lookup(&key);
let _ = cache.lookup(&X86CacheKey::from_source(
Path::new("missing.c"),
&make_options(),
));
let stats = cache.stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert!(stats.hit_rate() > 60.0);
}
#[test]
fn test_cache_disk_cache() {
let tmp = TestTempDir::new();
let cache = X86JobCache::new();
cache.set_disk_cache(tmp.path());
let src = make_temp_source(tmp.path(), "disk.c");
let key = X86CacheKey::from_source(&src, &make_options());
let obj = tmp.path().join("disk.o");
fs::write(&obj, b"disk object").unwrap();
cache.store(&key, &obj, None);
cache.memory_cache.write().unwrap().clear();
let entry = cache.lookup(&key);
assert!(entry.is_some());
}
#[test]
fn test_cache_http_cache_config() {
let cache = X86JobCache::new();
cache.set_http_cache("http://cache.example.com");
let url = cache.http_cache_url.read().unwrap();
assert_eq!(url.as_deref(), Some("http://cache.example.com"));
}
#[test]
fn test_cache_redis_config() {
let cache = X86JobCache::new();
let addr: SocketAddr = "127.0.0.1:6379".parse().unwrap();
cache.set_redis_cache(addr);
let redis = cache.redis_addr.read().unwrap();
assert!(redis.is_some());
}
#[test]
fn test_cache_s3_config() {
let cache = X86JobCache::new();
cache.set_s3_cache("my-bucket", Some("prefix/dir"));
assert_eq!(
cache.s3_bucket.read().unwrap().as_deref(),
Some("my-bucket")
);
assert_eq!(
cache.s3_prefix.read().unwrap().as_deref(),
Some("prefix/dir")
);
}
#[test]
fn test_cache_clear_all() {
let cache = X86JobCache::new();
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "clear.c");
let key = X86CacheKey::from_source(&src, &make_options());
let obj = tmp.path().join("clear.o");
fs::write(&obj, b"data").unwrap();
cache.store(&key, &obj, None);
assert_eq!(cache.memory_entry_count(), 1);
cache.clear_all();
assert_eq!(cache.memory_entry_count(), 0);
assert!(cache.lookup(&key).is_none());
}
#[test]
fn test_cache_key_hash_deterministic() {
let key1 = X86CacheKey::from_source(Path::new("a.c"), &make_options());
let key2 = X86CacheKey::from_source(Path::new("a.c"), &make_options());
assert_eq!(key1.combined(), key2.combined());
}
#[test]
fn test_cache_key_different_sources() {
let key1 = X86CacheKey::from_source(Path::new("a.c"), &make_options());
let key2 = X86CacheKey::from_source(Path::new("b.c"), &make_options());
assert_ne!(key1.combined(), key2.combined());
}
#[test]
fn test_cache_key_with_deps() {
let key = X86CacheKey::with_deps(
Path::new("a.c"),
&make_options(),
&[PathBuf::from("header.h")],
);
assert_ne!(key.deps_hash, 0);
}
#[test]
fn test_cache_stats_display() {
let stats = X86CacheStats {
hits: 90,
misses: 10,
stores: 100,
evictions: 5,
clears: 0,
};
let s = format!("{}", stats);
assert!(s.contains("rate=90.0%"));
}
#[test]
fn test_cached_entry_is_fresh() {
let entry = X86CachedEntry {
key: X86CacheKey::from_source(Path::new("x.c"), &make_options()),
output_path: PathBuf::from("x.o"),
created_at: SystemTime::now(),
size_bytes: 100,
hit_count: AtomicU64::new(0),
source_mtime: Some(UNIX_EPOCH + Duration::from_secs(1000)),
};
assert!(!entry.is_fresh(Some(UNIX_EPOCH + Duration::from_secs(2000))));
assert!(entry.is_fresh(Some(UNIX_EPOCH + Duration::from_secs(500))));
}
#[test]
fn test_cached_entry_record_hit() {
let entry = X86CachedEntry {
key: X86CacheKey::from_source(Path::new("x.c"), &make_options()),
output_path: PathBuf::from("x.o"),
created_at: SystemTime::now(),
size_bytes: 100,
hit_count: AtomicU64::new(0),
source_mtime: None,
};
entry.record_hit();
entry.record_hit();
assert_eq!(entry.hit_count.load(AtomicOrdering::Relaxed), 2);
}
#[test]
fn test_temp_file_manager_new() {
let mgr = X86TempFileManager::new("test");
assert_eq!(mgr.file_count(), 0);
assert!(mgr.temp_dir_path().exists());
}
#[test]
fn test_temp_file_manager_create() {
let mut mgr = X86TempFileManager::new("tmp_test");
let f = mgr.create_temp(".o");
assert_eq!(mgr.file_count(), 1);
assert!(f.extension().map(|e| e == "o").unwrap_or(false));
}
#[test]
fn test_temp_file_manager_file_types() {
let mut mgr = X86TempFileManager::new("types");
let pp = mgr.preprocessed_file();
let asm = mgr.assembly_file();
let obj = mgr.object_file();
let ir = mgr.ir_file();
let bc = mgr.bitcode_file();
assert!(pp.to_str().unwrap().ends_with(".i"));
assert!(asm.to_str().unwrap().ends_with(".s"));
assert!(obj.to_str().unwrap().ends_with(".o"));
assert!(ir.to_str().unwrap().ends_with(".ll"));
assert!(bc.to_str().unwrap().ends_with(".bc"));
assert_eq!(mgr.file_count(), 5);
}
#[test]
fn test_temp_file_manager_cleanup() {
let mgr = X86TempFileManager::new("cleanup_test");
let dir = mgr.temp_dir_path().to_path_buf();
assert!(dir.exists());
mgr.cleanup_all().unwrap();
assert!(!dir.exists());
}
#[test]
fn test_scheduler_config_default() {
let config = X86SchedulerConfig::default();
assert!(!config.fail_fast);
assert!(config.use_priority);
assert!(config.show_progress);
assert!(!config.verbose);
}
#[test]
fn test_full_pipeline_integration() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "main.c");
let mut jobs = X86DriverJobs::new(false, false);
jobs.add_source(&src, &make_options());
let stats = jobs.build_jobs();
assert!(stats.total_jobs >= 1);
let results = jobs.execute_parallel();
assert!(!results.is_empty());
for r in &results {
assert!(matches!(
r.status,
X86JobStatus::Completed | X86JobStatus::Skipped
));
}
}
#[test]
fn test_distributed_fallback() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "dist.c");
let mut jobs = X86DriverJobs::new(false, false);
jobs.enable_distcc(vec!["localhost".into()]);
jobs.add_source(&src, &make_options());
jobs.build_jobs();
let results = jobs.execute_distributed();
assert!(!results.is_empty());
}
#[test]
fn test_build_statistics_display() {
let stats = X86BuildStatistics {
total_jobs: 10,
completed: 9,
failed: 1,
skipped: 0,
elapsed_ms: 5000,
cache_hits: 3,
cache_misses: 7,
distributed_jobs: 0,
};
let s = format!("{}", stats);
assert!(s.contains("10"));
assert!(s.contains("9"));
assert!(s.contains("5000"));
}
#[test]
fn test_job_graph_stats_display() {
let stats = X86JobGraphStats {
total_jobs: 10,
compile_jobs: 5,
link_jobs: 1,
archive_jobs: 0,
lto_jobs: 0,
preprocess_jobs: 4,
dependency_edges: 5,
};
let s = format!("{}", stats);
assert!(s.contains("10"));
assert!(s.contains("C=5"));
}
#[test]
fn test_custom_job() {
let job = X86CustomJob::new("echo hello", "custom echo");
assert_eq!(job.command, "echo hello");
assert_eq!(job.description, "custom echo");
}
#[test]
fn test_scheduler_threaded_execution() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig {
max_parallel: 4,
..Default::default()
});
for i in 0..20 {
sched.add_job(
X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from(format!("{}.i", i)),
PathBuf::from(format!("{}.o", i)),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::Normal,
);
}
let results = sched.execute_parallel();
assert_eq!(results.len(), 20);
}
#[test]
fn test_resource_tracker() {
let mut rt = X86ResourceTracker::new();
assert!(rt.reserve(512));
assert_eq!(rt.peak_memory_mb, 512);
rt.release(512);
assert_eq!(rt.peak_memory_mb, 512);
}
#[test]
fn test_compile_job_all_flags_empty() {
let job = X86CompileJob::new(PathBuf::from("a.i"), PathBuf::from("a.o"));
let flags = job.all_flags();
assert!(flags.contains(&"-O2".to_string()));
assert!(flags.contains(&"-target".to_string()));
assert!(flags.contains(&"x86_64-unknown-linux-gnu".to_string()));
}
#[test]
fn test_response_file_generation() {
let mut job = X86DriverJob {
kind: X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("short.i"),
PathBuf::from("short.o"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
};
let tmp = TestTempDir::new();
let rsp = job.maybe_write_response_file(tmp.path());
assert!(rsp.is_none());
}
#[test]
fn test_distcc_frame_roundtrip() {
let payload = b"compilation request data";
let frame = DistccFrame::new(1, payload.to_vec());
let bytes = frame.to_bytes();
assert_eq!(u32::from_be_bytes(bytes[0..4].try_into().unwrap()), 1);
assert_eq!(
u32::from_be_bytes(bytes[4..8].try_into().unwrap()),
payload.len() as u32
);
assert_eq!(&bytes[8..], payload);
}
#[test]
fn test_cache_stats_hit_rate_zero() {
let stats = X86CacheStats::default();
assert_eq!(stats.hit_rate(), 0.0);
}
#[test]
fn test_cache_key_different_options() {
let mut opts1 = make_options();
let mut opts2 = make_options();
opts2.optimize = false;
let key1 = X86CacheKey::from_source(Path::new("a.c"), &opts1);
let key2 = X86CacheKey::from_source(Path::new("a.c"), &opts2);
assert_ne!(key1.combined(), key2.combined());
}
#[test]
fn test_farm_job_tracking() {
let farm = X86CompilationFarm::new();
farm.add_node("n1", 3632, 4);
farm.job_started("n1");
farm.job_started("n1");
assert_eq!(farm.available_slots(), 2);
farm.job_completed("n1", true, 100);
farm.job_completed("n1", false, 200);
assert_eq!(farm.available_slots(), 4);
}
#[test]
fn test_pipeline_build_with_deps() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "deps_test.c");
let mut pipeline = X86JobPipeline::new();
pipeline.set_output_dir(tmp.path());
pipeline.enable_dep_files(Some(tmp.path()));
pipeline.add_source(&src, &make_options());
let scheduler = Arc::new(Mutex::new(X86JobScheduler::new(
X86SchedulerConfig::default(),
)));
let cache = Arc::new(X86JobCache::new());
let stats = pipeline.build(&scheduler, &cache);
assert!(stats.total_jobs >= 2);
}
#[test]
fn test_pipeline_build_with_compile_db() {
let tmp = TestTempDir::new();
let src = make_temp_source(tmp.path(), "cdb_test.c");
let db_path = tmp.path().join("compile_commands.json");
let mut pipeline = X86JobPipeline::new();
pipeline.set_output_dir(tmp.path());
pipeline.enable_compile_db(&db_path);
pipeline.add_source(&src, &make_options());
let scheduler = Arc::new(Mutex::new(X86JobScheduler::new(
X86SchedulerConfig::default(),
)));
let cache = Arc::new(X86JobCache::new());
pipeline.build(&scheduler, &cache);
assert!(db_path.exists());
let content = fs::read_to_string(&db_path).unwrap();
assert!(content.contains("cdb_test.c"));
}
#[test]
fn test_link_job_to_command_line() {
let job = X86DriverJob {
kind: X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("prog"),
)),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 512,
use_response_file: false,
};
let cmd = job.to_command_line();
assert!(cmd.contains("a.o"));
assert!(cmd.contains("b.o"));
assert!(cmd.contains("-o"));
assert!(cmd.contains("prog"));
}
#[test]
fn test_archive_job_to_command_line() {
let job = X86DriverJob {
kind: X86JobKind::Archive(X86ArchiveJob::new(
vec![PathBuf::from("x.o"), PathBuf::from("y.o")],
PathBuf::from("lib.a"),
)),
priority: X86JobPriority::Normal,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 64,
use_response_file: false,
};
let cmd = job.to_command_line();
assert!(cmd.contains("ar"));
assert!(cmd.contains("x.o"));
assert!(cmd.contains("lib.a"));
}
#[test]
fn test_lto_job_to_command_line() {
let job = X86DriverJob {
kind: X86JobKind::LTO(X86LTOJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("prog"),
)),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: 120,
estimated_memory_mb: 1024,
use_response_file: false,
};
let cmd = job.to_command_line();
assert!(cmd.contains("-flto"));
}
#[test]
fn test_scheduler_multiple_deps() {
let mut sched = X86JobScheduler::new(X86SchedulerConfig::default());
let j0 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
)),
priority: X86JobPriority::High,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::High,
);
let j1 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("b.c"),
PathBuf::from("b.i"),
)),
priority: X86JobPriority::High,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 128,
use_response_file: false,
},
X86JobPriority::High,
);
let j2 = sched.add_job(
X86DriverJob {
kind: X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("prog"),
)),
priority: X86JobPriority::Critical,
working_dir: None,
timeout_secs: 60,
estimated_memory_mb: 512,
use_response_file: false,
},
X86JobPriority::Critical,
);
sched.add_dependency(j2, j0);
sched.add_dependency(j2, j1);
let deps = sched.dependencies_of(j2).unwrap();
assert_eq!(deps.len(), 2);
assert!(deps.contains(&j0));
assert!(deps.contains(&j1));
let rev0 = sched.dependents_of(j0).unwrap();
assert!(rev0.contains(&j2));
}
#[test]
fn test_pipeline_compile_db_with_output() {
let cmd = X86CompileCommand {
directory: PathBuf::from("/src"),
file: PathBuf::from("/src/main.c"),
arguments: vec!["clang".to_string(), "-c".to_string(), "main.c".to_string()],
output: Some("build/main.o".to_string()),
};
let json = serde_json::to_string(&cmd).unwrap();
let parsed: X86CompileCommand = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.output, Some("build/main.o".to_string()));
}
#[test]
fn test_cache_key_fnv_hash_basic() {
let h1 = X86CacheKey::hash_bytes(b"hello");
let h2 = X86CacheKey::hash_bytes(b"hello");
assert_eq!(h1, h2);
let h3 = X86CacheKey::hash_bytes(b"world");
assert_ne!(h1, h3);
}
#[test]
fn test_build_statistics_defaults() {
let stats = X86BuildStatistics {
total_jobs: 0,
completed: 0,
failed: 0,
skipped: 0,
elapsed_ms: 0,
cache_hits: 0,
cache_misses: 0,
distributed_jobs: 0,
};
let s = format!("{}", stats);
assert!(s.contains("0"));
}
#[test]
fn test_scheduler_config_memory_limit() {
let config = X86SchedulerConfig {
memory_limit_mb: 1024,
..Default::default()
};
assert_eq!(config.memory_limit_mb, 1024);
}
#[test]
fn test_daemon_new() {
let daemon = X86CompilationDaemon::new();
assert!(!daemon.is_running());
assert_eq!(daemon.uptime_secs(), 0);
}
#[test]
fn test_daemon_default() {
let daemon = X86CompilationDaemon::default();
assert!(!daemon.is_running());
}
#[test]
fn test_daemon_start_stop() {
let daemon = X86CompilationDaemon::new();
daemon.start();
assert!(daemon.is_running());
daemon.stop();
assert!(!daemon.is_running());
}
#[test]
fn test_daemon_with_config() {
let config = X86DaemonConfig {
max_pch_entries: 32,
max_memory_mb: 1024,
..Default::default()
};
let daemon = X86CompilationDaemon::with_config(config);
assert_eq!(daemon.config.max_pch_entries, 32);
}
#[test]
fn test_daemon_pch_cache() {
let daemon = X86CompilationDaemon::new();
let entry = X86PchEntry {
header_hash: 0xABCD,
data: vec![1, 2, 3],
size_bytes: 3,
dependencies: vec![PathBuf::from("header.h")],
build_flags: vec!["-O2".to_string()],
created_at: SystemTime::now(),
use_count: 0,
};
daemon.cache_pch(entry.clone());
let found = daemon.lookup_pch(0xABCD);
assert!(found.is_some());
assert_eq!(found.unwrap().data, vec![1, 2, 3]);
}
#[test]
fn test_daemon_module_cache() {
let daemon = X86CompilationDaemon::new();
let entry = X86ModuleEntry {
name: "std.core".to_string(),
bmi_data: vec![4, 5, 6],
dependencies: vec!["std.fundamental".to_string()],
size_bytes: 3,
created_at: SystemTime::now(),
};
daemon.cache_module(entry);
let found = daemon.lookup_module("std.core");
assert!(found.is_some());
}
#[test]
fn test_daemon_result_cache() {
let daemon = X86CompilationDaemon::new();
let key = X86CacheKey::from_source(Path::new("test.c"), &make_options());
let entry = X86CachedEntry {
key: key.clone(),
output_path: PathBuf::from("test.o"),
created_at: SystemTime::now(),
size_bytes: 100,
hit_count: AtomicU64::new(0),
source_mtime: None,
};
daemon.store_result(&key, entry);
let found = daemon.lookup_result(&key);
assert!(found.is_some());
}
#[test]
fn test_daemon_statistics() {
let daemon = X86CompilationDaemon::new();
daemon.start();
let stats = daemon.statistics();
assert!(stats.running);
assert_eq!(stats.pch_entries, 0);
assert_eq!(stats.module_entries, 0);
}
#[test]
fn test_daemon_health_check() {
let daemon = X86CompilationDaemon::new();
daemon.start();
let health = daemon.health_check();
assert!(health.running);
assert!(health.healthy);
}
#[test]
fn test_daemon_stats_display() {
let stats = X86DaemonStats {
running: true,
uptime_secs: 100,
pch_entries: 4,
module_entries: 2,
result_entries: 10,
total_served: 50,
cache_hits: 30,
memory_bytes: 1048576,
};
let s = format!("{}", stats);
assert!(s.contains("100s"));
assert!(s.contains("50"));
assert!(s.contains("1 MB"));
}
#[test]
fn test_daemon_config_default() {
let config = X86DaemonConfig::default();
assert_eq!(config.max_pch_entries, 64);
assert_eq!(config.idle_timeout_secs, 3600);
}
#[test]
fn test_jobserver_new_owner() {
let js = X86JobServer::new_owner(4);
assert!(js.is_owner());
assert_eq!(js.total_tokens(), 4);
assert_eq!(js.available(), 4);
}
#[test]
fn test_jobserver_acquire_release() {
let js = X86JobServer::new_owner(2);
assert!(js.acquire());
assert_eq!(js.available(), 1);
assert!(js.acquire());
assert_eq!(js.available(), 0);
assert!(!js.acquire());
js.release();
assert_eq!(js.available(), 1);
}
#[test]
fn test_jobserver_close() {
let js = X86JobServer::new_owner(4);
js.acquire();
js.acquire();
js.close();
assert_eq!(js.available(), 4);
}
#[test]
fn test_token_semaphore_new() {
let sem = X86TokenSemaphore::new(8);
assert_eq!(sem.max(), 8);
assert_eq!(sem.available(), 8);
}
#[test]
fn test_token_semaphore_try_acquire() {
let sem = X86TokenSemaphore::new(2);
assert!(sem.try_acquire());
assert!(sem.try_acquire());
assert!(!sem.try_acquire());
sem.release();
assert!(sem.try_acquire());
}
#[test]
fn test_token_semaphore_release_over_max() {
let sem = X86TokenSemaphore::new(1);
sem.release();
assert_eq!(sem.available(), 1);
}
#[test]
fn test_response_file_new() {
let rsp = X86ResponseFile::new(PathBuf::from("args.rsp"));
assert_eq!(rsp.arg_count(), 0);
}
#[test]
fn test_response_file_add_args() {
let mut rsp = X86ResponseFile::new(PathBuf::from("args.rsp"));
rsp.arg("-O2").arg("-c").args(&["-Wall", "-Wextra"]);
assert_eq!(rsp.arg_count(), 4);
}
#[test]
fn test_response_file_write_and_parse() {
let tmp = TestTempDir::new();
let rsp_path = tmp.path().join("test.rsp");
let mut rsp = X86ResponseFile::new(rsp_path.clone());
rsp.arg("-O2").arg("-c").arg("test.c");
rsp.write().unwrap();
let parsed = X86ResponseFile::parse(&rsp_path).unwrap();
assert_eq!(parsed.len(), 3);
assert!(parsed.contains(&"-O2".to_string()));
}
#[test]
fn test_response_file_is_needed() {
let mut rsp = X86ResponseFile::new(PathBuf::from("args.rsp"));
rsp.arg("-c").arg("test.c");
assert!(!rsp.is_needed());
}
#[test]
fn test_response_file_flag() {
let rsp = X86ResponseFile::new(PathBuf::from("/tmp/args.rsp"));
assert_eq!(rsp.flag(), "@/tmp/args.rsp");
}
#[test]
fn test_response_file_cleanup() {
let tmp = TestTempDir::new();
let rsp_path = tmp.path().join("cleanup.rsp");
let rsp = X86ResponseFile::new(rsp_path.clone());
rsp.write().unwrap();
assert!(rsp_path.exists());
rsp.cleanup().unwrap();
assert!(!rsp_path.exists());
}
#[test]
fn test_dep_scanner_new() {
let scanner = X86DependencyScanner::new();
assert!(scanner.include_paths.is_empty());
}
#[test]
fn test_dep_scanner_default() {
let scanner = X86DependencyScanner::default();
assert!(!scanner.include_system_headers);
}
#[test]
fn test_dep_scanner_add_include_paths() {
let mut scanner = X86DependencyScanner::new();
scanner
.add_include_path(Path::new("/usr/include"))
.add_system_include_path(Path::new("/usr/local/include"));
assert_eq!(scanner.include_paths.len(), 1);
assert_eq!(scanner.system_include_paths.len(), 1);
}
#[test]
fn test_dep_scanner_extract_include_quoted() {
let scanner = X86DependencyScanner::new();
let path = scanner.extract_include_path("#include \"header.h\"");
assert_eq!(path, Some("header.h".to_string()));
}
#[test]
fn test_dep_scanner_extract_include_angle() {
let scanner = X86DependencyScanner::new();
let path = scanner.extract_include_path("#include <stdio.h>");
assert_eq!(path, Some("stdio.h".to_string()));
}
#[test]
fn test_dep_scanner_scan_file() {
let tmp = TestTempDir::new();
let header = tmp.path().join("dep.h");
fs::write(&header, "// header\n").unwrap();
let src = tmp.path().join("dep_test.c");
fs::write(
&src,
format!("#include \"{}\"\nint main() {{}}", header.display()),
)
.unwrap();
let mut scanner = X86DependencyScanner::new();
scanner.add_include_path(tmp.path());
let deps = scanner.scan_file(&src).unwrap();
assert!(deps.contains(&header));
}
#[test]
fn test_dep_scanner_needs_rebuild_missing_output() {
let scanner = X86DependencyScanner::new();
assert!(scanner.needs_rebuild(Path::new("src.c"), Path::new("nonexistent.o")));
}
#[test]
fn test_dep_scanner_write_depfile() {
let tmp = TestTempDir::new();
let deppath = tmp.path().join("output.d");
let scanner = X86DependencyScanner::new();
scanner
.write_depfile(Path::new("test.o"), Path::new("test.c"), &deppath)
.unwrap();
let content = fs::read_to_string(&deppath).unwrap();
assert!(content.contains("test.o"));
}
#[test]
fn test_dep_scanner_clear() {
let mut scanner = X86DependencyScanner::new();
scanner.add_include_path(Path::new("/include"));
scanner.clear();
assert!(scanner.include_paths.is_empty());
}
#[test]
fn test_build_graph_new() {
let graph = X86BuildGraph::new();
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_build_graph_add_node() {
let mut graph = X86BuildGraph::new();
let node = X86BuildNode::new(
0,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
);
let id = graph.add_node(node);
assert_eq!(id, 0);
assert_eq!(graph.node_count(), 1);
}
#[test]
fn test_build_graph_add_edge() {
let mut graph = X86BuildGraph::new();
let n0 = graph.add_node(X86BuildNode::new(
0,
X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
)),
));
let n1 = graph.add_node(X86BuildNode::new(
1,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
));
graph.add_edge(n0, n1);
assert_eq!(graph.successors_of(n0).len(), 1);
assert_eq!(graph.predecessors_of(n1).len(), 1);
}
#[test]
fn test_build_graph_reaches() {
let mut graph = X86BuildGraph::new();
let a = graph.add_node(X86BuildNode::new(
0,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
));
let b = graph.add_node(X86BuildNode::new(
1,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("b.i"),
PathBuf::from("b.o"),
)),
));
let c = graph.add_node(X86BuildNode::new(
2,
X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o"), PathBuf::from("b.o")],
PathBuf::from("out"),
)),
));
graph.add_edge(a, c);
graph.add_edge(b, c);
assert!(graph.reaches(a, c));
assert!(!graph.reaches(b, a));
}
#[test]
fn test_build_graph_no_cycle() {
let mut graph = X86BuildGraph::new();
let a = graph.add_node(X86BuildNode::new(
0,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
));
let b = graph.add_node(X86BuildNode::new(
1,
X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("out"),
)),
));
graph.add_edge(a, b);
assert!(!graph.has_cycle());
}
#[test]
fn test_build_graph_topological_order() {
let mut graph = X86BuildGraph::new();
let a = graph.add_node(X86BuildNode::new(
0,
X86JobKind::Preprocess(X86PreprocessJob::new(
PathBuf::from("a.c"),
PathBuf::from("a.i"),
)),
));
let b = graph.add_node(X86BuildNode::new(
1,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
));
graph.add_edge(a, b);
let order = graph.topological_order().unwrap();
assert_eq!(order.len(), 2);
assert!(order.iter().position(|&x| x == a) < order.iter().position(|&x| x == b));
}
#[test]
fn test_build_graph_critical_path() {
let mut graph = X86BuildGraph::new();
let mut n0 = X86BuildNode::new(
0,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
);
n0.estimated_duration_ms = 100;
let mut n1 = X86BuildNode::new(
1,
X86JobKind::Link(X86LinkJob::new(
vec![PathBuf::from("a.o")],
PathBuf::from("out"),
)),
);
n1.estimated_duration_ms = 50;
let a = graph.add_node(n0);
let b = graph.add_node(n1);
graph.add_edge(a, b);
let path = graph.critical_path();
assert!(!path.is_empty());
}
#[test]
fn test_build_graph_to_dot() {
let mut graph = X86BuildGraph::new();
graph.add_node(X86BuildNode::new(
0,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
));
let dot = graph.to_dot();
assert!(dot.starts_with("digraph"));
assert!(dot.contains("n0"));
}
#[test]
fn test_build_graph_estimated_time() {
let mut graph = X86BuildGraph::new();
let mut n0 = X86BuildNode::new(
0,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("a.i"),
PathBuf::from("a.o"),
)),
);
n0.estimated_duration_ms = 500;
let mut n1 = X86BuildNode::new(
1,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("b.i"),
PathBuf::from("b.o"),
)),
);
n1.estimated_duration_ms = 300;
graph.add_node(n0);
graph.add_node(n1);
assert_eq!(graph.estimated_total_time_ms(), 800);
}
#[test]
fn test_build_node_label() {
let node = X86BuildNode::new(
42,
X86JobKind::Compile(X86CompileJob::new(
PathBuf::from("test.i"),
PathBuf::from("test.o"),
)),
);
let label = node.label();
assert!(label.contains("CC#42"));
assert!(label.contains("test.i"));
}
#[test]
fn test_metrics_new() {
let metrics = X86JobMetrics::new();
let (hits, total) = metrics.cache_stats();
assert_eq!(total, 0);
assert_eq!(hits, 0);
}
#[test]
fn test_metrics_job_started_finished() {
let metrics = X86JobMetrics::new();
metrics.job_started(0, "compile");
metrics.job_finished(0, false, false, None);
let slowest = metrics.slowest_jobs(1);
assert_eq!(slowest.len(), 1);
assert_eq!(slowest[0].job_id, 0);
assert!(!slowest[0].was_cache_hit);
}
#[test]
fn test_metrics_cache_stats() {
let metrics = X86JobMetrics::new();
metrics.job_started(0, "compile");
metrics.job_finished(0, true, false, None);
metrics.job_started(1, "compile");
metrics.job_finished(1, false, false, None);
let (hits, total) = metrics.cache_stats();
assert_eq!(hits, 1);
assert_eq!(total, 2);
}
#[test]
fn test_metrics_distributed_stats() {
let metrics = X86JobMetrics::new();
metrics.job_started(0, "compile");
metrics.job_finished(0, false, true, Some("node1"));
let (dist, total) = metrics.distributed_stats();
assert_eq!(dist, 1);
assert_eq!(total, 1);
}
#[test]
fn test_metrics_phase_finished() {
let metrics = X86JobMetrics::new();
metrics.phase_finished("compile", Duration::from_millis(500));
let report = metrics.report();
assert!(report.contains("compile"));
assert!(report.contains("500"));
}
#[test]
fn test_metrics_report() {
let metrics = X86JobMetrics::new();
metrics.job_started(0, "compile");
metrics.job_finished(0, false, false, None);
let report = metrics.report();
assert!(report.contains("X86 Build Timing Report"));
assert!(report.contains("compile"));
}
#[test]
fn test_metrics_slowest_jobs_truncation() {
let metrics = X86JobMetrics::new();
for i in 0..10 {
metrics.job_started(i, "compile");
}
for i in 0..10 {
metrics.job_finished(i, false, false, None);
}
let top = metrics.slowest_jobs(3);
assert_eq!(top.len(), 3);
}
#[test]
fn test_metrics_default() {
let _metrics = X86JobMetrics::default();
}
}