Skip to content

Commit b2feb58

Browse files
feat: allow caching installed executor instruments on ubuntu/debian
1 parent 2ee3731 commit b2feb58

13 files changed

Lines changed: 217 additions & 44 deletions

File tree

src/app.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::PathBuf;
2+
13
use crate::{
24
api_client::CodSpeedAPIClient,
35
auth,
@@ -38,6 +40,13 @@ pub struct Cli {
3840
#[arg(long, env = "CODSPEED_OAUTH_TOKEN", global = true, hide = true)]
3941
pub oauth_token: Option<String>,
4042

43+
/// The directory to use for caching installed tools
44+
/// The runner will restore cached tools from this directory before installing them.
45+
/// After successful installation, the runner will cache the installed tools to this directory.
46+
/// Only supported on ubuntu and debian systems.
47+
#[arg(long, env = "CODSPEED_SETUP_CACHE_DIR", global = true)]
48+
pub setup_cache_dir: Option<PathBuf>,
49+
4150
#[command(subcommand)]
4251
command: Commands,
4352
}
@@ -56,6 +65,7 @@ pub async fn run() -> Result<()> {
5665
let cli = Cli::parse();
5766
let codspeed_config = CodSpeedConfig::load_with_override(cli.oauth_token.as_deref())?;
5867
let api_client = CodSpeedAPIClient::try_from((&cli, &codspeed_config))?;
68+
let setup_cache_dir = cli.setup_cache_dir.as_deref();
5969

6070
match cli.command {
6171
Commands::Run(_) => {} // Run is responsible for its own logger initialization
@@ -65,9 +75,11 @@ pub async fn run() -> Result<()> {
6575
}
6676

6777
match cli.command {
68-
Commands::Run(args) => run::run(args, &api_client, &codspeed_config).await?,
78+
Commands::Run(args) => {
79+
run::run(args, &api_client, &codspeed_config, setup_cache_dir).await?
80+
}
6981
Commands::Auth(args) => auth::run(args, &api_client).await?,
70-
Commands::Setup => setup::setup().await?,
82+
Commands::Setup => setup::setup(setup_cache_dir).await?,
7183
}
7284
Ok(())
7385
}

src/run/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use instruments::mongo_tracer::{MongoTracer, install_mongodb_tracer};
99
use run_environment::interfaces::{RepositoryProvider, RunEnvironment};
1010
use runner::get_run_data;
1111
use serde::Serialize;
12+
use std::path::Path;
1213
use std::path::PathBuf;
1314

1415
pub mod check_system;
@@ -176,6 +177,7 @@ pub async fn run(
176177
args: RunArgs,
177178
api_client: &CodSpeedAPIClient,
178179
codspeed_config: &CodSpeedConfig,
180+
setup_cache_dir: Option<&Path>,
179181
) -> Result<()> {
180182
let output_json = args.message_format == Some(MessageFormat::Json);
181183
let mut config = Config::try_from(args)?;
@@ -202,7 +204,7 @@ pub async fn run(
202204

203205
if !config.skip_setup {
204206
start_group!("Preparing the environment");
205-
executor.setup(&system_info).await?;
207+
executor.setup(&system_info, setup_cache_dir).await?;
206208
// TODO: refactor and move directly in the Instruments struct as a `setup` method
207209
if config.instruments.is_mongodb_enabled() {
208210
install_mongodb_tracer().await?;

src/run/runner/executor.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use crate::prelude::*;
33
use crate::run::instruments::mongo_tracer::MongoTracer;
44
use crate::run::{check_system::SystemInfo, config::Config};
55
use async_trait::async_trait;
6+
use std::path::Path;
67

78
#[async_trait(?Send)]
89
pub trait Executor {
910
fn name(&self) -> ExecutorName;
1011

11-
async fn setup(&self, _system_info: &SystemInfo) -> Result<()> {
12+
async fn setup(&self, _system_info: &SystemInfo, _setup_cache_dir: Option<&Path>) -> Result<()> {
1213
Ok(())
1314
}
1415

src/run/runner/helpers/apt.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use super::run_with_sudo::run_with_sudo;
2+
use crate::prelude::*;
3+
use crate::run::check_system::SystemInfo;
4+
use std::path::Path;
5+
use std::process::Command;
6+
7+
fn is_system_compatible(system_info: &SystemInfo) -> bool {
8+
system_info.os == "ubuntu" || system_info.os == "debian"
9+
}
10+
11+
pub fn install(system_info: &SystemInfo, packages: &[&str]) -> Result<()> {
12+
if !is_system_compatible(system_info) {
13+
bail!(
14+
"Package installation is not supported on this system, please install necessary packages manually"
15+
);
16+
}
17+
18+
debug!("Installing packages: {packages:?}");
19+
20+
run_with_sudo(&["apt-get", "update"])?;
21+
let mut install_cmd = vec![
22+
"apt-get",
23+
"install",
24+
"-y",
25+
"--allow-downgrades",
26+
"--reinstall",
27+
];
28+
install_cmd.extend_from_slice(packages);
29+
run_with_sudo(&install_cmd)?;
30+
31+
info!("Packages installed successfully");
32+
Ok(())
33+
}
34+
35+
/// Restore cached tools from the cache directory to the root filesystem
36+
pub fn restore_from_cache(system_info: &SystemInfo, cache_dir: &Path) -> Result<()> {
37+
if !is_system_compatible(system_info) {
38+
warn!("Cache restore is not supported on this system, skipping");
39+
return Ok(());
40+
}
41+
42+
if !cache_dir.exists() {
43+
debug!("Cache directory does not exist: {}", cache_dir.display());
44+
return Ok(());
45+
}
46+
47+
// Check if the directory has any contents
48+
let has_contents = std::fs::read_dir(cache_dir)
49+
.map(|mut entries| entries.next().is_some())
50+
.unwrap_or(false);
51+
52+
if !has_contents {
53+
debug!("Cache directory is empty: {}", cache_dir.display());
54+
return Ok(());
55+
}
56+
57+
info!(
58+
"Restoring tools from cache directory: {}",
59+
cache_dir.display()
60+
);
61+
62+
// Use bash to properly handle glob expansion
63+
let cache_dir_str = cache_dir
64+
.to_str()
65+
.ok_or_else(|| anyhow!("Invalid cache directory path"))?;
66+
67+
let copy_cmd = format!("cp -r {cache_dir_str}/* /");
68+
69+
let output = Command::new("sudo")
70+
.args(["bash", "-c", &copy_cmd])
71+
.output()
72+
.context("Failed to run cache restore command")?;
73+
74+
if !output.status.success() {
75+
let stderr = String::from_utf8_lossy(&output.stderr);
76+
error!("stderr: {stderr}");
77+
bail!("Failed to restore cache");
78+
}
79+
80+
info!("Cache restored successfully");
81+
Ok(())
82+
}
83+
84+
/// Save installed packages to the cache directory
85+
pub fn save_to_cache(system_info: &SystemInfo, cache_dir: &Path, packages: &[&str]) -> Result<()> {
86+
if !is_system_compatible(system_info) {
87+
warn!("Caching of installed package is not supported on this system, skipping");
88+
return Ok(());
89+
}
90+
91+
info!(
92+
"Saving installed packages to cache: {}",
93+
cache_dir.display()
94+
);
95+
96+
// Create cache directory if it doesn't exist
97+
std::fs::create_dir_all(cache_dir).context("Failed to create cache directory")?;
98+
99+
let cache_dir_str = cache_dir
100+
.to_str()
101+
.ok_or_else(|| anyhow!("Invalid cache directory path"))?;
102+
103+
// Logic taken from https://stackoverflow.com/a/59277514
104+
// This shell command lists all the files outputted by the given packages and copy them to the cache directory
105+
let packages_str = packages.join(" ");
106+
let shell_cmd = format!(
107+
"sudo dpkg -L {packages_str} | while IFS= read -r f; do if test -f \"$f\"; then echo \"$f\"; fi; done | xargs cp --parents --target-directory {cache_dir_str}",
108+
);
109+
110+
debug!("Running cache save command: {shell_cmd}");
111+
112+
let output = Command::new("sh")
113+
.arg("-c")
114+
.arg(&shell_cmd)
115+
.output()
116+
.context("Failed to execute cache save command")?;
117+
118+
if !output.status.success() {
119+
let stderr = String::from_utf8_lossy(&output.stderr);
120+
error!("stderr: {stderr}");
121+
bail!("Failed to save packages to cache");
122+
}
123+
124+
info!("Packages cached successfully");
125+
Ok(())
126+
}

src/run/runner/helpers/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
pub mod apt;
12
pub mod env;
23
pub mod get_bench_command;
34
pub mod introspected_golang;
45
pub mod introspected_nodejs;
56
pub mod profile_folder;
67
pub mod run_command_with_log_pipe;
7-
pub mod setup;
8+
pub mod run_with_sudo;
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::prelude::*;
2-
use log::{debug, info};
32
use std::process::{Command, Stdio};
43

54
/// Run a command with sudo if available

src/run/runner/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ mod valgrind {
139139
.get_or_init(|| async {
140140
let executor = ValgrindExecutor;
141141
let system_info = SystemInfo::new().unwrap();
142-
executor.setup(&system_info).await.unwrap();
142+
executor.setup(&system_info, None).await.unwrap();
143143
executor
144144
})
145145
.await
@@ -196,7 +196,7 @@ mod walltime {
196196
.get_or_init(|| async {
197197
let executor = WallTimeExecutor::new();
198198
let system_info = SystemInfo::new().unwrap();
199-
executor.setup(&system_info).await.unwrap();
199+
executor.setup(&system_info, None).await.unwrap();
200200
})
201201
.await;
202202

src/run/runner/valgrind/executor.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use async_trait::async_trait;
2+
use std::path::Path;
23

34
use crate::prelude::*;
45
use crate::run::instruments::mongo_tracer::MongoTracer;
@@ -17,8 +18,8 @@ impl Executor for ValgrindExecutor {
1718
ExecutorName::Valgrind
1819
}
1920

20-
async fn setup(&self, system_info: &SystemInfo) -> Result<()> {
21-
install_valgrind(system_info).await?;
21+
async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> {
22+
install_valgrind(system_info, setup_cache_dir).await?;
2223

2324
if let Err(error) = venv_compat::symlink_libpython(None) {
2425
warn!("Failed to symlink libpython");

src/run/runner/valgrind/setup.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::run::runner::helpers::setup::run_with_sudo;
1+
use crate::run::runner::helpers::apt;
22
use crate::{VALGRIND_CODSPEED_DEB_VERSION, run::check_system::SystemInfo};
33
use crate::{VALGRIND_CODSPEED_VERSION, prelude::*, run::helpers::download_file};
4-
use std::{env, process::Command};
4+
use std::{env, path::Path, process::Command};
55
use url::Url;
66

77
fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result<String> {
@@ -54,11 +54,25 @@ fn is_valgrind_installed() -> bool {
5454
}
5555
}
5656

57-
pub async fn install_valgrind(system_info: &SystemInfo) -> Result<()> {
57+
pub async fn install_valgrind(
58+
system_info: &SystemInfo,
59+
setup_cache_dir: Option<&Path>,
60+
) -> Result<()> {
5861
if is_valgrind_installed() {
5962
info!("Valgrind is already installed with the correct version, skipping installation");
6063
return Ok(());
6164
}
65+
66+
// Try to restore from cache first
67+
if let Some(cache_dir) = setup_cache_dir {
68+
apt::restore_from_cache(system_info, cache_dir)?;
69+
70+
if is_valgrind_installed() {
71+
// Valgrind has been successfully restored from cache
72+
return Ok(());
73+
}
74+
}
75+
6276
debug!("Installing valgrind");
6377
let valgrind_deb_url = format!(
6478
"https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/{}/{}",
@@ -67,18 +81,15 @@ pub async fn install_valgrind(system_info: &SystemInfo) -> Result<()> {
6781
);
6882
let deb_path = env::temp_dir().join("valgrind-codspeed.deb");
6983
download_file(&Url::parse(valgrind_deb_url.as_str()).unwrap(), &deb_path).await?;
70-
71-
run_with_sudo(&["apt-get", "update"])?;
72-
run_with_sudo(&[
73-
"apt-get",
74-
"install",
75-
"--allow-downgrades",
76-
"-y",
77-
deb_path.to_str().unwrap(),
78-
])?;
84+
apt::install(system_info, &[deb_path.to_str().unwrap()])?;
7985

8086
info!("Valgrind installation completed successfully");
8187

88+
// Save to cache after successful installation
89+
if let Some(cache_dir) = setup_cache_dir {
90+
apt::save_to_cache(system_info, cache_dir, &["valgrind"])?;
91+
}
92+
8293
Ok(())
8394
}
8495

src/run/runner/wall_time/executor.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use crate::run::{check_system::SystemInfo, config::Config};
1313
use async_trait::async_trait;
1414
use std::fs::canonicalize;
1515
use std::io::Write;
16-
use std::path::{Path, PathBuf};
16+
use std::path::Path;
17+
use std::path::PathBuf;
1718
use std::process::Command;
1819
use tempfile::NamedTempFile;
1920

@@ -162,9 +163,9 @@ impl Executor for WallTimeExecutor {
162163
ExecutorName::WallTime
163164
}
164165

165-
async fn setup(&self, _system_info: &SystemInfo) -> Result<()> {
166+
async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> {
166167
if self.perf.is_some() {
167-
PerfRunner::setup_environment()?;
168+
PerfRunner::setup_environment(system_info, setup_cache_dir)?;
168169
}
169170

170171
Ok(())

0 commit comments

Comments
 (0)