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

Use HashMap for literals look up #3277

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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"))))
}

}
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
Loading