Skip to content

Commit 0adfd2b

Browse files
authored
Add planning-effort counters and prepare-stage timer to profile notes (#16)
Extends the profile-gated instrumentation (#15) with per-query planning-effort counters and a `prepare` stage timer. - `prepare` timer fills the gap between lowering and the published snapshot (order-by resolution, row-count evaluation, target config), the only previously uninstrumented stage around the graph-row call. - Counters: node_legal_universe_sources, edge_source_consults, edge_source_misses, secondary_index_followups. Counters use a crate-local thread-local the planner increments (always) and the GQL layer snapshots when profiling; values ride the existing GqlExecutionExplain.notes line, so no public struct or connector changes and the shared read substrate is untouched. profile=false is unchanged: timer mark() returns None and counter values are neither snapshotted nor surfaced. Measured fire count is 3-8 per query (~22-57 ns, ~0.001-0.005% of read latency).
1 parent 43d1615 commit 0adfd2b

3 files changed

Lines changed: 148 additions & 9 deletions

File tree

src/engine/query.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,14 @@ struct GqlQueryInstrumentation {
4444
enabled: bool,
4545
bind_ns: u64,
4646
lower_ns: u64,
47+
prepare_ns: u64,
4748
snapshot_ns: u64,
4849
graph_row_ns: u64,
4950
projection_ns: u64,
51+
edge_source_consults: u64,
52+
edge_source_misses: u64,
53+
node_legal_universe_sources: u64,
54+
secondary_index_followups: u64,
5055
}
5156

5257
impl GqlQueryInstrumentation {
@@ -55,9 +60,14 @@ impl GqlQueryInstrumentation {
5560
enabled,
5661
bind_ns: 0,
5762
lower_ns: 0,
63+
prepare_ns: 0,
5864
snapshot_ns: 0,
5965
graph_row_ns: 0,
6066
projection_ns: 0,
67+
edge_source_consults: 0,
68+
edge_source_misses: 0,
69+
node_legal_universe_sources: 0,
70+
secondary_index_followups: 0,
6171
}
6272
}
6373

@@ -68,14 +78,21 @@ impl GqlQueryInstrumentation {
6878

6979
fn note(&self) -> String {
7080
format!(
71-
"stage timing (nanoseconds): bind={b} lower={l} snapshot={s} \
72-
graph_row_plan_and_execute={g} projection={p}; graph_row covers normalize, \
73-
cost-based planning, index probe, and execution inside the shared read view",
81+
"stage timing (nanoseconds): bind={b} lower={l} prepare={pr} snapshot={s} \
82+
graph_row_plan_and_execute={g} projection={p}; planning counters: \
83+
node_legal_universe_sources={n} edge_source_consults={ec} edge_source_misses={em} \
84+
secondary_index_followups={sf}; graph_row covers normalize, cost-based planning, \
85+
index probe, and execution inside the shared read view",
7486
b = self.bind_ns,
7587
l = self.lower_ns,
88+
pr = self.prepare_ns,
7689
s = self.snapshot_ns,
7790
g = self.graph_row_ns,
7891
p = self.projection_ns,
92+
n = self.node_legal_universe_sources,
93+
ec = self.edge_source_consults,
94+
em = self.edge_source_misses,
95+
sf = self.secondary_index_followups,
7996
)
8097
}
8198
}
@@ -155,10 +172,14 @@ impl DatabaseEngine {
155172
if matches!(&lowered.native_target, GqlNativeTarget::GraphPipeline { .. }) {
156173
return self.execute_gql_pipeline_target(lowered, started_at, options);
157174
}
175+
let prepare_start = instr.mark();
158176
let resolved_order_by = resolve_order_by_return_aliases(&lowered)?;
159177
validate_gql_row_independent_order_keys(&resolved_order_by, &lowered, params)?;
160178
let row_counts = evaluate_gql_row_counts(&lowered, params, options)?;
161179
configure_gql_graph_row_target(&mut lowered, &resolved_order_by, &row_counts, options)?;
180+
if let Some(t) = prepare_start {
181+
instr.prepare_ns = t.elapsed().as_nanos() as u64;
182+
}
162183
let warnings = lowered.warnings.clone();
163184

164185
if row_counts.limit == Some(0) {
@@ -220,10 +241,21 @@ impl DatabaseEngine {
220241
let mut warnings = warnings;
221242

222243
let graph_row_start = instr.mark();
244+
if instr.enabled {
245+
reset_graph_row_planning_probe();
246+
}
223247
let graph_rows = execute_gql_graph_row_target(&published.view, &lowered)?;
248+
let secondary_index_followups = graph_rows.followups.len();
224249
if let Some(t) = graph_row_start {
225250
instr.graph_row_ns = t.elapsed().as_nanos() as u64;
226251
}
252+
if instr.enabled {
253+
let probe = snapshot_graph_row_planning_probe();
254+
instr.edge_source_consults = probe.edge_source_consults;
255+
instr.edge_source_misses = probe.edge_source_misses;
256+
instr.node_legal_universe_sources = probe.node_legal_universe_sources;
257+
instr.secondary_index_followups = secondary_index_followups as u64;
258+
}
227259
for followup in graph_rows.followups {
228260
self.runtime.enqueue_secondary_index_read_followup(followup);
229261
}

src/engine/query_plan.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ impl GraphRowEdgeSourceCostMemo {
169169

170170
const GRAPH_ROW_EDGE_INTERSECTION_TINY_SET: u64 = 64;
171171

172+
#[derive(Clone, Copy, Default, Debug)]
173+
pub(crate) struct GraphRowPlanningProbeCounters {
174+
pub(crate) edge_source_consults: u64,
175+
pub(crate) edge_source_misses: u64,
176+
pub(crate) node_legal_universe_sources: u64,
177+
}
178+
179+
thread_local! {
180+
static GRAPH_ROW_PLANNING_PROBE: std::cell::RefCell<GraphRowPlanningProbeCounters> =
181+
std::cell::RefCell::new(GraphRowPlanningProbeCounters::default());
182+
}
183+
184+
pub(crate) fn reset_graph_row_planning_probe() {
185+
GRAPH_ROW_PLANNING_PROBE.with(|c| *c.borrow_mut() = GraphRowPlanningProbeCounters::default());
186+
}
187+
188+
pub(crate) fn snapshot_graph_row_planning_probe() -> GraphRowPlanningProbeCounters {
189+
GRAPH_ROW_PLANNING_PROBE.with(|c| *c.borrow())
190+
}
191+
172192
#[derive(Clone, Debug)]
173193
struct GraphRowPhysicalSegment {
174194
segment_index: usize,
@@ -6011,13 +6031,24 @@ impl ReadView {
60116031
build_explain: bool,
60126032
memo: &'memo mut GraphRowEdgeSourceCostMemo,
60136033
) -> Result<&'memo GraphRowEdgeSourcePlanCost, EngineError> {
6014-
let Some(state_index) = GraphRowEdgeSourceCostMemo::bound_state_index(from_bound, to_bound)
6015-
else {
6016-
return Err(EngineError::InvalidOperation(
6017-
"graph row bound edge-source cost requested without a bound endpoint".into(),
6018-
));
6034+
let state_index = match GraphRowEdgeSourceCostMemo::bound_state_index(from_bound, to_bound)
6035+
{
6036+
Some(idx) => idx,
6037+
None => {
6038+
return Err(EngineError::InvalidOperation(
6039+
"graph row bound edge-source cost requested without a bound endpoint".into(),
6040+
));
6041+
}
60196042
};
6020-
if memo.bound_costs[edge_index][state_index].is_none() {
6043+
let is_miss = memo.bound_costs[edge_index][state_index].is_none();
6044+
GRAPH_ROW_PLANNING_PROBE.with(|c| {
6045+
let mut c = c.borrow_mut();
6046+
c.edge_source_consults += 1;
6047+
if is_miss {
6048+
c.edge_source_misses += 1;
6049+
}
6050+
});
6051+
if is_miss {
60216052
let cost = self.graph_row_edge_source_plan_cost_with_endpoints(
60226053
query,
60236054
edge,
@@ -8884,6 +8915,10 @@ impl ReadView {
88848915
}
88858916

88868917
let legal_universe_sources = self.legal_universe_sources(query)?;
8918+
GRAPH_ROW_PLANNING_PROBE.with(|c| {
8919+
c.borrow_mut().node_legal_universe_sources +=
8920+
legal_universe_sources.len() as u64;
8921+
});
88878922
let legal_universe_fallback =
88888923
Self::cheapest_node_legal_universe_source(&legal_universe_sources);
88898924
let cap_context = QueryCapContext {

src/engine/tests/gql_execution.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,78 @@ fn gql_query_profile_stage_timing_note_in_explain_plan() {
7979
);
8080
}
8181

82+
#[test]
83+
fn gql_query_profile_planning_counters_in_explain_note() {
84+
let (_dir, engine) = query_test_engine();
85+
insert_query_node(&engine, "Person", "ada", &[], 1.0);
86+
87+
let profiled = engine
88+
.execute_gql(
89+
"MATCH (n:Person) WHERE elementKey(n) = 'ada' RETURN id(n)",
90+
&GqlParams::new(),
91+
&GqlExecutionOptions {
92+
profile: true,
93+
include_plan: true,
94+
..gql_opts()
95+
},
96+
)
97+
.unwrap();
98+
let note = profiled
99+
.plan
100+
.as_ref()
101+
.and_then(|p| {
102+
p.notes
103+
.iter()
104+
.find(|n| n.contains("planning counters"))
105+
.cloned()
106+
})
107+
.expect("profile+include_plan should attach a planning-counters note");
108+
109+
assert!(
110+
note.contains("prepare="),
111+
"note should carry the prepare stage timer"
112+
);
113+
for key in [
114+
"node_legal_universe_sources=",
115+
"edge_source_consults=",
116+
"edge_source_misses=",
117+
"secondary_index_followups=",
118+
] {
119+
assert!(note.contains(key), "note should carry counter {key:?}");
120+
}
121+
122+
let universe = note
123+
.split("node_legal_universe_sources=")
124+
.nth(1)
125+
.and_then(|rest| rest.trim_start().split(|c: char| !c.is_ascii_digit()).next())
126+
.and_then(|s| s.parse::<u64>().ok())
127+
.expect("node_legal_universe_sources should be a number");
128+
assert!(
129+
universe >= 1,
130+
"a single-node graph-row query must enumerate >=1 universe source, got {universe}"
131+
);
132+
133+
let plain = engine
134+
.execute_gql(
135+
"MATCH (n:Person) WHERE elementKey(n) = 'ada' RETURN id(n)",
136+
&GqlParams::new(),
137+
&GqlExecutionOptions {
138+
include_plan: true,
139+
..gql_opts()
140+
},
141+
)
142+
.unwrap();
143+
let plain_plan = plain.plan.as_ref().unwrap();
144+
assert!(
145+
!plain_plan
146+
.notes
147+
.iter()
148+
.any(|n| n.contains("planning counters")),
149+
"profile=false must not attach planning counters, got: {:?}",
150+
plain_plan.notes
151+
);
152+
}
153+
82154
fn execute_gql_with_params(
83155
engine: &DatabaseEngine,
84156
source: &str,

0 commit comments

Comments
 (0)