-
-
Notifications
You must be signed in to change notification settings - Fork 250
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
Improved execution field merging #2019
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -306,27 +306,43 @@ object Executor { | |
ZIO.succeed(GraphQLResponse(NullValue, List(error))) | ||
|
||
private[caliban] def mergeFields(field: Field, typeName: String): List[Field] = { | ||
val map = new java.util.LinkedHashMap[String, Field]() | ||
var modified = false | ||
|
||
field.fields.foreach { field => | ||
if (field._condition.forall(_.contains(typeName))) { | ||
map.compute( | ||
field.aliasedName, | ||
(_, f) => | ||
if (f == null) field | ||
else { | ||
modified = true | ||
f.copy(fields = f.fields ::: field.fields) | ||
} | ||
) | ||
} else { | ||
modified = true | ||
def haveSameCondition(head: Field, tail: List[Field]): Boolean = { | ||
val condition = head._condition | ||
var remaining = tail | ||
while (!remaining.isEmpty) { | ||
if (remaining.head._condition != condition) return false | ||
remaining = remaining.tail | ||
} | ||
true | ||
} | ||
|
||
// Avoid conversions if no modification took place | ||
if (modified) map.values().asScala.toList else field.fields | ||
def matchesTypename(f: Field): Boolean = | ||
f._condition.isEmpty || f._condition.get.contains(typeName) | ||
|
||
def mergeFields(fields: List[Field]) = { | ||
val map = new java.util.LinkedHashMap[String, Field]((fields.size / 0.75d).toInt + 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a known formula? Or just an estimate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to explain what this is in a comment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The exact formula would be The Map resizes when the number of items in it exceed the capacity * loading factor (defaults to 0.75). Using this formula guarantees that we have adequate capacity so that the Map won't need to resize. In JDK17+ there is a method that allows you to create the Maps taking the expected number of items as a parameter (instead of using the initial size) and it used this formula. I'll add a comment for it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forget is scala list's size O(n) or is it stored on concat? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's O(n), although given that this List only contains the queried fields and that this method is only run for objects with multiple fragments, I think the overall impact of counting the number of elements in this list is extremely small |
||
var remaining = fields | ||
while (!remaining.isEmpty) { | ||
val h = remaining.head | ||
if (matchesTypename(h)) { | ||
map.compute( | ||
h.aliasedName, | ||
(_, f) => | ||
if (f eq null) h | ||
else f.copy(fields = f.fields ::: h.fields) | ||
) | ||
} | ||
remaining = remaining.tail | ||
} | ||
map.values().asScala.toList | ||
} | ||
|
||
field.fields match { | ||
// Shortcut if all the fields have the same condition, which means we don't need to merge as that's been handled in Field.apply | ||
case h :: t if haveSameCondition(h, t) => if (matchesTypename(h)) field.fields else Nil | ||
case Nil => Nil | ||
case fields => mergeFields(fields) | ||
} | ||
} | ||
|
||
private def fieldInfo(field: Field, path: List[Either[String, Int]], fieldDirectives: List[Directive]): FieldInfo = | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was gonna suggest tail recursion but this will probably iterate tighter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I think the compiler will rewrite the tailrec method likely to this implementation. I'll check, if it's the same I'll use tailrec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the generated code, it looks like
@tailrec
uses Java labels in an infinite while loop with return statements, whereas this one uses the!remaining.isEmpty
method as a condition to terminate thewhile
loop. Not sure which one would be more performant in theoryThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we're probably getting into a grey area of compiler optimization. I think the conditionless while might be able to skip a branch if equal instruction, though for modern chips branches aren't really performance killers anymore