diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 7856be47..1f5e6e70 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -177,6 +177,7 @@ fn run_experiment( threads_count, &agent.config, &|| agent.next_crate(&ex.name), + true, ) .map_err(|err| (Some(Box::new(ex)), err))?; Ok(()) diff --git a/src/cli.rs b/src/cli.rs index 92c01112..8a149b6b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -441,6 +441,7 @@ impl Crater { threads, &config, &|| Ok(crates.lock().unwrap().pop()), + false, ); workspace.purge_all_build_dirs()?; res?; diff --git a/src/config.rs b/src/config.rs index d356461f..e141c640 100644 --- a/src/config.rs +++ b/src/config.rs @@ -86,6 +86,8 @@ pub struct Config { pub local_crates: HashMap, pub server: ServerConfig, pub sandbox: SandboxConfig, + #[serde(default = "default_false")] + pub capture_timings: bool, } impl Config { @@ -253,6 +255,7 @@ impl Default for Config { experiment_completed: "".into(), }, }, + capture_timings: false, } } } diff --git a/src/db/migrations.rs b/src/db/migrations.rs index 6132a491..6480dd9f 100644 --- a/src/db/migrations.rs +++ b/src/db/migrations.rs @@ -363,6 +363,27 @@ fn migrations() -> Vec<(&'static str, MigrationKind)> { MigrationKind::SQL("alter table agents add column latest_work_for text;"), )); + migrations.push(( + "create_build_timings_table", + MigrationKind::SQL( + " + CREATE TABLE build_timings ( + experiment TEXT NOT NULL, + crate TEXT NOT NULL, + toolchain TEXT NOT NULL, + package_id TEXT NOT NULL, + target_name TEXT NOT NULL, + target_kind TEXT NOT NULL, + mode TEXT NOT NULL, + duration REAL NOT NULL, + rmeta_time REAL, + FOREIGN KEY (experiment) REFERENCES experiments(name) ON DELETE CASCADE + ); + CREATE INDEX build_timings__experiment ON build_timings (experiment); + ", + ), + )); + migrations } diff --git a/src/lib.rs b/src/lib.rs index b63f576b..55bfeace 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ pub mod report; pub mod results; pub mod runner; pub mod server; +pub mod timings; pub mod toolchain; pub(crate) static GIT_REVISION: Option<&str> = include!(concat!(env!("OUT_DIR"), "/sha")); diff --git a/src/results/db.rs b/src/results/db.rs index 90bd565e..09257e71 100644 --- a/src/results/db.rs +++ b/src/results/db.rs @@ -5,6 +5,7 @@ use crate::prelude::*; use crate::results::{ DeleteResults, EncodedLog, EncodingType, ReadResults, TestResult, WriteResults, }; +use crate::timings::TimingInfo; use crate::toolchain::Toolchain; use base64::Engine; use rustwide::logging::{self, LogStorage}; @@ -143,6 +144,69 @@ impl<'a> DatabaseDB<'a> { Ok(()) } + pub fn store_timings( + &self, + ex: &Experiment, + krate: &Crate, + toolchain: &Toolchain, + timings: &[TimingInfo], + ) -> Fallible<()> { + for timing in timings { + let target_kind = timing.target.kind.join(","); + self.db.execute_cached( + "INSERT INTO build_timings \ + (experiment, crate, toolchain, package_id, target_name, target_kind, mode, duration, rmeta_time) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);", + &[ + &ex.name as &dyn rusqlite::types::ToSql, + &krate.id(), + &toolchain.to_string(), + &timing.package_id, + &timing.target.name, + &target_kind, + &timing.mode, + &timing.duration, + &timing.rmeta_time, + ], + )?; + } + Ok(()) + } + + pub fn load_timings( + &self, + ex: &Experiment, + krate: &Crate, + toolchain: &Toolchain, + ) -> Fallible> { + self.db.query( + "SELECT package_id, target_name, target_kind, mode, duration, rmeta_time \ + FROM build_timings \ + WHERE experiment = ?1 AND crate = ?2 AND toolchain = ?3;", + [&ex.name, &krate.id(), &toolchain.to_string()], + |row| { + let target_kind: String = row.get("target_kind")?; + let kinds: Vec = target_kind.split(',').map(|s| s.to_string()).collect(); + Ok(TimingInfo { + package_id: row.get("package_id")?, + target: crate::timings::TimingTarget { + kind: kinds, + crate_types: Vec::new(), + name: row.get("target_name")?, + src_path: String::new(), + edition: String::new(), + doc: false, + doctest: false, + test: false, + }, + mode: row.get("mode")?, + duration: row.get("duration")?, + rmeta_time: row.get("rmeta_time")?, + }) + }, + ) + } + fn insert_into_results( &self, ex: &Experiment, @@ -272,6 +336,16 @@ impl crate::runner::RecordProgress for DatabaseDB<'_> { } Ok(()) } + + fn record_timings( + &self, + ex: &Experiment, + krate: &Crate, + toolchain: &Toolchain, + timings: &[TimingInfo], + ) -> Fallible<()> { + self.store_timings(ex, krate, toolchain, timings) + } } impl DeleteResults for DatabaseDB<'_> { @@ -468,6 +542,85 @@ mod tests { .is_none()); } + #[test] + fn test_store_and_load_timings() { + let db = Database::temp().unwrap(); + let results = DatabaseDB::new(&db); + let config = Config::default(); + let ctx = ActionsCtx::new(&db, &config); + + crate::crates::lists::setup_test_lists(&db, &config).unwrap(); + + CreateExperiment::dummy("dummy").apply(&ctx).unwrap(); + let ex = Experiment::get(&db, "dummy").unwrap().unwrap(); + + let krate = Crate::Registry(RegistryCrate { + name: "lazy_static".into(), + version: "1".into(), + }); + + let timings = vec![ + crate::timings::TimingInfo { + package_id: "serde 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" + .to_string(), + target: crate::timings::TimingTarget { + kind: vec!["lib".to_string()], + crate_types: vec!["lib".to_string()], + name: "serde".to_string(), + src_path: "/path/to/src/lib.rs".to_string(), + edition: "2021".to_string(), + doc: true, + doctest: true, + test: true, + }, + mode: "build".to_string(), + duration: 12.5, + rmeta_time: Some(8.3), + }, + crate::timings::TimingInfo { + package_id: "foo 0.1.0".to_string(), + target: crate::timings::TimingTarget { + kind: vec!["bin".to_string()], + crate_types: vec!["bin".to_string()], + name: "foo".to_string(), + src_path: "/path/to/main.rs".to_string(), + edition: "2021".to_string(), + doc: false, + doctest: false, + test: false, + }, + mode: "build".to_string(), + duration: 1.0, + rmeta_time: None, + }, + ]; + + results + .store_timings(&ex, &krate, &MAIN_TOOLCHAIN, &timings) + .unwrap(); + + let loaded = results.load_timings(&ex, &krate, &MAIN_TOOLCHAIN).unwrap(); + + assert_eq!(loaded.len(), 2); + + assert_eq!(loaded[0].package_id, timings[0].package_id); + assert_eq!(loaded[0].target.name, "serde"); + assert_eq!(loaded[0].target.kind, vec!["lib"]); + assert_eq!(loaded[0].mode, "build"); + assert!((loaded[0].duration - 12.5).abs() < f64::EPSILON); + assert!((loaded[0].rmeta_time.unwrap() - 8.3).abs() < f64::EPSILON); + + assert_eq!(loaded[1].package_id, "foo 0.1.0"); + assert_eq!(loaded[1].target.name, "foo"); + assert_eq!(loaded[1].target.kind, vec!["bin"]); + assert!((loaded[1].duration - 1.0).abs() < f64::EPSILON); + assert!(loaded[1].rmeta_time.is_none()); + + // Different toolchain should return empty + let empty = results.load_timings(&ex, &krate, &TEST_TOOLCHAIN).unwrap(); + assert!(empty.is_empty()); + } + #[test] fn test_store() { let db = Database::temp().unwrap(); diff --git a/src/runner/mod.rs b/src/runner/mod.rs index afdc2e53..3307ca05 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -29,6 +29,7 @@ pub fn run_ex( threads_count: usize, config: &Config, next_crate: &(dyn Fn() -> Fallible> + Send + Sync), + is_agent_mode: bool, ) -> Fallible<()> { // Attempt to spin indefinitely until docker is up. Ideally, we would // decomission this agent until docker is up, instead of leaving the @@ -89,6 +90,7 @@ pub fn run_ex( config, api, next_crate, + is_agent_mode, ) }) .collect::>(); diff --git a/src/runner/tasks.rs b/src/runner/tasks.rs index 8942b655..f0c4adea 100644 --- a/src/runner/tasks.rs +++ b/src/runner/tasks.rs @@ -4,6 +4,7 @@ use crate::experiments::Experiment; use crate::prelude::*; use crate::results::TestResult; use crate::runner::test; +use crate::timings::TimingVisitor; use crate::toolchain::Toolchain; use rustwide::{Build, BuildDirectory}; use std::collections::HashMap; @@ -91,12 +92,13 @@ impl Task { config: &'ctx Config, build_dir: &'ctx HashMap<&'ctx crate::toolchain::Toolchain, Mutex>, ex: &'ctx Experiment, + timing_visitor: &'ctx mut dyn TimingVisitor, logs: &LogStorage, ) -> Fallible { let (build_dir, action, test, toolchain, quiet): ( _, _, - fn(&TaskCtx, &Build, &_) -> _, + fn(&TaskCtx, &Build, &_, &mut dyn TimingVisitor) -> _, _, _, ) = match self.step { @@ -130,6 +132,6 @@ impl Task { }; let ctx = TaskCtx::new(build_dir, config, ex, toolchain, &self.krate, quiet); - test::run_test(action, &ctx, test, logs) + test::run_test(action, &ctx, test, timing_visitor, logs) } } diff --git a/src/runner/test.rs b/src/runner/test.rs index edf855ff..9614ef16 100644 --- a/src/runner/test.rs +++ b/src/runner/test.rs @@ -5,6 +5,7 @@ use crate::results::DiagnosticCode; use crate::results::{BrokenReason, FailureReason, TestResult}; use crate::runner::tasks::TaskCtx; use crate::runner::OverrideResult; +use crate::timings::{NoopTimingVisitor, TimingVisitor}; use anyhow::Error; use cargo_metadata::diagnostic::DiagnosticLevel; use cargo_metadata::{CrateType, Metadata, Package, Target, TargetKind}; @@ -92,10 +93,14 @@ fn run_cargo( env: HashMap<&'static str, String>, mount_kind: MountKind, cap_lints: Option, + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible<()> { let local_packages_id: HashSet<_> = local_packages.iter().map(|p| &p.id).collect(); let mut args = args.to_vec(); + if timing_visitor.is_capturing() { + args.push("--timings"); + } if let Some(ref target) = ctx.toolchain.target { args.extend(["--target", target]); } @@ -187,6 +192,10 @@ fn run_cargo( actions.replace_with_lines(inner_message.rendered.unwrap_or_default().split('\n')); } + Message::TimingInfo(timing) => { + timing_visitor.visit_timing(timing); + actions.remove_line(); + } _ => actions.remove_line(), } }; @@ -242,6 +251,8 @@ fn run_cargo( enum Message { /// The compiler wants to display a message CompilerMessage(CompilerMessage), + /// Timing data from cargo's `--timings` flag + TimingInfo(crate::timings::TimingInfo), #[serde(other)] Other, } @@ -275,7 +286,8 @@ struct Diagnostic { pub(super) fn run_test( action: &str, ctx: &TaskCtx, - test_fn: fn(&TaskCtx, &Build, &[Package]) -> Fallible, + test_fn: fn(&TaskCtx, &Build, &[Package], &mut dyn TimingVisitor) -> Fallible, + timing_visitor: &mut dyn TimingVisitor, logs: &LogStorage, ) -> Fallible { rustwide::logging::capture(logs, || { @@ -297,12 +309,17 @@ pub(super) fn run_test( detect_broken(build.run(|build| { let local_packages = get_local_packages(build)?; - test_fn(ctx, build, &local_packages) + test_fn(ctx, build, &local_packages, timing_visitor) })) }) } -fn build(ctx: &TaskCtx, build_env: &Build, local_packages: &[Package]) -> Fallible<()> { +fn build( + ctx: &TaskCtx, + build_env: &Build, + local_packages: &[Package], + timing_visitor: &mut dyn TimingVisitor, +) -> Fallible<()> { run_cargo( ctx, build_env, @@ -312,6 +329,7 @@ fn build(ctx: &TaskCtx, build_env: &Build, local_packages: &[Package]) -> Fallib HashMap::default(), MountKind::ReadOnly, Some(ctx.experiment.cap_lints), + timing_visitor, )?; run_cargo( ctx, @@ -322,6 +340,7 @@ fn build(ctx: &TaskCtx, build_env: &Build, local_packages: &[Package]) -> Fallib HashMap::default(), MountKind::ReadOnly, Some(ctx.experiment.cap_lints), + timing_visitor, )?; Ok(()) } @@ -336,6 +355,7 @@ fn test(ctx: &TaskCtx, build_env: &Build) -> Fallible<()> { HashMap::default(), MountKind::ReadOnly, Some(ctx.experiment.cap_lints), + &mut NoopTimingVisitor, ) } @@ -343,8 +363,9 @@ pub(super) fn test_build_and_test( ctx: &TaskCtx, build_env: &Build, local_packages_id: &[Package], + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { - let build_r = build(ctx, build_env, local_packages_id); + let build_r = build(ctx, build_env, local_packages_id, timing_visitor); let test_r = if build_r.is_ok() { Some(test(ctx, build_env)) } else { @@ -363,8 +384,9 @@ pub(super) fn test_build_only( ctx: &TaskCtx, build_env: &Build, local_packages_id: &[Package], + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { - if let Err(err) = build(ctx, build_env, local_packages_id) { + if let Err(err) = build(ctx, build_env, local_packages_id, timing_visitor) { Ok(TestResult::BuildFail(failure_reason(&err))) } else { Ok(TestResult::TestSkipped) @@ -375,6 +397,7 @@ pub(super) fn test_check_only( ctx: &TaskCtx, build_env: &Build, local_packages_id: &[Package], + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { if let Err(err) = run_cargo( ctx, @@ -391,6 +414,7 @@ pub(super) fn test_check_only( HashMap::default(), MountKind::ReadOnly, Some(ctx.experiment.cap_lints), + timing_visitor, ) { Ok(TestResult::BuildFail(failure_reason(&err))) } else { @@ -402,6 +426,7 @@ pub(super) fn test_clippy_only( ctx: &TaskCtx, build_env: &Build, local_packages: &[Package], + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { if let Err(err) = run_cargo( ctx, @@ -418,6 +443,7 @@ pub(super) fn test_clippy_only( HashMap::default(), MountKind::ReadOnly, Some(ctx.experiment.cap_lints), + timing_visitor, ) { Ok(TestResult::BuildFail(failure_reason(&err))) } else { @@ -429,8 +455,9 @@ pub(super) fn test_rustdoc( ctx: &TaskCtx, build_env: &Build, local_packages: &[Package], + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { - let run = |cargo_args, env| { + let mut run = |cargo_args, env| { let res = run_cargo( ctx, build_env, @@ -440,6 +467,7 @@ pub(super) fn test_rustdoc( env, MountKind::ReadOnly, Some(ctx.experiment.cap_lints), + &mut *timing_visitor, ); // Make sure to remove the built documentation @@ -503,6 +531,7 @@ pub(crate) fn fix( ctx: &TaskCtx, build_env: &Build, local_packages_id: &[Package], + timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { if let Err(err) = run_cargo( ctx, @@ -521,6 +550,7 @@ pub(crate) fn fix( HashMap::default(), MountKind::ReadWrite, None, + timing_visitor, ) { Ok(TestResult::BuildFail(failure_reason(&err))) } else { diff --git a/src/runner/unstable_features.rs b/src/runner/unstable_features.rs index aa505d90..060f1b34 100644 --- a/src/runner/unstable_features.rs +++ b/src/runner/unstable_features.rs @@ -1,6 +1,7 @@ use crate::prelude::*; use crate::results::TestResult; use crate::runner::tasks::TaskCtx; +use crate::timings::TimingVisitor; use cargo_metadata::Package; use rustwide::Build; use std::collections::HashSet; @@ -11,6 +12,7 @@ pub(super) fn find_unstable_features( _ctx: &TaskCtx, build: &Build, _local_packages_id: &[Package], + _timing_visitor: &mut dyn TimingVisitor, ) -> Fallible { let mut features = HashSet::new(); diff --git a/src/runner/worker.rs b/src/runner/worker.rs index 8019c21c..efb93607 100644 --- a/src/runner/worker.rs +++ b/src/runner/worker.rs @@ -6,6 +6,7 @@ use crate::results::{BrokenReason, TestResult}; use crate::runner::tasks::{Task, TaskStep}; use crate::runner::test::{detect_broken, failure_reason}; use crate::runner::OverrideResult; +use crate::timings::{TimingInfo, TimingVisitor}; use crate::toolchain::Toolchain; use crate::utils; use rustwide::logging::{self, LogStorage}; @@ -25,6 +26,18 @@ pub trait RecordProgress: Send + Sync { result: &TestResult, version: Option<(&Crate, &Crate)>, ) -> Fallible<()>; + + /// Records build timing data for a single crate on one toolchain. + /// Default implementation is a no-op (used by AgentApi). + fn record_timings( + &self, + _ex: &Experiment, + _krate: &Crate, + _toolchain: &Toolchain, + _timings: &[TimingInfo], + ) -> Fallible<()> { + Ok(()) + } } impl RecordProgress for AgentApi { @@ -49,6 +62,7 @@ pub(super) struct Worker<'a> { config: &'a crate::config::Config, api: &'a dyn RecordProgress, next_crate: &'a (dyn Fn() -> Fallible> + Send + Sync), + is_agent_mode: bool, // Called by the worker thread between crates, when no global state (namely caches) is in use. pub(super) between_crates: OnceLock>, @@ -62,6 +76,7 @@ impl<'a> Worker<'a> { config: &'a crate::config::Config, api: &'a dyn RecordProgress, next_crate: &'a (dyn Fn() -> Fallible> + Send + Sync), + is_agent_mode: bool, ) -> Self { let mut build_dir = HashMap::new(); build_dir.insert( @@ -80,6 +95,7 @@ impl<'a> Worker<'a> { config, next_crate, api, + is_agent_mode, between_crates: OnceLock::new(), } @@ -92,6 +108,7 @@ impl<'a> Worker<'a> { fn run_task( &self, task: &Task, + timing_visitor: &mut dyn TimingVisitor, storage: &LogStorage, ) -> Result { info!("running task: {task:?}"); @@ -102,7 +119,13 @@ impl<'a> Worker<'a> { // If we're running a task, we call ourselves healthy. crate::agent::set_healthy(); - match task.run(self.config, &self.build_dir, self.ex, storage) { + match task.run( + self.config, + &self.build_dir, + self.ex, + timing_visitor, + storage, + ) { Ok(res) => return Ok(res), Err(e) => { res = Some(e); @@ -276,6 +299,9 @@ impl<'a> Worker<'a> { } for tc in &self.ex.toolchains { + let mut visitor = + crate::timings::create_visitor(self.config, tc, self.is_agent_mode); + let quiet = self.config.is_quiet(&krate); let task = Task { krate: krate.clone(), @@ -320,8 +346,24 @@ impl<'a> Worker<'a> { // Fork logs off to distinct branch, so that each toolchain has its own log file, // while keeping the shared prepare step in common. let storage = logs.duplicate(); - match self.run_task(&task, &storage) { + match self.run_task(&task, &mut *visitor, &storage) { Ok(res) => { + let timing_results = visitor.take_results(); + if !timing_results.is_empty() { + info!( + "captured {} timing entries for {} on {}", + timing_results.len(), + krate, + tc + ); + if let Err(e) = + self.api + .record_timings(self.ex, &task.krate, tc, &timing_results) + { + warn!("failed to store timing data: {e:?}"); + } + } + self.api.record_progress( self.ex, &task.krate, diff --git a/src/timings.rs b/src/timings.rs new file mode 100644 index 00000000..88c3a010 --- /dev/null +++ b/src/timings.rs @@ -0,0 +1,200 @@ +//! Build timing capture from cargo's `--timings` JSON output. +//! +//! When enabled, the runner adds `--timings` to cargo invocations on nightly +//! toolchains and routes `"reason":"timing-info"` JSON messages through a +//! [`TimingVisitor`]. The [`CollectingTimingVisitor`] accumulates entries in +//! memory; the [`NoopTimingVisitor`] does nothing at close to zero cost. + +use std::mem; + +use crate::config::Config; +use crate::prelude::*; +use crate::toolchain::Toolchain; + +/// A single timing record emitted by cargo for one compilation unit. +/// +/// Cargo produces these as JSON lines with `"reason":"timing-info"` when +/// invoked with `--timings --message-format=json`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimingInfo { + pub package_id: String, + pub target: TimingTarget, + pub mode: String, + pub duration: f64, + pub rmeta_time: Option, +} + +/// The target (lib, bin, etc.) that was compiled. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimingTarget { + pub kind: Vec, + pub crate_types: Vec, + pub name: String, + pub src_path: String, + pub edition: String, + pub doc: bool, + pub doctest: bool, + pub test: bool, +} + +/// Visitor trait for processing timing data emitted by cargo. +pub trait TimingVisitor { + /// Called for each `timing-info` message parsed from cargo output. + fn visit_timing(&mut self, timing: TimingInfo); + + /// Drains and returns all collected timing entries. + fn take_results(&mut self) -> Vec; + + /// Returns `true` when this visitor is actively capturing (controls + /// whether `--timings` is appended to cargo args). + fn is_capturing(&self) -> bool; +} + +/// No-op visitor used when capture is disabled or in agent mode. +/// Each method is an empty body — close to zero cost (only the dynamic dispatch overhead). +pub struct NoopTimingVisitor; + +impl TimingVisitor for NoopTimingVisitor { + fn visit_timing(&mut self, _timing: TimingInfo) {} + + fn take_results(&mut self) -> Vec { + Vec::new() + } + + fn is_capturing(&self) -> bool { + false + } +} + +/// Collects timing entries into a `Vec`. +#[derive(Default)] +pub struct CollectingTimingVisitor { + timings: Vec, +} + +impl CollectingTimingVisitor { + pub fn new() -> Self { + Self::default() + } +} + +impl TimingVisitor for CollectingTimingVisitor { + fn visit_timing(&mut self, timing: TimingInfo) { + self.timings.push(timing); + } + + fn take_results(&mut self) -> Vec { + mem::take(&mut self.timings) + } + + fn is_capturing(&self) -> bool { + true + } +} + +/// Decides whether to capture timings for the given context. +pub fn should_capture_timings(config: &Config, toolchain: &Toolchain, is_agent_mode: bool) -> bool { + config.capture_timings // Only if it's enabled + && toolchain.is_nightly() // Required for --timings + && !is_agent_mode // TODO: remove this when implemented for agent mode +} + +/// Creates the appropriate visitor based on config, toolchain, and mode. +pub fn create_visitor( + config: &Config, + toolchain: &Toolchain, + is_agent_mode: bool, +) -> Box { + if should_capture_timings(config, toolchain, is_agent_mode) { + Box::new(CollectingTimingVisitor::new()) + } else { + Box::new(NoopTimingVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_timing_info() { + let json = r#"{ + "reason": "timing-info", + "package_id": "serde 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "target": { + "kind": ["lib"], + "crate_types": ["lib"], + "name": "serde", + "src_path": "/path/to/src/lib.rs", + "edition": "2021", + "doc": true, + "doctest": true, + "test": true + }, + "mode": "build", + "duration": 12.5, + "rmeta_time": 8.3 + }"#; + + let timing: TimingInfo = serde_json::from_str(json).unwrap(); + assert_eq!( + timing.package_id, + "serde 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" + ); + assert_eq!(timing.target.name, "serde"); + assert_eq!(timing.target.kind, vec!["lib"]); + assert_eq!(timing.mode, "build"); + assert!((timing.duration - 12.5).abs() < f64::EPSILON); + assert!((timing.rmeta_time.unwrap() - 8.3).abs() < f64::EPSILON); + } + + #[test] + fn test_deserialize_timing_info_no_rmeta() { + let json = r#"{ + "package_id": "foo 0.1.0", + "target": { + "kind": ["bin"], + "crate_types": ["bin"], + "name": "foo", + "src_path": "/path/to/main.rs", + "edition": "2021", + "doc": false, + "doctest": false, + "test": false + }, + "mode": "build", + "duration": 1.0, + "rmeta_time": null + }"#; + + let timing: TimingInfo = serde_json::from_str(json).unwrap(); + assert!(timing.rmeta_time.is_none()); + } + + #[test] + fn test_should_capture_timings() { + use std::str::FromStr; + + let mut config = Config::default(); + + let nightly = Toolchain::from_str("nightly-2024-01-01").unwrap(); + let stable = Toolchain::from_str("stable").unwrap(); + let ci = Toolchain::from_str("try#0000000000000000000000000000000000000000").unwrap(); + + // Disabled by default + assert!(!should_capture_timings(&config, &nightly, false)); + + // Enabled config + nightly + local = true + config.capture_timings = true; + assert!(should_capture_timings(&config, &nightly, false)); + + // Enabled config + stable + local = false (not nightly) + assert!(!should_capture_timings(&config, &stable, false)); + + // Enabled config + nightly + agent = false (agent mode) + assert!(!should_capture_timings(&config, &nightly, true)); + + // Enabled config + CI build + local = true (CI uses nightly) + assert!(should_capture_timings(&config, &ci, false)); + } +} diff --git a/src/toolchain.rs b/src/toolchain.rs index 67d6e0a2..0b477c92 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -47,6 +47,16 @@ impl Toolchain { encode(&self.to_string(), &utils::FILENAME_ENCODE_SET).to_string() } + + /// Returns `true` if this is a nightly distribution or a CI (try/master) build. + pub fn is_nightly(&self) -> bool { + if let Some(dist) = self.source.as_dist() { + dist.name().starts_with("nightly") + } else { + // CI builds (try#sha, master#sha) use nightly compilers. + self.source.as_ci().is_some() + } + } } impl std::ops::Deref for Toolchain { @@ -353,6 +363,23 @@ mod tests { }, }; + // Test is_nightly + assert!(!Toolchain::from_str("stable").unwrap().is_nightly()); + assert!(!Toolchain::from_str("beta-1970-01-01").unwrap().is_nightly()); + assert!(Toolchain::from_str("nightly-1970-01-01") + .unwrap() + .is_nightly()); + assert!( + Toolchain::from_str("try#0000000000000000000000000000000000000000") + .unwrap() + .is_nightly() + ); + assert!( + Toolchain::from_str("master#0000000000000000000000000000000000000000") + .unwrap() + .is_nightly() + ); + // Test invalid reprs assert!(Toolchain::from_str("").is_err()); assert!(Toolchain::from_str("master#").is_err());