takafumi blog

日々の勉強メモ

scala: jmhでreifnedのパフォーマンスを(適当に)調べてみる

環境   Scala 2.13.8

ちょっと気になってrefinedのPositiveなどに比べて、requireで契約的に正数を縛る場合、どのくらいのパフォーマンス差があるか調べてみた。

コードはだいぶ適当だが以下

package bench

import cats.implicits._
import eu.timepit.refined._
import eu.timepit.refined.api._
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
import eu.timepit.refined.types.numeric._
import scala.util._

import org.openjdk.jmh.annotations._

class JMHSample {
  @throws
  def convertPositiveInt(i: Int): Int = {
    require(i >= 0)
    i
  }

  @Benchmark
  def measureAgreementPosInt(): Either[String, Int] = {
    try {
      Right[String, Int](convertPositiveInt(1))
    } catch {
      case t: Throwable => Left[String, Int](t.getMessage)
    }
  }

  @Benchmark
  def measureAgreementPosIntFPLike(): Either[String, Int] = {
    Try(convertPositiveInt(1)).toEither.leftMap(t => t.getMessage)
  }

  @Benchmark
  def measurePosInt(): Either[String, PosInt] = {
    RefType.applyRef[PosInt](1)
  }

  @Benchmark
  def measureIntRefinedPositive(): Either[String, Int Refined Positive] = {
    refineV[Positive](1)
  }
}

結果

sbt:jmh-example> Jmh / run -i 3 -wi 3 -f1 -t 1
...
[info] Benchmark                                Mode  Cnt          Score          Error  Units
[info] JMHSample.measureAgreementPosInt        thrpt    3  365097651.422 ± 19080986.848  ops/s
[info] JMHSample.measureAgreementPosIntFPLike  thrpt    3  344466648.190 ± 20924629.667  ops/s
[info] JMHSample.measureIntRefinedPositive     thrpt    3   73142259.692 ± 16294716.321  ops/s
[info] JMHSample.measurePosInt                 thrpt    3   78952274.799 ± 58359188.751  ops/s
[success] Total time: 241 s (04:01), completed 2022/10/16 14:03:36

おおむね1/3~1/5くらいrefinedの方がパフォーマンスは劣化する。 とはいえ普通にアプリケーション実装するときとかは、I/Oとかと比べれば微細なレベルだと思うので、有用な時は使えばよいと思う。

github discussionのコメントをslackに投稿するaction

2022/07/24 現在、まだslack integrationが本家で対応していないのでざっくり書いてみた。

Get notifications of Discussions in slack (or equivalent) · Discussion #2844 · github-community/community · GitHub

ただしインジェクション対応など適当なので、privateで信頼できるところ以外では使わない方がよい。

詳しくはこの辺参照。

https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions

で以下がYAML

# .github/workflows/discussions_comment_to_slack_api.yml
on:
  discussion_comment:
    types: [created]
jobs:
  on_comment:
    runs-on: ubuntu-latest
    if: github.event.comment
    # outputs:
    #   formatted_body: ${{ steps.format.outputs.body }}
    steps:
      - name: 'format comment body & title'
        id: format
        run: |
          title='${{ github.event.discussion.title}}'
          body='${{ github.event.comment.body }}'
          # all newline convert to \n
          body=${body//$'\r'/'%0A'}
          body=${body//$'\n'/'%0A'}
          # escape double quote
          title=${title//\"/'%5C%22'}
          body=${body//\"/'%5C%22'}
          echo "title: $title"
          echo "body: $body"
          echo "::set-output name=title::${title}"
          echo "::set-output name=body::${body}"
      - name: 'request to slack'
        id: request
        run: |
          curl -X POST 'https://slack.com/api/chat.postMessage' \
            -d 'token=${{ secrets.SLACK_API_TOKEN_FOR_CHAT_WRITE}}' \
            -d 'channel=general' \
            -d 'blocks=[
                 {
                   "type": "section",
                   "text": {
                     "type": "mrkdwn",
                     "text": "<${{ github.event.comment.user.html_url}}|${{ github.event.comment.user.login }}> commented <${{ github.event.comment.html_url }}|${{ github.event.discussion.number }}/discussioncomment-${{ github.event.comment.id }}>"
                   }
                 },
                 {
                   "type": "divider"
                 },
                 {
                   "type": "section",
                   "text": {
                     "type": "mrkdwn",
                     "text": "*POST* ${{ github.event.discussion.category.emoji}}"
                   }
                 },
                 {
                   "type": "section",
                   "text": {
                     "type": "mrkdwn",
                     "text": "${{ steps.format.outputs.title }} #${{ github.event.discussion.number}}"
                   }
                 },
                 {
                   "type": "section",
                   "text": {
                     "type": "mrkdwn",
                     "text": "*COMMENT :speech_balloon:*"
                   }
                 },
                 {
                   "type": "section",
                   "text": {
                     "type": "mrkdwn",
                     "text": "${{ steps.format.outputs.body }}"
                   }
                 }
               ]'

後は、Slack側のBotchat:write の権限つけて、GitHubのsecretにtokenを入れてやる。

出力はこんな感じ

デザインを変更したい場合は、https://app.slack.com/block-kit-builder/ とかで blocks のところのデザイン作って書き換える。

そのうち本家で対応してくれるまでの間に合わせという事で。

github.com

scala3: Matchableとパターンマッチ

環境   Scala 3.1.2

scala3では AnyAnyVal & AnyRef の間に Matchable というタイプが追加された。

A First Look at Types | Scala 3 — Book | Scala Documentation

scala3_class_hierarchy

理由はこちら

The Matchable Trait

何が変わったのか?

この変更は ` immutable に定義したタイプがパターンマッチで不変性が破壊されてしまう事への対処として導入された。

簡単に言うとパターンマッチのセレクター、 C(_)_ :C のような、には Matchable が実装される必要がある。

一番影響がありそうなのは、型パラメータが Matchable を実装しないとパターンマッチにでWarningが出る事だろう。

例として以下のようなメソッドがある。

def matchableTest[T](t: T) =
  t match {
    case _: Int => println("Int")
    case _      => println("Other")
}

scala2であれば当然問題ない。しかし型パラーメーター TMatchable かどうかの保証がないため、scala3ではWarningになる。

sbt:scala3> compile
[info] compiling 1 Scala source to /path/to/target/scala-3.1.2/classes ...
[warn] -- [E165] Type Warning: /path/to/src/main/scala/Main.scala:7:11
[warn] 7 |      case _: Int => println("Int")
[warn]   |           ^^^^^^
[warn]   |           pattern selector should be an instance of Matchable,,
[warn]   |           but it has unmatchable type T instead
[warn] one warning found

つまり TT <: Matchable としてやる必要がある。

// OK
def matchableTest[T <: Matchable](t: T) =
  t match {
    case _: Int => println("Int")
    case _      => println("Other")
}

またこの問題は 型 C[T] をパターンマッチした時も発生する

case class Foo[T](val t: T)

def matchableTest2[T](foo: Foo[T]) =
  foo match {
    case Foo(i: Int) => println(i)
    case Foo(_) => println("other")
sbt:scala3> compile
[info] compiling 1 Scala source to /path/to/target/scala-3.1.2/classes ...
[warn] -- [E165] Type Warning: /path/to/src/main/scala/Main.scala:16:18
[warn] 16 |      case Foo(i: Int) => println(i)
[warn]    |                  ^^^
[warn]    |                  pattern selector should be an instance of Matchable,,
[warn]    |                  but it has unmatchable type T instead
[warn] one warning found

また余談だが、 scala2 だと List[Any] となる以下のような場合 scala3 だと List[Matchable] になる

scala> List(1, "a")
val res1: List[Matchable] = List(1, a)

Matchable のメンバ

現状 Matchable はメンバを持たない

abstract class Any:
  def isInstanceOf
  def getClass
  def asInstanceOf      // Cast to a new type: myAny.asInstanceOf[String]
  def ==
  def !=
  def ##   // Alias for hashCode
  def equals
  def hashCode
  def toString

trait Matchable extends Any

class AnyVal extends Any, Matchable

class AnyRef extends Any, Matchable

が将来的には getClassgetInstanceOfMatchable に移動する可能性もあるらしい。

Matchable is currently a marker trait without any methods. Over time we might migrate methods getClass and isInstanceOf to it, since these are closely related to pattern-matching.

-source:future とscala2互換性

と説明を書いたが、この機能はデフォルトではscala3でも互換性を考慮しWarningを出さない。 scalacOptions-source:future を追加する事で有効になる。

Scala3で弱適合性(= Week Conformance)の削除で問題が発生するコードについて

環境   Scala 2.13.8  Scala 3.1.1

scala3で削除される機能の一つに「弱適合性(= Week Conformance)」というのがある。 docs.scala-lang.org

しかし説明を読み、コードを書いてみても実際に何が問題になるのかが最初よく分からなかった。 ここではscala2からscala3への移行、もしくはそれを前提としてscala2のコードを書くとき、で問題が発生しそうなパターンを書き残しておく。

そもそも弱適合性とは?

弱適合性についてをそもそも理解したい場合はscala2のREFLECTION シンボル、構文木、型#弱適合性関係 を参照。
削除されるものなので細かくは説明は書かない。

問題と解決方法

結論

先に結論を書くと、アプリケーションのレベルであれば、変数や関数に返り型を記述してあれば問題は発生しない

実際のコード

scala2とscala3で同じように書いて、scala3で問題が出るのは以下のようなコードになる。

// scala2
val d = if (true) 1f else 1d
val d2: Double = d

 

// scala3
val d = if (true) 1f else 1d
val d2: Double = d // Error
[error] -- [E007] Type Mismatch Error: /path/to/Main.scala:4:21
[error] 4 |    val d2: Double = d // Error
[error]   |                     ^
[error]   |                     Found:    (d : AnyVal)
[error]   |                     Required: Double

上記のような場合は Floatコンパイル時に自動変換されず AnyVal として扱われるためエラーとなる。

しかしscala3のドキュメントに

Therefore, Scala 3 drops the general notion of weak conformance, and instead keeps one rule: Int literals are adapted to other numeric types if necessary.

とある通り Int の場合は Double へ自動変換されるためcompile可能である。

// scala3
val d = if (true) 1 else 1d // from 1f to 1:Int
val d2: Double = d // OK 

また、次のように関数に返り型が明示されている場合は自動変換され、コンパイルを通過する。
これは Float から Double のように精度が下がらない型変換は暗黙に定義されるので許容される https://dotty.epfl.ch/api/scala/Float$.html#float2double-72d

// scala3
val d: Double = if (true) 1f else 1d // <- Add return type `Double`
val d2: Double = d // OK 

つまり複数の数値型が一つの式の中に存在した時、明示的に型指定をしている場合は、暗黙で変換される。
(ただし高精度から低い精度への変換は定義されていないためエラーとなる)
しかし明示的に型を指定しない場合は、 Int 以外は AnyVal として扱われてしまい、その後の特定の数値型として呼び出すと AnyVal から数値型への変換は暗黙変換が定義されていないためエラーになる。

結論

つまるところ、返り型を常に書いておけば基本的にこの機能削除による影響は受けないと思われる。
またそれ以外の場合も AnyVal からの変換が不可能であるというエラーがcompile時に発生するため、修正せずに妙な動きをするという可能性は低い。

・・・のではないかと考えられる。

余談

scala3ドキュメントに書いてる List の例も、返り型をきちんと書いておけば問題なくcompileできる。

NG

// scala3

def main(args: Array[String]): Unit =
  val ls: List[Double] = foo()

def foo() =  // NG: No return type
  val n: Int = 3
  val c: Char = 'X'
  val d: Double = math.sqrt(3.0)
  List(n, c, d)
[error] -- [E007] Type Mismatch Error: /path/to/Main.scala:3:30
[error] 3 |    val ls: List[Double] = foo()
[error]   |                           ^^^^^
[error]   |                           Found:    List[AnyVal]
[error]   |                           Required: List[Double]

OK

// scala3

def main(args: Array[String]): Unit =
  val ls: List[Double] = foo()

def foo(): List[Double] = // OK
  val n: Int = 3
  val c: Char = 'X'
  val d: Double = math.sqrt(3.0)
  List(n, c, d)

ポストモーテムとは

基本的にSRE本を自分で実施する際に理解しやすい形にまとめたもの。

前提

この文章はインシデント(障害や緊急対応など)発生時の対応関するポストモーテムに関して記述する。

プロジェクト終了時などその他のタイミングで行うポストモーテムとは異なる。

用語

  • ポストモーテム
    • この文章ないではインシデント時に作成するドキュメントをポストモーテムと呼ぶ
    • ドキュメント作成とそれにまつわるレビューやなど一連の流れ、仕組みを指すときはポストモーテムのプロセスと呼称する
  • インシデント
    • 障害・緊急対応などサービスに何かが起きた時の呼称
  • トイル(SRE本 p51)
    • プロダクションサービスを動作させることに関係する作業で以下のような傾向を持つもの
      • 手作業で繰り返し行われる
      • 自動化することが可能である
      • 戦術的で長期的な価値を持たない
      • 作業量がサービスの成長に比例する

まとめ

目的・文化・準備

プロセス

ポストモーテムのプロセスは以下のようになる

  1. インシデントが発生する
  2. インシデントの対応が完了する
  3. ポストモーテムを書く条件に当てはまるかを判定
  4. 必要な情報を収集する
  5. 原因の分析と対策を検討する
  6. ポストモーテムを書く
  7. レビューを行う
  8. 共有する

参考: https://postmortems.pagerduty.com/what_is/


ポストモーテムの目的

過去のインシデントから学習し、再発の可能性や影響を削減するために行われる。

具体的には以下のような事を目的となる。

  • 根本原因、教訓が十分(具体的・広範囲に)に理解されること
  • 効果的な予防策が確実に導入される事

参考: - https://postmortems.pagerduty.com/what_is/ - SRE本 p175

ポストモーテムの準備

ツールの選定

  • ポストモーテムを書くためには以下の機能を備えているツールが望ましい
    • リアルタイムコラボレーション
    • オープンなコメント/アノテーションシステム
    • メール、チャットによる自動通知
  • 管理システム

参考: SRE本 p177

ポストモーテムの作成

まずポストモーテムを書く前にポストモーテムを作成する必要があるかを判断する

書く場合は

  1. インシデント事態の情報、対応に関する情報収集
  2. インパクトの分析
  3. 根本原因の分析
  4. 対策についてアクションプランを検討
  5. 調査・対策が十分かステークホルダーに確認する

を行い上記を十分に吟味し(もしくは行いながら)、ポストモーテムの作成する。

ポストモーテムは事前に用意したテンプレートに従い記述し、どのようにな書き方がよいポストモーテムかを理解しておく必要がある

いつポストモーテムを作成するか

  • 事前に決めた条件に則り、条件を満たす場合に作成する
    • ユーザー影響あるダウンタイムの発生
    • エンジニアが介入が必要だったとき
    • 解決までの時間
    • データの損失が発生した場合
    • 支払い、請求などお金に関わる問題発生時
    • モニタリングシステム自体の障害
    • など
  • 上記の決まった条件以外で問題となったイベントのステークホルダーは条件に以外で求める事ができる

参考: SRE本 p176~p178

ポストモーテムのテンプレート

例としてポストモーテムには以下のような項目を記述する。

- 担当者
    - ポストモーテム担当者
    - 対応担当者
- 概要
- 原因分析
    - 根本原因(e.g. ○○のバグのため)
    - 発生原因(e.g. △△の操作を行ったため)
- 対応内容
- 対応自体の分析
- 教訓
- 今後のアクションリスト
    - 事後対応内容
    - 現在のステータス
    - 担当者
    - 優先度

よいポストモーテムの書き方

TODO

参考: サイトリライアビリティワークブック―SREの実践方法/10章 ポストモーテムの文化:失敗からの学び

レビュー

レビューが行われていないポストモーテムは適切さを証明できないため、無いも同然と考える必要がある。

実際の流れとしては - チームに下書きを内部共有してレビューを行う - この時、上位の役職者は下書きが完全かを評価する

となる。場合によっては広範囲に意見を求める事もある。

レビューの観点

レビューでは事前にある程度観点を定めておく。

以下は一例

  • 不要な非難が含まれていないか
    • 個人に対する指摘
  • インシデントの主要なデータが収集されているか
  • インパクトの分析が十分か
  • 根本原因の分析が十分か
  • アクションプランは適切か
    • 担当者が決まっているか
    • 優先度が与えられているか
  • 結果はステークホルダーと共有されているか

など

レビューを必ず行う仕組み

レビューが行われなければポストモーテムは無意味といっていい。

必ず行うために、何時、どのように行うかをあらかじめ考えておくとよい。

  • どんなタイミングでレビューをするか
    • 下書きの段階でチーム内や直属の上位者がレビューするのが望ましい
  • レビューされていないポストモーテムが存在しない仕組みづくり
    • レビューMTGを行いポストモーテムを完成させる
      • レビューMTGの議論内容はとりまとめ、ポストモーテムに記載する

参考: SRE本 p178

共有

ポストモーテムを広く共有し、知識や教訓を広い範囲で役立てる事を目標とする。

より広範囲なチーム、内部メーリングリスト、チャットチャンネルなどへ共有する。

ポストモーテム文化の導入

参考: SRE本 p179

非難なく行う

ポストモーテムの記述・レビューなど全てのプロセスを通して、非難なく行うことが重要である - 避難的な雰囲気は問題の隠蔽へつながる - ポストモーテムを頻繁に作成している事を非難しない - ポストモーテムはサービス改善のための提案である - ポストモーテムの作成を称賛する文化を作る事が望ましい

参考: SRE本 P176~P177

継続的な育成と強化

継続にはポストモーテム文化の育成と強化が必要となる。 - 上位管理職の積極的な参加 - 積極的な情報の発信 - よいポストモーテムの選出・定期的な発信 - 公開されたポストモーテム議論の場 - チャットチャンネル、メールグループなど - ポストモーテムの読書会 - 過去の興味深いポストモーテムの振り返り - 新旧を問わず - ディザスターロールプレイング - ポストモーテムに書かれている役割をエンジニアたちが演じ、過去のポストモーテムの再現する

ポストモーテムプロセスのフィードバック

ポストモーテムのプロセス自体に無駄がないかを見直し、改善を行う。 - ポストモーテムは業務の支援となっているか? - トイルが発生しすぎていないか? - ツールは適切か?新しい開発が必要か?

参考: SRE本 p180

将来的な価値を考える

参考: SRE本 P181

準備コストと価値に対する理解

ポストモーテム文化を組織に導入する上での最大の課題は、準備コストよりも価値があるのか?と考える人が多いかもしれないこと。

これを解決するには以下のような対策が考えられる - ワークフローに落とし込む - よいポストモーテムを書くことは評価されるように - ピアボーナスを与える - 上位のリーダーたちの承認と参加を奨励する

理解に関しては - ポストモーテムプロセスのフィードバックを行う - 継続的な育成と強化 - 将来的な価値を考える

などと合わせて考えることが理解を得る助けとなる

参考: SRE本 P179

ポストモーテムの例

SRE本 p507

参考