Skip to content

Commit d8f9c69

Browse files
authored
Use HashMap for literals look up (#2960) (#3277)
1 parent 5418a2a commit d8f9c69

File tree

7 files changed

+146
-21
lines changed

7 files changed

+146
-21
lines changed

.github/workflows/ci.yml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,44 @@ jobs:
636636
name: Jmh_Main_ProbeContentTypeBenchmark
637637
path: Main_ProbeContentTypeBenchmark.txt
638638

639+
Jmh_RoutesBenchmark:
640+
name: Jmh RoutesBenchmark
641+
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
642+
strategy:
643+
matrix:
644+
os: [ubuntu-latest]
645+
scala: [2.13.14]
646+
java: [temurin@8]
647+
runs-on: ${{ matrix.os }}
648+
steps:
649+
- uses: coursier/setup-action@v1
650+
with:
651+
apps: sbt
652+
653+
- uses: actions/checkout@v4
654+
with:
655+
path: zio-http
656+
657+
- uses: actions/setup-java@v4
658+
with:
659+
distribution: temurin
660+
java-version: 11
661+
662+
- name: Benchmark_Main
663+
id: Benchmark_Main
664+
env:
665+
GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}}
666+
run: |
667+
cd zio-http
668+
sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")' project/plugins.sbt
669+
cat > Main_RoutesBenchmark.txt
670+
sbt -no-colors -v "zioHttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 RoutesBenchmark" | grep -e "thrpt" -e "avgt" >> ../Main_RoutesBenchmark.txt
671+
672+
- uses: actions/upload-artifact@v4
673+
with:
674+
name: Jmh_Main_RoutesBenchmark
675+
path: Main_RoutesBenchmark.txt
676+
639677
Jmh_SchemeDecodeBenchmark:
640678
name: Jmh SchemeDecodeBenchmark
641679
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
@@ -752,7 +790,7 @@ jobs:
752790

753791
Jmh_cache:
754792
name: Cache Jmh benchmarks
755-
needs: [Jmh_CachedDateHeaderBenchmark, Jmh_ClientBenchmark, Jmh_CookieDecodeBenchmark, Jmh_EndpointBenchmark, Jmh_HttpCollectEval, Jmh_HttpCombineEval, Jmh_HttpNestedFlatMapEval, Jmh_HttpRouteTextPerf, Jmh_ProbeContentTypeBenchmark, Jmh_SchemeDecodeBenchmark, Jmh_ServerInboundHandlerBenchmark, Jmh_UtilBenchmark]
793+
needs: [Jmh_CachedDateHeaderBenchmark, Jmh_ClientBenchmark, Jmh_CookieDecodeBenchmark, Jmh_EndpointBenchmark, Jmh_HttpCollectEval, Jmh_HttpCombineEval, Jmh_HttpNestedFlatMapEval, Jmh_HttpRouteTextPerf, Jmh_ProbeContentTypeBenchmark, Jmh_RoutesBenchmark, Jmh_SchemeDecodeBenchmark, Jmh_ServerInboundHandlerBenchmark, Jmh_UtilBenchmark]
756794
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
757795
strategy:
758796
matrix:
@@ -824,6 +862,13 @@ jobs:
824862
- name: Format_Main_ProbeContentTypeBenchmark
825863
run: cat Main_ProbeContentTypeBenchmark.txt >> Main_benchmarks.txt
826864

865+
- uses: actions/download-artifact@v4
866+
with:
867+
name: Jmh_Main_RoutesBenchmark
868+
869+
- name: Format_Main_RoutesBenchmark
870+
run: cat Main_RoutesBenchmark.txt >> Main_benchmarks.txt
871+
827872
- uses: actions/download-artifact@v4
828873
with:
829874
name: Jmh_Main_SchemeDecodeBenchmark

profiling/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name := "zio-http"
22
version := "1.0.0"
33
scalaVersion := "2.13.6"
4-
lazy val zhttp = ProjectRef(file("/zio-http/zio-http-src"), "zioHttp")
4+
lazy val zioHttp = ProjectRef(file("/zio-http/zio-http-src"), "zioHttp")
55
lazy val root = (project in file("."))
66
.settings(
77
name := "helloExample",
@@ -14,4 +14,4 @@ lazy val root = (project in file("."))
1414
oldStrategy(x)
1515
},
1616
)
17-
.dependsOn(zhttp)
17+
.dependsOn(zioHttp)

profiling/project/build.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version = 1.8.3
1+
sbt.version = 1.10.7

profiling/project/plugins.sbt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
2-
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0")
3-
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
1+
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
2+
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.0")
3+
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4")
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package zio.http.benchmark
2+
3+
import java.util.concurrent.TimeUnit
4+
5+
import scala.util.Random
6+
7+
import zio.{Runtime, Unsafe, ZIO}
8+
9+
import zio.http.endpoint.Endpoint
10+
import zio.http.{Handler, Method, Request, Routes}
11+
12+
import org.openjdk.jmh.annotations._
13+
14+
@State(Scope.Thread)
15+
@BenchmarkMode(Array(Mode.Throughput))
16+
@OutputTimeUnit(TimeUnit.SECONDS)
17+
class RoutesBenchmark {
18+
19+
val REPEAT_N = 1000
20+
21+
val paths = ('a' to 'z').inits.map(_.mkString).toList.reverse.tail
22+
23+
val routes = Routes.fromIterable(paths.map(p => Endpoint(Method.GET / p).out[Unit].implementHandler(Handler.unit)))
24+
25+
val requests = paths.map(p => Request.get(p))
26+
27+
def request: Request = requests(Random.nextInt(requests.size))
28+
val smallDataRequests = Array.fill(REPEAT_N)(request)
29+
30+
def unsafeRun[E, A](zio: ZIO[Any, E, A]): Unit = Unsafe.unsafe { implicit unsafe =>
31+
Runtime.default.unsafe
32+
.run(zio.unit)
33+
.getOrThrowFiberFailure()
34+
}
35+
36+
val paths2 = ('b' to 'z').inits.map(_.mkString).toList.reverse.tail
37+
38+
val routes2 = Routes.fromIterable(paths2.map(p => Endpoint(Method.GET / p).out[Unit].implementHandler(Handler.unit)))
39+
40+
val requests2 = requests ++ paths2.map(p => Request.get(p))
41+
42+
def request2: Request = requests2(Random.nextInt(requests2.size))
43+
44+
val smallDataRequests2 = Array.fill(REPEAT_N)(request2)
45+
46+
val routes3 = Routes(
47+
Endpoint(Method.GET / "api").out[Unit].implementHandler(Handler.unit),
48+
Endpoint(Method.GET / "ui").out[Unit].implementHandler(Handler.unit),
49+
)
50+
51+
val requests3 = Array.fill(REPEAT_N)(List(Request.get("api"), Request.get("ui"))(Random.nextInt(2)))
52+
53+
@Benchmark
54+
def benchmarkSmallDataZioApi(): Unit =
55+
for (r <- smallDataRequests) routes.isDefinedAt(r)
56+
57+
@Benchmark
58+
def benchmarkSmallDataZioApi2(): Unit =
59+
for (r <- smallDataRequests2) routes2.isDefinedAt(r)
60+
61+
@Benchmark
62+
def notFound1(): Unit =
63+
for (_ <- 1 to REPEAT_N) {
64+
routes.isDefinedAt(Request.get("not-found"))
65+
}
66+
67+
@Benchmark
68+
def notFound2(): Unit =
69+
for (_ <- 1 to REPEAT_N) {
70+
routes2.isDefinedAt(Request.get("not-found"))
71+
}
72+
73+
@Benchmark
74+
def benchmarkSmallDataZioApi3(): Unit =
75+
for (r <- requests3) routes3.isDefinedAt(r)
76+
77+
}

zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,10 @@ object RoutePatternSpec extends ZIOHttpSpec {
230230
var tree: Tree[Int] = RoutePattern.Tree.empty
231231

232232
val pattern1 = Method.GET / "users" / "123"
233-
val pattern2 = Method.GET / "users" / trailing
233+
val pattern2 = Method.GET / "users" / trailing / "123"
234234

235235
tree = tree.add(pattern2, 2)
236+
println(tree.get(Method.GET, Path("/users/bla/123")))
236237
tree = tree.add(pattern1, 1)
237238

238239
assertTrue(tree.get(Method.GET, Path("/users/123")).contains(1))

zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package zio.http.codec
1818

1919
import scala.annotation.tailrec
20-
import scala.collection.immutable.ListMap
20+
import scala.collection.immutable.{HashMap, ListMap}
2121
import scala.collection.mutable
2222
import scala.language.implicitConversions
2323

@@ -763,7 +763,7 @@ object PathCodec {
763763
}
764764

765765
private[http] final case class SegmentSubtree[+A](
766-
literals: ListMap[String, SegmentSubtree[A]],
766+
literals: Map[String, SegmentSubtree[A]],
767767
others: ListMap[SegmentCodec[_], SegmentSubtree[A]],
768768
literalsWithCollisions: Set[String],
769769
value: Chunk[A],
@@ -778,8 +778,8 @@ object PathCodec {
778778
newOthers.keys,
779779
)
780780
SegmentSubtree(
781-
newLiterals,
782-
newOthers,
781+
Map(newLiterals.toList: _*),
782+
ListMap(newOthers.toList: _*),
783783
newLiteralCollisions,
784784
self.value ++ that.value,
785785
)
@@ -791,7 +791,7 @@ object PathCodec {
791791
def get(path: Path): Chunk[A] =
792792
get(path, 0)
793793

794-
private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = null): Chunk[A] = {
794+
private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = Set.empty): Chunk[A] = {
795795
val segments = path.segments
796796
val nSegments = segments.length
797797
var subtree = self
@@ -804,7 +804,10 @@ object PathCodec {
804804
val segment = segments(i)
805805

806806
// Fast path, jump down the tree:
807-
if ((skipLiteralsFor.eq(null) || !skipLiteralsFor.contains(i)) && subtree.literals.contains(segment)) {
807+
if (
808+
subtree.literals.contains(segment)
809+
&& (subtree.literalsWithCollisions.eq(Set.empty) || !skipLiteralsFor.contains(i))
810+
) {
808811

809812
// this subtree segment have conflict with others
810813
// will try others if result was empty
@@ -830,7 +833,7 @@ object PathCodec {
830833
result = subtree0.value
831834
i += matched
832835
}
833-
case n => // Slowest fallback path. Have to to find the first predicate where the subpath returns a result
836+
case n => // Slowest fallback path. Have to find the first predicate where the subpath returns a result
834837
val matches = Array.ofDim[Int](n)
835838
var index = 0
836839
var nPositive = 0
@@ -886,11 +889,10 @@ object PathCodec {
886889

887890
if (trySkipLiteralIdx.nonEmpty && result.isEmpty) {
888891
trySkipLiteralIdx = trySkipLiteralIdx.reverse
889-
val skipLiteralsFor0 = if (skipLiteralsFor eq null) Set.empty[Int] else skipLiteralsFor
890892
while (trySkipLiteralIdx.nonEmpty && result.isEmpty) {
891893
val skipIdx = trySkipLiteralIdx.head
892894
trySkipLiteralIdx = trySkipLiteralIdx.tail
893-
result = get(path, from, skipLiteralsFor0 + skipIdx)
895+
result = get(path, from, skipLiteralsFor + skipIdx)
894896
}
895897
result
896898
} else result
@@ -914,7 +916,7 @@ object PathCodec {
914916
object SegmentSubtree {
915917
def single[A](segments: Iterable[SegmentCodec[_]], value: A): SegmentSubtree[A] =
916918
segments.collect { case x if x.nonEmpty => x }
917-
.foldRight[SegmentSubtree[A]](SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk(value))) {
919+
.foldRight[SegmentSubtree[A]](SegmentSubtree(Map.empty, ListMap(), Set.empty, Chunk(value))) {
918920
case (segment, subtree) =>
919921
val literals =
920922
segment match {
@@ -928,14 +930,14 @@ object PathCodec {
928930
case _ => Chunk((segment, subtree))
929931
}): _*)
930932

931-
SegmentSubtree(literals, others, Set.empty, Chunk.empty)
933+
SegmentSubtree(Map(literals.toList: _*), others, Set.empty, Chunk.empty)
932934
}
933935

934936
val empty: SegmentSubtree[Nothing] =
935-
SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk.empty)
937+
SegmentSubtree(Map(), ListMap(), Set.empty, Chunk.empty)
936938
}
937939

938-
private def mergeMaps[A, B](left: ListMap[A, B], right: ListMap[A, B])(f: (B, B) => B): ListMap[A, B] =
940+
private def mergeMaps[A, B](left: Map[A, B], right: Map[A, B])(f: (B, B) => B): Map[A, B] =
939941
right.foldLeft(left) { case (acc, (k, v)) =>
940942
acc.get(k) match {
941943
case None => acc.updated(k, v)

0 commit comments

Comments
 (0)