Skip to content

bluejay-validator performance optimizations#96

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

bluejay-validator performance optimizations#96
swalkinshaw wants to merge 1 commit intomainfrom
validator-perf-optimizations

Conversation

@swalkinshaw
Copy link
Contributor

Reduce validator allocation overhead — ~28% faster on representative queries

Optimizations:

  • Replace HashSet/HashMap with Vec + linear scan in hot paths where N is small (parent_fragments, cycle detection, required arguments, argument equivalence, type overlap checks)
  • Eliminate Path Vec allocation by making Path Copy
  • Optimize duplicates() to skip BTreeMap allocation when no duplicates found (common case), and avoid intermediate (K,T) vec
  • Reuse Cache's fragment_definitions HashMap in FragmentSpreadTargetDefined instead of building a separate HashSet
  • Collect errors via &mut Vec in FieldSelectionMerging instead of returning intermediate Vecs

Also adds a criterion benchmark suite for the validation pipeline.

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

…queries

Optimizations:

- Replace HashSet/HashMap with Vec + linear scan in hot paths where N
  is small (parent_fragments, cycle detection, required arguments,
  argument equivalence, type overlap checks)
- Eliminate Path Vec allocation by making Path Copy
- Optimize duplicates() to skip BTreeMap allocation when no duplicates
  found (common case), and avoid intermediate (K,T) vec
- Reuse Cache's fragment_definitions HashMap in FragmentSpreadTargetDefined
  instead of building a separate HashSet
- Collect errors via &mut Vec in FieldSelectionMerging instead of
  returning intermediate Vecs

Also adds a criterion benchmark suite for the validation pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@swalkinshaw swalkinshaw requested a review from adampetro March 17, 2026 18:15
Comment on lines +142 to +145
(
TypeDefinitionReference::Interface(_),
TypeDefinitionReference::Interface(_),
) => true,
Copy link
Contributor

Choose a reason for hiding this comment

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

I found this a bit confusing but see how it works based on the existing code. Maybe we could add a comment explaining this case?

Comment on lines +77 to +80
// Fast path: if either type is not a composite type, spread is not applicable
if !Self::is_composite(parent_type) || !Self::is_composite(fragment_type) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

😅 kinda confusing how I wrote this because I would think spread would not be possible for non-composite parent types, I guess it kinda makes sense because we shouldn't be returning an error here because there would be a different error from a different rule (FragmentsOnCompositeTypes)

Comment on lines +98 to 105
fn is_composite(t: TypeDefinitionReference<'a, S::TypeDefinition>) -> bool {
matches!(
(parent_type_possible_types, fragment_possible_types),
(Some(parent_type_possible_types), Some(fragment_possible_types)) if parent_type_possible_types
.intersection(&fragment_possible_types)
.next()
.is_none(),
t,
TypeDefinitionReference::Object(_)
| TypeDefinitionReference::Interface(_)
| TypeDefinitionReference::Union(_)
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

there should already be an is_composite method on TypeDefinitionReference

pub fn is_composite(&self) -> bool {
matches!(self, Self::Object(_) | Self::Union(_) | Self::Interface(_))
}

Comment on lines +107 to +144
fn type_contains_name(
&self,
t: TypeDefinitionReference<'a, S::TypeDefinition>,
name: &str,
) -> bool {
match t {
TypeDefinitionReference::Object(_) => t.name() == name,
TypeDefinitionReference::Interface(itd) => self
.schema_definition
.get_interface_implementors(itd)
.any(|otd| ObjectTypeDefinition::name(otd) == name),
TypeDefinitionReference::Union(utd) => utd
.union_member_types()
.iter()
.any(|member| member.name() == name),
_ => false,
}
}

fn types_have_overlap(
&self,
a: TypeDefinitionReference<'a, S::TypeDefinition>,
b: TypeDefinitionReference<'a, S::TypeDefinition>,
) -> bool {
// Iterate over possible types of `a` and check if any is in `b`
match a {
TypeDefinitionReference::Object(_) => self.type_contains_name(b, a.name()),
TypeDefinitionReference::Interface(itd) => self
.schema_definition
.get_interface_implementors(itd)
.any(|otd| self.type_contains_name(b, ObjectTypeDefinition::name(otd))),
TypeDefinitionReference::Union(utd) => utd
.union_member_types()
.iter()
.any(|member| self.type_contains_name(b, member.name())),
_ => false,
}
}
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 makes sense to combine these and do a single match (a, b). I think the case where both are abstract would be more expensive than it needs to be because we're fetching the possible types of b from the schema every single time we iterate through a's possible types, whereas with a match (a, b) I think we could get both beforehand before doing the looping

Comment on lines +29 to +32
if self
.cache
.fragment_definition(fragment_spread.name())
.is_none()
Copy link
Contributor

Choose a reason for hiding this comment

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

👌

Comment on lines 33 to 35
pub fn members(&self) -> &[&'a E::Selection] {
&self.members
&[]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

this is just wrong now, no?

Comment on lines +9 to +15
// Collect items first to check for duplicates without BTreeMap
let items: Vec<T> = iter.collect();

indexed.into_iter().filter(|(_, values)| values.len() > 1)
// If 0 or 1 items, no duplicates possible — avoid any allocation
if items.len() <= 1 {
return Vec::new().into_iter();
}
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 we could optimize this further by calling next() twice into local variables and if the second one is None then we return without ever allocating a vector or putting anything on the heap?

Comment on lines +18 to +21
let has_dupes = items.iter().enumerate().any(|(i, el)| {
let k = key(*el);
items[..i].iter().any(|prev| key(*prev) == k)
});
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 we could use Itertools::array_combinations to achieve the same thing with less code?

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