feat: GraphQLSchema.FastBuilder for High-Performance Schema Construction#4197
feat: GraphQLSchema.FastBuilder for High-Performance Schema Construction#4197andimarek merged 28 commits intographql-java:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request introduces GraphQLSchema.FastBuilder, a high-performance alternative to the standard GraphQLSchema.Builder that achieves ~7x faster build times and 85% reduction in memory allocation for large schemas by eliminating multiple full-schema traversals.
Key Changes:
- New
GraphQLSchema.FastBuilderclass that requires explicit type registration and eliminates traversal-based type collection ShallowTypeRefCollectorfor efficient type reference resolution without deep traversalFindDetachedTypesutility to identify types not reachable from root typesFastSchemaGeneratorthat uses FastBuilder for improved performance- Comprehensive test coverage including comparison tests against standard builder
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
GraphQLSchema.java |
Adds FastBuilder inner class with streamlined schema construction |
ShallowTypeRefCollector.java |
New utility for collecting and resolving type references without recursive traversal |
FindDetachedTypes.java |
New utility for identifying detached types via DFS from roots |
FastSchemaGenerator.java |
Schema generator implementation using FastBuilder |
SchemaGeneratorHelper.java |
Adds getTypes() method to expose built types |
Assert.java |
Optimizes name validation by replacing regex with character-by-character validation |
FindDetachedTypesTest.groovy |
1017 lines of tests for detached type detection |
FastBuilderTest.groovy |
2048 lines of tests for FastBuilder functionality |
FastBuilderComparison*.groovy |
Multiple test files comparing FastBuilder with standard builder |
CreateSchemaBenchmark.java |
Adds benchmark for FastBuilder performance |
BuildSchemaBenchmark.java |
New benchmark isolating schema building from parsing |
build.gradle |
Adds JMH configuration options |
| if (!(unwrapped instanceof GraphQLNamedType)) { | ||
| return this; | ||
| } |
There was a problem hiding this comment.
There is no need to test whether an instance of GraphQLUnmodifiedType is also an instance of GraphQLNamedType - it always is.
| if (!(unwrapped instanceof GraphQLNamedType)) { | |
| return this; | |
| } |
|
hey @rstata ... thanks a lot of this PR. I think the fundamental idea and your use case makes a lot of sense and the implementation also looks acceptable broadly. Is this PR shape what would solve your problem? Do you want to work through this PR to get it merged or you would prefer opening a new one? |
|
This PR works for us. We would use the You should take a look at the following: This class converts a |
|
@rstata one thought: we don't need the FindDetachedTypes traversal I think. Additionally types don't have to be restricted, but can contain all types (minus the root types of query/mutation/subscription) |
|
question @rstata: is this builder to be used programmatically directly or only every via FastSchemaGenerator? How are you using it? |
Our intent is to use it directly. We have a binary file representation for schemas that is fast and memory-efficient to read. Here is the performance for reading the large4 schema:
The result of reading the binary file is something that is close in spirit to a type-definition registry, but is not a type-definition registry. The fast builder was written expressly to support going from our TDR-like schema representation to a |
…er check Replace Pattern.matcher().matches() with direct character validation in Assert.assertValidName(). This method is called in the constructor of every GraphQL type (objects, interfaces, unions, enums, inputs, scalars), field, argument, and directive during schema construction. For large schemas (18k+ types), this eliminates tens of thousands of regex compilations and matches, providing significant performance improvement: - FastBuilder: 25% faster (364ms → 272ms on 18,837-type schema) - Standard Builder: 4% faster (1967ms → 1883ms on same schema) The character-by-character check maintains identical validation semantics while avoiding regex Pattern compilation and Matcher object allocation overhead on every name validation.
Add GraphQLSchema.FastBuilder, a high-performance schema builder that avoids full-schema traversals. This initial implementation includes: - FastBuilder class with constructor accepting code registry builder and root types (query, mutation, subscription) - New private GraphQLSchema constructor for FastBuilder - ShallowTypeRefCollector stub class for future type reference handling - Support for adding scalar types via additionalType() - Automatic addition of built-in directives - Optional validation support (disabled by default) - Comprehensive test suite in FastBuilderTest.groovy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Implement directive handling in FastBuilder with support for type references in directive arguments: - Move ShallowTypeRefCollector to graphql.schema package (for access to package-private replaceType methods) - Implement additionalDirective() with duplicate detection - Implement ShallowTypeRefCollector.handleDirective() to scan arguments - Implement type reference resolution for directive arguments - Support List and NonNull wrapped type references - Throw AssertException for missing type references - Add comprehensive tests for directive type reference resolution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Enumeration types work with the existing additionalType() implementation. Add tests to verify: - Enum types can be added to schema - Enum type matches standard builder output - Directive arguments can reference enum types via GraphQLTypeReference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Extend ShallowTypeRefCollector to handle GraphQLInputObjectType: - Scan input object fields for type references - Implement replaceInputFieldType() for field type resolution - Support nested input types via type references - Support List and NonNull wrapped type references in input fields - Add comprehensive tests for input object type handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Extend ShallowTypeRefCollector to handle applied directives: - Add scanAppliedDirectives() method for scanning applied directive args - Scan applied directives on directive container types (enum, scalar, etc.) - Scan applied directives on input object fields - Implement replaceAppliedDirectiveArgumentType() for type resolution - Update FastBuilder.withSchemaAppliedDirective() to scan for type refs - Add comprehensive tests for applied directive type reference resolution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Extended ShallowTypeRefCollector to handle GraphQLObjectType: - Scans field types for type references - Scans field arguments for type references - Scans applied directives on fields - Scans interfaces for type references - Added resolveOutputType() for output type reference resolution - Added replaceFieldType() for field type replacement - Added ObjectInterfaceReplaceTarget wrapper and replaceObjectInterfaces() - Updated FastBuilder.additionalType() to maintain interface→implementations map - Added comprehensive tests for object type handling: - Field type reference resolution (direct, NonNull, List) - Interface type reference resolution - Interface→implementations map building - Field argument type reference resolution - Applied directive on fields - Error cases for missing types/interfaces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Extended ShallowTypeRefCollector to handle GraphQLInterfaceType: - Scans field types for type references - Scans field arguments for type references - Scans applied directives on fields - Scans extended interfaces for type references - Added handleInterfaceType() method - Added InterfaceInterfaceReplaceTarget wrapper class - Added replaceInterfaceInterfaces() for interface extension resolution - Updated FastBuilder.additionalType() to wire type resolvers from interfaces - Added comprehensive tests for interface type handling: - Basic interface type addition - Field type reference resolution - Interface extending interface via type reference - Type resolver wiring from interface - Field argument type reference resolution - Applied directive on interface fields - Error cases for missing extended interfaces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Extended ShallowTypeRefCollector to handle GraphQLUnionType: - Scans possible types for type references - Applied directives already handled by directive container scan - Added handleUnionType() method - Added UnionTypesReplaceTarget wrapper class - Added replaceUnionTypes() for union member type resolution - Updated FastBuilder.additionalType() to wire type resolvers from unions - Added comprehensive tests for union type handling: - Basic union type addition - Union member type reference resolution - Type resolver wiring from union - Error cases for missing member types - Applied directive on union types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Fixed additionalTypes not being set in FastBuilder path: - Added additionalTypes parameter to FastBuilder private constructor - Added buildAdditionalTypes() method to compute additionalTypes from typeMap - Validation now properly traverses all types in the schema - Added comprehensive tests for validation and edge cases: - withValidation(false) skips validation - withValidation(true) runs validation and catches errors - Circular type reference resolution - Deeply nested type reference resolution (NonNull + List + NonNull) - Complex schema with interfaces, unions, input types - Null handling for types/directives - Built-in directives are added automatically 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add BuildSchemaBenchmark to measure schema construction performance in isolation from SDL parsing. This benchmark compares standard SchemaGenerator against FastSchemaGenerator using the large-schema-4.graphqls test schema (~18,800 types). Changes: - Add FastSchemaGenerator that uses FastBuilder for optimized schema construction from pre-parsed TypeDefinitionRegistry - Add BuildSchemaBenchmark.java with side-by-side comparison of standard vs fast schema generation, parsing SDL once in @setup to isolate build performance - Add jmhProfilers gradle property support for GC profiling (e.g., -PjmhProfilers="gc") - Add FastSchemaGeneratorTest to verify equivalence with standard SchemaGenerator Usage: ./gradlew jmh -PjmhInclude=".*BuildSchemaBenchmark.*" ./gradlew jmh -PjmhInclude=".*BuildSchemaBenchmark.*" -PjmhProfilers="gc"
A reviewer pointed out an unnecessary check was being performed, the commit removes it. Also, in re-reading that code I noticed that the behavior of `Builder.additionalType` and `FastBuilder.additionalType` were subtly different. To prevent potential confusion, this commit renames it to `addType` and documents its behavior more explicitly.
just pushed a commit that removes the detached-type detection. it does remove a lot of code (esp. a lot of testing code, because that was tested pretty extensively). so this change is a win in terms of simplicity. here is the impact on the benchmarks:
|
Clean up test files by removing development phase comments like "==================== Phase X: ... ====================" that were used during incremental implementation.
- Fix reference to non-existent addAdditionalType(s) method - Correct additionalTypes documentation to reflect actual behavior - Simplify type unwrapping code
- addType() and addTypes() now take GraphQLNamedType instead of GraphQLType - Remove unnecessary unwrapping logic since only named types are accepted - Remove null checks from builder methods (fail fast on null) - Update FastSchemaGenerator to use GraphQLNamedType - Update FastBuilderComparisonTest to use GraphQLNamedType
|
FastBuilder was not handling introspective types nor the scalars needed by them correctly. The most recent commit ("Fixes") adds tests for these bugs, and fixes the bugs. We also removed some tests for FastBuilder.addType (et al) to adjust for recent changes in its behavior. |
A reviewer noted that the dispirate ways that the classic builder and the new "fast" builder handled built-in directives created two parallel lists, which is a maintenance burden. This commit consolidates the lists.
|
I've added another commit here because an airbnb reviewer noted that the classic builder and the new "fast" builder handled built-in directives using two separate lists of built-ins, which creates a maintenance burden (because changes need to be updated in two places). This commit consolidates the lists into one place. This PR maintains and also documents the behavior that says that the |
@rstata the different treatment of skip/include vs others seems weird and not intentionally ... I will double check, thanks |
…erences The batch method was adding applied directives directly without scanning their arguments for GraphQLTypeReference instances. This left unresolved type references in the built schema. Fix by delegating to the singular withSchemaAppliedDirective() method which already performs the scan, matching the pattern used by the standard Builder. Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
@andimarek - i've merged |
Fixed a bug after resolving conflicts but forgot to add it to the merge commit.
|
oops - i left a small fixed unstaged in the previous push, the latest commit fixes that. |
- Remove unused Pattern import from Assert.java - Add @NullMarked annotation to ShallowTypeRefCollector - Expand FastBuilder class documentation with clear guidance on: - When to use FastBuilder (large schemas, known types, pre-validated) - When NOT to use FastBuilder (type discovery, type reuse, dynamic construction) - Key differences from standard Builder - Example usage code - Add mutation warnings to addType() and build() methods - Document type mutation behavior in ShallowTypeRefCollector.replaceTypes() Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
@rstata I am happy to merge it. Your thoughts? One question: Is FastSchemaGenerator useable in general? We could make it even public or experimental or are there edges cases with SDL based fast schema generation that you know of? |
- Mark assertValidName parameter as @nullable since callers may pass null - Keep null check in assertValidName (required for @nullable contract) - Remove redundant empty check that was duplicated in isValidName - Use String.valueOf() for null-safe error message formatting Co-Authored-By: Claude Opus 4.5 <[email protected]>
1411c38 to
cd32b5c
Compare
- Change from @internal to @experimentalapi, making it part of the public API - Add @NullMarked and @nullable annotations for null safety - Enable schema validation by default (previously skipped for performance) - Add withValidation option to SchemaGenerator.Options for explicit opt-out - Update documentation to reference FastBuilder limitations - Add tests for validation behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
FastBuilder now validates that all interfaces and unions have type resolvers, matching the behavior of the standard GraphQLSchema.Builder. This validation is always performed regardless of the withValidation() setting, which only controls GraphQL spec validation via SchemaValidator. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
|
@andimarek - i do think I made changes to make it more appropriate for public use. the most important of these changes is that i added a switch to toggle validation which is set to do validation by default, which seems safer for a public api. i also updated some documentation, added nullability annotations, and added some tests. (regarding the validation switch, i originally drafted this as an additional argument to while making these changes i noticed that the new fast-builder does not do the type-resolver validation that the existing builder does. when the |
|
@rstata looking good ... I agree with making the FastSchemaGenerator experimental, it will be very useful as most schema generation is done this way. I am happy to merge it. |
|
@andimarek - i'm ready to have it merged, thanks for your help!! |
Defines a new
GraphQLSchemabuilder class that is more restrictive than the existing one but reduces time and space requirements by more than a factor of five. See Issue #4196 for details.