use crate::CellRef;
use crate::engine::{DependencyGraph, EvalConfig};
use formualizer_common::LiteralValue;
use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
use std::time::Instant;
fn sum_range_ast(
sheet: Option<&str>,
start_row: u32,
start_col: u32,
end_row: u32,
end_col: u32,
) -> ASTNode {
ASTNode {
node_type: ASTNodeType::Function {
name: "SUM".to_string(),
args: vec![ASTNode {
node_type: ASTNodeType::Reference {
original: format!(
"{}R{}C{}:R{}C{}",
sheet.map(|s| format!("{}!", s)).unwrap_or_default(),
start_row,
start_col,
end_row,
end_col
),
reference: ReferenceType::Range {
sheet: sheet.map(|s| s.to_string()),
start_row: Some(start_row),
start_col: Some(start_col),
end_row: Some(end_row),
end_col: Some(end_col),
start_row_abs: false,
start_col_abs: false,
end_row_abs: false,
end_col_abs: false,
},
},
source_token: None,
contains_volatile: false,
}],
},
source_token: None,
contains_volatile: false,
}
}
fn create_overlapping_ranges_graph(num_formulas: usize, enable_stripes: bool) -> DependencyGraph {
let mut config = EvalConfig::default();
if enable_stripes {
config = config.with_range_expansion_limit(16); } else {
config = config.with_range_expansion_limit(1000000); }
config = config.with_block_stripes(enable_stripes);
let mut graph = DependencyGraph::new_with_config(config);
for i in 0..num_formulas {
let start_row = i as u32 + 1;
let end_row = start_row + 999; let formula_row = i as u32 + 1;
let formula_col = 2;
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, start_row, 1, end_row, 1),
)
.unwrap();
}
graph
}
#[test]
fn test_mark_dirty_stripe_vs_expansion_performance() {
const NUM_FORMULAS: usize = 1000;
println!(
"\n=== Benchmark: mark_dirty with {} overlapping ranges ===",
NUM_FORMULAS
);
let mut stripe_graph = create_overlapping_ranges_graph(NUM_FORMULAS, true);
let all_ids: Vec<_> = stripe_graph.cell_to_vertex().values().copied().collect();
stripe_graph.clear_dirty_flags(&all_ids);
let test_row = 500; let test_col = 1;
let stripe_start = Instant::now();
stripe_graph
.set_cell_value("Sheet1", test_row, test_col, LiteralValue::Int(42))
.unwrap();
let stripe_duration = stripe_start.elapsed();
let stripe_dirty_count = stripe_graph.get_evaluation_vertices().len();
println!(
"Stripe model: {} ms, {} formulas marked dirty",
stripe_duration.as_millis(),
stripe_dirty_count
);
let mut expansion_graph = create_overlapping_ranges_graph(
std::cmp::min(NUM_FORMULAS, 100), false,
);
let all_ids: Vec<_> = expansion_graph.cell_to_vertex().values().copied().collect();
expansion_graph.clear_dirty_flags(&all_ids);
let expansion_start = Instant::now();
expansion_graph
.set_cell_value("Sheet1", test_row, test_col, LiteralValue::Int(42))
.unwrap();
let expansion_duration = expansion_start.elapsed();
let expansion_dirty_count = expansion_graph.get_evaluation_vertices().len();
println!(
"Expansion model (100 formulas): {} ms, {} formulas marked dirty",
expansion_duration.as_millis(),
expansion_dirty_count
);
assert!(expansion_dirty_count > 0, "Should mark some formulas dirty");
assert!(
stripe_dirty_count >= expansion_dirty_count,
"Stripe model should find at least as many dependencies"
);
assert!(
stripe_duration.as_millis() < 1000,
"Stripe model should complete mark_dirty in under 1 second"
);
println!("✓ Stripe model performance test passed");
}
#[test]
fn test_mark_dirty_scaling_with_stripe_model() {
println!("\n=== Benchmark: mark_dirty scaling test ===");
let test_sizes = vec![100, 500, 1000, 2000];
for &size in &test_sizes {
let mut graph = create_overlapping_ranges_graph(size, true);
let all_ids: Vec<_> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let test_row = size as u32 / 2; let test_col = 1;
let start = Instant::now();
graph
.set_cell_value("Sheet1", test_row, test_col, LiteralValue::Int(42))
.unwrap();
let duration = start.elapsed();
let dirty_count = graph.get_evaluation_vertices().len();
println!(
"Size {}: {} ms, {} formulas dirty",
size,
duration.as_millis(),
dirty_count
);
assert!(
duration.as_millis() < (size as u128 / 10).max(100),
"mark_dirty should scale sub-linearly with number of formulas"
);
}
println!("✓ Scaling test passed");
}
#[test]
fn test_mark_dirty_hotspot_performance() {
println!("\n=== Benchmark: mark_dirty hotspot test ===");
const NUM_FORMULAS: usize = 1000;
let mut graph = create_overlapping_ranges_graph(NUM_FORMULAS, true);
let all_ids: Vec<_> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let hotspot_row = 500; let hotspot_col = 1;
for _ in 0..3 {
graph
.set_cell_value("Sheet1", hotspot_row, hotspot_col, LiteralValue::Int(1))
.unwrap();
graph.clear_dirty_flags(&graph.get_evaluation_vertices());
}
let iterations = 10;
let mut total_duration = std::time::Duration::default();
let mut total_dirty = 0;
for i in 0..iterations {
let start = Instant::now();
graph
.set_cell_value(
"Sheet1",
hotspot_row,
hotspot_col,
LiteralValue::Int(i as i64),
)
.unwrap();
let duration = start.elapsed();
let dirty_count = graph.get_evaluation_vertices().len();
total_duration += duration;
total_dirty += dirty_count;
graph.clear_dirty_flags(&graph.get_evaluation_vertices());
}
let avg_duration = total_duration / iterations;
let avg_dirty = total_dirty / iterations as usize;
println!(
"Hotspot average: {} ms, {} formulas dirty per update",
avg_duration.as_millis(),
avg_dirty
);
assert!(
avg_duration.as_millis() < 100,
"Average hotspot mark_dirty should be under 100ms"
);
assert!(
avg_dirty > NUM_FORMULAS / 4,
"Should affect a significant portion of formulas"
);
println!("✓ Hotspot performance test passed");
}
#[test]
fn test_mark_dirty_sparse_vs_dense_ranges() {
println!("\n=== Benchmark: sparse vs dense range performance ===");
let mut config = EvalConfig::default();
config = config.with_range_expansion_limit(16);
config = config.with_block_stripes(true);
let mut sparse_graph = DependencyGraph::new_with_config(config.clone());
for i in 0..500 {
let col = (i % 26) + 1; sparse_graph
.set_cell_formula(
"Sheet1",
i + 1,
27, sum_range_ast(None, 1, col, 1000, col),
)
.unwrap();
}
let mut dense_graph = DependencyGraph::new_with_config(config);
for i in 0..100 {
let start_row = (i / 10) * 50 + 1;
let start_col = (i % 10) * 5 + 1;
dense_graph
.set_cell_formula(
"Sheet1",
i + 1,
27, sum_range_ast(None, start_row, start_col, start_row + 49, start_col + 4),
)
.unwrap();
}
let sparse_ids: Vec<_> = sparse_graph.cell_to_vertex().values().copied().collect();
sparse_graph.clear_dirty_flags(&sparse_ids);
let dense_ids: Vec<_> = dense_graph.cell_to_vertex().values().copied().collect();
dense_graph.clear_dirty_flags(&dense_ids);
let sparse_start = Instant::now();
sparse_graph
.set_cell_value("Sheet1", 500, 1, LiteralValue::Int(42))
.unwrap(); let sparse_duration = sparse_start.elapsed();
let sparse_dirty = sparse_graph.get_evaluation_vertices().len();
let dense_start = Instant::now();
dense_graph
.set_cell_value("Sheet1", 25, 3, LiteralValue::Int(42))
.unwrap(); let dense_duration = dense_start.elapsed();
let dense_dirty = dense_graph.get_evaluation_vertices().len();
println!(
"Sparse ranges (column stripes): {} ms, {} dirty",
sparse_duration.as_millis(),
sparse_dirty
);
println!(
"Dense ranges (block stripes): {} ms, {} dirty",
dense_duration.as_millis(),
dense_dirty
);
assert!(
sparse_duration.as_millis() < 100,
"Sparse ranges should be fast"
);
assert!(
dense_duration.as_millis() < 100,
"Dense ranges should be fast"
);
println!("✓ Sparse vs dense performance test passed");
}
#[test]
fn test_mark_dirty_cross_sheet_performance() {
println!("\n=== Benchmark: cross-sheet range performance ===");
let mut config = EvalConfig::default();
config = config.with_range_expansion_limit(16);
let mut graph = DependencyGraph::new_with_config(config);
for i in 0..500 {
graph
.set_cell_formula(
"Sheet1",
i + 1,
1,
sum_range_ast(Some("Sheet2"), i + 1, 1, i + 1000, 1),
)
.unwrap();
}
let all_ids: Vec<_> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let start = Instant::now();
graph
.set_cell_value("Sheet2", 500, 1, LiteralValue::Int(42))
.unwrap();
let duration = start.elapsed();
let dirty_count = graph.get_evaluation_vertices().len();
println!(
"Cross-sheet: {} ms, {} formulas dirty",
duration.as_millis(),
dirty_count
);
assert!(
duration.as_millis() < 200,
"Cross-sheet mark_dirty should be reasonably fast"
);
assert!(dirty_count > 100, "Should affect many cross-sheet formulas");
println!("✓ Cross-sheet performance test passed");
}
#[test]
fn test_mark_dirty_memory_efficiency() {
println!("\n=== Benchmark: memory efficiency test ===");
const NUM_FORMULAS: usize = 2000;
let mut graph = create_overlapping_ranges_graph(NUM_FORMULAS, true);
let stripe_count = graph.stripe_to_dependents().len();
let total_stripe_entries: usize = graph
.stripe_to_dependents()
.values()
.map(|deps| deps.len())
.sum();
println!(
"Stripes: {} unique stripes, {} total entries",
stripe_count, total_stripe_entries
);
let estimated_cell_deps = NUM_FORMULAS * 1000; let compression_ratio = estimated_cell_deps as f64 / total_stripe_entries as f64;
println!(
"Estimated compression: {:.1}x fewer entries than cell-based tracking",
compression_ratio
);
assert!(
compression_ratio > 10.0,
"Stripe model should compress dependencies significantly"
);
assert!(
stripe_count < NUM_FORMULAS,
"Should have fewer stripes than formulas"
);
let all_ids: Vec<_> = graph.cell_to_vertex().values().copied().collect();
for &id in &all_ids {
if let Some(vertex) = graph.get_vertex(id) {
if vertex.row.is_some() && vertex.col.is_some() && vertex.col.unwrap() == 2 {
drop(graph.set_cell_value(
"Sheet1",
vertex.row.unwrap(),
vertex.col.unwrap(),
LiteralValue::Empty,
));
}
}
}
let remaining_stripe_count = graph.stripe_to_dependents().len();
println!("Stripes after cleanup: {}", remaining_stripe_count);
assert!(
remaining_stripe_count <= stripe_count / 2,
"Stripe cleanup should significantly reduce memory usage (from {} to {})",
stripe_count,
remaining_stripe_count
);
println!("✓ Memory efficiency test passed");
}
#[test]
fn test_mark_dirty_pathological_case() {
println!("\n=== Benchmark: pathological case test ===");
let mut config = EvalConfig::default();
config = config.with_range_expansion_limit(16);
let mut graph = DependencyGraph::new_with_config(config);
const NUM_FORMULAS: usize = 3000;
for i in 0..NUM_FORMULAS {
let formula_row = (i / 100) as u32 + 2; let formula_col = (i % 100) as u32 + 2;
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, 1, 1, 1, 1), )
.unwrap();
}
let all_ids: Vec<_> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let start = Instant::now();
graph
.set_cell_value("Sheet1", 1, 1, LiteralValue::Int(42))
.unwrap();
let duration = start.elapsed();
let dirty_count = graph.get_evaluation_vertices().len();
println!(
"Pathological case: {} ms, {} formulas dirty",
duration.as_millis(),
dirty_count
);
assert!(
duration.as_millis() < 500,
"Even pathological cases should complete in reasonable time"
);
assert_eq!(
dirty_count, NUM_FORMULAS,
"Should mark all dependent formulas dirty"
);
println!("✓ Pathological case test passed");
}
#[test]
fn test_mark_dirty_correctness_vs_expansion() {
println!("\n=== Correctness: stripe vs expansion equivalence ===");
const TEST_FORMULAS: usize = 50;
let mut stripe_graph = create_overlapping_ranges_graph(TEST_FORMULAS, true);
let mut expansion_graph = create_overlapping_ranges_graph(TEST_FORMULAS, false);
let test_cells = vec![
(1, 1), (25, 1), (49, 1), (999, 1), (1500, 1), ];
for (test_row, test_col) in test_cells {
let stripe_ids: Vec<_> = stripe_graph.cell_to_vertex().values().copied().collect();
stripe_graph.clear_dirty_flags(&stripe_ids);
let expansion_ids: Vec<_> = expansion_graph.cell_to_vertex().values().copied().collect();
expansion_graph.clear_dirty_flags(&expansion_ids);
stripe_graph
.set_cell_value("Sheet1", test_row, test_col, LiteralValue::Int(42))
.unwrap();
expansion_graph
.set_cell_value("Sheet1", test_row, test_col, LiteralValue::Int(42))
.unwrap();
let stripe_dirty = stripe_graph.get_evaluation_vertices();
let expansion_dirty = expansion_graph.get_evaluation_vertices();
println!(
"Cell ({}, {}): stripe={} dirty, expansion={} dirty",
test_row,
test_col,
stripe_dirty.len(),
expansion_dirty.len()
);
assert!(
stripe_dirty.len() >= expansion_dirty.len(),
"Stripe model should find at least as many dependencies as expansion model"
);
assert_eq!(
stripe_dirty.len(),
expansion_dirty.len(),
"For simple overlapping ranges, stripe and expansion should find identical dependencies"
);
}
println!("✓ Correctness test passed");
}