use rocksgraph::{
schema::{DataType, GraphOptions},
BulkSchema, EdgeListSource, Primitive, RocksOptions, SstBulkLoader,
};
use rand::Rng;
use std::{env, path::PathBuf, time::Instant};
const VERTEX_LABEL: &str = "Person";
const EDGE_LABEL: &str = "Knows";
const NAME_KEY: &str = "name";
const AGE_KEY: &str = "age";
const WEIGHT_KEY: &str = "weight";
const TIMESTAMP_KEY: &str = "timestamp";
fn generate_random_string(len: usize) -> String {
rand::thread_rng().sample_iter(rand::distributions::Alphanumeric).take(len).map(char::from).collect()
}
fn format_count(n: u64) -> String {
let s = n.to_string();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().enumerate() {
if i > 0 && (s.len() - i) % 3 == 0 {
out.push('_');
}
out.push(c);
}
out
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
run_with_args(args)
}
fn run_with_args(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let data_dir = args
.iter()
.position(|a| a == "--data-dir")
.and_then(|p| args.get(p + 1).map(PathBuf::from))
.expect("--data-dir <path> is required");
let file_path = args
.iter()
.position(|a| a == "--file-path")
.and_then(|p| args.get(p + 1).map(PathBuf::from))
.expect("--file-path <path> is required");
let max_memory: Option<usize> =
args.iter().position(|a| a == "--max-memory").and_then(|p| args.get(p + 1)).and_then(|s| s.parse().ok());
let max_sst: Option<usize> =
args.iter().position(|a| a == "--max-sst").and_then(|p| args.get(p + 1)).and_then(|s| s.parse().ok());
if data_dir.exists() {
std::fs::remove_dir_all(&data_dir)?;
}
let schema = BulkSchema {
vertex_labels: vec![VERTEX_LABEL.to_string()],
edge_labels: vec![EDGE_LABEL.to_string()],
prop_keys: vec![
(NAME_KEY.to_string(), DataType::String),
(AGE_KEY.to_string(), DataType::Int64),
(WEIGHT_KEY.to_string(), DataType::Float64),
(TIMESTAMP_KEY.to_string(), DataType::Int64),
],
};
let source = EdgeListSource {
path: file_path,
vertex_label: VERTEX_LABEL.to_string(),
edge_label: EDGE_LABEL.to_string(),
comment_char: '#',
};
let t0 = Instant::now();
let (raw_vertices, raw_edges) = source.open().map_err(|e| format!("failed to open source: {e}"))?;
let mut rng_v = rand::thread_rng();
let vertices = raw_vertices.into_iter().map(move |mut v| {
v.props.insert(NAME_KEY.to_string(), Primitive::String(generate_random_string(10).into()));
v.props.insert(AGE_KEY.to_string(), Primitive::Int64(rng_v.gen_range(18..100)));
v
});
let mut rng_e = rand::thread_rng();
let edges = raw_edges.into_iter().map(move |mut e| {
e.props.insert(WEIGHT_KEY.to_string(), Primitive::Float64(rng_e.gen_range(0.1..10.0)));
e.props.insert(TIMESTAMP_KEY.to_string(), Primitive::Int64(rng_e.gen_range(0..1_000_000)));
e
});
let work_dir = data_dir.join("_bulk_work");
let mut loader = SstBulkLoader::new(&data_dir, &work_dir);
if let Some(m) = max_memory {
loader = loader.with_max_memory(m);
}
if let Some(s) = max_sst {
loader = loader.with_max_sst_size(s);
}
let stats = loader.load_initial(schema, vertices, edges, GraphOptions::default(), &RocksOptions::default())?;
let elapsed = t0.elapsed();
println!("=== Bulk SST Load Complete ===");
println!("Vertices: {}", format_count(stats.vertices_written));
println!("Edges: {}", format_count(stats.edges_written));
println!("SST files: {}", stats.sst_files);
println!("Elapsed: {:.2}s", elapsed.as_secs_f64());
let throughput = if elapsed.as_secs_f64() > 0.0 {
(stats.edges_written as f64 / elapsed.as_secs_f64()) as u64
} else {
stats.edges_written
};
println!("Throughput: {} edges/s", format_count(throughput));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_bench_write_bulk_sst() {
let dir = tempdir().unwrap();
let file_dir = tempdir().unwrap();
let file_path = file_dir.path().join("graph.txt");
std::fs::write(&file_path, "1 2\n2 3\n3 1\n").unwrap();
let args = vec![
"bench_write".to_string(),
"--data-dir".to_string(),
dir.path().join("db").to_str().unwrap().to_string(),
"--file-path".to_string(),
file_path.to_str().unwrap().to_string(),
];
assert!(run_with_args(args).is_ok());
}
}