# prperf マニュアル

prperf は、コードの変更で性能が悪化していないかを PR ごとに自動チェックする薄い GitHub App です。
測定は CI のなかで OSS の Ruby 向けサンプリングプロファイラ [rperf](https://github.com/ko1/rperf) が行い、prperf は base（main など）とこの PR を比べて結果を PR に通知するだけです。
テストカバレッジを CI で追う Codecov を知っていれば、その性能版にあたります。

PR を作ると、Check Run に次のような数字が出ます。

> 2,001ms → 2,140ms (+7%) · alloc 48,741 → 59,950 (+23%) · GC 4 → 7

全体像は次章「このサービスとは」で説明します。

## prperf ひとめぐり

### 導入

1. GitHub App をインストールします。
2. ベンチマークを用意します。ここでは `bin/rails runner ""` でブート時間を計るベンチマークとします。
3. それを実行するワークフローを追加します。`push`（既定ブランチ）と `pull_request` の両方をトリガにします。

```yaml
# .github/workflows/prperf.yml
name: prperf
on:
  push:
    branches: [main, master]   # base を記録（既定ブランチ。両方書けば main でも master でも動く）
  pull_request:                # PR を base と比較
jobs:
  bench:
    runs-on: ubuntu-latest
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@v6
      - uses: ruby/setup-ruby@v1
        with: { bundler-cache: true }
      - uses: rperf-dev/prperf-action@v1
        with:
          run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- bin/rails runner ""   # ← 計測コマンド（手順2のベンチ）
```

閾値による警告、複数ベンチマーク（`benchmark`）、コメントの制御（`comment`）、計測回数（`count`、既定 3 回、中央値）といった設定もあります（詳しくは「登録編」）。

### 結果

各 PR では、GitHub の PR 画面の Checks にそのまま結果が出ます（base と比べた要約）。
閾値を超えたときだけ PR にコメントが付きます。
どのメソッドが重くなったかは、フレームグラフの diff でわかります（詳しくは「読み方編」）。

PR と push のたびに結果が記録され、[prperf.atdot.net](https://prperf.atdot.net) でこれまでの履歴（推移）を確認できます。

prperf は CI を落とさず、シークレットも要りません。
ただし fork からの PR は計測できず、無料βの間は public リポジトリだけが対象です。

# このサービスとは

## ひとことで

prperf は、コードの変更で性能が悪化していないかを PR ごとに自動チェックして PR に知らせる GitHub App です。
測定はあなたの CI の中で OSS の Ruby プロファイラ [rperf](https://github.com/ko1/rperf) が行います。
prperf 自身は、マージ先ブランチ(ふつう main)の最新の計測と、この PR の計測を比べて結果を通知するだけの薄い App です。

このマージ先ブランチ側の基準を base、PR 側を head と呼びます(GitHub の PR 用語と同じ)。
本マニュアルでは以降この呼び方を使います。

> テストカバレッジを CI で追う Codecov を知っていれば、その「性能版」と思ってください。

rperf は時間(CPU)ベースのサンプリングプロファイラで、本体は「どこで時間を使ったか」を表すフレームグラフです。
prperf はそのプロファイルから実行時間、GC、アロケーションを取り出し、base と head で比較します。
PR を作ると、Check Run にこんな要約が出ます。

> 2,001ms → 2,140ms (+7%) · alloc 48,741 → 59,950 (+23%) · GC 4 → 7

このコミットで性能がどう変わったかを、マージ前の PR の時点で気づけるようにするのが prperf です。

## 何をするか、しないか

する:

- PR ごとに base から head への性能差(アロケーション、GC、時間)を Check Run に表示
- 閾値を超えたときだけ PR にコメント(sticky、通知は静か)
- フレームグラフの diff で「どのメソッドが重くなったか」を可視化

しないこと:

- 本番監視ではありません。
  Datadog や Grafana の代替ではなく、補完です(本番の継続監視はそちら、PR 時点の回帰検知が prperf)。
- CI を落としません。
  判定はあくまで参考表示で、Check の conclusion は常に success です。
- あなたのコードをサービス側で実行しません。
  測定はあなたの CI の中で行われ、prperf はその結果(プロファイル)を受け取って比較するだけです。
  これが「薄い App」として成立する理由で、セキュリティとコストの両面で軽くなります。

つまり prperf は、計測と判定を CI 側に置き、サービス側は比較と通知だけを担当します。

## 全体像

```
あなたの CI (GitHub Actions)
  └─ prperf-action
       ├─ rperf でベンチを N 回計測
       └─ プロファイル(.json.gz)を prperf サーバーへアップロード
            │  (GitHub OIDC トークンで認証 → シークレット設定不要)
            ▼
prperf サーバー
  ├─ base と head を比較
  └─ Check Run / PR コメントに結果を通知
```

この構成では CI が計測を実行し、prperf サーバーは受け取ったプロファイルを base と head の組として比較します。

## 使う側の体験

1. GitHub App をリポジトリにインストール
2. 提供する GitHub Action をワークフローに数行追加
3. PR を作ると Check と PR コメントに結果が付く

ワークフローは 1 本です。
push(既定ブランチ)で base を記録し、pull_request でその base と比較します。
base か head かは、prperf が OIDC トークンの ref から判定します。

```yaml
# .github/workflows/prperf.yml
name: prperf
on:
  push:
    branches: [main, master]   # base を記録（既定ブランチ）
  pull_request:                # PR を base と比較
jobs:
  bench:
    runs-on: ubuntu-latest
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@v6
      - uses: ruby/setup-ruby@v1
        with: { bundler-cache: true }
      - uses: rperf-dev/prperf-action@v1
        with:
          run: bundle exec ruby bench/run.rb
```

## なぜ信頼できるか(設計の勘所)

判定では、実行時間よりもアロケーション数と GC 回数を重視します(CI の実行時間は ±10〜20% ブレるためです)。
rperf が測る中心は時間(フレームグラフ)ですが、その時間は参考にとどめます。
結果には時間、GC、アロケーションのすべてと、フレームグラフが出ます。

シークレットは要りません。
認証は GitHub Actions の OIDC トークンで、API キーの発行や管理が不要です。

通知は静かです。
Check Run は常設の置き場で通知ゼロ、コメントは閾値超過時のみ、PR につき 1 通を編集します。

重くなった理由まで分かります。
フレームグラフ diff で、重くなったメソッドを特定できます。

## 誰のためか

性能回帰を PR で止めたい、public な gem やライブラリの作者に向いています。
依存の更新やリファクタで、気づかないうちにアロケーションや起動が重くなるのを、PR の時点で止められます。

性能が UX や売上に直結する private な Rails アプリのチームにも向いています。
重くなる変更を、本番に出る前に、レビューと同じ場所で捕まえられます。

料金は、public リポジトリが無料、private が有料プランです（現在は無料βのため public のみ）。

## このマニュアルの読み進め方

- 登録編：インストールとワークフロー追加
- ベンチマークの書き方、Rails クイックスタート：何を測るか
- 読み方編、フレームグラフの読み方：結果の読み取り

次は「登録編」へ。

# 登録編

prperf を使い始めるまでの流れです。
所要 10〜15 分。
サービスの概要は「このサービスとは」を参照してください。

やることは 3 つです。

1. GitHub App をインストールする
2. ベンチマークを用意する
3. それを実行するワークフローを追加する

## 前提

rperf 0.10 以上を Gemfile に入れていることが必要です。
prperf はプロファイルに埋め込まれた `meta` と `summary` を使います。
古い rperf では action が明確なエラーで止まります。

計測対象のベンチマークコマンドがあることも必要です（後述）。

対象は public リポジトリです。
private リポジトリは有料プランで提供します（現在は無料βのため public 専用）。

## GitHub App をインストール

prperf の GitHub App ページからリポジトリにインストールします。
これにより、prperf がそのリポジトリの Check Run と PR コメントを書けるようになります。

## ベンチマークを用意する

何を測るかが、検知できる回帰の範囲を決めます。
良いベンチマークは、決定的で、気にしている経路を通り、そこそこの規模があります。
何をどう測るかはプロジェクトしだいなので、書き方とプロジェクト別の例は「ベンチマークの書き方」にまとめています。

この登録編では、例として Rails アプリの起動（boot）を計ります。
ベンチマークは `bin/rails runner ""` だけです。
アプリをブートして空のスクリプトを走らせるので、ベンチ用のファイルを書かずにそのまま使えます。
次の節で、これを rperf でくるんでワークフローに置きます。

## ワークフローを追加

用意したベンチを実行するワークフローを追加します。
`push`（既定ブランチ）と `pull_request` の両方をトリガにします。
push が base を、pull_request が head を供給します。
base と head の区別は、prperf が OIDC トークンの ref から判定します。

既定ブランチは `main` と `master` の両方を書いておけば、どちらでも動きます。
既定ブランチへの push が一度も無いと比較対象が無く、「比較対象なし、今回の数値のみ」になります。

```yaml
# .github/workflows/prperf.yml
name: prperf
on:
  push:
    branches: [ main, master ]   # base を記録（既定ブランチ）
  pull_request:                  # head を base と比較

jobs:
  bench:
    runs-on: ubuntu-latest
    permissions:
      contents: read         # checkout 用
      id-token: write        # OIDC アップロードに必須
    steps:
      - uses: actions/checkout@v6
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - uses: rperf-dev/prperf-action@v1
        with:
          run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- bin/rails runner ""
```

`run:` には、計測したいコマンドを `rperf record` で包んで渡します。
これが profile を 1 本以上書き出す必要があります。
出力先は `--snapshot-dir "$PRPERF_DIR"` を指定します（`$PRPERF_DIR` は action が用意します）。

`permissions: id-token: write` は必ず付けてください。
これが無いと OIDC トークンが取れず、アップロードできません。
`contents: read` は `actions/checkout` がリポジトリを取得するためです。
`permissions:` を書くと、挙げていない権限は none になるので、両方を明示します。

## 閾値とコメントの設定（任意）

閾値は、回帰したときに Check Run で警告（⚠️）を出すための仕組みです。
どこからを回帰とみなすか、その基準は指標ごとに自分で決められます。
基準は、base から head への各指標の増加（アロケーション、GC、時間など）の上限として与えます。
超えると Check Run に ⚠️ が付き、`comment` 設定に応じて PR コメントが出ます。
閾値は任意で、設定しなければ数字は出ますが、警告もコメントも付きません。
回帰したら警告してほしいときだけ設定します。

設定は全部ワークフローに書きます（別ファイルは不要）。
全体設定を job の `env` に一度だけ書き、必要ならベンチごとに上書きします。

```yaml
jobs:
  bench:
    runs-on: ubuntu-latest
    permissions: { contents: read, id-token: write }
    env:
      PRPERF_DEFAULT_THRESHOLDS: |     # 全ベンチ共通の既定
        alloc: "+10%"
        total_ms: "+20%"
    steps:
      - uses: actions/checkout@v6
      - uses: ruby/setup-ruby@v1
        with: { bundler-cache: true }
      - uses: rperf-dev/prperf-action@v1
        with:
          run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- bin/rails runner ""
```

閾値のキーと、まず設定するなら使いたい値は次のとおりです（prperf 自体に既定値はなく、設定して初めて効きます）。

| キー | 推奨デフォルト | 意味 |
|---|---|---|
| `alloc` | `"+10%"` | アロケーション数の増加。`"+5000"` のように絶対値でも書ける |
| `gc_count` | `"+2"` | GC 回数（minor+major）の増加 |
| `total_ms` | `"+20%"` | 実行時間の増加。ノイズが大きいので相対（%）で |
| `cpu_ms` | `"+15%"` | CPU 時間の増加 |
| `method` | （指定なし） | 名前を挙げたメソッドの self 占有率が、その % を超えたら。例 `{ "JSON.generate": "15%" }` |

値は、summary 系が `"+N%"`（相対）か `"+N"`（絶対）、method 系が `"N%"` です。
不正な値は無視され、Check Run に警告が 1 行出ます（CI は落ちません）。
相対閾値（`+10%`）はベンチをまたいで素直に機能します。
絶対閾値や method はベンチごとに意味が変わるので、必要なときだけベンチ別に上書きしてください。

コメントの出し方は `comment` 入力で制御します（既定 `on_threshold`）。

| 値 | 挙動 |
|---|---|
| `on_threshold` | 閾値超過時のみ PR コメント(既定) |
| `always` | 毎回コメント |
| `never` | コメントしない(Check Run だけ) |

コメントは PR につき 1 通で、push のたびに同じ sticky コメントを編集します（通知が増えません）。

## action の入力一覧

| 入力 | 既定 | 説明 |
|---|---|---|
| `run` | (必須) | 計測コマンド。`.json.gz` を 1 本以上吐くこと |
| `prepare_run` | `""` | 計測前に1回だけ走るセットアップ（fixture 生成や seed など）。計測には含めない |
| `count` | `3` | 計測回数。サーバーは中央値で比較 |
| `benchmark` | `default` | ベンチ系列名。1 コミットで複数ベンチを独立比較できる |
| `thresholds` | `""` | このベンチの閾値(全体設定をキー単位で上書き) |
| `comment` | `on_threshold` | コメントの出し方 |
| `server` | `https://prperf.atdot.net` | prperf サーバー(差し替え可) |
| `upload` | `true` | `false` で計測のみ(アップロードしない) |

通常は `run` だけを指定し、必要に応じて `benchmark`、`thresholds`、`comment` を足します。

## 複数ベンチマーク

1 コミットを複数のベンチで測れます。
ステップを分けて、それぞれに違う `benchmark` 名を付けるだけです。
サーバーは各ベンチを自分の base と比較し、1 つの Check Run にまとめて表示します。

```yaml
- uses: rperf-dev/prperf-action@v1
  with:
    benchmark: boot
    run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- bin/rails runner ""
- uses: rperf-dev/prperf-action@v1
  with:
    benchmark: render
    run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- ruby bench/render.rb
```

ワークフローが 1 本なので、base 側（push）と PR 側で同じ `benchmark` 名が自然に揃います。
各系列が自分の base を持つには、この一致が必要です。

## 動作確認

1. まず既定ブランチ（main または master）に push します。ワークフローが走り、base スナップショットがサーバーに届きます。
2. 適当な PR を作る。ワークフローが走り、Check Run に数字が出れば成功です。
3. アップロード結果のリンクは各ジョブの Summary にも出ます。

Check Run に数字が出れば、base の記録と PR 側の比較がどちらも動いています。

## 制約

fork からの PR は計測できません。
GitHub は fork 起因のワークフローに `id-token: write` を与えないため、OIDC トークンが取れません。
同一リポジトリのブランチ PR は問題ありません。

アップロードの失敗（プラン上限、レート制限、サーバーエラー）は警告のみで、ステップは成功扱いです。
計測コマンド自体の失敗だけがステップを落とします。

無料β期間中は public リポジトリのみです。
private は有料プランで近日提供予定です。

# ベンチマークの書き方

prperf が出す数字は、何を測るかでほぼ決まります。
ベンチマークの設計は、この章で最も結果を左右し、最も手間がかかるところです。

## ベンチマークとは

prperf にとってのベンチマークは、`run:` に渡すコマンドです。
多くの場合は小さな Ruby スクリプト（例 `bench/main.rb`）を `rperf record` で包みます。

```yaml
run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- ruby bench/main.rb
```

rperf は Gemfile に入れておきます（0.10 以上。`bundle exec rperf` で呼ぶためです）。
action はこれを `count` 回（既定 3）まわし、サーバーが中央値を base と比較します。
あなたが書くのは `bench/main.rb` の中身、つまり代表的な処理を一定量こなすスクリプトです。

## 良いベンチマークの条件

良いベンチマークは次の三つを満たします。

1. **気にしている処理を通す**。そのコードに触らない PR では数字が動きません。
2. **決定的である**（毎回まったく同じ仕事をする）。さもないと alloc や GC がブレて、回帰ではないのに警告が出ます。
3. **一定量こなす**。一瞬で終わるとサンプルが少なく、結果が不安定になります。

回帰判定の軸になるのはアロケーション数です。
ベンチマークが決定的なら、アロケーション数は PR 間で 1 個単位まで安定し、わずかな増加も捉えられます。
GC 回数も決定的で数えやすいので、併せて表示します。
逆にベンチマークがブレると、この安定性が失われます。

## 書き方

`bench/main.rb` は、固定入力を一度用意し、ウォームアップしてから、本番のループを十分な回数くりかえす形が基本です。

```ruby
# bench/main.rb
require "json"
require_relative "../config/environment"   # 必要なら（Rails 等）

# 1) 固定の入力を一度だけ用意（乱数・時刻・ネットワークを使わない）
DATA = { "users" => Array.new(100) { |i| { "id" => i, "name" => "user#{i}" } } }

# 2) ウォームアップ（初回限りの遅延読み込み・初期化を計測から外す）
JSON.generate(DATA)

# 3) 本番: 十分な回数くりかえす
5_000.times do
  JSON.generate(DATA)
end
```

入力は固定し、`rand` や `Time.now`、DB、外部 API、ファイルの列挙順などに依存させません。
どうしても乱数が要るなら `srand(42)` で固定します。
ウォームアップで、初回だけ走る処理（autoload、定数初期化、遅延ロード）を計測対象から外します。
回数（ここでは 5,000）は、全体が数百 ms から数秒になるくらいに調整します。
短すぎると不安定になり、長すぎると CI が遅くなります。

## 決定的か確かめる

CI に入れる前に、手元で 2〜3 回流して、アロケーション数と GC 回数が毎回同じことを確認します。
`rperf stat` が summary を stderr に出します。

```sh
bundle exec rperf stat -- ruby bench/main.rb
bundle exec rperf stat -- ruby bench/main.rb
```

2 回とも `allocated_objects` と GC 回数が一致すれば、そのベンチマークは決定的です。
ブレるなら、次を一つずつ潰します。

- [ ] `rand` や `SecureRandom` を使っていない（使うなら `srand` で固定）
- [ ] `Time.now` や `Date.today` に結果が依存しない
- [ ] ネットワークや外部サービスを叩いていない
- [ ] 変動する DB 状態に依存しない（固定のインメモリデータ、fixture、固定 seed を使う）
- [ ] ファイルの列挙順（`Dir.glob` の順序など）に依存しない
- [ ] 入力サイズが毎回同じ

原因の見当をつけるには、フレームグラフで中身を見ます。

```sh
bundle exec rperf record -o out.json.gz -- ruby bench/main.rb
bundle exec rperf report out.json.gz       # viewer が開く
```

非決定要素を固定したら、もう一度 `rperf stat` で一致を確認してから CI に入れます。

## 前準備（任意）

ベンチマークの前に1回だけ走らせたい準備があれば、`prepare_run:` に書きます。
fixture の生成、DB の seed、アセットのビルドなどが該当します。
計測の前に1回だけ実行され、計測には含まれません。

```yaml
- uses: rperf-dev/prperf-action@v1
  with:
    prepare_run: bin/rails db:prepare db:seed   # 計測前に1回だけ
    run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- ruby bench/request.rb
```

失敗するとステップが落ちます。
毎回同じ状態になるよう、固定の seed や入力を使ってください。

## プロジェクト別の例

ワークフローは「登録編」のものをそのまま使い、`run:` を各 `bench/*.rb` に向けるだけです。
前準備が要るベンチ（fixture の生成、DB の seed、アセットのビルドなど）だけ、`prepare_run:` を足します。

### gem / ライブラリ

公開 API を、決定的な固定入力で N 回呼びます。
入力と呼び出し回数を固定したまま、公開 API の回帰だけを測れます。

```ruby
# bench/main.rb
require "your_gem"

# 固定入力を決定的に組む（乱数・時刻・ネットワークを使わない）
DATA = { "items" => Array.new(200) { |i| { "id" => i, "name" => "item-#{i}" } } }

YourGem.encode(DATA)                 # ウォームアップ
5_000.times { YourGem.encode(DATA) }
```

変えるのは `require`、固定入力の `DATA`、呼ぶ API、回数です。
既存の `benchmark/` スクリプト（benchmark-ips など）も流用できますが、時間ベースのループは反復回数が変わって alloc がブレるので、回数固定のループにしてください。

### Sinatra / Rack アプリ

Rack アプリなら何でも、1 リクエストをフルスタックで N 回通します。

```ruby
# bench/request.rb
require_relative "../app"            # あなたの Sinatra/Rack アプリを読み込む
require "rack/mock"

app  = Sinatra::Application           # クラシック。モジュラーなら app = MyApp
PATH = ENV.fetch("BENCH_PATH", "/")   # 測りたいパスに変更
make = -> { Rack::MockRequest.env_for(PATH, "HTTP_HOST" => "localhost") }
pump = ->(r) { b = r[2]; b.each { |_| }; b.close if b.respond_to?(:close) }

3.times    { pump.call(app.call(make.call)) }   # ウォームアップ
2_000.times { pump.call(app.call(make.call)) }
```

`run:` を `ruby bench/request.rb` にします。
変えるのは、アプリの読み込み行、`app`（`config.ru` で `run` しているオブジェクト）、`PATH`、回数です。
DB を引くなら、前準備に seed を、ワークフローに postgres サービスを足します（「Rails クイックスタート」を参照）。

### CLI / 素の Ruby

エントリポイントをプロセス内で固定引数で N 回呼ぶと、起動コストや外部状態の影響を避けやすく、サンプルも安定します。

```ruby
# bench/main.rb
require_relative "../lib/my_cli"

ARGS = %w[build --format json]        # 固定の引数
200.times { MyCli.run(ARGS) }         # あなたのエントリポイントを呼ぶ
```

実行ファイルそのものを測るなら、十分な仕事量があることを確認してから `run:` に直接渡します。
1 回の起動が短いとサンプルが少なく不安定なので、ループで回すか、上のプロセス内ループにしてください。

### Rails アプリ

Rails は次章「Rails クイックスタート」で扱います。
boot、エンドポイント、典型的なクエリ、ジョブの測り方はそこにまとめています。
Roda や grape は Rack アプリなので「Sinatra / Rack」と同じ、Hanami は Rails と同じ発想で測れます。

## やってはいけないこと

次のような測り方は、PR の変更内容と無関係な要因で数字が動くので避けます。

- **テストスイートをそのまま測る**（`rperf record -- rspec`）。PR でテストを足すだけで alloc が増え、回帰と区別できません。
- **乱数や時刻、ネットワークに依存する**。毎回ブレて誤検知になります。
- **短すぎる**。時間がブレ、alloc も小さすぎて差が見えません。
- **関心の薄い経路を測る**。PR が触らず、毎回「変化なし」になります。
- **本物の外部依存（API や DB）を使う**。ネットワーク次第でブレます。

巨大な全部入りベンチマークも、どこが回帰したか分かりにくくなるので避けます。
関心ごとに分け、1 コミットで複数のベンチマークとして測ると、回帰した処理を Check Run と diff で追いやすくなります（分け方は「登録編」の複数ベンチマークを参照）。

## 閾値とのつながり

ベンチマークが決定的だと、きつい相対閾値（例 `alloc: "+5%"`）を誤検知なしでかけられます。
ブレるベンチマークだと閾値を緩めるしかなく、信号が弱くなります。
まずは 1 本、PR が最も触りやすい経路を決定的に測り、base と head の差が出る状態を作るところから始めてください。

# Rails クイックスタート

「何を測るか」で悩む前に、ほぼコピペで動く Rails 向けの出発点を 2 段階で示します。

まず ① で「数字が出る」体験を 30 秒で作り、必要なら ② に進んでください。

## まず boot を測る(ファイル追加ゼロ)

`bin/rails runner ""` はアプリを起動して何もせず終わるので、起動そのものを計測できます。

gem 追加や initializer の重さ、autoload 構成の変化を捕まえられます。
結果は決定的で、追加ファイルも DB も要りません。

`.github/workflows/prperf.yml` をそのまま貼ってください。

```yaml
name: prperf
on:
  push:
    branches: [main, master]   # base を記録(既定ブランチ)
  pull_request:                # PR を base と比較

jobs:
  bench:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - uses: rperf-dev/prperf-action@v1
        with:
          benchmark: boot
          run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- bin/rails runner ""
```

この 1 本で、既定ブランチ(main または master)への push が記録した boot の base と、PR の head が比較されます。

> rperf 0.10 以上を Gemfile に入れておいてください。

## 1 リクエストを測る(本格版)

1 つのエンドポイントを実際に動かし、リクエスト処理が通る経路のアロケーションと GC を測ります。

3 ファイルを貼るだけです。

### 計測用の環境 `config/environments/benchmark.rb`

production 相当だが CI で動かしやすい専用環境を作ります。

```ruby
# config/environments/benchmark.rb
require_relative "production"

Rails.application.configure do
  config.eager_load = true          # 本番同様に全コードを読む
  config.force_ssl = false          # SSL リダイレクトで計測が空になるのを防ぐ
  config.hosts.clear                # ホスト制限を外す(ベンチ用)
  config.require_master_key = false # master key 無しでも起動
  config.log_level = :warn
  config.consider_all_requests_local = false
end
```

### ベンチ本体 `bench/request.rb`

アプリを起動し、固定のエンドポイントへのリクエストを Rack 経由で N 回通すスクリプトです。
レスポンスの body まで消費して、レンダリングまで含めて測ります。
最初の数回はウォームアップとして、autoload やテンプレートのコンパイルを計測から外します。

```ruby
# bench/request.rb — フルスタックを通る 1 リクエストを N 回
require_relative "../config/environment"
require "rack/mock"

PATH = ENV.fetch("BENCH_PATH", "/api/health")  # ← 測りたいエンドポイントに変更

app = Rails.application
build_env = -> { Rack::MockRequest.env_for(PATH, "HTTP_HOST" => "localhost") }

consume = lambda do |result|
  body = result[2]
  body.each { |_| }                 # body を消費してレンダリングまで測る
  body.close if body.respond_to?(:close)
end

# ウォームアップ(autoload・テンプレートコンパイル・コネクション確立)
3.times { consume.call(app.call(build_env.call)) }

1_000.times { consume.call(app.call(build_env.call)) }
```

### ワークフロー `.github/workflows/prperf.yml`

```yaml
name: prperf
on:
  push:
    branches: [main, master]   # base を記録(既定ブランチ)
  pull_request:                # PR を base と比較

jobs:
  bench:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    services:
      postgres:                      # DB を使わないなら services と db:prepare は消す
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports: [ "5432:5432" ]
        options: >-
          --health-cmd pg_isready --health-interval 10s
          --health-timeout 5s --health-retries 5
    env:
      RAILS_ENV: benchmark
      SECRET_KEY_BASE: dummy-for-benchmark
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_benchmark
    steps:
      - uses: actions/checkout@v6
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - run: bin/rails db:prepare db:seed   # DB を使う場合のみ。seed は固定データで
      - uses: rperf-dev/prperf-action@v1
        with:
          benchmark: boot
          run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- bin/rails runner ""
      - uses: rperf-dev/prperf-action@v1
        with:
          benchmark: request
          run: bundle exec rperf record --snapshot-dir "$PRPERF_DIR" -- ruby bench/request.rb
```

この場合も、既定ブランチへの push が base を記録し、PR の head が比較されます。
boot も残すと、起動の回帰とリクエストの回帰を同じ Check Run で別系列として見られます。

## 変えるのはここだけ

- **`PATH`**(`bench/request.rb`)。測りたいエンドポイントです。JSON/API エンドポイントが楽です(アセットのプリコンパイル不要、認証で弾かれにくい)。
- **`db:seed`**。リクエストが DB を引くなら、固定の seed データを用意します。引かないなら postgres service と db 行ごと削除します。
- **回数(1,000)**。全体が数百 ms から数秒になるよう調整します。

まず `PATH` と seed を固定し、それでもブレる場合に回数や対象経路を見直します。

## うまくいかないとき

- **空の結果やリダイレクトばかり**。`force_ssl` で 301、または認証で弾かれています。`benchmark` 環境では `force_ssl=false` 済みです。認証が要る経路なら、公開エンドポイントを選ぶか、`bench/request.rb` でログイン済み env を組んでください。
- **アセット関連のエラー**。ビュー内の asset ヘルパーが原因です。API/JSON エンドポイントを選ぶと、asset ヘルパーの影響を避けやすくなります。
- **数字が毎回ブレる**。seed が固定か、リクエストに時刻や乱数が混ざっていないかを確認してください(「ベンチマークの書き方」のチェックリスト)。ローカルで `RAILS_ENV=benchmark bundle exec rperf stat -- ruby bench/request.rb` を 2 回流して alloc/GC が一致するか見てください。

いずれも、結果が決定的になるよう入力を固定してから、計測の対象や回数を調整するのが近道です。

# 読み方編

prperf の結果は 3 か所に出ます。
Check Run（数字）、PR コメント（閾値超過時）、フレームグラフ viewer（詳細確認）です。
それぞれの読み方を説明します。

## Check Run

PR の Checks に `prperf` という Check Run が付きます。
**conclusion は常に success** で、prperf が CI を落とすことはありません。
判定はあくまで参考表示です。

### タイトル

```
2,001ms → 2,140ms (+7%) · alloc 48,741 → 59,950 (+23%) · GC 4 → 7
```

base と head の主要指標が並びます。
閾値を超えた指標があると先頭に ⚠️ が付きます。

### サマリ（本文）

サマリには次の項目が並びます。

- **base / head の指標表**：alloc、GC（minor/major）、GC 時間、total ms、CPU ms、最大 RSS。閾値超過の行は太字。
- **メソッド diff の上位 10 行**：`base self% → head self% → Δpt`。占有率が増えたメソッドが分かります。
- **viewer への diff リンク**：フレームグラフで詳しく見るための入口。
- base が無いときは「比較対象なし、今回の数値のみ」と表示されます。

まず指標表で増加量を確認し、必要なら method diff と viewer で原因を追います。

### どの数字を信じるか

ベンチマークが決定的なら、アロケーション数と GC 回数は実行時間より安定します。
CI ランナーが変わっても基本ブレません。
ここが回帰判定の主役です。

時間（total_ms / cpu_ms）はノイズが大きい指標です。
CI の wall time は ±10〜20% ブレます。
`count`（既定 3）回の中央値で比較していますが、時間系は「大きく動いたとき」だけ気にするのが安全です。

## PR コメント（sticky）

閾値を超えると（`comment` 設定に応じて）PR にコメントが 1 通付きます。

PR につき 1 通です。
push のたびに同じコメントを編集するので、通知は最大 1 通に保たれます（コメント欄が荒れません）。
超過した指標が `⚠️ **alloc** 48,741 → 59,950 (threshold +10%)` のように並びます。
複数ベンチのときはベンチ名が付きます。

`comment: never` ならコメントは付かず Check Run だけ、`always` なら超過が無くても毎回コメントします。

## フレームグラフ viewer

Check Run の diff リンク、または共有 URL `/view/<repo>/<sha>` から開きます。
rperf の viewer をそのまま使っているので、操作はそちらと同じです。

- **フレームグラフ**：横幅が時間（重み）。広いほど重い。
- **Top タブ（表）**：同じデータを、メソッドを self と累積の重み順に並べた表で見られます。フレームグラフが読みにくいときや、重い順に対処したいときに使います。`Tags` タブは rperf のラベル（tag）ごとの内訳を表で見るものです。
- **diff モード**：base と head の差を色で表示。占有率が増えたメソッドが赤、減ったメソッドが青。Check Run の diff リンクはこのモードで開きます。
- **時間旅行サイドバー**：そのベンチ系列の過去スナップショットが並び、既定ブランチの推移を遡れます。`j` / `k` で新しい/古いへ移動。
- **メソッドの pin**：Shift+クリックでメソッドを固定すると、スナップショット間の占有率の推移がスパークラインで出ます。

URL は恒久リンク（`/view/<repo>/<sha>`）を共有してください。
実体は短寿命の署名 URL を viewer が都度取得する仕組みで、アクセス権の剥奪が最大 10 分で効きます。

複数ベンチを使っているときは、`/view/<repo>/diff?base=<sha>&head=<sha>&bench=<名前>` でベンチを指定して diff を開けます。
viewer のサイドバーは 1 つのベンチ系列にスコープされるので、boot と endpoint のスナップショットが混ざりません。

## ダッシュボード

- **`/`（トップ）**：サービスの説明ページ。
- **`/me`（ログイン後）**：あなたが見られるリポジトリの一覧と、ベンチごとの最新結果へのリンク。public リポジトリは誰でも、private は GitHub の read 権限がある人だけに表示されます。

ログインは GitHub OAuth で、prperf 側に固有のアカウント登録はありません。
認可は常に GitHub の権限に従います（public のスナップショットはログイン不要で閲覧可）。

## 回帰が出たときにすること

1. タイトルの ⚠️ と指標表で、何が（alloc / GC / time / 特定メソッド）どれだけ増えたかを掴む。
2. diff リンクでフレームグラフを開き、赤くなったメソッドを特定する。
3. アロケーション増なら「どこで余計にオブジェクトを作っているか」、GC 増ならその帰結、time 増は CI ノイズの範囲内でないかを疑う。
4. 修正後に push すれば、同じ Check Run と sticky コメントがその場で更新されます。

## よくある状態

- **「比較対象なし」**：base の最新スナップショットがまだありません。既定ブランチ（main/master）への push が、その PR の分岐元より前に最低 1 回走っている必要があります。
- **毎回「変化なし」**：その PR がベンチの通る経路に触れていないだけです。ベンチが気にしたい経路をカバーしているか見直してください。
- **数字が毎回ブレる**：ベンチに非決定要素（乱数、時刻、I/O）があります。決定的にするか、時間系の閾値を緩める、または外すのが有効です。

これらは、base の有無、ベンチマークの対象範囲、非決定要素の三つを順に確認すると切り分けられます。

# フレームグラフの読み方

フレームグラフは、プログラムがどこで時間を使ったかを示す絵です。
読み方にはコツが要るので、前提を置かずに説明します。

## この絵が表しているもの

prperf(rperf)は計測中、プログラムを高い頻度で繰り返しサンプリングし、そのときどの処理が動いていたか(コールスタック)を記録します。
そのサンプルを何千枚も重ねて集計したものがフレームグラフです。

- **1 つの箱**：1 つのメソッド(関数)を表します。
- **箱の幅**：そのメソッド(と、その先で呼ばれた処理)に費やされた時間の割合を表します。言い換えると、その処理が写っていたサンプルの枚数です。幅が広いほど重い処理です。
- **縦の積み重なり**：呼び出しの深さを表します。下が入口、上に行くほど実際に動いていた末端(leaf)です。

下の例を見てください(下が入口、上が末端)。

```
          ┌──────────┐
          │ String#* │                      ← 末端(leaf)。実際に CPU を使っていた処理。幅が広いほど重い
      ┌───┴──────────┴───┐
      │  JSON.generate   │                  ← その呼び出し元
   ┌──┴──────────────────┴────────────┐
   │          Integer#times           │     ← ループ。幅は広いが、中身を呼んでいるだけ
┌──┴──────────────────────────────────┴──┐
│                 <main>                 │  ← プログラム全体。下が入口(root)で、ほぼ 100% 幅
└────────────────────────────────────────┘
```

この絵からは次のことが読み取れます。
一番下の `<main>` は幅 100% ですが、これはプログラム全体を表すので当然であり、注目する箇所ではありません。
実際に時間を食っているのは、上のほうにある幅の広い箱(この例では `String#*`)です。
そこが、CPU が張り付いていた場所です。

## 幅を見る、高さは気にしない

読むときの基本は二つです。

一つは、幅だけを見ることです。
幅がコストを表します。
この箱の幅をゼロにできればその分だけ速くなる、と考えてください。

もう一つは、高さを気にしないことです。
深いネストで高い塔になっていても、幅が細ければコストは小さいです。
逆に低くても、幅が広ければ重い処理です。
深さではなく、横幅の大きい箱を優先して見ます。

## 読むときに陥りやすい誤解

横方向は時間の順番ではありません。
左から順に実行されたわけではなく、名前順などで並べているだけです。
タイムラインとして読まないでください。

単体表示の色には意味がありません。
箱を見分けるための色分けであって、赤いほど熱いという意味ではありません(色に意味があるのは、後述の diff モードだけです)。

一番下の箱が 100% 幅でも正常です。
それは全体を表しているだけです。

## 読む手順

1. 上のほうにある幅の広い箱を探します。そこが時間の使いどころです。
2. その箱をクリックして拡大(ズーム)します。その部分木だけが画面いっぱいに広がり、内訳が見やすくなります。
3. 気になるメソッド名があれば検索して、グラフ全体で光らせます。同じメソッドがあちこちに散っていても、合計の幅で重さが分かります。
4. 絵が読みにくければ Top タブ(フラットな表)を見ます。メソッドが self と累積の重み順に並ぶので、上から潰していけます。

まず幅で候補を絞り、ズーム、検索、Top タブの順に情報量を増やします。

## 回帰調査で使う diff モード

Check Run の diff リンクから開くと、viewer は **diff モード**になります。
回帰調査ではこのモードを使います。

このモードでは色に意味が出ます。
base と比べて占有率が増えたメソッドが赤、減ったメソッドが青、ほぼ変化のないものが中間色です。
そのため diff モードでは、一番広い箱ではなく一番赤い箱を探します。
そこが、この PR で重くなった場所です。

色と幅は次のように読み分けます。

- **広くて赤い**：もともと重かった処理がさらに悪化したことを表します。最優先で見ます。
- **細いが真っ赤**：新しく現れた、または割合が急増した処理です。新規の原因の可能性があります。

幅は head の占有率を、色は base からの増減を表します。

## viewer の操作

- **クリック**：その箱にズームします(部分木を全幅表示)。
- **検索ボックス**：名前で絞り込み、ハイライトします。`JSON` などで関連箇所をまとめて把握できます。
- **Top タブ、Tags タブ**：表で見たいときに使います。self と累積の重み順に並びます。
- **Shift+クリック**：メソッドを pin します。スナップショット間での占有率の推移をスパークラインで表示します。
- **j / k**：新しいスナップショット、古いスナップショットへ移動します。

## この絵が表すのは時間

prperf のフレームグラフは、時間(CPU の重み)の絵です。
アロケーション数や GC 回数は、summary の数字(Check Run の表)で見ます。
alloc が増えた回帰では、フレームグラフで時間が増えた(赤くなった)メソッドを手がかりに、そこで余計にオブジェクトを作っていないかを当たります。

## 読み方の要点

- 幅がコストを表し、高さは無視します。
- 上のほうの幅広な箱が、時間の使いどころです。
- 横方向は順番ではなく、単体表示の色には意味がありません。
- diff モードでは赤い箱を探します。そこが今回悪くなった場所です。

通常表示では幅を、diff モードでは色を優先して読みます。
