218 lines
7.3 KiB
Rust
218 lines
7.3 KiB
Rust
|
|
//! WriteBuffer performance benchmarks
|
||
|
|
//!
|
||
|
|
//! These benchmarks measure the performance improvements from the O(n²) → O(1)
|
||
|
|
//! optimization that replaced Vec::retain() with HashMap-based indexing.
|
||
|
|
|
||
|
|
use criterion::{
|
||
|
|
black_box,
|
||
|
|
criterion_group,
|
||
|
|
criterion_main,
|
||
|
|
BenchmarkId,
|
||
|
|
Criterion,
|
||
|
|
};
|
||
|
|
use lib::persistence::{
|
||
|
|
PersistenceOp,
|
||
|
|
WriteBuffer,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Benchmark: Add many updates to the same component (worst case for old implementation)
|
||
|
|
///
|
||
|
|
/// This scenario heavily stresses the deduplication logic. In the old O(n)
|
||
|
|
/// implementation, each add() would scan the entire buffer. With HashMap
|
||
|
|
/// indexing, this is now O(1).
|
||
|
|
fn bench_repeated_updates_same_component(c: &mut Criterion) {
|
||
|
|
let mut group = c.benchmark_group("WriteBuffer::add (same component)");
|
||
|
|
|
||
|
|
for num_updates in [10, 100, 500, 1000] {
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::from_parameter(num_updates),
|
||
|
|
&num_updates,
|
||
|
|
|b, &num_updates| {
|
||
|
|
b.iter(|| {
|
||
|
|
let mut buffer = WriteBuffer::new(2000);
|
||
|
|
let entity_id = uuid::Uuid::new_v4();
|
||
|
|
|
||
|
|
// Apply many updates to the same (entity, component) pair
|
||
|
|
for i in 0..num_updates {
|
||
|
|
let op = PersistenceOp::UpsertComponent {
|
||
|
|
entity_id,
|
||
|
|
component_type: "Transform".to_string(),
|
||
|
|
data: vec![i as u8; 100], // 100 bytes per update
|
||
|
|
};
|
||
|
|
buffer.add(op).ok(); // Ignore errors for benchmarking
|
||
|
|
}
|
||
|
|
|
||
|
|
black_box(buffer);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Benchmark: Add updates to different components (best case, no deduplication)
|
||
|
|
///
|
||
|
|
/// This tests the performance when there's no deduplication happening.
|
||
|
|
/// Both old and new implementations should perform similarly here.
|
||
|
|
fn bench_different_components(c: &mut Criterion) {
|
||
|
|
let mut group = c.benchmark_group("WriteBuffer::add (different components)");
|
||
|
|
|
||
|
|
for num_components in [10, 100, 500, 1000] {
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::from_parameter(num_components),
|
||
|
|
&num_components,
|
||
|
|
|b, &num_components| {
|
||
|
|
b.iter(|| {
|
||
|
|
let mut buffer = WriteBuffer::new(2000);
|
||
|
|
let entity_id = uuid::Uuid::new_v4();
|
||
|
|
|
||
|
|
// Add different components (no deduplication)
|
||
|
|
for i in 0..num_components {
|
||
|
|
let op = PersistenceOp::UpsertComponent {
|
||
|
|
entity_id,
|
||
|
|
component_type: format!("Component{}", i),
|
||
|
|
data: vec![i as u8; 100],
|
||
|
|
};
|
||
|
|
buffer.add(op).ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
black_box(buffer);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Benchmark: Mixed workload (realistic scenario)
|
||
|
|
///
|
||
|
|
/// This simulates a realistic workload with a mix of repeated updates to
|
||
|
|
/// popular components and unique component updates.
|
||
|
|
fn bench_mixed_workload(c: &mut Criterion) {
|
||
|
|
let mut group = c.benchmark_group("WriteBuffer::add (mixed workload)");
|
||
|
|
|
||
|
|
for num_ops in [100, 500, 1000] {
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::from_parameter(num_ops),
|
||
|
|
&num_ops,
|
||
|
|
|b, &num_ops| {
|
||
|
|
b.iter(|| {
|
||
|
|
let mut buffer = WriteBuffer::new(2000);
|
||
|
|
let entity_id = uuid::Uuid::new_v4();
|
||
|
|
|
||
|
|
// 70% updates to Transform, 20% to Material, 10% to unique components
|
||
|
|
for i in 0..num_ops {
|
||
|
|
let (component_type, data_size) = match i % 10 {
|
||
|
|
0..=6 => ("Transform".to_string(), 64), // 70%
|
||
|
|
7..=8 => ("Material".to_string(), 128), // 20%
|
||
|
|
_ => (format!("Component{}", i), 32), // 10%
|
||
|
|
};
|
||
|
|
|
||
|
|
let op = PersistenceOp::UpsertComponent {
|
||
|
|
entity_id,
|
||
|
|
component_type,
|
||
|
|
data: vec![i as u8; data_size],
|
||
|
|
};
|
||
|
|
buffer.add(op).ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
black_box(buffer);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Benchmark: Entity updates (different code path)
|
||
|
|
///
|
||
|
|
/// Tests the performance of entity-level deduplication.
|
||
|
|
fn bench_entity_updates(c: &mut Criterion) {
|
||
|
|
let mut group = c.benchmark_group("WriteBuffer::add (entity updates)");
|
||
|
|
|
||
|
|
for num_updates in [10, 100, 500] {
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::from_parameter(num_updates),
|
||
|
|
&num_updates,
|
||
|
|
|b, &num_updates| {
|
||
|
|
b.iter(|| {
|
||
|
|
let mut buffer = WriteBuffer::new(2000);
|
||
|
|
let entity_id = uuid::Uuid::new_v4();
|
||
|
|
|
||
|
|
// Repeatedly update the same entity
|
||
|
|
for i in 0..num_updates {
|
||
|
|
let op = PersistenceOp::UpsertEntity {
|
||
|
|
id: entity_id,
|
||
|
|
data: lib::persistence::EntityData {
|
||
|
|
id: entity_id,
|
||
|
|
created_at: chrono::Utc::now(),
|
||
|
|
updated_at: chrono::Utc::now(),
|
||
|
|
entity_type: format!("Type{}", i),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
buffer.add(op).ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
black_box(buffer);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Benchmark: take_operations() performance
|
||
|
|
///
|
||
|
|
/// Measures the cost of clearing the buffer and returning operations.
|
||
|
|
fn bench_take_operations(c: &mut Criterion) {
|
||
|
|
let mut group = c.benchmark_group("WriteBuffer::take_operations");
|
||
|
|
|
||
|
|
for buffer_size in [10, 100, 500, 1000] {
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::from_parameter(buffer_size),
|
||
|
|
&buffer_size,
|
||
|
|
|b, &buffer_size| {
|
||
|
|
b.iter_batched(
|
||
|
|
|| {
|
||
|
|
// Setup: Create a buffer with many operations
|
||
|
|
let mut buffer = WriteBuffer::new(2000);
|
||
|
|
let entity_id = uuid::Uuid::new_v4();
|
||
|
|
|
||
|
|
for i in 0..buffer_size {
|
||
|
|
let op = PersistenceOp::UpsertComponent {
|
||
|
|
entity_id,
|
||
|
|
component_type: format!("Component{}", i),
|
||
|
|
data: vec![i as u8; 64],
|
||
|
|
};
|
||
|
|
buffer.add(op).ok();
|
||
|
|
}
|
||
|
|
|
||
|
|
buffer
|
||
|
|
},
|
||
|
|
|mut buffer| {
|
||
|
|
// Benchmark: Take all operations
|
||
|
|
black_box(buffer.take_operations())
|
||
|
|
},
|
||
|
|
criterion::BatchSize::SmallInput,
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
criterion_group!(
|
||
|
|
benches,
|
||
|
|
bench_repeated_updates_same_component,
|
||
|
|
bench_different_components,
|
||
|
|
bench_mixed_workload,
|
||
|
|
bench_entity_updates,
|
||
|
|
bench_take_operations,
|
||
|
|
);
|
||
|
|
criterion_main!(benches);
|