aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/benches
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/benches')
-rw-r--r--crates/shirabe/benches/composer_repository.rs195
-rw-r--r--crates/shirabe/benches/metadata_minifier.rs98
-rw-r--r--crates/shirabe/benches/packagist_fixture.rs26
3 files changed, 319 insertions, 0 deletions
diff --git a/crates/shirabe/benches/composer_repository.rs b/crates/shirabe/benches/composer_repository.rs
new file mode 100644
index 00000000..7c8f05da
--- /dev/null
+++ b/crates/shirabe/benches/composer_repository.rs
@@ -0,0 +1,195 @@
+//! Benchmarks for `ComposerRepository::load_packages` against a v2 (metadata-url) repository.
+//!
+//! The responses are real packagist p2 files served through the HTTP downloader mock, so one
+//! iteration covers what `composer update` does per package: decode the response, expand the
+//! minified metadata, filter by constraint and stability, and build the accepted packages. The
+//! constraint decides how many versions survive that filter, which is what the lazy expansion is
+//! meant to exploit.
+
+use criterion::BatchSize;
+use criterion::BenchmarkId;
+use criterion::Criterion;
+use criterion::criterion_group;
+use criterion::criterion_main;
+use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::io::IOInterface;
+use shirabe::io::null_io::NullIO;
+use shirabe::json::json_file::JsonFile;
+use shirabe::package::base_package::STABILITY_STABLE;
+use shirabe::package::version::VersionParser;
+use shirabe::repository::composer_repository::ComposerRepository;
+use shirabe::util::http_downloader::{
+ HttpDownloader, HttpDownloaderMockExpectation, HttpDownloaderMockHandler,
+};
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::constraint::AnyConstraint;
+use tempfile::TempDir;
+
+#[path = "packagist_fixture.rs"]
+mod packagist_fixture;
+
+const PACKAGES: &[&str] = &["symfony/console", "laravel/framework"];
+
+/// Constraints spanning the acceptance rates a repository sees, from one no version satisfies to
+/// one that takes every version there is. Each case is labelled with how many packages survived,
+/// since that rate is what decides whether deferring the copy pays off.
+const CONSTRAINTS: &[(&str, &str)] = &[
+ ("none", "^99.0"),
+ ("major7", "^7.0"),
+ ("major6-up", ">=6.0"),
+ ("major4-up", ">=4.0"),
+ ("major2-up", ">=2.0"),
+ ("any", "*"),
+];
+
+const REPOSITORY_URL: &str = "https://example.org/packages.json";
+const METADATA_URL: &str = "https://example.org/p2/%package%.json";
+
+/// A root file with nothing but a metadata-url, which is what puts the repository on the lazy
+/// provider path `load_async_packages` serves.
+fn root_file() -> String {
+ let mut root: IndexMap<String, PhpMixed> = IndexMap::new();
+ root.insert("packages".to_string(), PhpMixed::Array(IndexMap::new()));
+ root.insert(
+ "metadata-url".to_string(),
+ PhpMixed::String(METADATA_URL.to_string()),
+ );
+ JsonFile::encode(&PhpMixed::Array(root)).expect("failed to encode the root file")
+}
+
+/// A config whose cache is read-only, so the measurement stays off the filesystem.
+fn config(home: &TempDir) -> Config {
+ let mut config = Config::new(true, None);
+ let mut settings: IndexMap<String, PhpMixed> = IndexMap::new();
+ settings.insert(
+ "home".to_string(),
+ PhpMixed::String(home.path().display().to_string()),
+ );
+ settings.insert("cache-read-only".to_string(), PhpMixed::Bool(true));
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ top.insert("config".to_string(), PhpMixed::Array(settings));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ config
+}
+
+/// A repository answering exactly two requests, in the order `load_packages` makes them: the root
+/// file, then the package's metadata.
+fn repository(package: &str, root: &str, metadata: &str, config: &Config) -> ComposerRepository {
+ let expectations = vec![
+ HttpDownloaderMockExpectation {
+ url: REPOSITORY_URL.to_string(),
+ options: None,
+ status: 200,
+ body: root.to_string(),
+ headers: vec![String::new()],
+ },
+ HttpDownloaderMockExpectation {
+ url: METADATA_URL.replace("%package%", package),
+ options: None,
+ status: 200,
+ body: metadata.to_string(),
+ headers: vec![String::new()],
+ },
+ ];
+
+ let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()));
+ let http_downloader = std::rc::Rc::new(std::cell::RefCell::new(HttpDownloader::__new_mock(
+ io.clone(),
+ std::rc::Rc::new(std::cell::RefCell::new(Config::new(false, None))),
+ )));
+ http_downloader.borrow_mut().__expects(
+ expectations,
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert(
+ "url".to_string(),
+ PhpMixed::String(REPOSITORY_URL.to_string()),
+ );
+
+ ComposerRepository::new(repo_config, io, config, http_downloader, None)
+ .expect("failed to build the repository")
+}
+
+fn load(
+ repository: &mut ComposerRepository,
+ package: &str,
+ constraint: &AnyConstraint,
+ acceptable_stabilities: &IndexMap<String, i64>,
+) -> usize {
+ let mut package_name_map: IndexMap<String, Option<AnyConstraint>> = IndexMap::new();
+ package_name_map.insert(package.to_string(), Some(constraint.clone()));
+ repository
+ .load_packages(
+ package_name_map,
+ acceptable_stabilities.clone(),
+ IndexMap::new(),
+ IndexMap::new(),
+ )
+ .expect("load_packages failed")
+ .packages
+ .len()
+}
+
+/// How many versions the p2 file carries, for the `<loaded>of<total>` labels.
+fn version_count(package: &str, metadata: &str) -> usize {
+ JsonFile::parse_json(Some(metadata), None)
+ .expect("invalid fixture JSON")
+ .as_array()
+ .and_then(|response| response.get("packages"))
+ .and_then(|v| v.as_array())
+ .and_then(|packages| packages.get(package))
+ .and_then(|v| v.as_list())
+ .expect("the fixture has no version list for the package")
+ .len()
+}
+
+fn bench_load_packages(c: &mut Criterion) {
+ let home = TempDir::new().expect("failed to create the config home");
+ let config = config(&home);
+ let root = root_file();
+ let version_parser = VersionParser::new();
+
+ let mut acceptable_stabilities: IndexMap<String, i64> = IndexMap::new();
+ acceptable_stabilities.insert("stable".to_string(), STABILITY_STABLE);
+
+ for package in PACKAGES {
+ let metadata = packagist_fixture::fetch(package);
+ let total = version_count(package, &metadata);
+ let mut group = c.benchmark_group(format!("load_packages/{package}"));
+
+ for (label, constraint) in CONSTRAINTS {
+ let constraint: AnyConstraint = version_parser
+ .parse_constraints(constraint)
+ .expect("failed to parse the constraint");
+
+ let mut probe = repository(package, &root, &metadata, &config);
+ let loaded = load(&mut probe, package, &constraint, &acceptable_stabilities);
+ let parameter = format!("{label}/{loaded}of{total}");
+
+ group.bench_function(BenchmarkId::from_parameter(parameter), |b| {
+ b.iter_batched(
+ || repository(package, &root, &metadata, &config),
+ |mut repository| {
+ load(
+ &mut repository,
+ package,
+ &constraint,
+ &acceptable_stabilities,
+ )
+ },
+ BatchSize::PerIteration,
+ );
+ });
+ }
+
+ group.finish();
+ }
+}
+
+criterion_group!(benches, bench_load_packages);
+criterion_main!(benches);
diff --git a/crates/shirabe/benches/metadata_minifier.rs b/crates/shirabe/benches/metadata_minifier.rs
new file mode 100644
index 00000000..dff37740
--- /dev/null
+++ b/crates/shirabe/benches/metadata_minifier.rs
@@ -0,0 +1,98 @@
+//! Benchmarks for expanding minified package metadata.
+//!
+//! The workload mirrors `ComposerRepository::load_async_packages`: every expanded version is
+//! inspected, but only the ones accepted by the constraint and stability filters are kept. The
+//! `keep` parameter is that acceptance rate, which is what decides how much of the expansion has
+//! to be copied out.
+
+use criterion::BatchSize;
+use criterion::BenchmarkId;
+use criterion::Criterion;
+use criterion::criterion_group;
+use criterion::criterion_main;
+use indexmap::IndexMap;
+use shirabe::json::json_file::JsonFile;
+use shirabe_metadata_minifier::MetadataMinifier;
+use shirabe_php_shim::PhpMixed;
+use std::hint::black_box;
+
+#[path = "packagist_fixture.rs"]
+mod packagist_fixture;
+
+/// Real packagist metadata, fetched on first run. `symfony/console` and `laravel/framework` are
+/// long-lived packages whose p2 files carry hundreds of versions each.
+const PACKAGES: &[&str] = &["monolog/monolog", "symfony/console", "laravel/framework"];
+
+/// Percentages of the expanded versions the caller keeps.
+const KEEP_PERCENTS: &[u32] = &[0, 10, 50, 100];
+
+/// The minified version list for `package`, in the shape `load_async_packages` hands to the
+/// minifier.
+fn minified_versions(package: &str) -> Vec<IndexMap<String, PhpMixed>> {
+ let response = JsonFile::parse_json(Some(&packagist_fixture::fetch(package)), None)
+ .expect("invalid fixture JSON");
+ let response = response.as_array().expect("the fixture is not an object");
+ assert_eq!(
+ response.get("minified").and_then(|v| v.as_string()),
+ Some("composer/2.0"),
+ "{package} is not served in the minified format",
+ );
+
+ response
+ .get("packages")
+ .and_then(|v| v.as_array())
+ .and_then(|packages| packages.get(package))
+ .and_then(|v| v.as_list())
+ .expect("the fixture has no version list for the package")
+ .iter()
+ .map(|version| {
+ version
+ .as_array()
+ .expect("a version is not an object")
+ .clone()
+ })
+ .collect()
+}
+
+/// Reads one field of every expanded version and keeps the newest `keep` of them, the way the
+/// repository keeps only the versions its filters accept.
+fn consume(
+ versions: Vec<IndexMap<String, PhpMixed>>,
+ keep: usize,
+) -> Vec<IndexMap<String, PhpMixed>> {
+ let expanded = MetadataMinifier::expand(versions);
+ let first_kept = expanded.len() - keep;
+ let mut kept: Vec<IndexMap<String, PhpMixed>> = Vec::with_capacity(keep);
+ for index in 0..expanded.len() {
+ black_box(expanded.version(index).get("version_normalized"));
+ if index >= first_kept {
+ kept.push(expanded.materialize(index));
+ }
+ }
+ kept
+}
+
+fn bench_expand(c: &mut Criterion) {
+ for package in PACKAGES {
+ let versions = minified_versions(package);
+ let mut group = c.benchmark_group(format!("expand/{package}"));
+
+ for keep_percent in KEEP_PERCENTS {
+ let keep = versions.len() * (*keep_percent as usize) / 100;
+ let parameter = format!("keep{keep_percent}%/{}of{}", keep, versions.len());
+
+ group.bench_function(BenchmarkId::from_parameter(parameter), |b| {
+ b.iter_batched(
+ || versions.clone(),
+ |versions| consume(versions, keep),
+ BatchSize::PerIteration,
+ );
+ });
+ }
+
+ group.finish();
+ }
+}
+
+criterion_group!(benches, bench_expand);
+criterion_main!(benches);
diff --git a/crates/shirabe/benches/packagist_fixture.rs b/crates/shirabe/benches/packagist_fixture.rs
new file mode 100644
index 00000000..86184879
--- /dev/null
+++ b/crates/shirabe/benches/packagist_fixture.rs
@@ -0,0 +1,26 @@
+//! Real packagist p2 metadata for the benchmarks, fetched on first run and cached under the
+//! target directory so later runs work offline.
+
+use std::path::PathBuf;
+
+pub fn fetch(package: &str) -> String {
+ let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("packagist-p2");
+ std::fs::create_dir_all(&dir).expect("failed to create the fixture directory");
+ let path = dir.join(format!("{}.json", package.replace('/', "-")));
+ if let Ok(fixture) = std::fs::read_to_string(&path) {
+ return fixture;
+ }
+
+ let url = format!("https://repo.packagist.org/p2/{package}.json");
+ let fixture = reqwest::blocking::get(&url)
+ .and_then(|response| response.error_for_status())
+ .and_then(|response| response.text())
+ .unwrap_or_else(|e| {
+ panic!(
+ "failed to fetch {url}: {e}; download it to {} by hand to run this benchmark offline",
+ path.display(),
+ )
+ });
+ std::fs::write(&path, &fixture).expect("failed to cache the fixture");
+ fixture
+}