use super::ast::CypherQuery;
use super::executor::CypherExecutor;
use super::parser::{parse_cypher, MAX_EXPRESSION_DEPTH};
use super::planner::optimize;
use crate::datatypes::Value;
use crate::graph::algorithms::Interrupt;
use crate::graph::dir_graph::DirGraph;
use crate::graph::session::QUERY_THREAD_STACK_SIZE;
use std::collections::HashMap;
const PREP_STACK: usize = 512 * 1024 * 1024;
const PAINT: u8 = 0xA5;
const PAINT_WORD: u64 = u64::from_ne_bytes([PAINT; 8]);
const PAGE: usize = 4096;
fn probe_stack() -> usize {
std::env::var("KGL_PROBE_STACK_KIB")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.map(|kib| kib * 1024)
.unwrap_or(512 * 1024 * 1024)
}
fn paint_len() -> usize {
(probe_stack() / 4 * 3).min(64 * 1024 * 1024) / PAGE * PAGE
}
fn query(shape: &str, depth: usize) -> String {
match shape {
"parens" => format!("RETURN {}1{} AS x", "(".repeat(depth), ")".repeat(depth)),
"lists" => format!("RETURN {}1{} AS x", "[".repeat(depth), "]".repeat(depth)),
"not" => format!("RETURN {}false AS x", "NOT ".repeat(depth)),
"neg" => format!("RETURN {}5 AS x", "-".repeat(depth)),
"or" => format!("RETURN {} AS x", vec!["false"; depth + 1].join(" OR ")),
"and" => format!("RETURN {} AS x", vec!["true"; depth + 1].join(" AND ")),
"add" => format!("RETURN {} AS x", vec!["1"; depth + 1].join(" + ")),
"concat" => format!("RETURN {} AS x", vec!["'a'"; depth + 1].join(" || ")),
"subscript" => format!("RETURN [1]{} AS x", "[0]".repeat(depth)),
"where_or" => format!(
"MATCH (n:T) WHERE {} RETURN count(n) AS c",
(0..=depth)
.map(|i| format!("n.id = {i}"))
.collect::<Vec<_>>()
.join(" OR ")
),
"where_or_mixed" => format!(
"MATCH (n:T) WHERE {} RETURN count(n) AS c",
(0..=depth)
.map(|i| format!("n.p{i} = {i}"))
.collect::<Vec<_>>()
.join(" OR ")
),
"where_in" => format!(
"MATCH (n:T) WHERE n.id IN [{}] RETURN count(n) AS c",
(0..=depth)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(", ")
),
other => panic!("unknown shape {other}"),
}
}
const ALL_SHAPES: &[&str] = &[
"or",
"and",
"not",
"neg",
"lists",
"add",
"concat",
"subscript",
"parens",
"where_or",
"where_or_mixed",
"where_in",
];
fn seeded_graph() -> DirGraph {
let mut graph = DirGraph::new();
let create = parse_cypher("CREATE (:T {id: 1})").expect("seed parses");
super::executor::write::execute_mutable(
&mut graph,
&create,
HashMap::new(),
Interrupt::default(),
)
.expect("seed executes");
graph
}
fn run_full_pipeline(graph: &DirGraph, text: &str) {
let params: HashMap<String, Value> = HashMap::new();
let mut q = parse_cypher(text).expect("query must parse within the budget");
optimize(&mut q, graph, ¶ms);
let exec = CypherExecutor::with_params(graph, ¶ms, None);
exec.execute(&q).expect("query must execute");
}
#[test]
fn budget_ceiling_query_fits_the_query_thread_stack() {
let depth = MAX_EXPRESSION_DEPTH - 1;
std::thread::Builder::new()
.stack_size(QUERY_THREAD_STACK_SIZE)
.spawn(move || {
let graph = seeded_graph();
for shape in ALL_SHAPES {
run_full_pipeline(&graph, &query(shape, depth));
}
})
.expect("spawn query-sized thread")
.join()
.expect("budget-ceiling query overflowed the query-thread stack");
}
#[test]
fn past_budget_is_refused_with_the_in_rewrite_named() {
let err = parse_cypher(&query("or", MAX_EXPRESSION_DEPTH + 50))
.expect_err("past-budget query must be refused");
let msg = err.to_string();
assert!(
msg.contains("nesting exceeds"),
"unexpected error text: {msg}"
);
assert!(
msg.contains("IN ["),
"the budget error must name the IN [...] rewrite, got: {msg}"
);
}
fn prepared_off_thread(text: String, plan: bool) -> CypherQuery {
std::thread::Builder::new()
.stack_size(PREP_STACK)
.spawn(move || {
let mut q = parse_cypher(&text).expect("probe query must parse");
if plan {
let graph = seeded_graph();
let params: HashMap<String, Value> = HashMap::new();
optimize(&mut q, &graph, ¶ms);
}
q
})
.expect("spawn")
.join()
.expect("prep thread")
}
fn measure(body: impl FnOnce()) -> usize {
let anchor = 0u64;
const MARGIN: usize = 4 * 1024;
let reference = ((&anchor as *const u64 as usize) & !7) - MARGIN;
let len = paint_len();
let bottom = reference - len;
let mut page = reference - PAGE;
while page >= bottom {
unsafe { std::ptr::write_bytes(page as *mut u8, PAINT, PAGE) };
page -= PAGE;
}
body();
let mut deepest = reference;
for i in 0..len / 8 {
let addr = bottom + i * 8;
if unsafe { std::ptr::read_volatile(addr as *const u64) } != PAINT_WORD {
deepest = addr;
break;
}
}
assert!(
deepest > bottom,
"stage reached the bottom of the paint window; raise KGL_PROBE_STACK_KIB"
);
reference - deepest
}
#[inline(never)]
fn calibration_recurse(depth: usize, sink: &mut u64) {
let mut pad = [0u64; 16]; pad[depth % 16] = depth as u64;
if depth > 0 {
calibration_recurse(depth - 1, sink);
}
*sink = sink.wrapping_add(std::hint::black_box(pad)[depth % 16]);
}
fn on_probe_thread<R: Send + 'static>(body: impl FnOnce() -> R + Send + 'static) -> R {
std::thread::Builder::new()
.stack_size(probe_stack())
.spawn(body)
.expect("spawn probe thread")
.join()
.expect("probe thread panicked")
}
fn run_bench() {
let rounds: usize = std::env::var("KGL_PROBE_ROUNDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(100);
let nodes: usize = std::env::var("KGL_PROBE_NODES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(20_000);
let mut graph = DirGraph::new();
for i in 0..nodes {
let create =
parse_cypher(&format!("CREATE (:T {{id: {i}, v: {}}})", i * 3)).expect("seed parses");
super::executor::write::execute_mutable(
&mut graph,
&create,
HashMap::new(),
Interrupt::default(),
)
.expect("seed executes");
}
let params: HashMap<String, Value> = HashMap::new();
let mut q = parse_cypher("MATCH (n:T) RETURN sum(n.id * 2 + n.v * 3 - n.id + 1) AS s")
.expect("bench query parses");
optimize(&mut q, &graph, ¶ms);
let mut best = std::time::Duration::from_secs(3600);
for _ in 0..rounds {
let exec = CypherExecutor::with_params(&graph, ¶ms, None);
let t0 = std::time::Instant::now();
let r = exec.execute(&q).expect("bench executes");
let dt = t0.elapsed();
std::hint::black_box(&r);
best = best.min(dt);
}
println!(
"PROBE-BENCH nodes={nodes} rounds={rounds} min_us={}",
best.as_micros()
);
}
#[test]
fn stack_probe() {
if std::env::var_os("KGL_STACK_PROBE").is_none() {
return;
}
let env = |k: &str| std::env::var(k).unwrap_or_else(|_| panic!("{k} must be set"));
let stage = env("KGL_PROBE_STAGE");
match stage.as_str() {
"calibrate" => {
for depth in [1usize, 101, 1101] {
let used = on_probe_thread(move || {
measure(|| {
let mut sink = 0u64;
calibration_recurse(depth, &mut sink);
std::hint::black_box(sink);
})
});
println!("PROBE-RESULT stage=calibrate shape=none depth={depth} bytes={used}");
}
println!("PROBE-OK");
return;
}
"bench" => {
run_bench();
println!("PROBE-OK");
return;
}
_ => {}
}
let shape = env("KGL_PROBE_SHAPE");
let depth: usize = env("KGL_PROBE_DEPTH").parse().expect("depth");
let text = query(&shape, depth);
let used = match stage.as_str() {
"parse" => on_probe_thread(move || {
measure(|| {
let q = parse_cypher(&text).expect("parse");
std::mem::forget(q);
})
}),
"drop" => {
let q = prepared_off_thread(text, false);
on_probe_thread(move || measure(move || drop(q)))
}
"plan" => {
let q = prepared_off_thread(text, false);
on_probe_thread(move || {
let mut q = q;
let graph = seeded_graph();
let params: HashMap<String, Value> = HashMap::new();
let used = measure(|| optimize(&mut q, &graph, ¶ms));
std::mem::forget(q);
used
})
}
"exec" => {
let q = prepared_off_thread(text, true);
on_probe_thread(move || {
let graph = seeded_graph();
let params: HashMap<String, Value> = HashMap::new();
let used = measure(|| {
let exec = CypherExecutor::with_params(&graph, ¶ms, None);
let result = exec.execute(&q).expect("execute");
std::mem::forget(result);
});
std::mem::forget(q);
used
})
}
"full" => on_probe_thread(move || {
let graph = seeded_graph();
measure(|| run_full_pipeline(&graph, &text))
}),
other => panic!("unknown stage {other}"),
};
println!("PROBE-RESULT stage={stage} shape={shape} depth={depth} bytes={used}");
println!("PROBE-OK");
}