Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
## 2026-04-08 - [Performance: Defer Allocation during Traversal]
**Learning:** During DAG traversals, creating owned variants of identifiers (like `file.to_path_buf()`) *before* checking `visited` HashSets results in heap allocations (O(E)) for every edge instead of every visited node (O(V)). By moving the `&PathBuf` allocation strictly *after* all HashSet `contains` checks using the borrowed reference (`&Path`), we drastically reduce memory churn.
**Action:** Always check `HashSet::contains` with a borrowed reference *before* creating the owned version required by `HashSet::insert`, especially in performance-critical graph traversal paths.

## 2024-05-18 - [Performance: Dynamic SQL Generation for Cloudflare D1]
**Learning:** During dynamic SQL generation for Cloudflare D1 targets (e.g., `build_upsert_stmt` and `build_delete_stmt`), using intermediate `Vec` allocations and `format!` in loops causes unnecessary heap allocations and string copies.
**Action:** For dynamic SQL generation, always use `String::with_capacity` to preallocate the exact or approximate size, and use the `write!` macro (via `std::fmt::Write`) to construct queries directly to minimize heap allocations.
10 changes: 6 additions & 4 deletions crates/ast-engine/src/tree_sitter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,8 @@ impl ContentExt for String {
let mut bytes = std::mem::take(self).into_bytes();
let original_len = bytes.len();
bytes.splice(safe_start..safe_end, full_inserted);
*self = Self::from_utf8(bytes).unwrap_or_else(|e| {
Self::from_utf8_lossy(&e.into_bytes()).into_owned()
});
*self = Self::from_utf8(bytes)
.unwrap_or_else(|e| Self::from_utf8_lossy(&e.into_bytes()).into_owned());

// We calculate new_end_byte using the difference in the new overall string length
// to correctly align the end offset, taking any potential replacement bytes from
Expand Down Expand Up @@ -791,7 +790,10 @@ mod test {

let tree2 = parse_lang(|p| p.parse(&src, Some(&tree)), &Tsx.get_ts_language())?;
let fresh_tree = parse(&src)?;
assert_eq!(tree2.root_node().to_sexp(), fresh_tree.root_node().to_sexp());
assert_eq!(
tree2.root_node().to_sexp(),
fresh_tree.root_node().to_sexp()
);
Ok(())
}
}
84 changes: 65 additions & 19 deletions crates/flow/src/targets/d1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use recoco::setup::{ChangeDescription, CombinedState, ResourceSetupChange, Setup
use recoco::utils::prelude::Error as RecocoError;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::fmt::Write;
use std::hash::Hash;
use std::sync::Arc;

Expand Down Expand Up @@ -303,7 +304,6 @@ impl D1ExportContext {
let mut columns = vec![];
let mut placeholders = vec![];
let mut params = vec![];
let mut update_clauses = vec![];

// Extract key parts - KeyValue is a wrapper around Box<[KeyPart]>
for (idx, _key_field) in self.key_fields_schema.iter().enumerate() {
Expand All @@ -320,44 +320,90 @@ impl D1ExportContext {
columns.push(value_field.name.clone());
placeholders.push("?".to_string());
params.push(value_to_json(value)?);
update_clauses.push(format!(
"{} = excluded.{}",
value_field.name, value_field.name
));
}
}

let sql = format!(
"INSERT INTO {} ({}) VALUES ({}) ON CONFLICT DO UPDATE SET {}",
self.table_name,
columns.join(", "),
placeholders.join(", "),
update_clauses.join(", ")
// Calculate capacity needed:
// "INSERT INTO " = 12
// table_name len
// " (" = 2
// columns approx
// ") VALUES (" = 10
// placeholders approx
// ") ON CONFLICT DO UPDATE SET " = 28
// update_clauses approx

let mut sql = String::with_capacity(
52 + self.table_name.len()
+ columns.iter().map(|c| c.len() + 2).sum::<usize>()
+ placeholders.len() * 3
+ self
.value_fields_schema
.iter()
.map(|f| f.name.len() * 2 + 13)
.sum::<usize>(),
);

write!(sql, "INSERT INTO {} (", self.table_name).unwrap();
for (i, col) in columns.iter().enumerate() {
if i > 0 {
write!(sql, ", ").unwrap();
}
write!(sql, "{}", col).unwrap();
}
write!(sql, ") VALUES (").unwrap();
for (i, p) in placeholders.iter().enumerate() {
if i > 0 {
write!(sql, ", ").unwrap();
}
write!(sql, "{}", p).unwrap();
}
write!(sql, ") ON CONFLICT DO UPDATE SET ").unwrap();

let mut first_update = true;
for (idx, _) in values.fields.iter().enumerate() {
if let Some(value_field) = self.value_fields_schema.get(idx) {
if !first_update {
write!(sql, ", ").unwrap();
}
write!(sql, "{0} = excluded.{0}", value_field.name).unwrap();
first_update = false;
}
}

Ok((sql, params))
}

pub fn build_delete_stmt(
&self,
key: &KeyValue,
) -> Result<(String, Vec<serde_json::Value>), RecocoError> {
let mut where_clauses = vec![];
let mut params = vec![];

let mut sql = String::with_capacity(
14 + self.table_name.len()
+ 7
+ self
.key_fields_schema
.iter()
.map(|f| f.name.len() + 5)
.sum::<usize>(),
Comment on lines +383 to +390
);

write!(sql, "DELETE FROM {} WHERE ", self.table_name).unwrap();

let mut first = true;
for (idx, _key_field) in self.key_fields_schema.iter().enumerate() {
if let Some(key_part) = key.0.get(idx) {
where_clauses.push(format!("{} = ?", self.key_fields_schema[idx].name));
if !first {
write!(sql, " AND ").unwrap();
}
write!(sql, "{} = ?", self.key_fields_schema[idx].name).unwrap();
params.push(key_part_to_json(key_part)?);
first = false;
}
}

let sql = format!(
"DELETE FROM {} WHERE {}",
self.table_name,
where_clauses.join(" AND ")
);

Ok((sql, params))
}

Expand Down
4 changes: 0 additions & 4 deletions crates/flow/tests/d1_cache_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,5 @@ mod d1_no_cache_tests {
.expect("Failed to create context without caching");

// Should compile and work without cache field
assert!(
true,
"D1ExportContext created successfully without caching feature"
);
}
}
20 changes: 10 additions & 10 deletions crates/rule-engine/src/check_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub enum CheckHint<'r> {
pub fn check_rule_with_hint<'r>(
rule: &'r Rule,
utils: &'r RuleRegistration,
constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
transform: &'r Option<Transform>,
constraints: &RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
transform: &Option<Transform>,
fixer: &Vec<Fixer>,
hint: CheckHint<'r>,
) -> RResult<()> {
Expand Down Expand Up @@ -56,8 +56,8 @@ pub fn check_rule_with_hint<'r>(
fn check_vars_in_rewriter<'r>(
rule: &'r Rule,
utils: &'r RuleRegistration,
constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
transform: &'r Option<Transform>,
constraints: &RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
transform: &Option<Transform>,
fixer: &Vec<Fixer>,
upper_var: &RapidSet<String>,
) -> RResult<()> {
Expand Down Expand Up @@ -85,8 +85,8 @@ fn check_utils_defined(
fn check_vars<'r>(
rule: &'r Rule,
utils: &'r RuleRegistration,
constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
transform: &'r Option<Transform>,
constraints: &RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
transform: &Option<Transform>,
fixer: &Vec<Fixer>,
) -> RResult<()> {
let vars = get_vars_from_rules(rule, utils);
Expand All @@ -104,9 +104,9 @@ fn get_vars_from_rules<'r>(rule: &'r Rule, utils: &'r RuleRegistration) -> Rapid
vars
}

fn check_var_in_constraints<'r>(
fn check_var_in_constraints(
mut vars: RapidSet<String>,
constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
constraints: &RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
) -> RResult<RapidSet<String>> {
for rule in constraints.values() {
for var in rule.defined_vars() {
Expand All @@ -125,9 +125,9 @@ fn check_var_in_constraints<'r>(
Ok(vars)
}

fn check_var_in_transform<'r>(
fn check_var_in_transform(
mut vars: RapidSet<String>,
transform: &'r Option<Transform>,
transform: &Option<Transform>,
) -> RResult<RapidSet<String>> {
let Some(transform) = transform else {
return Ok(vars);
Expand Down
6 changes: 5 additions & 1 deletion crates/rule-engine/src/rule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,11 @@ impl Rule {

pub fn defined_vars(&self) -> RapidSet<String> {
match self {
Rule::Pattern(p) => p.defined_vars().into_iter().map(|s| s.to_string()).collect(),
Rule::Pattern(p) => p
.defined_vars()
.into_iter()
.map(|s| s.to_string())
.collect(),
Rule::Kind(_) => RapidSet::default(),
Rule::Regex(_) => RapidSet::default(),
Rule::NthChild(n) => n.defined_vars(),
Expand Down
5 changes: 1 addition & 4 deletions crates/rule-engine/src/rule/referent_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ impl<R> Clone for Registration<R> {

impl<R> Registration<R> {
fn read(&self) -> Arc<RapidMap<String, R>> {
self.0
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
self.0.read().unwrap_or_else(|e| e.into_inner()).clone()
}
pub(crate) fn contains_key(&self, key: &str) -> bool {
self.read().contains_key(key)
Expand Down