From 45cef4ca611e085dfe18432fa2d58fccaa5a68be Mon Sep 17 00:00:00 2001 From: SayanthRock <202829406+SayanthRock@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:20:06 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[performance=20improvement]?= =?UTF-8?q?=20Avoid=20String=20allocation=20in=20Segment=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ compiler/rockql-parser/src/lib.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 99f4cf5..3644ef5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -2,3 +2,7 @@ **Learning:** In tight parsing loops in Rust (e.g., `normalize_keywords` and `remove_numeric_separators`), using `.chars().collect::>()` introduces severe performance overhead due to O(N) memory allocation per operation. Similarly, repeatedly allocating new `String` instances for word extraction and `.to_ascii_lowercase()` within loops causes significant heap traffic. **Action:** When performing string transformations or extractions in high-frequency functions, prefer slicing directly with `&str`, checking bytes directly (e.g. `value.as_bytes()`) where possible (such as when scanning for single-byte ASCII characters), and using non-allocating comparison methods like `eq_ignore_ascii_case()` instead of allocating a lowercase string. Always avoid `.chars().collect::>()` when a streaming iterator (`chars()` or `as_bytes()`) suffices. +## 2026-08-12 - [Avoid String allocation during initial parsing segment split] +**Learning:** In `split_segments`, constructing `Segment` previously performed `text.to_owned()` for every split segment. This caused unnecessary memory allocation, as the parsed segment string could just borrow from the original input `&str`. + +**Action:** Update parsing intermediate structs (like `Segment`) to carry string slices (`&'a str`) representing chunks of the input string rather than owning `String`s when they are only used briefly to route segments to transformation parsers. diff --git a/compiler/rockql-parser/src/lib.rs b/compiler/rockql-parser/src/lib.rs index 04e1d44..1ddfaa9 100644 --- a/compiler/rockql-parser/src/lib.rs +++ b/compiler/rockql-parser/src/lib.rs @@ -28,8 +28,8 @@ impl Display for Diagnostic { } #[derive(Debug)] -struct Segment { - text: String, +struct Segment<'a> { + text: &'a str, span: Span, } @@ -47,7 +47,7 @@ pub fn parse(source: &str) -> Result> { let mut diagnostics = Vec::new(); for segment in segments { - match parse_transform(&segment.text, segment.span) { + match parse_transform(segment.text, segment.span) { Ok(transform) => transforms.push(SpannedTransform::new(segment.span, transform)), Err(diagnostic) => diagnostics.push(diagnostic), } @@ -64,7 +64,7 @@ pub fn format_source(source: &str) -> Result> { parse(source).map(|query| format!("{query}\n")) } -fn split_segments(source: &str) -> Vec { +fn split_segments(source: &str) -> Vec> { let mut segments = Vec::new(); for (line_index, line) in source.lines().enumerate() { @@ -88,7 +88,7 @@ fn split_segments(source: &str) -> Vec { segments } -fn push_segment(segments: &mut Vec, raw: &str, line: usize, byte_start: usize) { +fn push_segment<'a>(segments: &mut Vec>, raw: &'a str, line: usize, byte_start: usize) { let text = raw.trim(); if text.is_empty() { return; @@ -96,7 +96,7 @@ fn push_segment(segments: &mut Vec, raw: &str, line: usize, byte_start: let leading_bytes = raw.find(text).unwrap_or(0); segments.push(Segment { - text: text.to_owned(), + text, span: Span::new(line, byte_start + leading_bytes + 1), }); }