Skip to content
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

Parser microoptimizations #2246

Merged
merged 5 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 90 additions & 93 deletions benchmarks/src/main/scala/caliban/ComplexQueryBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,97 +58,94 @@ class ComplexQueryBenchmark {
}

object ComplexQueryBenchmark {
val fullIntrospectionQuery = """
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}

fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}

fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}

fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
"""
val fullIntrospectionQuery =
"""query IntrospectionQuery {
| __schema {
| queryType { name }
| mutationType { name }
| subscriptionType { name }
| types {
| ...FullType
| }
| directives {
| name
| description
| locations
| args {
| ...InputValue
| }
| }
| }
|}
|fragment FullType on __Type {
| kind
| name
| description
| fields(includeDeprecated: true) {
| name
| description
| args {
| ...InputValue
| }
| type {
| ...TypeRef
| }
| isDeprecated
| deprecationReason
| }
| inputFields {
| ...InputValue
| }
| interfaces {
| ...TypeRef
| }
| enumValues(includeDeprecated: true) {
| name
| description
| isDeprecated
| deprecationReason
| }
| possibleTypes {
| ...TypeRef
| }
|}
|fragment InputValue on __InputValue {
| name
| description
| type { ...TypeRef }
| defaultValue
|}
|fragment TypeRef on __Type {
| kind
| name
| ofType {
| kind
| name
| ofType {
| kind
| name
| ofType {
| kind
| name
| ofType {
| kind
| name
| ofType {
| kind
| name
| ofType {
| kind
| name
| ofType {
| kind
| name
| }
| }
| }
| }
| }
| }
| }
|}
|""".stripMargin
}
42 changes: 16 additions & 26 deletions benchmarks/src/main/scala/caliban/ParserBenchmark.scala
Original file line number Diff line number Diff line change
@@ -1,51 +1,41 @@
package caliban

import caliban.parsing.Parser
import cats.effect.IO
import io.circe.Json
import caliban.parsing.adt.Document
import cats.data.NonEmptyList
import cats.parse.Caret
import gql.parser.QueryAst
import grackle.Operation
import org.openjdk.jmh.annotations._
import sangria.execution._
import sangria.marshalling.circe._
import sangria.parser.QueryParser

import java.util.concurrent.TimeUnit
import scala.concurrent.duration._
import scala.concurrent.{ Await, ExecutionContextExecutor, Future }
import scala.concurrent.ExecutionContextExecutor

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
class ParserBenchmark {
import ComplexQueryBenchmark._

implicit val executionContext: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global

@Benchmark
def runCaliban(): Unit = {
val io = Parser.parseQuery(fullIntrospectionQuery)
Caliban.run(io)
()
}
def runCaliban(): Document =
Parser.parseQueryEither(fullIntrospectionQuery).fold(throw _, identity)

@Benchmark
def runSangria(): Unit = {
val future = Future.fromTry(QueryParser.parse(fullIntrospectionQuery))
Await.result(future, 1.minute)
()
}
def runSangria(): sangria.ast.Document =
QueryParser.parse(fullIntrospectionQuery).fold(throw _, identity)

@Benchmark
def runGrackle(): Unit = {
Grackle.compiler.compile(fullIntrospectionQuery)
()
}
def runGrackle(): Operation =
Grackle.compiler.compile(fullIntrospectionQuery).getOrElse(throw new Throwable("Grackle failed to parse query"))

@Benchmark
def runGql(): Unit = {
gql.parser.parseQuery(fullIntrospectionQuery)
()
}
def runGql(): NonEmptyList[QueryAst.ExecutableDefinition[Caret]] =
gql.parser.parseQuery(fullIntrospectionQuery).fold(e => throw new Throwable(e.prettyError.value), identity)
}
2 changes: 1 addition & 1 deletion core/src/main/scala/caliban/parsing/parsers/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import fastparse._

import scala.annotation.nowarn

@nowarn("msg=NoWhitespace") // False positive warning in Scala 2.x
@nowarn("msg=NoWhitespace") // False positive warning in Scala 2.12.x
object Parsers extends SelectionParsers {
def argumentDefinition(implicit ev: P[Any]): P[InputValueDefinition] =
(stringValue.? ~ name ~ ":" ~ type_ ~ defaultValue.? ~ directives.?).map {
Expand Down
12 changes: 2 additions & 10 deletions core/src/main/scala/caliban/parsing/parsers/SelectionParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import fastparse._

import scala.annotation.nowarn

@nowarn("msg=NoWhitespace") // False positive warning in Scala 2.x
@nowarn("msg=NoWhitespace") // False positive warning in Scala 2.12.x
private[caliban] trait SelectionParsers extends ValueParsers {

@deprecated("Kept for bincompat only, scheduled to be removed")
def alias(implicit ev: P[Any]): P[String] = name ~ ":"
def aliasOrName(implicit ev: P[Any]): P[String] = ":" ~/ name

def argument(implicit ev: P[Any]): P[(String, InputValue)] = name ~ ":" ~ value
Expand All @@ -30,19 +28,13 @@ private[caliban] trait SelectionParsers extends ValueParsers {
def namedType(implicit ev: P[Any]): P[NamedType] = name.filter(_ != "null").map(NamedType(_, nonNull = false))
def listType(implicit ev: P[Any]): P[ListType] = ("[" ~ type_ ~ "]").map(t => ListType(t, nonNull = false))

@deprecated("Kept for bincompat only, scheduled to be removed")
def nonNullType(implicit ev: P[Any]): P[Type] = ((namedType | listType) ~ "!").map {
case t: NamedType => t.copy(nonNull = true)
case t: ListType => t.copy(nonNull = true)
}

def type_(implicit ev: P[Any]): P[Type] = ((namedType | listType) ~ "!".!.?).map {
case (t: NamedType, nn) => if (nn.isDefined) t.copy(nonNull = true) else t
case (t: ListType, nn) => if (nn.isDefined) t.copy(nonNull = true) else t
}

def field(implicit ev: P[Any]): P[Field] =
(Index ~ name ~ aliasOrName.? ~ arguments.? ~ directives.? ~ selectionSet.?).map {
(Index ~~ name ~ aliasOrName.? ~ arguments.? ~ directives.? ~ selectionSet.?).map {
case (index, alias, Some(name), args, dirs, sels) =>
Field(Some(alias), name, args.getOrElse(Map()), dirs.getOrElse(Nil), sels.getOrElse(Nil), index)
case (index, name, _, args, dirs, sels) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ private[caliban] trait StringParsers {

def sourceCharacter(implicit ev: P[Any]): P[Unit] = CharIn("\u0009\u000A\u000D\u0020-\uFFFF")
def sourceCharacterWithoutLineTerminator(implicit ev: P[Any]): P[Unit] = CharIn("\u0009\u0020-\uFFFF")
def name(implicit ev: P[Any]): P[String] = (CharIn("_A-Za-z") ~~ CharIn("_0-9A-Za-z").repX).!
def name(implicit ev: P[Any]): P[String] =
CharsWhileIn("_0-9A-Za-z", 1).!.flatMap { s =>
// Less efficient in case of an error, but more efficient in case of success (happy path)
if (s.charAt(0) <= '9') ev.freshFailure()
else ev.freshSuccess(s)
}
def nameOnly(implicit ev: P[Any]): P[String] = Start ~ name ~ End

def hexDigit(implicit ev: P[Any]): P[Unit] = CharIn("0-9a-fA-F")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import fastparse._

import scala.annotation.nowarn

@nowarn("msg=NoWhitespace") // False positive warning in Scala 2.x
@nowarn("msg=NoWhitespace") // False positive warning in Scala 2.12.x
private[caliban] trait ValueParsers extends NumberParsers {
def booleanValue(implicit ev: P[Any]): P[BooleanValue] =
StringIn("true", "false").!.map(v => BooleanValue(v.toBoolean))
Expand Down
Loading