diff --git a/Cargo.lock b/Cargo.lock index 3311e5e43d..ec039e103c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1887,6 +1887,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", + "futures", "is-terminal", "itertools", "num-traits", @@ -11407,6 +11408,7 @@ dependencies = [ "blake3", "bytesize", "clap", + "criterion", "derive_more", "event-listener-primitives", "fdlimit", diff --git a/crates/subspace-farmer/Cargo.toml b/crates/subspace-farmer/Cargo.toml index c45f099fb8..f313b99b8e 100644 --- a/crates/subspace-farmer/Cargo.toml +++ b/crates/subspace-farmer/Cargo.toml @@ -21,6 +21,7 @@ blake2 = "0.10.6" blake3 = { version = "1.4.1", default-features = false } bytesize = "1.3.0" clap = { version = "4.4.3", features = ["color", "derive"] } +criterion = { version = "0.5.1", default-features = false, features = ["rayon", "async"] } derive_more = "0.99.17" event-listener-primitives = "2.0.1" fdlimit = "0.2" diff --git a/crates/subspace-farmer/README.md b/crates/subspace-farmer/README.md index 7896b85435..ff25e36f10 100644 --- a/crates/subspace-farmer/README.md +++ b/crates/subspace-farmer/README.md @@ -62,6 +62,11 @@ This will connect to local node and will try to solve on every slot notification *NOTE: You need to have a `subspace-node` running before starting farmer, otherwise it will not be able to start* +### Benchmark auditing +``` +target/production/subspace-farmer benchmark audit /path/to/farm +``` + ### Show information about the farm ``` target/production/subspace-farmer info /path/to/farm diff --git a/crates/subspace-farmer/src/bin/subspace-farmer/commands.rs b/crates/subspace-farmer/src/bin/subspace-farmer/commands.rs index 831da88e64..5f5ad44665 100644 --- a/crates/subspace-farmer/src/bin/subspace-farmer/commands.rs +++ b/crates/subspace-farmer/src/bin/subspace-farmer/commands.rs @@ -1,3 +1,4 @@ +pub(crate) mod benchmark; pub(crate) mod farm; mod info; mod scrub; diff --git a/crates/subspace-farmer/src/bin/subspace-farmer/commands/benchmark.rs b/crates/subspace-farmer/src/bin/subspace-farmer/commands/benchmark.rs new file mode 100644 index 0000000000..52ed3a1836 --- /dev/null +++ b/crates/subspace-farmer/src/bin/subspace-farmer/commands/benchmark.rs @@ -0,0 +1,140 @@ +use crate::PosTable; +use anyhow::anyhow; +use clap::Subcommand; +use criterion::async_executor::AsyncExecutor; +use criterion::{black_box, BatchSize, Criterion, Throughput}; +#[cfg(windows)] +use memmap2::Mmap; +use parking_lot::Mutex; +use std::fs::OpenOptions; +use std::future::Future; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use subspace_core_primitives::crypto::kzg::{embedded_kzg_settings, Kzg}; +use subspace_core_primitives::{Record, SolutionRange}; +use subspace_erasure_coding::ErasureCoding; +use subspace_farmer::single_disk_farm::farming::{plot_audit, PlotAuditOptions}; +use subspace_farmer::single_disk_farm::{SingleDiskFarm, SingleDiskFarmSummary}; +use subspace_farmer_components::sector::sector_size; +use subspace_proof_of_space::Table; +use subspace_rpc_primitives::SlotInfo; +use tokio::runtime::Handle; + +struct TokioAsyncExecutor(Handle); + +impl AsyncExecutor for TokioAsyncExecutor { + fn block_on(&self, future: impl Future) -> T { + tokio::task::block_in_place(|| self.0.block_on(future)) + } +} + +impl TokioAsyncExecutor { + fn new() -> Self { + Self(Handle::current()) + } +} + +/// Arguments for benchmark +#[derive(Debug, Subcommand)] +pub(crate) enum BenchmarkArgs { + /// Audit benchmark + Audit { + /// Disk farm to audit + /// + /// Example: + /// /path/to/directory + disk_farm: PathBuf, + #[arg(long, default_value_t = 10)] + sample_size: usize, + }, +} + +pub(crate) async fn benchmark(benchmark_args: BenchmarkArgs) -> anyhow::Result<()> { + match benchmark_args { + BenchmarkArgs::Audit { + disk_farm, + sample_size, + } => audit(disk_farm, sample_size).await, + } +} + +async fn audit(disk_farm: PathBuf, sample_size: usize) -> anyhow::Result<()> { + let (single_disk_farm_info, disk_farm) = match SingleDiskFarm::collect_summary(disk_farm) { + SingleDiskFarmSummary::Found { info, directory } => (info, directory), + SingleDiskFarmSummary::NotFound { directory } => { + return Err(anyhow!( + "No single disk farm info found, make sure {} is a valid path to the farm and \ + process have permissions to access it", + directory.display() + )); + } + SingleDiskFarmSummary::Error { directory, error } => { + return Err(anyhow!( + "Failed to open single disk farm info, make sure {} is a valid path to the farm \ + and process have permissions to access it: {error}", + directory.display() + )); + } + }; + + let sector_size = sector_size(single_disk_farm_info.pieces_in_sector()); + let kzg = Kzg::new(embedded_kzg_settings()); + let erasure_coding = ErasureCoding::new( + NonZeroUsize::new(Record::NUM_S_BUCKETS.next_power_of_two().ilog2() as usize).unwrap(), + ) + .map_err(|error| anyhow::anyhow!(error))?; + let table_generator = Mutex::new(PosTable::generator()); + + let sectors_metadata = SingleDiskFarm::read_all_sectors_metadata(&disk_farm) + .map_err(|error| anyhow::anyhow!("Failed to read sectors metadata: {error}"))?; + + let plot_file = OpenOptions::new() + .read(true) + .open(disk_farm.join(SingleDiskFarm::PLOT_FILE)) + .map_err(|error| anyhow::anyhow!("Failed to open single disk farm: {error}"))?; + #[cfg(windows)] + let plot_mmap = unsafe { Mmap::map(&plot_file)? }; + + let mut criterion = Criterion::default().sample_size(sample_size); + criterion + .benchmark_group("audit") + .throughput(Throughput::Bytes( + sector_size as u64 * sectors_metadata.len() as u64, + )) + .bench_function("plot", |b| { + b.to_async(TokioAsyncExecutor::new()).iter_batched( + rand::random, + |global_challenge| { + let options = PlotAuditOptions:: { + public_key: single_disk_farm_info.public_key(), + reward_address: single_disk_farm_info.public_key(), + sector_size, + slot_info: SlotInfo { + slot_number: 0, + global_challenge, + // No solution will be found, pure audit + solution_range: SolutionRange::MIN, + // No solution will be found, pure audit + voting_solution_range: SolutionRange::MIN, + }, + sectors_metadata: §ors_metadata, + kzg: &kzg, + erasure_coding: &erasure_coding, + #[cfg(not(windows))] + plot_file: &plot_file, + #[cfg(windows)] + plot_mmap: &plot_mmap, + maybe_sector_being_modified: None, + table_generator: &table_generator, + }; + + black_box(plot_audit(black_box(options))) + }, + BatchSize::SmallInput, + ) + }); + + criterion.final_summary(); + + Ok(()) +} diff --git a/crates/subspace-farmer/src/bin/subspace-farmer/main.rs b/crates/subspace-farmer/src/bin/subspace-farmer/main.rs index 6d5845a3a4..37c37ee362 100644 --- a/crates/subspace-farmer/src/bin/subspace-farmer/main.rs +++ b/crates/subspace-farmer/src/bin/subspace-farmer/main.rs @@ -24,6 +24,9 @@ type PosTable = ChiaTable; enum Command { /// Start a farmer, does plotting and farming Farm(commands::farm::FarmingArgs), + /// Run various benchmarks + #[clap(subcommand)] + Benchmark(commands::benchmark::BenchmarkArgs), /// Print information about farm and its content Info { /// One or more farm located at specified path. @@ -77,6 +80,9 @@ async fn main() -> anyhow::Result<()> { Command::Farm(farming_args) => { commands::farm::farm::(farming_args).await?; } + Command::Benchmark(benchmark_args) => { + commands::benchmark::benchmark(benchmark_args).await?; + } Command::Info { disk_farms } => { if disk_farms.is_empty() { info!("No farm was specified, so there is nothing to do"); diff --git a/crates/subspace-farmer/src/single_disk_farm.rs b/crates/subspace-farmer/src/single_disk_farm.rs index a48440b2d3..c26882eecb 100644 --- a/crates/subspace-farmer/src/single_disk_farm.rs +++ b/crates/subspace-farmer/src/single_disk_farm.rs @@ -560,8 +560,8 @@ impl Drop for SingleDiskFarm { } impl SingleDiskFarm { - const PLOT_FILE: &'static str = "plot.bin"; - const METADATA_FILE: &'static str = "metadata.bin"; + pub const PLOT_FILE: &'static str = "plot.bin"; + pub const METADATA_FILE: &'static str = "metadata.bin"; const SUPPORTED_PLOT_VERSION: u8 = 0; /// Create new single disk farm instance @@ -1137,6 +1137,60 @@ impl SingleDiskFarm { } } + /// Read all sectors metadata + pub fn read_all_sectors_metadata( + directory: &Path, + ) -> io::Result> { + let mut metadata_file = OpenOptions::new() + .read(true) + .open(directory.join(Self::METADATA_FILE))?; + + let metadata_size = metadata_file.seek(SeekFrom::End(0))?; + let sector_metadata_size = SectorMetadataChecksummed::encoded_size(); + + let mut metadata_header_bytes = vec![0; PlotMetadataHeader::encoded_size()]; + metadata_file.read_exact_at(&mut metadata_header_bytes, 0)?; + + let metadata_header = PlotMetadataHeader::decode(&mut metadata_header_bytes.as_ref()) + .map_err(|error| { + io::Error::new( + io::ErrorKind::Other, + format!("Failed to decode metadata header: {}", error), + ) + })?; + + if metadata_header.version != SingleDiskFarm::SUPPORTED_PLOT_VERSION { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Unsupported metadata version {}", metadata_header.version), + )); + } + + let mut sectors_metadata = Vec::::with_capacity( + ((metadata_size - RESERVED_PLOT_METADATA) / sector_metadata_size as u64) as usize, + ); + + let mut sector_metadata_bytes = vec![0; sector_metadata_size]; + for sector_index in 0..metadata_header.plotted_sector_count { + metadata_file.read_exact_at( + &mut sector_metadata_bytes, + RESERVED_PLOT_METADATA + sector_metadata_size as u64 * u64::from(sector_index), + )?; + sectors_metadata.push( + SectorMetadataChecksummed::decode(&mut sector_metadata_bytes.as_ref()).map_err( + |error| { + io::Error::new( + io::ErrorKind::Other, + format!("Failed to decode sector metadata: {}", error), + ) + }, + )?, + ); + } + + Ok(sectors_metadata) + } + /// ID of this farm pub fn id(&self) -> &SingleDiskFarmId { self.single_disk_farm_info.id() diff --git a/docs/farming.md b/docs/farming.md index fe2440afc4..73af7327f0 100644 --- a/docs/farming.md +++ b/docs/farming.md @@ -319,11 +319,12 @@ There are extra commands and parameters you can use on farmer or node, use the ` Below are some helpful samples: -- `./FARMER_FILE_NAME info PATH_TO_FARM` : show information about the farm at `PATH_TO_FARM` -- `./FARMER_FILE_NAME scrub PATH_TO_FARM` : Scrub the farm to find and fix farm at `PATH_TO_FARM` corruption -- `./FARMER_FILE_NAME wipe PATH_TO_FARM` : erases everything related to farmer if data were stored in `PATH_TO_FARM` -- `./NODE_FILE_NAME --base-path NODE_DATA_PATH --chain gemini-3f ...` : start node and store data in `NODE_DATA_PATH` instead of default location -- `./NODE_FILE_NAME purge-chain --base-path NODE_DATA_PATH --chain gemini-3f` : erases data related to the node if data were stored in `NODE_DATA_PATH` +- `./FARMER_FILE_NAME benchmark audit PATH_TO_FARM`: benchmark auditing performance of the farm at `PATH_TO_FARM` +- `./FARMER_FILE_NAME info PATH_TO_FARM`: show information about the farm at `PATH_TO_FARM` +- `./FARMER_FILE_NAME scrub PATH_TO_FARM`: Scrub the farm to find and fix farm at `PATH_TO_FARM` corruption +- `./FARMER_FILE_NAME wipe PATH_TO_FARM`: erases everything related to farmer if data were stored in `PATH_TO_FARM` +- `./NODE_FILE_NAME --base-path NODE_DATA_PATH --chain gemini-3f ...`: start node and store data in `NODE_DATA_PATH` instead of default location +- `./NODE_FILE_NAME purge-chain --base-path NODE_DATA_PATH --chain gemini-3f`: erases data related to the node if data were stored in `NODE_DATA_PATH` Examples: ```bash