Skip to content

Improved access speed of isPossibleType #4040

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 10, 2025
Merged

Conversation

bbakerman
Copy link
Member

@bbakerman bbakerman commented Jul 4, 2025

This builds on #4033 but it creates a cache in the ImmutableTypeRegistry so that we don't need to iterate all types on each call

@xuorig - This takes your PR and extends it more to have a reverse cache of interface -> implementing type def AND it uses your cheaper TypeInfo method in more places

This means that algo change you made in your PR is not really needed. Why? Because when we are in read only mode, we don't have to loop all types to find all implemeenting types of an interface, we did that once at build time for all interfaces. So later the type checking will be faster

I originally had a cache of interface to implementing types but it turns out its slower to eagerly build this cache than it is to run the code that used it so I brought @xuorig changes into this PR

So in benchmark terms

c7495f921d2bd10afcb4db4616817a4c67de5ddb - master back before we started @xuorig changes

Benchmark                                                         Mode  Cnt      Score      Error    Units
CreateExtendedSchemaBenchmark.benchmarkLargeSchemaCreate         thrpt    9      4.979 ±    1.058  ops/min
CreateExtendedSchemaBenchmark.benchmarkLargeSchemaCreateAvgTime   avgt    9  11205.547 ± 1306.952    ms/op


This PR

Benchmark                                                         Mode  Cnt     Score    Error    Units
CreateExtendedSchemaBenchmark.benchmarkLargeSchemaCreate         thrpt    9    46.436 ±  2.512  ops/min
CreateExtendedSchemaBenchmark.benchmarkLargeSchemaCreateAvgTime   avgt    9  1250.350 ± 26.185    ms/op


master without this PR

Benchmark                                                         Mode  Cnt     Score    Error    Units
CreateExtendedSchemaBenchmark.benchmarkLargeSchemaCreate         thrpt    9    45.801 ±  2.211  ops/min
CreateExtendedSchemaBenchmark.benchmarkLargeSchemaCreateAvgTime   avgt    9  1253.292 ± 58.405    ms/op

So overall things have gotten significantly faster in building large schemas with lots of interfaces and extends. in fact since @xuorig started his investigations and inspired changes, we are 10x faster on this benchmark

This PR does not add much more raw throughput BUT it will reduce memory used

@bbakerman bbakerman added this to the 25.x breaking changes milestone Jul 4, 2025
Copy link
Contributor

github-actions bot commented Jul 4, 2025

Test Results

  323 files    323 suites   2m 53s ⏱️
4 942 tests 4 932 ✅ 10 💤 0 ❌
5 031 runs  5 021 ✅ 10 💤 0 ❌

Results for commit d0cbc63.

♻️ This comment has been updated with latest results.

}
}
return mapBuilder.build();
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This now pre-computes the interface -> ImplementingTypeDefinition s at registry build time and hence later in the schema type checking stage, the repeated ask for interface information is much faster

*
* @return true if the registry has a type by that name
*/
public boolean hasType(String name) {
return types.containsKey(name) || ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(name) || scalarTypes.containsKey(name) || objectTypeExtensions.containsKey(name);
}
Copy link
Member Author

@bbakerman bbakerman Jul 4, 2025

Choose a reason for hiding this comment

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

extra helper by string name - this will be lighter in lookup terms later on

public Optional<TypeDefinition> getType(Type type) {
String typeName = TypeInfo.typeInfo(type).getName();
return getType(typeName);
return getType(typeName(type));
Copy link
Member Author

@bbakerman bbakerman Jul 4, 2025

Choose a reason for hiding this comment

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

TypeInfo.typeInfo(type).getName() allocates a TypeInfo object in order to just get a single attribute of it - the name of the type (and not its null wrapping etc...)

So this is more light weight and we avoid an extra allocation

This is then repeated in all the getType lookup methods

public Optional<TypeDefinition> getType(String typeName) {
return Optional.ofNullable(getTypeOrNull(typeName));
Copy link
Member Author

Choose a reason for hiding this comment

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

The older Optional returning methods are left in place but the new getTypeOrNull are more efficient - not need for a Optional allocation in order to know if something is present or not

So all the other inner code that used Optional<T> getType now used T getTypeOrNull variants

}
return Optional.empty();
return null;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is cheaper - no Optional allocated

return definition instanceof UnionTypeDefinition || definition instanceof InterfaceTypeDefinition;
TypeDefinition typeDefinition = getTypeOrNull(type);
if (typeDefinition != null) {
return typeDefinition instanceof UnionTypeDefinition || typeDefinition instanceof InterfaceTypeDefinition;
Copy link
Member Author

Choose a reason for hiding this comment

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

See now we use the cheaper getTypeOrNull methods internally

return definition instanceof ObjectTypeDefinition || definition instanceof InterfaceTypeDefinition;
TypeDefinition typeDefinition = getTypeOrNull(type);
if (typeDefinition != null) {
return typeDefinition instanceof ObjectTypeDefinition || typeDefinition instanceof InterfaceTypeDefinition;
Copy link
Member Author

Choose a reason for hiding this comment

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

cheaper

@@ -569,7 +665,7 @@ public boolean isObjectTypeOrInterface(Type type) {
* @return true if its an object type
*/
public boolean isObjectType(Type type) {
return getType(type, ObjectTypeDefinition.class).isPresent();
return getTypeOrNull(type, ObjectTypeDefinition.class) != null;
Copy link
Member Author

Choose a reason for hiding this comment

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

cheaper

getAllImplementationsOf(targetInterface),
typeDefinition -> typeDefinition instanceof ObjectTypeDefinition,
typeDefinition -> (ObjectTypeDefinition) typeDefinition
);
Copy link
Member Author

Choose a reason for hiding this comment

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

unwound the .stream() and replaced it with a cheaper immutable builder version. This is known to be faster

TypeDefinition targetObjectTypeDef = getType(possibleType).get();
TypeDefinition abstractTypeDef = getType(abstractType).get();
TypeDefinition targetObjectTypeDef = Objects.requireNonNull(getTypeOrNull(possibleType));
TypeDefinition abstractTypeDef = Objects.requireNonNull(getTypeOrNull(abstractType));
Copy link
Member Author

Choose a reason for hiding this comment

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

required for nullaway now that I marked a few things

if (checkType.isPresent()) {
if (checkType.get().getName().equals(targetObjectTypeDef.getName())) {
ObjectTypeDefinition checkType = getTypeOrNull(memberType, ObjectTypeDefinition.class);
if (checkType != null) {
Copy link
Member Author

Choose a reason for hiding this comment

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

cheaper

}
}
return (TypeName) type;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is take from @xuorig s code in his PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is cheaper than allocating a TypeInfo object just to get its name

*/
public static String typeName(Type<?> type) {
return getTypeName(type).getName();
}
Copy link
Member Author

Choose a reason for hiding this comment

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

a version that is just the string of the TypeName - easier to use in map lookups

allImplementationsOf.collect({ it.getName() }).every { it.startsWith("DT") }

implementationsOf.size() == 5
implementationsOf.collect({ it.getName() }).every { it.startsWith("DT") }
Copy link
Member Author

Choose a reason for hiding this comment

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

This is just to make sure we did what we thought we would do and I can debug and confirm the pre-built map is as expected and its going to be cheaper when a full schema build is done and repeated calls are made on it to check interfaces and types

Copy link
Member Author

Choose a reason for hiding this comment

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

However I killed the cache code but I figured this code is still valid

@@ -32,7 +32,7 @@ import static java.lang.String.format
class SchemaTypeCheckerTest extends Specification {

static TypeDefinitionRegistry parseSDL(String spec) {
new SchemaParser().parse(spec)
new SchemaParser().parse(spec).readOnly()
Copy link
Member Author

Choose a reason for hiding this comment

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

This is more real - in the production code the registry is immutable read only mode

where:
typeOfReg | _
"mutable" | _
"immutable" | _
Copy link
Member Author

Choose a reason for hiding this comment

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

test both variations work as expected

where:
typeOfReg | _
"mutable" | _
"immutable" | _
Copy link
Member Author

Choose a reason for hiding this comment

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

test both variations work as expected

where:
typeOfReg | _
"mutable" | _
"immutable" | _
Copy link
Member Author

Choose a reason for hiding this comment

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

test both variations work as expected

@xuorig
Copy link

xuorig commented Jul 5, 2025

.I would have thought the schema checks hit every interface but perhaps not. I need to look into this more

Yeah not quite, at least not for this particular issue. isPossibleType is mostly called from isSubType which is used to check that each field on interface / matching concrete object type are compatible types. So depending on the benchmark, could vary lot. Need lots of abstract type fields on interfaces to see an effect.


./gradlew clean jmhJar

java -jar "$JAR" "$@"
Copy link
Member Author

Choose a reason for hiding this comment

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

A little helper to make it easier to run the JMH tests on the command line

f1 : String
}
*/
private static String mkSDL() {
Copy link
Member Author

Choose a reason for hiding this comment

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

a benchmark with lots of extends type X and interfaces

if (!typeRegistry.hasType(unwrapped)) {
String name = TypeInfo.typeName(t);
if (!typeRegistry.hasType(name)) {
TypeName unwrapped = TypeInfo.typeInfo(t).getTypeName();
Copy link
Member Author

Choose a reason for hiding this comment

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

faster look up - we only create a TypeInfo if its in error

if (!typeRegistry.hasType(unwrapped)) {
String name = TypeInfo.typeName(ivType);
if (!typeRegistry.hasType(name)) {
TypeName unwrapped = TypeInfo.typeInfo(ivType).getTypeName();
Copy link
Member Author

Choose a reason for hiding this comment

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

faster look up - we only create a TypeInfo if its in error

if (interfaceTypeDef.getName().equals(targetInterface.getName())) {
return true;
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

moved away from .stream() to the faster immutable code

}
}
}
return false;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is @xuorig s code from his PR in this place

"[named!]" | "named"
"[named!]!" | "named"
"[[named!]!]" | "named"
}
Copy link
Member Author

Choose a reason for hiding this comment

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

from @xuorig PR

@dondonz
Copy link
Member

dondonz commented Jul 6, 2025

Wow the benchmark improvement is super impressive. Thanks so much @xuorig !

*
* @return an optional {@link TypeDefinition} or empty if it's not found
*/
public <T extends TypeDefinition> Optional<T> getType(String typeName, Class<T> ofType) {
Copy link
Member

Choose a reason for hiding this comment

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

Just return null instead using Optional? Saves also an object allocation

Copy link
Member

Choose a reason for hiding this comment

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

Ah .. I see you have it actually below ... how about just killing this method then?

Copy link
Member Author

Choose a reason for hiding this comment

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

how about just killing this method then?

I will deprecate them but I think they are still used in our code

Copy link
Member Author

Choose a reason for hiding this comment

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

They are all now deprecated

@@ -29,7 +29,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
Copy link
Member Author

Choose a reason for hiding this comment

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

Getting rid of the Optional methods and using the getOrNull ones

public Optional<TypeDefinition> getType(Type type) {
String typeName = TypeInfo.typeInfo(type).getName();
return getType(typeName);
return Optional.ofNullable(getTypeOrNull(type));
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Now deprecated - getTypeOrNull preferred

@bbakerman bbakerman requested a review from andimarek July 7, 2025 23:10
@dondonz dondonz merged commit ecaebb1 into master Jul 10, 2025
2 checks passed
@dondonz dondonz deleted the is-possible-type-improvements branch July 10, 2025 00:42
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.

4 participants