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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,7 @@ harness = false
[[bench]]
name = "serialize"
harness = false

[[bench]]
name = "intern"
harness = false
47 changes: 47 additions & 0 deletions benches/intern.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use clvmr::allocator::Allocator;
use clvmr::serde::{intern, node_from_bytes, node_from_bytes_backrefs, node_to_bytes_limit};
use criterion::{Criterion, criterion_group, criterion_main};
use std::include_bytes;
use std::time::Instant;

fn intern_benchmark(c: &mut Criterion) {
let block0: &[u8] = include_bytes!("0.generator");
let block1: &[u8] = include_bytes!("1.generator");
let block2: &[u8] = include_bytes!("2.generator");
let block3: &[u8] = include_bytes!("3.generator");
let block4: &[u8] = include_bytes!("4.generator");

let mut group = c.benchmark_group("intern");

for (block, name) in [
(&block0, "0"),
(&block1, "1"),
(&block2, "2"),
(&block3, "3"),
(&block4, "4"),
] {
let mut a = Allocator::new();
let node = node_from_bytes_backrefs(&mut a, block).expect("node_from_bytes_backrefs");

// if the inflated form takes too much space, just run the benchmark on the compact form
let node = if let Ok(inflated) = node_to_bytes_limit(&a, node, 2000000) {
a = Allocator::new();
node_from_bytes(&mut a, inflated.as_slice()).expect("node_from_bytes")
} else {
node
};

group.bench_function(format!("intern {name}"), |b| {
b.iter(|| {
let start = Instant::now();
let _tree = intern(&a, node).expect("intern");
start.elapsed()
})
});
}

group.finish();
}

criterion_group!(intern_bench, intern_benchmark);
criterion_main!(intern_bench);
6 changes: 6 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,9 @@ name = "canonical-serialization-br"
path = "fuzz_targets/canonical_serialization_br.rs"
test = false
doc = false

[[bin]]
name = "intern"
path = "fuzz_targets/intern.rs"
test = false
doc = false
87 changes: 87 additions & 0 deletions fuzz/fuzz_targets/intern.rs
Comment thread
arvidn marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#![no_main]

use clvm_fuzzing::make_tree_limits;
use clvmr::allocator::Allocator;
use clvmr::serde::{ObjectCache, intern, node_to_bytes, treehash};
use libfuzzer_sys::fuzz_target;

// Fuzzer for the interning functionality.
// Build and run with allocator-debug enabled (default for this fuzz crate) so NodePtr
// don't get mixed up between the source and interned allocators.
// Verifies that:
// 1. Interning succeeds on valid nodes
// 2. The interned node serializes to the same bytes as the original
// 3. The tree hash is preserved
// 4. Interned nodes have fewer or equal unique atoms/pairs (deduplication works)
Comment thread
richardkiss marked this conversation as resolved.
fuzz_target!(|data: &[u8]| {
let mut unstructured = arbitrary::Unstructured::new(data);
let mut allocator = Allocator::new();
let (program, _) =
make_tree_limits(&mut allocator, &mut unstructured, 600_000, false).expect("out of memory");

// Serialize the original node
let Ok(original_serialized) = node_to_bytes(&allocator, program) else {
return;
};
Comment thread
cursor[bot] marked this conversation as resolved.

// Compute original tree hash
let mut original_cache = ObjectCache::new(treehash);
let Some(original_tree_hash) = original_cache.get_or_calculate(&allocator, &program, None)
else {
return;
};
let original_tree_hash = *original_tree_hash;

// Count original atoms and pairs before interning
let original_atoms = allocator.atom_count();
let original_pairs = allocator.pair_count();
let original_allocated_atoms = allocator.allocated_atom_count();
let original_allocated_pairs = allocator.allocated_pair_count();

// Create interned version using new API
let Ok(tree) = intern(&allocator, program) else {
return;
};

// Serialize the interned node
let Ok(interned_serialized) = node_to_bytes(&tree.allocator, tree.root) else {
panic!("Interned node should serialize successfully");
};

// The serializations must match
assert_eq!(
original_serialized, interned_serialized,
"Serialized bytes differ after interning"
);

// Verify deduplication: interned unique counts should not exceed original
let interned_atoms = tree.atoms.len();
let interned_pairs = tree.pairs.len();
assert!(
interned_atoms <= original_atoms,
"Interning increased atoms: {original_atoms} -> {interned_atoms}"
);
Comment thread
arvidn marked this conversation as resolved.
assert!(
interned_pairs <= original_pairs,
"Interning increased pairs: {original_pairs} -> {interned_pairs}",
);

// Verify allocated counts (RAM usage) do not increase
let interned_allocated_atoms = tree.allocator.allocated_atom_count();
let interned_allocated_pairs = tree.allocator.allocated_pair_count();
assert!(
interned_allocated_atoms <= original_allocated_atoms,
"Interning increased allocated atoms: {original_allocated_atoms} -> {interned_allocated_atoms}",
);
assert!(
interned_allocated_pairs <= original_allocated_pairs,
"Interning increased allocated pairs: {original_allocated_pairs} -> {interned_allocated_pairs}",
);
Comment thread
arvidn marked this conversation as resolved.

// Verify tree hash is preserved
let interned_tree_hash = tree.tree_hash();
assert_eq!(
original_tree_hash, interned_tree_hash,
"Tree hash differs after interning"
);
});
Loading
Loading