Skip to content

Commit a0e346d

Browse files
committed
Add build timing capture from cargo's --timings JSON output
Introduces a timings module with a visitor pattern for optionally intercepting cargo timing-info messages during builds. Timings are captured only when capture_timings is enabled in config, the toolchain is nightly/CI, and running in local/CLI mode (not agent mode).
1 parent 6554554 commit a0e346d

File tree

13 files changed

+495
-10
lines changed

13 files changed

+495
-10
lines changed

src/agent/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ fn run_experiment(
177177
threads_count,
178178
&agent.config,
179179
&|| agent.next_crate(&ex.name),
180+
true,
180181
)
181182
.map_err(|err| (Some(Box::new(ex)), err))?;
182183
Ok(())

src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ impl Crater {
441441
threads,
442442
&config,
443443
&|| Ok(crates.lock().unwrap().pop()),
444+
false,
444445
);
445446
workspace.purge_all_build_dirs()?;
446447
res?;

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub struct Config {
8686
pub local_crates: HashMap<String, CrateConfig>,
8787
pub server: ServerConfig,
8888
pub sandbox: SandboxConfig,
89+
#[serde(default = "default_false")]
90+
pub capture_timings: bool,
8991
}
9092

9193
impl Config {
@@ -253,6 +255,7 @@ impl Default for Config {
253255
experiment_completed: "".into(),
254256
},
255257
},
258+
capture_timings: false,
256259
}
257260
}
258261
}

src/db/migrations.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,27 @@ fn migrations() -> Vec<(&'static str, MigrationKind)> {
363363
MigrationKind::SQL("alter table agents add column latest_work_for text;"),
364364
));
365365

366+
migrations.push((
367+
"create_build_timings_table",
368+
MigrationKind::SQL(
369+
"
370+
CREATE TABLE build_timings (
371+
experiment TEXT NOT NULL,
372+
crate TEXT NOT NULL,
373+
toolchain TEXT NOT NULL,
374+
package_id TEXT NOT NULL,
375+
target_name TEXT NOT NULL,
376+
target_kind TEXT NOT NULL,
377+
mode TEXT NOT NULL,
378+
duration REAL NOT NULL,
379+
rmeta_time REAL,
380+
FOREIGN KEY (experiment) REFERENCES experiments(name) ON DELETE CASCADE
381+
);
382+
CREATE INDEX build_timings__experiment ON build_timings (experiment);
383+
",
384+
),
385+
));
386+
366387
migrations
367388
}
368389

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub mod report;
2727
pub mod results;
2828
pub mod runner;
2929
pub mod server;
30+
pub mod timings;
3031
pub mod toolchain;
3132

3233
pub(crate) static GIT_REVISION: Option<&str> = include!(concat!(env!("OUT_DIR"), "/sha"));

src/results/db.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::prelude::*;
55
use crate::results::{
66
DeleteResults, EncodedLog, EncodingType, ReadResults, TestResult, WriteResults,
77
};
8+
use crate::timings::TimingInfo;
89
use crate::toolchain::Toolchain;
910
use base64::Engine;
1011
use rustwide::logging::{self, LogStorage};
@@ -143,6 +144,69 @@ impl<'a> DatabaseDB<'a> {
143144
Ok(())
144145
}
145146

147+
pub fn store_timings(
148+
&self,
149+
ex: &Experiment,
150+
krate: &Crate,
151+
toolchain: &Toolchain,
152+
timings: &[TimingInfo],
153+
) -> Fallible<()> {
154+
for timing in timings {
155+
let target_kind = timing.target.kind.join(",");
156+
self.db.execute_cached(
157+
"INSERT INTO build_timings \
158+
(experiment, crate, toolchain, package_id, target_name, target_kind, mode, duration, rmeta_time) \
159+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);",
160+
&[
161+
&ex.name as &dyn rusqlite::types::ToSql,
162+
&krate.id(),
163+
&toolchain.to_string(),
164+
&timing.package_id,
165+
&timing.target.name,
166+
&target_kind,
167+
&timing.mode,
168+
&timing.duration,
169+
&timing.rmeta_time,
170+
],
171+
)?;
172+
}
173+
Ok(())
174+
}
175+
176+
pub fn load_timings(
177+
&self,
178+
ex: &Experiment,
179+
krate: &Crate,
180+
toolchain: &Toolchain,
181+
) -> Fallible<Vec<TimingInfo>> {
182+
self.db.query(
183+
"SELECT package_id, target_name, target_kind, mode, duration, rmeta_time \
184+
FROM build_timings \
185+
WHERE experiment = ?1 AND crate = ?2 AND toolchain = ?3;",
186+
[&ex.name, &krate.id(), &toolchain.to_string()],
187+
|row| {
188+
let target_kind: String = row.get("target_kind")?;
189+
let kinds: Vec<String> = target_kind.split(',').map(|s| s.to_string()).collect();
190+
Ok(TimingInfo {
191+
package_id: row.get("package_id")?,
192+
target: crate::timings::TimingTarget {
193+
kind: kinds,
194+
crate_types: Vec::new(),
195+
name: row.get("target_name")?,
196+
src_path: String::new(),
197+
edition: String::new(),
198+
doc: false,
199+
doctest: false,
200+
test: false,
201+
},
202+
mode: row.get("mode")?,
203+
duration: row.get("duration")?,
204+
rmeta_time: row.get("rmeta_time")?,
205+
})
206+
},
207+
)
208+
}
209+
146210
fn insert_into_results(
147211
&self,
148212
ex: &Experiment,
@@ -272,6 +336,16 @@ impl crate::runner::RecordProgress for DatabaseDB<'_> {
272336
}
273337
Ok(())
274338
}
339+
340+
fn record_timings(
341+
&self,
342+
ex: &Experiment,
343+
krate: &Crate,
344+
toolchain: &Toolchain,
345+
timings: &[TimingInfo],
346+
) -> Fallible<()> {
347+
self.store_timings(ex, krate, toolchain, timings)
348+
}
275349
}
276350

277351
impl DeleteResults for DatabaseDB<'_> {
@@ -468,6 +542,85 @@ mod tests {
468542
.is_none());
469543
}
470544

545+
#[test]
546+
fn test_store_and_load_timings() {
547+
let db = Database::temp().unwrap();
548+
let results = DatabaseDB::new(&db);
549+
let config = Config::default();
550+
let ctx = ActionsCtx::new(&db, &config);
551+
552+
crate::crates::lists::setup_test_lists(&db, &config).unwrap();
553+
554+
CreateExperiment::dummy("dummy").apply(&ctx).unwrap();
555+
let ex = Experiment::get(&db, "dummy").unwrap().unwrap();
556+
557+
let krate = Crate::Registry(RegistryCrate {
558+
name: "lazy_static".into(),
559+
version: "1".into(),
560+
});
561+
562+
let timings = vec![
563+
crate::timings::TimingInfo {
564+
package_id: "serde 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"
565+
.to_string(),
566+
target: crate::timings::TimingTarget {
567+
kind: vec!["lib".to_string()],
568+
crate_types: vec!["lib".to_string()],
569+
name: "serde".to_string(),
570+
src_path: "/path/to/src/lib.rs".to_string(),
571+
edition: "2021".to_string(),
572+
doc: true,
573+
doctest: true,
574+
test: true,
575+
},
576+
mode: "build".to_string(),
577+
duration: 12.5,
578+
rmeta_time: Some(8.3),
579+
},
580+
crate::timings::TimingInfo {
581+
package_id: "foo 0.1.0".to_string(),
582+
target: crate::timings::TimingTarget {
583+
kind: vec!["bin".to_string()],
584+
crate_types: vec!["bin".to_string()],
585+
name: "foo".to_string(),
586+
src_path: "/path/to/main.rs".to_string(),
587+
edition: "2021".to_string(),
588+
doc: false,
589+
doctest: false,
590+
test: false,
591+
},
592+
mode: "build".to_string(),
593+
duration: 1.0,
594+
rmeta_time: None,
595+
},
596+
];
597+
598+
results
599+
.store_timings(&ex, &krate, &MAIN_TOOLCHAIN, &timings)
600+
.unwrap();
601+
602+
let loaded = results.load_timings(&ex, &krate, &MAIN_TOOLCHAIN).unwrap();
603+
604+
assert_eq!(loaded.len(), 2);
605+
606+
assert_eq!(loaded[0].package_id, timings[0].package_id);
607+
assert_eq!(loaded[0].target.name, "serde");
608+
assert_eq!(loaded[0].target.kind, vec!["lib"]);
609+
assert_eq!(loaded[0].mode, "build");
610+
assert!((loaded[0].duration - 12.5).abs() < f64::EPSILON);
611+
assert!((loaded[0].rmeta_time.unwrap() - 8.3).abs() < f64::EPSILON);
612+
613+
assert_eq!(loaded[1].package_id, "foo 0.1.0");
614+
assert_eq!(loaded[1].target.name, "foo");
615+
assert_eq!(loaded[1].target.kind, vec!["bin"]);
616+
assert!((loaded[1].duration - 1.0).abs() < f64::EPSILON);
617+
assert!(loaded[1].rmeta_time.is_none());
618+
619+
// Different toolchain should return empty
620+
let empty = results.load_timings(&ex, &krate, &TEST_TOOLCHAIN).unwrap();
621+
assert!(empty.is_empty());
622+
}
623+
471624
#[test]
472625
fn test_store() {
473626
let db = Database::temp().unwrap();

src/runner/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub fn run_ex(
2929
threads_count: usize,
3030
config: &Config,
3131
next_crate: &(dyn Fn() -> Fallible<Option<Crate>> + Send + Sync),
32+
is_agent_mode: bool,
3233
) -> Fallible<()> {
3334
// Attempt to spin indefinitely until docker is up. Ideally, we would
3435
// decomission this agent until docker is up, instead of leaving the
@@ -89,6 +90,7 @@ pub fn run_ex(
8990
config,
9091
api,
9192
next_crate,
93+
is_agent_mode,
9294
)
9395
})
9496
.collect::<Vec<_>>();

src/runner/tasks.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::experiments::Experiment;
44
use crate::prelude::*;
55
use crate::results::TestResult;
66
use crate::runner::test;
7+
use crate::timings::TimingVisitor;
78
use crate::toolchain::Toolchain;
89
use rustwide::{Build, BuildDirectory};
910
use std::collections::HashMap;
@@ -91,12 +92,13 @@ impl Task {
9192
config: &'ctx Config,
9293
build_dir: &'ctx HashMap<&'ctx crate::toolchain::Toolchain, Mutex<BuildDirectory>>,
9394
ex: &'ctx Experiment,
95+
timing_visitor: &'ctx mut dyn TimingVisitor,
9496
logs: &LogStorage,
9597
) -> Fallible<TestResult> {
9698
let (build_dir, action, test, toolchain, quiet): (
9799
_,
98100
_,
99-
fn(&TaskCtx, &Build, &_) -> _,
101+
fn(&TaskCtx, &Build, &_, &mut dyn TimingVisitor) -> _,
100102
_,
101103
_,
102104
) = match self.step {
@@ -130,6 +132,6 @@ impl Task {
130132
};
131133

132134
let ctx = TaskCtx::new(build_dir, config, ex, toolchain, &self.krate, quiet);
133-
test::run_test(action, &ctx, test, logs)
135+
test::run_test(action, &ctx, test, timing_visitor, logs)
134136
}
135137
}

0 commit comments

Comments
 (0)