Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The project follows a modular workspace architecture with clear separation of co

#### `xtask` (Development Automation)
- **Purpose**: Development task automation following cargo-xtask pattern
- **Role**: Build scripts and development utilities, including `cargo xtask archive-bench` — the archive segment benchmarks (store-level presets, node-level paired runs of two `dolos` binaries, dictionary tooling, report); see `xtask/archive-bench/README.md`
- **Role**: Build scripts and development utilities, including `cargo xtask perf` — storage and minibf performance experiments with shared measurement and reporting; see `xtask/perf/README.md`

## Dependency Flow

Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion benches/archive_backends.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Regression guards for the archive backend's three read-shape families.
//! Regression guards for logs, store-local indexes and compressed bodies.
//!
//! The store is populated with the synthetic content from
//! `dolos_testing::archive` and measured against a fixed key sample: block
//! location resolution and full block reads (`get_block_by_slot`), the
//! per-account reward point reads (`read_logs` per epoch), and the per-epoch
//! temporal-prefix scan (`iter_logs`).
//! The queries module adds populated-domain index lookups, valid compressed
//! bodies, a real Alonzo block and logs-only scale cases.
//!
//! These are *relative* regression guards on small tempdir populations.
//! Absolute numbers say nothing about production behavior — the
Expand All @@ -16,6 +18,9 @@ use dolos_core::{
};
use dolos_testing::archive::{populate_archive, ArchiveShape};

#[path = "archive_backends/queries.rs"]
mod queries;

/// The one namespace the log shapes exercise; the name is the real bulk
/// namespace so the bench reads like the node.
const NS: &str = "account-epochs";
Expand Down
182 changes: 182 additions & 0 deletions benches/archive_backends/queries.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use dolos_core::{ArchiveStore, Domain, StateStore};
use dolos_testing::{
performance::{ApiFixture, FixtureShape},
toy_domain::{FjallStores, ToyStores},
};
use pallas::ledger::{addresses::Address, traverse::MultiEraBlock};

fn fixture(blocks: usize) -> ApiFixture<FjallStores> {
ApiFixture::new(
FjallStores::open(),
FixtureShape {
blocks,
..Default::default()
},
)
.unwrap()
}

#[divan::bench(args = [16, 64], sample_count = 30)]
fn archive_address_tags(bencher: divan::Bencher, blocks: usize) {
let fixture = fixture(blocks);
let key = Address::from_bech32(&fixture.vectors.address)
.unwrap()
.to_vec();
let expected: Vec<_> = fixture
.vectors
.blocks
.iter()
.take(blocks)
.map(|block| block.slot)
.collect();
bencher.bench_local(|| {
let actual = fixture
.domain
.archive()
.slots_by_tag("address", &key, 0, u64::MAX)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(divan::black_box(actual), expected);
});
}

#[divan::bench(args = [16, 64], sample_count = 30)]
fn exact_transaction_lookup(bencher: divan::Bencher, blocks: usize) {
let fixture = fixture(blocks);
let keys: Vec<_> = fixture
.vectors
.blocks
.iter()
.take(blocks)
.map(|block| (hex::decode(&block.tx_hashes[0]).unwrap(), block.slot))
.collect();
bencher.bench_local(|| {
for (hash, slot) in &keys {
assert_eq!(
fixture
.domain
.archive()
.slot_by_tx_hash(divan::black_box(hash))
.unwrap(),
Some(*slot)
);
}
assert_eq!(
fixture
.domain
.archive()
.slot_by_tx_hash(&[0xff; 32])
.unwrap(),
None
);
});
}

#[divan::bench(args = [16, 64], sample_count = 30)]
fn state_address_tags(bencher: divan::Bencher, blocks: usize) {
let fixture = fixture(blocks);
let key = Address::from_bech32(&fixture.vectors.address)
.unwrap()
.to_vec();
bencher.bench_local(|| {
let actual = fixture
.domain
.state()
.utxos_by_tag("address", divan::black_box(&key))
.unwrap();
assert_eq!(actual.len(), blocks);
});
}

#[divan::bench(args = [16, 64], sample_count = 30)]
fn compressed_valid_block_reads(bencher: divan::Bencher, blocks: usize) {
let fixture = fixture(blocks);
bencher.bench_local(|| {
for (index, expected) in fixture.blocks.iter().enumerate().step_by(3) {
let slot = fixture.vectors.blocks[index].slot;
let body = fixture
.domain
.archive()
.get_block_by_slot(&slot)
.unwrap()
.unwrap();
assert_eq!(divan::black_box(body.as_slice()), expected.as_slice());
}
});
}

#[divan::bench(args = [16, 64], sample_count = 30)]
fn compressed_reverse_block_scan(bencher: divan::Bencher, blocks: usize) {
let fixture = fixture(blocks);
bencher.bench_local(|| {
let mut count = 0;
for (slot, body) in fixture
.domain
.archive()
.get_range(Some(1), Some(blocks as u64 + 1))
.unwrap()
.rev()
{
let block = MultiEraBlock::decode(&body).unwrap();
assert_eq!(block.slot(), slot);
divan::black_box(block.header().hash());
count += 1;
}
assert_eq!(count, blocks);
});
}

#[divan::bench(args = [1000, 10000], sample_count = 30)]
fn logs_only_scale(bencher: divan::Bencher, rows: u64) {
use dolos_testing::archive::{populate_archive, ArchiveShape};
let shape = ArchiveShape {
epochs: 4,
blocks_per_epoch: 0,
log_rows_per_epoch: rows,
slots_per_epoch: 432_000,
seed: 0,
};
let directory = tempfile::tempdir().unwrap();
let store = dolos_fjall::archive::ArchiveStore::open(
dolos_cardano::model::build_schema(),
directory.path(),
&Default::default(),
)
.unwrap();
populate_archive(&store, "account-epochs", &shape).unwrap();
assert!(store.get_tip().unwrap().is_none());
let range = shape.log_key(2, 0)..shape.log_key(3, 0);
bencher.bench_local(|| {
let mut count = 0;
for entry in store.iter_logs("account-epochs", range.clone()).unwrap() {
divan::black_box(entry.unwrap());
count += 1;
}
assert_eq!(count, rows);
});
}

#[divan::bench(sample_count = 50)]
fn compressed_real_alonzo_block(bencher: divan::Bencher) {
use dolos_core::{ArchiveWriter, ChainPoint};
let body = std::sync::Arc::new(
hex::decode(include_str!("../../crates/cardano/test_data/alonzo27.block").trim()).unwrap(),
);
let block = MultiEraBlock::decode(&body).unwrap();
let point = ChainPoint::Specific(block.slot(), block.hash());
let directory = tempfile::tempdir().unwrap();
let store = dolos_fjall::archive::ArchiveStore::open(
dolos_cardano::model::build_schema(),
directory.path(),
&Default::default(),
)
.unwrap();
let writer = store.start_writer().unwrap();
writer.apply(&point, &body).unwrap();
writer.commit().unwrap();
bencher.bench_local(|| {
let actual = store.get_block_by_slot(&point.slot()).unwrap().unwrap();
assert_eq!(divan::black_box(actual.as_slice()), body.as_slice());
});
}
2 changes: 1 addition & 1 deletion crates/flatfiles/dictionary/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ command over the same files yields the same bytes.

Three candidates were trained from the same corpus with the same seed and
evaluated with the harness's `evaluate` command (then
`dolos-archive-bench evaluate`, now `cargo xtask archive-bench evaluate`)
`dolos-archive-bench evaluate`, now `cargo xtask perf storage evaluate`)
on data disjoint from every
training segment: mainnet segments 448–455 (156,621 Conway blocks, 877 MiB,
the held-out window), one 20,000-block fixture per mainnet era, preprod
Expand Down
1 change: 1 addition & 0 deletions crates/testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ tokio-stream = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
itertools = { workspace = true }
serde = { workspace = true, features = ["derive"] }
59 changes: 45 additions & 14 deletions crates/testing/src/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl ArchiveShape {
/// [`Self::block_slots`] and [`populate_archive`] so the slots callers
/// sample are exactly the slots the population wrote.
pub fn stride(&self) -> u64 {
(self.slots_per_epoch / self.blocks_per_epoch).max(1)
(self.slots_per_epoch / self.blocks_per_epoch.max(1)).max(1)
}

/// Every block slot the population writes, in ascending order. Callers
Expand Down Expand Up @@ -97,33 +97,64 @@ fn filler(rng: &mut SplitMix64, len: usize) -> Vec<u8> {
out
}

/// Write `shape` into `store`: one committed writer per epoch carrying that
/// epoch's blocks (~1-2 KiB bodies) and its log rows (~30-60 B values) under
/// `ns`, which must exist in the store's schema.
/// Write `shape` into `store` in batches of at most 1,000 records. Blocks
/// contain filler (~1-2 KiB) and log values contain filler (~30-60 B).
/// Zero blocks per epoch produces a logs-only population.
pub fn populate_archive<S: ArchiveStore>(
store: &S,
ns: Namespace,
shape: &ArchiveShape,
) -> Result<(), ArchiveError> {
populate_archive_batched(store, ns, shape, 1000)
}

pub fn populate_archive_batched<S: ArchiveStore>(
store: &S,
ns: Namespace,
shape: &ArchiveShape,
batch_size: usize,
) -> Result<(), ArchiveError> {
if batch_size == 0
|| shape.slots_per_epoch == 0
|| shape.blocks_per_epoch > shape.slots_per_epoch
|| shape.epochs.checked_mul(shape.slots_per_epoch).is_none()
{
return Err(ArchiveError::InternalError(
"invalid archive population shape or batch size".into(),
));
}
let mut rng = SplitMix64(shape.seed);
let stride = shape.stride();

for epoch in 0..shape.epochs {
let writer = store.start_writer()?;
let epoch_start = shape.epoch_start(epoch);

for i in 0..shape.blocks_per_epoch {
let slot = epoch_start + i * stride;
let len = 1024 + (rng.next_u64() % 1024) as usize;
writer.apply(&block_point(slot), &Arc::new(filler(&mut rng, len)))?;
for start in (0..shape.blocks_per_epoch).step_by(batch_size) {
let writer = store.start_writer()?;
for block in start
..start
.saturating_add(batch_size as u64)
.min(shape.blocks_per_epoch)
{
let slot = epoch_start + block * stride;
let len = 1024 + (rng.next_u64() % 1024) as usize;
writer.apply(&block_point(slot), &Arc::new(filler(&mut rng, len)))?;
}
writer.commit()?;
}

for i in 0..shape.log_rows_per_epoch {
let len = 30 + (rng.next_u64() % 31) as usize;
writer.write_log(ns, &shape.log_key(epoch, i), &filler(&mut rng, len))?;
for start in (0..shape.log_rows_per_epoch).step_by(batch_size) {
let writer = store.start_writer()?;
for row in start
..start
.saturating_add(batch_size as u64)
.min(shape.log_rows_per_epoch)
{
let len = 30 + (rng.next_u64() % 31) as usize;
writer.write_log(ns, &shape.log_key(epoch, row), &filler(&mut rng, len))?;
}
writer.commit()?;
}

writer.commit()?;
}

Ok(())
Expand Down
2 changes: 2 additions & 0 deletions crates/testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use dolos_core::*;
pub mod archive;
pub mod blocks;
pub mod faults;
pub mod measured;
pub mod performance;
pub mod synthetic;

pub mod toy_domain;
Expand Down
Loading
Loading