diff --git a/.jules/bolt.md b/.jules/bolt.md index 32f1ff6..fa1d4ea 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -36,3 +36,9 @@ ## 2024-05-14 - Remove hot-path BlockId cloning in scheduler tick **Learning:** In the fast path of the scheduler (`process_instance`), `BlockId` was being cloned and looked up in a HashMap for every incomplete step on every tick, regardless of whether a deadline was configured. The fix avoids this allocation using zero-allocation references and an early return. **Action:** Always verify if nested loops are lazily evaluating or bypassing unnecessary object clones/lookups on hot execution paths. Utilize Rust's HashMap with custom Borrow implementations or locally constructed reference maps to query without allocating strings. +## 2025-10-28 - [Zero-Allocation Reference Tuples in Hot Path Iterators] +**Learning:** In the fast path of the scheduler (), passing by reference into avoids allocating and copying s for every step that requires a deadline check. Prior to this, was cloned on every iteration inside a loop processing active steps, introducing significant memory overhead across the cluster. +**Action:** Always prefer accepting references within collections () for database batch operations if the caller only holds references, preventing O(N) string clones on execution hot paths. +## 2025-10-28 - [Zero-Allocation Reference Tuples in Hot Path Iterators] +**Learning:** In the fast path of the scheduler (scheduler.rs:1026), passing &BlockId by reference into get_block_outputs_batch avoids allocating and copying Strings for every step that requires a deadline check. Prior to this, BlockId was cloned on every iteration inside a loop processing active steps, introducing significant memory overhead across the cluster. +**Action:** Always prefer accepting references within collections (&[(InstanceId, &BlockId)]) for database batch operations if the caller only holds references, preventing O(N) string clones on execution hot paths. diff --git a/orch8-engine/src/scheduler.rs b/orch8-engine/src/scheduler.rs index e20976e..d9bb63c 100644 --- a/orch8-engine/src/scheduler.rs +++ b/orch8-engine/src/scheduler.rs @@ -686,7 +686,7 @@ async fn process_waiting_deadlines( for block in &seq.blocks { if let orch8_types::sequence::BlockDefinition::Step(step_def) = block { if step_def.deadline.is_some() { - deadline_keys.push((instance.id, step_def.id.clone())); + deadline_keys.push((instance.id, &step_def.id)); } } } @@ -1025,11 +1025,14 @@ async fn process_instance( // Fast path SLA deadline check for all steps BEFORE concurrency checks. // Batch-fetch any previous block outputs so the loop is N queries -> 1 query. - let deadline_keys: Vec<(InstanceId, BlockId)> = blocks + // ⚡ Bolt: By passing &s.id as a reference to get_block_outputs_batch, we avoid + // an unnecessary `.clone()` on the `BlockId` (String allocation) for every deadline step + // inside this hot scheduler tick loop. + let deadline_keys: Vec<(InstanceId, &BlockId)> = blocks .iter() .filter_map(|b| match b { orch8_types::sequence::BlockDefinition::Step(s) if s.deadline.is_some() => { - Some((instance.id, s.id.clone())) + Some((instance.id, &s.id)) } _ => None, }) diff --git a/orch8-engine/tests/feature_gaps.rs b/orch8-engine/tests/feature_gaps.rs index 8316bd5..85f7304 100644 --- a/orch8-engine/tests/feature_gaps.rs +++ b/orch8-engine/tests/feature_gaps.rs @@ -498,7 +498,8 @@ async fn sqlite_get_batch_chunking_does_not_drop_keys() { keys.push((instance, block_id)); } - let batch = storage.get_block_outputs_batch(&keys).await.unwrap(); + let ref_keys: Vec<(InstanceId, &BlockId)> = keys.iter().map(|(i, b)| (*i, b)).collect(); + let batch = storage.get_block_outputs_batch(&ref_keys).await.unwrap(); assert_eq!(batch.len(), 450, "batch must return all 450 outputs"); // Spot-check a few keys. diff --git a/orch8-storage/src/encrypting.rs b/orch8-storage/src/encrypting.rs index 5c0ae20..e396fbc 100644 --- a/orch8-storage/src/encrypting.rs +++ b/orch8-storage/src/encrypting.rs @@ -673,7 +673,7 @@ impl crate::OutputStore for EncryptingStorage { } async fn get_block_outputs_batch( &self, - keys: &[(InstanceId, orch8_types::ids::BlockId)], + keys: &[(InstanceId, &orch8_types::ids::BlockId)], ) -> Result< std::collections::HashMap< (InstanceId, orch8_types::ids::BlockId), diff --git a/orch8-storage/src/lib.rs b/orch8-storage/src/lib.rs index 856d87a..7c4414d 100644 --- a/orch8-storage/src/lib.rs +++ b/orch8-storage/src/lib.rs @@ -664,9 +664,12 @@ pub trait OutputStore: Send + Sync + 'static { /// /// Returns a map from `(InstanceId, BlockId)` to the latest output /// (ordered by `created_at DESC`). Missing pairs are omitted. + /// + /// Performance Note: `keys` accepts `&BlockId` by reference inside the tuple + /// to avoid expensive allocations/clones in hot paths like the scheduler loop. async fn get_block_outputs_batch( &self, - keys: &[(InstanceId, BlockId)], + keys: &[(InstanceId, &BlockId)], ) -> Result, StorageError>; async fn get_all_outputs( diff --git a/orch8-storage/src/postgres/mod.rs b/orch8-storage/src/postgres/mod.rs index c2a0334..941dbf9 100644 --- a/orch8-storage/src/postgres/mod.rs +++ b/orch8-storage/src/postgres/mod.rs @@ -545,7 +545,7 @@ impl crate::OutputStore for PostgresStorage { async fn get_block_outputs_batch( &self, - keys: &[(InstanceId, BlockId)], + keys: &[(InstanceId, &BlockId)], ) -> Result, StorageError> { outputs::get_batch(self, keys).await } diff --git a/orch8-storage/src/postgres/outputs.rs b/orch8-storage/src/postgres/outputs.rs index 115982b..ce0f33b 100644 --- a/orch8-storage/src/postgres/outputs.rs +++ b/orch8-storage/src/postgres/outputs.rs @@ -68,7 +68,7 @@ pub(super) async fn get( pub(super) async fn get_batch( store: &PostgresStorage, - keys: &[(InstanceId, BlockId)], + keys: &[(InstanceId, &BlockId)], ) -> Result, StorageError> { if keys.is_empty() { return Ok(std::collections::HashMap::new()); diff --git a/orch8-storage/src/sqlite/mod.rs b/orch8-storage/src/sqlite/mod.rs index d09fc9c..dbcff4a 100644 --- a/orch8-storage/src/sqlite/mod.rs +++ b/orch8-storage/src/sqlite/mod.rs @@ -641,7 +641,7 @@ impl crate::OutputStore for SqliteStorage { async fn get_block_outputs_batch( &self, - keys: &[(InstanceId, BlockId)], + keys: &[(InstanceId, &BlockId)], ) -> Result, StorageError> { outputs::get_batch(self, keys).await } diff --git a/orch8-storage/src/sqlite/outputs.rs b/orch8-storage/src/sqlite/outputs.rs index db936ca..3f5dd98 100644 --- a/orch8-storage/src/sqlite/outputs.rs +++ b/orch8-storage/src/sqlite/outputs.rs @@ -45,7 +45,7 @@ const GET_BATCH_CHUNK_SIZE: usize = 400; pub(super) async fn get_batch( storage: &SqliteStorage, - keys: &[(InstanceId, BlockId)], + keys: &[(InstanceId, &BlockId)], ) -> Result, StorageError> { if keys.is_empty() { return Ok(std::collections::HashMap::new());