Put
Some[Types]
on your
HTTP endpoints

Why Care?

型を気に掛けてくれるのは TCP や HTTP ではなくコンパイラ
あなたも気に掛けるべき

Typed HTTP?

[A]

Endpoint[A]

Finch: HTTP Combinators

Finch は Finagle 等を使って開発した高速な HTTP コンビネータ
20 以上のビジネスで本番運用されている

Understanding Endpoints

Endpoint は functor と applicative であり、中に state を持つ

Finagle vs. Finch

new Service[Request, Response] {
 def apply(req: Request): Future[Response] =
  if (req.method != Method.Post)
   Future.value(Response(req.version, Status.NotFound))
  else {
   val payloadIn =
    mapper.readValue(req.contentString, classOf[Payload])
   val payloadOut =
    mapper.writeValueAsBytes(payloadIn)

   val rep = Response(req.version, Status.Ok)
   rep.content =  Buf.ByteArray.Owned(payloadOut)
   rep.contentType = "application/json"

   Future.value(rep)
 }
}
post(jsonBody[Payload])

Finagle と Finch の比較

Finch Overhead

Finagle に対する Finch のオーバヘッド

Why Only 10%?

たった 10%? コンパイル時の処理は非常に軽い

Why As Many As 10%?

10% も? 合成はメモリ確保のコストがかかる

Fast Scala code looks less like Scala

高速な Scala のコードは Scala っぽくない

「X をより早くするには X を減らせ」

Example 1

How to make composition faster? Compose less.

例 1: 合成を早くするには? 合成を減らせ

Modeling Endpoint Result

type EndpointResult[A] = Option[(Input, Future[Output[A]])]

vs


sealed abstract class EndpointResult[+A]
case object Skipped extends EndpointResult[Nothing]
case object Matched[A](
  rem: Input, out: Future[Output[A]]) extends EndpoinResult[A]

エンドポイントの結果をモデル化する

Benchmarking Endpoint Results


-------------------------------------------------------------------------------------
TA: Type Alias | ADT: sealed abstract class | Running Time Mode
-------------------------------------------------------------------------------------
MapBenchmark.mapAsyncTA                             avgt    429.113 ±   43.297  ns/op
MapBenchmark.mapAsyncADT                            avgt    407.126 ±   12.807  ns/op
MapBenchmark.mapOutputAsyncTA                       avgt    821.786 ±   52.045  ns/op
MapBenchmark.mapOutputAsyncADT                      avgt    777.654 ±   26.444  ns/op

MapBenchmark.mapAsyncTA:·gc.alloc.rate.norm         avgt    776.000 ±    0.001   B/op
MapBenchmark.mapAsyncADT:·gc.alloc.rate.norm        avgt    720.000 ±    0.001   B/op
MapBenchmark.mapOutputAsyncTA:·gc.alloc.rate.norm   avgt   1376.001 ±    0.001   B/op
MapBenchmark.mapOutputAsyncADT:·gc.alloc.rate.norm  avgt   1320.001 ±    0.001   B/op

See MapBenchmark and #707.

ベンチマーク結果

Example 1: Takeaways

まとめ: 性能のためには新しい型を導入する
カリカリな局面で使う抽象データでは型合成を避ける

Example 2

How to make codecs faster? Encode/decode less.

例 2: コーデックを早くするには? エンコード・デコードを減らせ

HTTP Payload Decoding

For UTF-8 we need at least 1 character per byte and each char on JVM is 2 bytes.

UTF-8 の 1 文字は JVM 上では 2 バイトになる

Parsing JSON from Bytes

多くの JSON ライブラリはバイト列から JSON をパースできる
Finch は 0.11 からサポート

Parsing with Circe

implicit def fromCirce[A: Decoder]: Decode.Json[A] =
  Decode.json { (b, cs) =>
    val attemptJson = cs match {
      case StandardCharsets.UTF_8 => decodeByteBuffer[A](b.asByteBuffer)
      case _ => decode[A](BufText.extract(b, cs))
    }

    attemptJson.fold[Try[A]](Throw.apply, Return.apply)
  }

Circe を使ったバイト列から JSON へのパース

Benchmarking Decoding (w/ Circe)


---------------------------------------------------------------------------------
S: parse string | BA: parse byte array | Running Time Mode
---------------------------------------------------------------------------------
JsonBenchmark.decodeS                         avgt   5950.402 ±  464.246   ns/op
JsonBenchmark.decodeBA                        avgt   3232.696 ±  171.160   ns/op

JsonBenchmark.decodeS:·gc.alloc.rate.norm     avgt   7992.005 ±   12.749    B/op
JsonBenchmark.decodeBA:·gc.alloc.rate.norm    avgt   4908.003 ±    6.374    B/op

See JsonBenchmark.

デコードのベンチマーク

HTTP Payload Encoding

Each UTF-8 character on JVM produces up to 3 bytes. See UTF_8.newEncoder.maxBytesPerChar

JVM 上の UTF-8 文字をエンコードすると最大で 3 バイトになる
UTF_8.newEncoder.maxBytesPerChar を参照

Printing JSON to Bytes

Finch では 0.12 より JSON からのバイト列出力をサポート

Printing with Circe

implicit def fromCirce[A](implicit e: Encoder[A]): Encode.Json[A] =
  Encode.json {
    case (a, StandardCharsets.UTF_8) =>
      Buf.ByteBuffer.Owned(printBytes(e(a)))
    case (a, cs) =>
      BufText(printString(e(a)), cs)
  }

Circe を使った JSON のバイト列出力

Benchmarking Encoding (w/ Circe)


---------------------------------------------------------------------------------
S: print string | BA: print byte array | Running Time Mode
---------------------------------------------------------------------------------
JsonBenchmark.encodeS                         avgt   16400.327 ±  621.935  ns/op
JsonBenchmark.encodeBA                        avgt   12645.070 ±  391.591  ns/op

JsonBenchmark.encodeS:·gc.alloc.rate.norm     avgt   46900.015 ±  19.123   B/op
JsonBenchmark.encodeBA:·gc.alloc.rate.norm    avgt   30360.011 ±   0.001   B/op

See JsonBenchmark.

エンコードのベンチマーク

Example 2: Takeaways

まとめ: マイクロな最適化も良いけど、問題を端から端まで見て何を減らせるか考えよう

Try string-less codecs with Finch 0.12!

Finch 0.12 の文字列レスのコーデックを試してみてください!

Performance Lessons Learned

性能を推測せずに、JMH で計測する
メモリ確保を減らす方法を学ぶ

https://github.com/finagle/finch