Skip to content

Commit

Permalink
Use HashMap for literals look up
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil committed Jan 18, 2025
1 parent 0c11434 commit 46e1623
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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"))))
}

}
3 changes: 2 additions & 1 deletion zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
28 changes: 15 additions & 13 deletions zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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],
Expand All @@ -778,8 +778,8 @@ object PathCodec {
newOthers.keys,
)
SegmentSubtree(
newLiterals,
newOthers,
newLiterals.to(HashMap),
newOthers.to(ListMap),
newLiteralCollisions,
self.value ++ that.value,
)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand Down

0 comments on commit 46e1623

Please sign in to comment.