Skip to content

Commit f4a3d62

Browse files
committed
perf(compile): analyze models as soon as their producers finish
Replace wave-by-wave model analysis with a dependency-driven scheduler. A model is analyzed once every producer shape it binds against is final, so a slow model no longer holds back unrelated models in the next topological level. Worker threads serialize Python preparation and projection under one lock, while self-contained native analysis releases it, so the next ready batch is prepared while earlier batches run natively. The calling thread never takes that lock: it only starts and joins the workers, and on an interrupt or start failure it cancels and joins every started worker before re-raising. Each compile catalog now owns one native analysis pool that every compact analysis and binding batch reuses, instead of building a pool per call. Compact analysis splits into a catalog-resolving prepare step and a run step that no longer borrows the catalog, so catalog updates for later batches proceed while earlier batches run. Spawned drainer tasks replace the pool broadcast, so a batch no longer needs every pool thread to finish. Every model sees exactly the producer shapes, cache entries, and changed upstream signatures it saw under wave scheduling, so compile output does not depend on completion order. An ordinary analysis error replays the wave schedule to raise the same first error; an interrupt or other non-Exception failure always wins and propagates without a replay.
1 parent 0b86fac commit f4a3d62

25 files changed

Lines changed: 1902 additions & 189 deletions

File tree

‎crates/sqlbuild-rules-native/src/bindings/_helpers/functions.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
515515
.get_type::<crate::bindings::_helpers::panics::NativeCompilerError>(),
516516
)?;
517517
module.add_class::<crate::semantic_validation::models::ProjectCatalog>()?;
518+
module.add_class::<crate::semantic_validation::models::CompactAnalysisJob>()?;
518519
module.add_class::<crate::semantic_validation::models::BindingPositions>()?;
519520
module.add_function(wrap_pyfunction!(normalize_analysis_sql, module)?)?;
520521
module.add_function(wrap_pyfunction!(normalize_analysis_sqls, module)?)?;

‎crates/sqlbuild-rules-native/src/query_analysis/_helpers/engine.rs‎

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,24 @@ pub(crate) fn analyze_project_compact_with_catalog(
323323
request_json: &str,
324324
catalog: Option<&crate::semantic_validation::models::ProjectCatalog>,
325325
) -> Result<String, String> {
326+
analyze_resolved_compact_batch(resolve_compact_batch(request_json, catalog)?, catalog)
327+
}
328+
329+
/// Resolve catalog schemas now and return the analysis to run later without catalog tables.
330+
pub(crate) fn prepare_project_compact_with_catalog(
331+
request_json: &str,
332+
catalog: &crate::semantic_validation::models::ProjectCatalog,
333+
) -> Result<crate::semantic_validation::types::PreparedCompactAnalysis, String> {
334+
let request = resolve_compact_batch(request_json, Some(catalog))?;
335+
Ok(Box::new(move |view| {
336+
analyze_resolved_compact_batch(request, Some(view))
337+
}))
338+
}
339+
340+
fn resolve_compact_batch(
341+
request_json: &str,
342+
catalog: Option<&crate::semantic_validation::models::ProjectCatalog>,
343+
) -> Result<CompactProjectAnalysisBatchRequest, String> {
326344
let mut request: CompactProjectAnalysisBatchRequest =
327345
serde_json::from_str(request_json).map_err(|error| error.to_string())?;
328346
for query in &mut request.queries {
@@ -349,6 +367,14 @@ pub(crate) fn analyze_project_compact_with_catalog(
349367
}
350368
}
351369
}
370+
Ok(request)
371+
}
372+
373+
/// Analyse a resolved batch; a catalog supplies options, probes, and its compile-wide pool.
374+
fn analyze_resolved_compact_batch(
375+
request: CompactProjectAnalysisBatchRequest,
376+
catalog: Option<&crate::semantic_validation::models::ProjectCatalog>,
377+
) -> Result<String, String> {
352378
let workers = request.workers.clamp(1, MAX_WORKERS);
353379
if request
354380
.templates
@@ -400,12 +426,16 @@ pub(crate) fn analyze_project_compact_with_catalog(
400426
})
401427
.collect();
402428
let sql_bytes: usize = request.queries.iter().map(|query| query.sql.len()).sum();
403-
let analysis_workers = workers.min(unique_query_count.max(1));
404-
let pool = rayon::ThreadPoolBuilder::new()
405-
.num_threads(analysis_workers)
406-
.stack_size(ANALYSIS_WORKER_STACK_BYTES)
407-
.build()
408-
.map_err(|error| error.to_string())?;
429+
let pool = match catalog {
430+
Some(catalog) => catalog.analysis_pool()?,
431+
None => std::sync::Arc::new(
432+
rayon::ThreadPoolBuilder::new()
433+
.num_threads(workers.min(unique_query_count.max(1)))
434+
.stack_size(ANALYSIS_WORKER_STACK_BYTES)
435+
.build()
436+
.map_err(|error| error.to_string())?,
437+
),
438+
};
409439
let query_work: Vec<CompactQueryWork> = request
410440
.queries
411441
.into_iter()
@@ -450,7 +480,7 @@ pub(crate) fn analyze_project_compact_with_catalog(
450480
serde_json::to_string(&response).map_err(|error| error.to_string())
451481
}
452482

453-
/// Analyse a batch largest SQL first so no late wide query becomes its critical path.
483+
/// Analyse largest SQL first on spawned drainers rather than a broadcast to every pool thread.
454484
fn analyze_largest_first(
455485
pool: &rayon::ThreadPool,
456486
batch: Vec<CompactQueryWork>,
@@ -463,21 +493,33 @@ fn analyze_largest_first(
463493
.map(|work| Mutex::new(Some(work)))
464494
.collect();
465495
let next = AtomicUsize::new(0);
466-
let finished: Vec<Vec<(usize, CompiledQueryWorkResult)>> = pool.broadcast(|_| {
467-
let mut finished: Vec<(usize, CompiledQueryWorkResult)> = Vec::new();
468-
while let Some(index) = order.get(next.fetch_add(1, Ordering::Relaxed)).copied() {
469-
let work = slots[index]
470-
.lock()
471-
.unwrap_or_else(PoisonError::into_inner)
472-
.take();
473-
if let Some(work) = work {
474-
finished.push((index, analyze_compact_query_work(work, catalog)));
475-
}
496+
let finished: Mutex<Vec<(usize, CompiledQueryWorkResult)>> = Mutex::new(Vec::new());
497+
let drainers = pool.current_num_threads().min(slots.len());
498+
pool.scope(|scope| {
499+
for _ in 0..drainers {
500+
scope.spawn(|_| {
501+
let mut drained: Vec<(usize, CompiledQueryWorkResult)> = Vec::new();
502+
while let Some(index) = order.get(next.fetch_add(1, Ordering::Relaxed)).copied() {
503+
let work = slots[index]
504+
.lock()
505+
.unwrap_or_else(PoisonError::into_inner)
506+
.take();
507+
if let Some(work) = work {
508+
drained.push((index, analyze_compact_query_work(work, catalog)));
509+
}
510+
}
511+
finished
512+
.lock()
513+
.unwrap_or_else(PoisonError::into_inner)
514+
.extend(drained);
515+
});
476516
}
477-
finished
478517
});
479518
let mut results: Vec<Option<CompiledQueryWorkResult>> = slots.iter().map(|_| None).collect();
480-
for (index, result) in finished.into_iter().flatten() {
519+
for (index, result) in finished
520+
.into_inner()
521+
.unwrap_or_else(PoisonError::into_inner)
522+
{
481523
results[index] = Some(result);
482524
}
483525
results.into_iter().flatten().collect()
@@ -497,7 +539,7 @@ fn analyze_compact_query_work(
497539
&& let Some(validation) = result.validation.take()
498540
{
499541
let local_probes = crate::semantic_validation::models::FunctionProbes::default();
500-
let probes = catalog.map_or(&local_probes, |catalog| &catalog.function_probes);
542+
let probes = catalog.map_or(&local_probes, |catalog| catalog.function_probes.as_ref());
501543
result.validation = Some(validation.and_then(|value| {
502544
crate::semantic_validation::main::map_diagnostics::map_diagnostics(
503545
&sql, dialect, value, probes,

‎crates/sqlbuild-rules-native/src/query_analysis/main/analyze_project_catalog.rs‎

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
pub(crate) mod analyze;
22
pub(crate) mod analyze_project;
3-
pub(crate) mod analyze_project_catalog;
43
pub(crate) mod analyze_project_compact;
4+
pub(crate) mod prepare_project_catalog;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub(crate) fn prepare_project_compact_with_catalog(
2+
request_json: &str,
3+
catalog: &crate::semantic_validation::models::ProjectCatalog,
4+
) -> Result<crate::semantic_validation::types::PreparedCompactAnalysis, String> {
5+
crate::query_analysis::engine::prepare_project_compact_with_catalog(request_json, catalog)
6+
}

‎crates/sqlbuild-rules-native/src/query_analysis/tests/helpers.rs‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,64 @@ pub(crate) fn borrowed_facts_preserve_union_dependencies_and_join_nullability()
578578
);
579579
true
580580
}
581+
582+
pub(crate) fn catalog_views_reuse_one_analysis_pool() -> bool {
583+
let catalog = crate::semantic_validation::models::ProjectCatalog::with_options(
584+
polyglot_sql::DialectType::DuckDB,
585+
polyglot_sql::SchemaValidationOptions::default(),
586+
false,
587+
);
588+
let first = catalog.analysis_pool().expect("test assumption must hold");
589+
let second = catalog.analysis_pool().expect("test assumption must hold");
590+
let from_view = catalog
591+
.analysis_view()
592+
.analysis_pool()
593+
.expect("test assumption must hold");
594+
std::sync::Arc::ptr_eq(&first, &second)
595+
&& std::sync::Arc::ptr_eq(&first, &from_view)
596+
&& first.current_num_threads() == 4
597+
}
598+
599+
pub(crate) fn concurrent_batches_match_serial_batches() -> bool {
600+
let requests: Vec<String> = (0..12)
601+
.map(|index| {
602+
json!({
603+
"queries": (0..=index % 5).map(|query| json!({
604+
"sql": format!(
605+
"WITH ranked AS (SELECT {query} AS order_id, {index} AS amount) \
606+
SELECT order_id, amount * {query} AS amount FROM ranked"
607+
),
608+
"dialect": "duckdb"
609+
})).collect::<Vec<Value>>(),
610+
"templates": (0..=index % 5).map(|query| json!({
611+
"queryIndex": query, "recoverCteFacts": true
612+
})).collect::<Vec<Value>>(),
613+
"projections": (0..=index % 5).map(|query| json!({
614+
"templateIndex": query
615+
})).collect::<Vec<Value>>(),
616+
"workers": 4
617+
})
618+
.to_string()
619+
})
620+
.collect();
621+
let serial: Vec<String> = requests
622+
.iter()
623+
.map(|request| analyze_project_compact_json(request).expect("test assumption must hold"))
624+
.collect();
625+
let concurrent: Vec<String> = std::thread::scope(|scope| {
626+
let handles: Vec<_> = requests
627+
.iter()
628+
.map(|request| scope.spawn(move || analyze_project_compact_json(request)))
629+
.collect();
630+
handles
631+
.into_iter()
632+
.map(|handle| {
633+
handle
634+
.join()
635+
.expect("test assumption must hold")
636+
.expect("test assumption must hold")
637+
})
638+
.collect()
639+
});
640+
concurrent == serial
641+
}

‎crates/sqlbuild-rules-native/src/query_analysis/tests/test_query_analysis.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use crate::query_analysis::tests::helpers::{
55
borrowed_facts_preserve_named_outputs_and_terminal_sources,
66
borrowed_facts_preserve_union_dependencies_and_join_nullability,
77
bound_lineage_survives_type_recovery, canonical_queries_reuse_semantics_and_project_resources,
8-
combined_queries_preserve_standalone_binding,
8+
catalog_views_reuse_one_analysis_pool, combined_queries_preserve_standalone_binding,
99
compact_project_query_interns_repeated_lineage_strings,
10-
interleaved_query_templates_preserve_template_order, larger_later_queries_keep_request_order,
11-
native_compatibility_types_preserve_result_semantics,
10+
concurrent_batches_match_serial_batches, interleaved_query_templates_preserve_template_order,
11+
larger_later_queries_keep_request_order, native_compatibility_types_preserve_result_semantics,
1212
repeated_project_facts_intern_complete_facts,
1313
unannotated_types_require_complete_nested_evidence,
1414
widening_aggregates_require_compatibility_recovery,
@@ -86,6 +86,16 @@ fn given_compact_query_cases_when_analyzing_projects_then_expected_behavior_hold
8686
run: larger_later_queries_keep_request_order,
8787
expected_success: true,
8888
},
89+
CompactQueryAnalysisTestCase {
90+
description: "catalog views reuse one analysis pool",
91+
run: catalog_views_reuse_one_analysis_pool,
92+
expected_success: true,
93+
},
94+
CompactQueryAnalysisTestCase {
95+
description: "concurrent batches match serial batches",
96+
run: concurrent_batches_match_serial_batches,
97+
expected_success: true,
98+
},
8999
];
90100

91101
for test_case in test_cases {

0 commit comments

Comments
 (0)