1use crate::blocks::{Tipset, TipsetKey};
5use crate::cid_collections::CidHashSet;
6use crate::db::car::ManyCar;
7use crate::db::car::forest::DEFAULT_FOREST_CAR_FRAME_SIZE;
8use crate::ipld::{stream_chain, stream_graph};
9use crate::prelude::*;
10use crate::shim::clock::ChainEpoch;
11use crate::utils::db::car_stream::{CarBlock, CarStream};
12use crate::utils::encoding::extract_cids;
13use crate::utils::multihash::MultihashCode;
14use crate::utils::stream::par_buffer;
15use crate::{
16 chain::{
17 ChainEpochDelta,
18 index::{ChainIndex, ResolveNullTipset},
19 },
20 db::{Either, parity_db::ParityDb, parity_db_config::ParityDbConfig},
21};
22use clap::Subcommand;
23use futures::{StreamExt, TryStreamExt};
24use fvm_ipld_encoding::DAG_CBOR;
25use human_repr::HumanCount as _;
26use indicatif::{ProgressBar, ProgressStyle};
27use itertools::Itertools;
28use std::path::{Path, PathBuf};
29use std::sync::Arc;
30use std::time::Instant;
31use tokio::{
32 fs::File,
33 io::{AsyncWrite, AsyncWriteExt, BufReader},
34};
35
36#[derive(Debug, Clone, Copy, clap::ValueEnum)]
37pub enum DbType {
38 Parity,
39 ParityOpt,
40}
41
42#[derive(Debug, Subcommand)]
43pub enum BenchmarkCommands {
44 CarStreaming {
46 #[arg(required = true)]
48 snapshot_files: Vec<PathBuf>,
49 #[arg(long)]
51 inspect: bool,
52 },
53 GraphTraversal {
55 #[arg(required = true)]
57 snapshot_files: Vec<PathBuf>,
58 },
59 ForestEncoding {
61 snapshot_file: PathBuf,
63 #[arg(long, default_value_t = 3)]
64 compression_level: u16,
65 #[arg(long, default_value_t = DEFAULT_FOREST_CAR_FRAME_SIZE)]
67 frame_size: usize,
68 },
69 Export {
71 #[arg(required = true)]
73 snapshot_files: Vec<PathBuf>,
74 #[arg(long, default_value_t = 3)]
75 compression_level: u16,
76 #[arg(long, default_value_t = DEFAULT_FOREST_CAR_FRAME_SIZE)]
78 frame_size: usize,
79 #[arg(short, long)]
82 epoch: Option<ChainEpoch>,
83 #[arg(short, long, default_value_t = 2000)]
85 depth: ChainEpochDelta,
86 },
87 Blockstore {
89 #[arg(required = true)]
91 snapshot_file: PathBuf,
92 #[arg(long, default_value = "parity")]
93 db: DbType,
94 },
95}
96
97impl BenchmarkCommands {
98 pub async fn run(self) -> anyhow::Result<()> {
99 match self {
100 Self::CarStreaming {
101 snapshot_files,
102 inspect,
103 } => match inspect {
104 true => benchmark_car_streaming_inspect(snapshot_files).await,
105 false => benchmark_car_streaming(snapshot_files).await,
106 },
107 Self::GraphTraversal { snapshot_files } => {
108 benchmark_graph_traversal(snapshot_files).await
109 }
110 Self::ForestEncoding {
111 snapshot_file,
112 compression_level,
113 frame_size,
114 } => benchmark_forest_encoding(snapshot_file, compression_level, frame_size).await,
115 Self::Export {
116 snapshot_files,
117 compression_level,
118 frame_size,
119 epoch,
120 depth,
121 } => {
122 benchmark_exporting(snapshot_files, compression_level, frame_size, epoch, depth)
123 .await
124 }
125 Self::Blockstore { snapshot_file, db } => benchmark_blockstore(snapshot_file, db).await,
126 }
127 }
128}
129
130async fn benchmark_car_streaming(input: Vec<PathBuf>) -> anyhow::Result<()> {
133 let mut sink = indicatif_sink("traversed");
134
135 let mut s = Box::pin(
136 futures::stream::iter(input)
137 .then(File::open)
138 .map_ok(BufReader::new)
139 .and_then(CarStream::new)
140 .try_flatten(),
141 );
142 while let Some(block) = s.try_next().await? {
143 sink.write_all(&block.data).await?
144 }
145 Ok(())
146}
147
148async fn benchmark_car_streaming_inspect(input: Vec<PathBuf>) -> anyhow::Result<()> {
152 let mut sink = indicatif_sink("traversed");
153 let mut s = Box::pin(
154 futures::stream::iter(input)
155 .then(File::open)
156 .map_ok(BufReader::new)
157 .and_then(CarStream::new)
158 .try_flatten(),
159 );
160 while let Some(block) = s.try_next().await? {
161 let block: CarBlock = block;
162 if block.cid.codec() == DAG_CBOR {
163 let cid_vec = extract_cids(&block.data)?;
164 let _ = cid_vec.iter().unique().count();
165 }
166 sink.write_all(&block.data).await?
167 }
168 Ok(())
169}
170
171async fn benchmark_graph_traversal(input: Vec<PathBuf>) -> anyhow::Result<()> {
174 let store = open_store(input)?;
175 let heaviest = store.heaviest_tipset()?;
176
177 let mut sink = indicatif_sink("traversed");
178
179 let mut s = stream_graph(&store, heaviest.chain(&store), 0, CidHashSet::default());
180 while let Some(block) = s.try_next().await? {
181 sink.write_all(&block.data).await?
182 }
183
184 Ok(())
185}
186
187async fn benchmark_forest_encoding(
189 input: PathBuf,
190 compression_level: u16,
191 frame_size: usize,
192) -> anyhow::Result<()> {
193 let file = tokio::io::BufReader::new(File::open(&input).await?);
194
195 let mut block_stream = CarStream::new(file).await?;
196 let roots = std::mem::replace(
197 &mut block_stream.header_v1.roots,
198 nunny::vec![Default::default()],
199 );
200
201 let mut dest = indicatif_sink("encoded");
202 let (blocks, _drop_guard) = par_buffer(1024, block_stream.map_err(anyhow::Error::from));
203 let frames =
204 crate::db::car::forest::Encoder::compress_stream(frame_size, compression_level, blocks);
205 crate::db::car::forest::Encoder::write(&mut dest, roots, frames).await?;
206 dest.flush().await?;
207 Ok(())
208}
209
210async fn benchmark_exporting(
214 input: Vec<PathBuf>,
215 compression_level: u16,
216 frame_size: usize,
217 epoch: Option<ChainEpoch>,
218 depth: ChainEpochDelta,
219) -> anyhow::Result<()> {
220 let store = Arc::new(open_store(input)?);
221 let heaviest = store.heaviest_tipset()?;
222 let idx = ChainIndex::new(
223 store.shallow_clone(),
224 heaviest.genesis(store.shallow_clone()).await?,
225 );
226 let ts = idx
227 .load_required_tipset_by_height(
228 epoch.unwrap_or(heaviest.epoch()),
229 heaviest,
230 ResolveNullTipset::TakeOlder,
231 )
232 .await?;
233 let stateroot_lookup_limit = ts.epoch() - depth;
236
237 let mut dest = indicatif_sink("exported");
238
239 let blocks = stream_chain(
240 Arc::clone(&store),
241 ts.clone().chain_owned(Arc::clone(&store)),
242 stateroot_lookup_limit,
243 CidHashSet::default(),
244 );
245
246 let (blocks, _drop_guard) = par_buffer(1024, blocks.map_err(anyhow::Error::from));
247 let frames =
248 crate::db::car::forest::Encoder::compress_stream(frame_size, compression_level, blocks);
249 crate::db::car::forest::Encoder::write(&mut dest, ts.key().to_cids(), frames).await?;
250 dest.flush().await?;
251 Ok(())
252}
253
254async fn benchmark_blockstore(snapshot: PathBuf, db: DbType) -> anyhow::Result<()> {
255 let tmp_db_path = tempfile::tempdir()?;
256 let bs = open_blockstore(&db, tmp_db_path.path())?;
257 let head_tsk = benchmark_blockstore_import(&snapshot, &db, &bs, tmp_db_path.path()).await?;
258 benchmark_blockstore_traversal(&bs, &head_tsk).await?;
259 Ok(())
260}
261
262fn open_blockstore(db: &DbType, db_path: &Path) -> anyhow::Result<impl Blockstore> {
263 println!("temp db path: {}", db_path.display());
264 Ok(match db {
265 DbType::Parity => Either::Left(ParityDb::open(db_path, &ParityDbConfig::default())?),
266 DbType::ParityOpt => Either::Right(ParityDbOpt::open(db_path)?),
267 })
268}
269
270async fn benchmark_blockstore_import(
271 snapshot: &Path,
272 db: &DbType,
273 bs: &impl Blockstore,
274 bs_path: &Path,
275) -> anyhow::Result<TipsetKey> {
276 let mut car_stream = CarStream::new_from_path(snapshot).await?;
277 let head_tsk = car_stream.head_tipset_key();
278 println!("head tipset key: {head_tsk}");
279 println!("importing CAR into {db:?} blockstore...");
280 let start = Instant::now();
281 let mut n = 0;
282 while let Some(CarBlock { cid, data }) = car_stream.try_next().await? {
283 bs.put_keyed(&cid, &data)?;
284 n += 1;
285 }
286 let db_size = fs_extra::dir::get_size(bs_path).unwrap_or_default();
287 println!(
288 "imported {n} records into {db:?} blockstore(size={}), took {}",
289 db_size.human_count_bytes(),
290 humantime::format_duration(start.elapsed())
291 );
292 Ok(head_tsk)
293}
294
295async fn benchmark_blockstore_traversal(
296 bs: &impl Blockstore,
297 head_tsk: &TipsetKey,
298) -> anyhow::Result<()> {
299 println!("Traversing the chain...");
300 let head = Tipset::load_required(bs, head_tsk)?;
301 let mut sink = indicatif_sink("traversed");
302 let start = Instant::now();
303 let mut s = stream_graph(bs, head.chain(bs), 0, CidHashSet::default());
304 let mut n = 0;
305 while let Some(block) = s.try_next().await? {
306 sink.write_all(&block.data).await?;
307 n += 1;
308 }
309 println!(
310 "Traversed {n} records, took {}",
311 humantime::format_duration(start.elapsed())
312 );
313 Ok(())
314}
315
316fn indicatif_sink(task: &'static str) -> impl AsyncWrite {
318 let sink = tokio::io::sink();
319 let pb = ProgressBar::new_spinner()
320 .with_style(
321 ProgressStyle::with_template(
322 "{spinner} {prefix} {total_bytes} at {binary_bytes_per_sec} in {elapsed_precise}",
323 )
324 .expect("infallible"),
325 )
326 .with_prefix(task)
327 .with_finish(indicatif::ProgressFinish::AndLeave);
328 pb.enable_steady_tick(std::time::Duration::from_secs_f32(0.1));
329 pb.wrap_async_write(sink)
330}
331
332fn open_store(input: Vec<PathBuf>) -> anyhow::Result<ManyCar> {
336 let pb = indicatif::ProgressBar::new_spinner().with_style(
337 indicatif::ProgressStyle::with_template("{spinner} opening block store")
338 .expect("indicatif template must be valid"),
339 );
340 pb.enable_steady_tick(std::time::Duration::from_secs_f32(0.1));
341
342 let store = ManyCar::try_from(input).context("couldn't read input CAR file")?;
343
344 pb.finish_and_clear();
345
346 Ok(store)
347}
348
349struct ParityDbOpt {
350 db: parity_db::Db,
351}
352
353impl ParityDbOpt {
354 fn open(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
355 let opts = parity_db::Options {
356 path: path.into(),
357 sync_wal: true,
358 sync_data: true,
359 stats: false,
360 salt: None,
361 columns: vec![
362 parity_db::ColumnOptions {
363 preimage: true,
364 uniform: true,
365 compression: parity_db::CompressionType::Lz4,
366 ..Default::default()
367 },
368 parity_db::ColumnOptions {
369 preimage: true,
370 compression: parity_db::CompressionType::Lz4,
371 ..Default::default()
372 },
373 ],
374 compression_threshold: [(0, 128)].into_iter().collect(),
375 };
376 let db = parity_db::Db::open_or_create(&opts)?;
377 Ok(Self { db })
378 }
379}
380
381impl Blockstore for ParityDbOpt {
382 fn get(&self, k: &cid::Cid) -> anyhow::Result<Option<Vec<u8>>> {
383 Ok(if is_dag_cbor_blake2b256(k) {
384 self.db.get(0, k.hash().digest())?
385 } else {
386 self.db.get(1, &k.to_bytes())?
387 })
388 }
389
390 fn put_keyed(&self, k: &cid::Cid, block: &[u8]) -> anyhow::Result<()> {
391 if is_dag_cbor_blake2b256(k) {
392 self.db
393 .commit([(0, k.hash().digest(), Some(block.to_vec()))])?;
394 } else {
395 self.db.commit([(1, k.to_bytes(), Some(block.to_vec()))])?;
396 }
397 Ok(())
398 }
399}
400
401fn is_dag_cbor_blake2b256(cid: &Cid) -> bool {
402 cid.codec() == DAG_CBOR && cid.hash().code() == u64::from(MultihashCode::Blake2b256)
403}