Skip to content

bluejay-parser: performance optimizations#95

Open
swalkinshaw wants to merge 1 commit intomainfrom
parser-perf-optimizations
Open

bluejay-parser: performance optimizations#95
swalkinshaw wants to merge 1 commit intomainfrom
parser-perf-optimizations

Conversation

@swalkinshaw
Copy link
Contributor

~16% faster schema parsing, ~12% faster executable parsing

Key optimizations:

  • Rewrite block string parser: direct string processing instead of sub-lexer + Vec<Vec> (~10%)
  • Compact Span: u32 start+len (8 bytes) instead of Range (16 bytes), add Copy (~3%)
  • Field: consume-then-check for alias instead of peek(1) (~2%)
  • Optimize next_if_* methods: peek+consume in single buffer operation (~1%)
  • Lazy depth_limiter.bump(): only bump when optional elements exist
  • Add Copy to DepthLimiter + preallocate Vec capacity in DefinitionDocument

Also adds benchmarks for ExecutableDocument parsing with a large fixture, and updates downstream crates to use Copy semantics on Span (clone → deref).

Note: this is a manually curated (with the help of Claude) and cleaned up version of an /autoresearch run

…rsing

Key optimizations:
- Rewrite block string parser: direct string processing instead of sub-lexer + Vec<Vec<Token>> (~10%)
- Compact Span: u32 start+len (8 bytes) instead of Range<usize> (16 bytes), add Copy (~3%)
- Field: consume-then-check for alias instead of peek(1) (~2%)
- Optimize next_if_* methods: peek+consume in single buffer operation (~1%)
- Lazy depth_limiter.bump(): only bump when optional elements exist
- Add Copy to DepthLimiter + preallocate Vec capacity in DefinitionDocument

Also adds benchmarks for ExecutableDocument parsing with a large fixture,
and updates downstream crates to use Copy semantics on Span (clone → deref).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@swalkinshaw swalkinshaw requested a review from adampetro March 17, 2026 16:47
Comment on lines +35 to +42
let arguments = if VariableArguments::is_match(tokens) {
Some(VariableArguments::from_tokens(
tokens,
depth_limiter.bump()?,
)?)
} else {
None
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just change the return type of TryFromTokens::try_from_tokens to return the transposed type and have this be the implementation? I think we call transpose almost every time we call this

Comment on lines +41 to +42
// Check if there are any escaped block quotes
let has_escapes = raw.contains("\\\"\"\"");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be more efficient to set a bool in our scan above?

Comment on lines +49 to 68
// Count lines first for pre-allocation
let mut line_count = 1usize;
{
let mut j = 0;
while j < raw_len {
if raw_bytes[j] == b'\r' {
line_count += 1;
if j + 1 < raw_len && raw_bytes[j + 1] == b'\n' {
j += 2;
} else {
j += 1;
}
} else if raw_bytes[j] == b'\n' {
line_count += 1;
j += 1;
} else {
j += 1;
}
Self::Newline => lines.push(Vec::new()),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pull this out into a helper?

.position(|token| !matches!(token, Self::Whitespace(_)))
.filter_map(|&(start, end)| {
let line = &raw[start..end];
let indent = line.len() - line.trim_start_matches([' ', '\t']).len();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be faster to do something like

line.as_bytes().iter().position(|b| b != b' ' && b != b'\t')

if let Some((front_offset, end_offset)) = front_offset.zip(end_offset) {
let start = front_offset;
let end = lines.len() - end_offset;
if !has_escapes && first + 1 == last && first == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if !has_escapes && first + 1 == last && first == 0 {
if !has_escapes && first == 0 && last == 1 {

Comment on lines +7 to +8
start: u32,
len: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why u32 instead of usize?


/// A depth limiter is used to limit the depth of the AST. This is useful to prevent stack overflows.
/// This intentionally does not implement `Clone` or `Copy` to prevent passing this down the call stack without bumping.
#[derive(Clone, Copy)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually not implementing these was an explicit design decision, see the comment above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants