diff --git a/zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala b/zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala new file mode 100644 index 0000000000..732c5c3094 --- /dev/null +++ b/zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala @@ -0,0 +1,67 @@ +package zio.http.benchmark + +import java.util.concurrent.TimeUnit + +import scala.util.Random + +import zio.{Runtime, Unsafe, ZIO} + +import zio.http.endpoint.Endpoint +import zio.http.{Handler, Method, Request, Routes} + +import org.openjdk.jmh.annotations._ + +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +class RoutesBenchmark { + + val REPEAT_N = 1000 + + val paths = ('a' to 'z').inits.map(_.mkString).toList.reverse.tail + + val routes = Routes.fromIterable(paths.map(p => Endpoint(Method.GET / p).out[Unit].implementHandler(Handler.unit))) + + val requests = paths.map(p => Request.get(p)) + + def request: Request = requests(Random.nextInt(requests.size)) + + def unsafeRun[E, A](zio: ZIO[Any, E, A]): Unit = Unsafe.unsafe { implicit unsafe => + Runtime.default.unsafe + .run(zio.unit) + .getOrThrowFiberFailure() + } + + val paths2 = ('b' to 'z').inits.map(_.mkString).toList.reverse.tail + + val routes2 = Routes.fromIterable(paths2.map(p => Endpoint(Method.GET / p).out[Unit].implementHandler(Handler.unit))) + + val requests2 = requests ++ paths2.map(p => Request.get(p)) + + def request2: Request = requests2(Random.nextInt(requests2.size)) + + @Benchmark + def benchmarkSmallDataZioApi(): Unit = + unsafeRun { + ZIO.collectAllDiscard(1.to(REPEAT_N).map(_ => routes.run(request))) + } + + @Benchmark + def benchmarkSmallDataZioApi2(): Unit = + unsafeRun { + ZIO.collectAllDiscard(1.to(REPEAT_N).map(_ => routes2.run(request2))) + } + + @Benchmark + def notFound1(): Unit = + unsafeRun { + ZIO.collectAllDiscard(1.to(REPEAT_N).map(_ => routes.run(Request.get("not-found")))) + } + + @Benchmark + def notFound2(): Unit = + unsafeRun { + ZIO.collectAllDiscard(1.to(REPEAT_N).map(_ => routes2.run(Request.get("not-found")))) + } + +} diff --git a/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala b/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala index c59f853344..1583fbcf81 100644 --- a/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala @@ -230,9 +230,10 @@ object RoutePatternSpec extends ZIOHttpSpec { var tree: Tree[Int] = RoutePattern.Tree.empty val pattern1 = Method.GET / "users" / "123" - val pattern2 = Method.GET / "users" / trailing + val pattern2 = Method.GET / "users" / trailing / "123" tree = tree.add(pattern2, 2) + println(tree.get(Method.GET, Path("/users/bla/123"))) tree = tree.add(pattern1, 1) assertTrue(tree.get(Method.GET, Path("/users/123")).contains(1)) diff --git a/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala index 75c7e34140..edd34ec369 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala @@ -17,7 +17,7 @@ package zio.http.codec import scala.annotation.tailrec -import scala.collection.immutable.ListMap +import scala.collection.immutable.{HashMap, ListMap} import scala.collection.mutable import scala.language.implicitConversions @@ -763,7 +763,7 @@ object PathCodec { } private[http] final case class SegmentSubtree[+A]( - literals: ListMap[String, SegmentSubtree[A]], + literals: HashMap[String, SegmentSubtree[A]], others: ListMap[SegmentCodec[_], SegmentSubtree[A]], literalsWithCollisions: Set[String], value: Chunk[A], @@ -778,8 +778,8 @@ object PathCodec { newOthers.keys, ) SegmentSubtree( - newLiterals, - newOthers, + newLiterals.to(HashMap), + newOthers.to(ListMap), newLiteralCollisions, self.value ++ that.value, ) @@ -791,7 +791,7 @@ object PathCodec { def get(path: Path): Chunk[A] = get(path, 0) - private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = null): Chunk[A] = { + private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = Set.empty): Chunk[A] = { val segments = path.segments val nSegments = segments.length var subtree = self @@ -804,7 +804,10 @@ object PathCodec { val segment = segments(i) // Fast path, jump down the tree: - if ((skipLiteralsFor.eq(null) || !skipLiteralsFor.contains(i)) && subtree.literals.contains(segment)) { + if ( + subtree.literals.contains(segment) + && (subtree.literalsWithCollisions.eq(Set.empty) || !skipLiteralsFor.contains(i)) + ) { // this subtree segment have conflict with others // will try others if result was empty @@ -830,7 +833,7 @@ object PathCodec { result = subtree0.value i += matched } - case n => // Slowest fallback path. Have to to find the first predicate where the subpath returns a result + case n => // Slowest fallback path. Have to find the first predicate where the subpath returns a result val matches = Array.ofDim[Int](n) var index = 0 var nPositive = 0 @@ -886,11 +889,10 @@ object PathCodec { if (trySkipLiteralIdx.nonEmpty && result.isEmpty) { trySkipLiteralIdx = trySkipLiteralIdx.reverse - val skipLiteralsFor0 = if (skipLiteralsFor eq null) Set.empty[Int] else skipLiteralsFor while (trySkipLiteralIdx.nonEmpty && result.isEmpty) { val skipIdx = trySkipLiteralIdx.head trySkipLiteralIdx = trySkipLiteralIdx.tail - result = get(path, from, skipLiteralsFor0 + skipIdx) + result = get(path, from, skipLiteralsFor + skipIdx) } result } else result @@ -914,7 +916,7 @@ object PathCodec { object SegmentSubtree { def single[A](segments: Iterable[SegmentCodec[_]], value: A): SegmentSubtree[A] = segments.collect { case x if x.nonEmpty => x } - .foldRight[SegmentSubtree[A]](SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk(value))) { + .foldRight[SegmentSubtree[A]](SegmentSubtree(HashMap.empty, ListMap(), Set.empty, Chunk(value))) { case (segment, subtree) => val literals = segment match { @@ -928,14 +930,14 @@ object PathCodec { case _ => Chunk((segment, subtree)) }): _*) - SegmentSubtree(literals, others, Set.empty, Chunk.empty) + SegmentSubtree(literals.to(HashMap), others, Set.empty, Chunk.empty) } val empty: SegmentSubtree[Nothing] = - SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk.empty) + SegmentSubtree(HashMap(), ListMap(), Set.empty, Chunk.empty) } - private def mergeMaps[A, B](left: ListMap[A, B], right: ListMap[A, B])(f: (B, B) => B): ListMap[A, B] = + private def mergeMaps[A, B](left: Map[A, B], right: Map[A, B])(f: (B, B) => B): Map[A, B] = right.foldLeft(left) { case (acc, (k, v)) => acc.get(k) match { case None => acc.updated(k, v)