Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

インターフェースとしてのRSpecとAIで実現する、パフォーマンスチューニングの自動化

まえがき

この記事は「福岡Tech LT大忘年会」でスポンサーセッション中にお話しした、「あの日僕が見た胡蝶の夢 〜人の夢は終わらねェ AIによるパフォーマンスチューニングのすゝめ〜」をより詳しく解説した記事になります。
当日の様子については以下ブログからご覧ください。

moneyforward-dev.jp

当日の資料についてはこちらです(再掲)。



はじめに

皆さんこんにちは。
クラウド経費本部 プロダクト開発部 Guardianグループでリーダーをやりつつ、福岡TechPR(技術広報)で色々やっている、@tosite(てっしー)と申します。
本日は「パフォーマンスチューニング」についてお話をしたいと思います。

皆さんご存知の通り、パフォーマンスチューニングは、Webアプリケーション開発において重要な課題です。
私たちのグループでも「攻めの運用」を実践するべく、パフォーマンスチューニングに取り組もうと思っています。
ですが、「どこから手を付けるべきか」「どのような影響があるのか」「何を見ればいいのか」が分かりにくく、特に大規模なRailsアプリケーションでは属人化しがちです。

本記事では、RSpecとAI(Claude Code)を組み合わせることで、パフォーマンスチューニングの「計測」「分析」を自動化し、誰もが取り組みやすくする仕組みを紹介します!
「テストコードをインターフェースとしてAIと連携する」という新しいアプローチを通じて、どのようにしてパフォーマンスチューニングを民主化したのか。ご興味ある方はぜひご一読ください。

パフォーマンスチューニングに取り組むときの困りごと

早速パフォーマンスチューニングの話を始める……前に、 パフォーマンスチューニングを行うに当たって、皆さんも以下のような困りごとに直面したことはないでしょうか。

どこから手を付けるべきか分からない

コードレビューの段階で「このコードはパフォーマンスに影響しそう」と気づいても、具体的にどこが問題なのか、どの程度の影響があるのかを判断するのは困難です。
ドメイン知識やインフラ知識が豊富なシニアエンジニアであればともかく、全員が同じ視座でレビューをして安全に改善をしていくのは難しいと感じています。

大量のデータ準備と測定ステップの多さ

パフォーマンス検証には「大量のテストデータ」「クエリログ」「メモリ使用量」「EXPLAINログ」など様々な計測データが必要ですが、毎回準備するのは大変です。
ものによっては本番でしか検証できない、というようなこともたまにありますが、このことはパフォーマンスチューニングを行う妨げとなります。
分析に必要なログはそもそも大量のデータがないと効果的なものが取得できず、悩まされた開発者もいるのではないでしょうか。

本番環境で問題が発覚する

特に厄介なのが、ローカル環境では問題なかったとしても、データ量の違いによって本番環境のみでエラーが発生するケースです。
ローカル環境ではデータ量が少ないため問題にはならず、本番環境で例えばメモリ展開エラーやスロークエリなどの問題を引き起こすことがあります。
「起きてしまった」後ではなく、事前に傾向値から分析・検知ができればいいのですが・・・。

解決すべき3つの課題

パフォーマンス検証にあたっての困りごとを整理すると、以下の3つの課題が浮かび上がってきます。

  • 課題1: パフォーマンス問題の発見が遅れる
    • パフォーマンス劣化がPRレビュー段階で検出されず、本番環境で問題が発覚
    • 特にメモリ使用量やクエリパフォーマンスの変化を定量的に測定する仕組みがない
    • レビュアーがコードを見ただけではパフォーマンス影響を推測しづらい
  • 課題2: パフォーマンスを測定しづらい、あるいは測定コストが高い
    • パフォーマンス検証のための環境や準備を行うのに時間がかかる
    • 状況によってはより本番に近い環境でないと測定できないこともある
    • 測定手順に再現性がない
  • 課題3: パフォーマンス分析の属人化
    • パフォーマンス問題の分析には高度な知識が必要
    • 分析作業そのものが担当者のスキルに依存する
    • 分析プロセスやフォーマットが標準化されていないため、見落としが発生しやすい

ではどう解決していくか?そうだ、テストコードだ!

これらの課題を解決するために色々と考えた結果、以前に対応した「バッチジョブのパフォーマンスチューニング」を思い出しました。
当時、課金処理にまつわる重要なバッチジョブを高速化する必要があったのですが、これは本番にしか存在し得ないデータで、今回と同様、ステージングやローカルなどで状況を再現することが難しい案件でした。
そこで私は、「テスト環境にパフォーマンス測定のための大量データをセットアップし、改修前後で測定を行う」というアプローチをとりました。

今回、その時の経験を活かし 「テストコードをインターフェースとしてAIと連携」 してみることにしました。
テストコードという標準化されたフォーマットを通じて、AIが分析しやすい形でデータを提供するだけでなく、そもそものパフォーマンス測定用のテストデータすらAIに作らせよう、という試みです。

忙しい人のための結論ファースト

ということで、最終的にできあがったのが以下フローです。
このように、「ベンチマークSpec」というテストコードを介してパフォーマンスの計測・分析までをAIに任せる形となりました。
多少手動で対応しないといけない部分はありますが、フロー構築前に比べて非常に少ないコストでパフォーマンスの傾向値がわかるようになりました。

全体のフロー。プルリクエストを媒体としてベンチマークSpecを作成し、それをmain・featureブランチで実行、その結果をAIに渡して分析させてレポートを作り出している。

では、実際にどのようなフェーズで実装を進めていったのかを見てみましょう。

フェーズ1: ロギング用ヘルパーメソッドの整備

RSpecテスト実行時にパフォーマンスメトリクスを自動収集する基盤を構築しました。
集計するデータは上述の通り、以下のデータです。

  • クエリログ
  • メモリ使用量ログ
  • EXPLAINログ

このとき、可能であればよりエンドツーエンドに近いほうが、具体的にはコントローラーもしくはジョブに対するテストが望ましいと考えました。
なぜなら、Railsのアプリケーションではbefore_actionなどで暗黙的に処理をしていることもあり、そこが原因でパフォーマンスが変化することもあるからです。

しかし、エンドツーエンドに近くなれば近くなるほどログに余計なノイズが入ってくる可能性があります。
そこで、特定のブロックで囲んだ部分のパフォーマンスだけを測定するようなヘルパーメソッドを先に実装しました。

ヘルパーメソッドの設計思想

  • テストコード内の特定のブロックのみを計測対象とする(Factory作成などのセットアップは除外)
  • クエリログ、パフォーマンスログ、EXPLAINログを別ファイルで管理
  • タイムスタンプ付きファイル名で複数実行の比較を容易にする

結果、具体的には以下のような処理をテストケース中に挟むことで容易にログを取得できるようになりました。

describe 'GET /example' do
  it 'measures performance with variable record count' do
    explain_queries = []
    with_performance_logging(explain_queries: explain_queries) do
      get example_path
      expect(response).to have_http_status(:ok)
    end
  end
end

これでフェーズ1は準備完了です。

フェーズ2: Claude Codeコマンドによる自動化

AIを活用して、ベンチマークSpec生成と分析を自動化しました。

フェーズ2-1. ベンチマークSpec自動生成プロンプト

プルリクエストがわかれば、どの部分が修正されたかは一目瞭然です。
修正されたコードがどのエンドポイントから呼び出されるかの調査は、AIの得意領域です。
そして、テストデータを作るためのガイドレールはすでにコードベース、特に既存のテストケースの中に潜んでいます。
具体的には、Factoryやその使い方、ドメイン知識に対する説明などが実際のテストコードやコメントの中にたくさん散りばめられています。

これをベースにして、1,000件や10,000件のテストデータを作ったうえでテストを実行するためのSpec(便宜上これをベンチマークSpecと呼びます)をAIに作らせます。
ただし、これを一発で作らせるのは、いかにAIと言えど至難の業です。
プロンプトの一部に「テストが通るまで修正を続けてほしい」こと、そして「テスト作成中は時間がかかるので少ないデータでテストを通してほしい」こと、「30秒待って結果が返ってこなければ再度テストを作り直してほしい」ことなどを追加しました。

また、ベンチマークSpecを作成した後の最終レビュー項目も別で作っておき、その項目を満たすまではループし続けるような内容にしています。
これによって、プルリクエストから自動でベンチマークSpecを生成するための土壌が完成しました。

フェーズ2-2. パフォーマンス分析レポート作成プロンプト

ここまでで以下のものが出揃いました。

  • ①パフォーマンスに影響を与える可能性のあるプルリクエスト
  • ②パフォーマンスを測定するためのヘルパーメソッド

あとは、②で測定したログを用いて計測するだけです!
この分析も当然AIを用いて行うわけですが、今回はプロンプトをモジュール化したうえで、「Skills」として分離、並列実行を可能にしました。
なお、分析に使用する指標は以下のとおりです。

  • プルリクエストの変更をベースにコードから分析を行う(この際に変更履歴も追う)
  • 修正前後の測定ログを比較して分析を行う
  • 上記を踏まえて統括的なレポートを作成する

実装詳細

ロギング用ヘルパーメソッド

パフォーマンス測定に必要な3種類のログを自動収集する基盤を構築しました。

クエリログ

  • ActiveRecord実行時のSQLクエリを全てキャプチャ
  • バインド変数、キャッシュ情報も記録

出力例:

================================================================================
Test File: resources_controller_spec
Timestamp: 2025-12-03T10:44:17+09:00
Example: GET #index returns http success
Location: ./spec/controllers/resources_controller_spec.rb:10
================================================================================

[Query 1] Resource Load
SELECT `resources`.* FROM `resources` WHERE `resources`.`user_id` = 123
Binds: [["user_id", 123]]

[Query 2] User Load
SELECT `users`.* FROM `users` WHERE `users`.`id` = 456

Total queries: 76
================================================================================

メモリ使用量ログ

  • 実行時間の測定(マイクロ秒精度)
  • メモリ使用量の測定(GC統計ベース)
  • GC強制実行による正確な測定
  • 測定メトリクス
    • Total Memory(総メモリ使用量)
    • Heap Live Slots(生存オブジェクト数)
    • Heap Free Slots(空きスロット数)
    • Total Allocated/Freed Objects(割り当て/解放オブジェクト数)

出力例:

================================================================================
Test File: resources_controller_spec
Timestamp: 2025-12-03T10:44:17+09:00
Example: GET #index returns http success
================================================================================

Execution Time:
  120.500 ms (0.120500 seconds)

Memory Usage (Before):
  Total Memory: 456.37 MB (478,543,872 bytes)
  Heap Live Slots: 1,234,567

Memory Usage (After):
  Total Memory: 504.95 MB (529,407,488 bytes)
  Heap Live Slots: 1,345,678

Memory Change:
  Total Memory: +48.57 MB (+50,863,616 bytes)
  Live Slots: +111,111
================================================================================

EXPLAINログ

  • PR差分から抽出したクエリのEXPLAIN分析
  • インデックス使用状況の確認
  • フルテーブルスキャン検出

出力例

================================================================================
EXPLAIN Results (from PR diff analysis)
Test File: resources_controller_spec
Generated: 2025-12-03T10:44:17+09:00
================================================================================

--------------------------------------------------------------------------------
Query #1: Resource.where(user_id: user.id).includes(:details, :category)
--------------------------------------------------------------------------------
+----+-------------+-------------------+------------+--------+----------------------------------+----------------------------------+---------+------------------+------+----------+-------------+
| id | select_type | table             | partitions | type   | possible_keys                    | key                              | key_len | ref              | rows | filtered | Extra       |
+----+-------------+-------------------+------------+--------+----------------------------------+----------------------------------+---------+------------------+------+----------+-------------+
|  1 | SIMPLE      | resources         | NULL       | ref    | index_resources_on_user_id       | index_resources_on_user_id       | 8       | const            |  100 |   100.0  | NULL        |
|  1 | SIMPLE      | resource_details  | NULL       | eq_ref | PRIMARY,index_on_resource_id     | index_on_resource_id             | 8       | resources.id     |    1 |   100.0  | Using where |
|  1 | SIMPLE      | categories        | NULL       | eq_ref | PRIMARY                          | PRIMARY                          | 8       | resources.cat_id |    1 |   100.0  | NULL        |
+----+-------------+-------------------+------------+--------+----------------------------------+----------------------------------+---------+------------------+------+----------+-------------+
3 rows in set (0.00 sec)


Total queries analyzed: 1
================================================================================

ベンチマークSpec自動生成

ベンチマークSpecの自動生成については以下のとおりです。

ワークフロー

1. プルリクエスト番号を指定
   ↓
2. プルリクエストの差分を解析してベンチマーク対象を特定
   ↓
3. ユーザーに確認(コントローラー/サービス/ワーカー)
   ↓
4. データ量を確認(100件/1000件/10000件)
   ↓
5. ベンチマークSpecを生成
   ↓
6. 件数を絞った状態で実行可能性を検証
   ↓
7. 静的検証(Rubocop及び生成ルールに準拠しているか)
   ↓
8. 完成

生成されるSpecの例

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Benchmark: ResourcesController#index', type: :request do
  let(:user) { create(:user) }
  let!(:resources) { create_list(:resource, 1000, user: user) }

  before do
    sign_in user
  end

  it 'benchmarks GET /resources' do
    # EXPLAIN分析対象のクエリ(PR差分から抽出)
    explain_queries = [
      -> { Resource.where(user_id: user.id).includes(:details) }
    ]

    # パフォーマンス測定 + クエリログ + EXPLAIN分析
    with_query_and_performance_and_explain_logging(explain_queries: explain_queries) do
      get resources_path
      expect(response).to have_http_status(:success)
    end
  end
end

パフォーマンス分析プロンプト

生成されたベンチマークSpecを変更前(現在のmainブランチ)と変更後(featureブランチ)でそれぞれ実行すると、ヘルパーメソッドを経由して測定ログが生成されます。
そのログを用いて、カスタムコマンドでパフォーマンス分析を実行します。
分析は以下の3段階のフェーズでそれぞれ処理されます。

フェーズ1: コード分析

分析内容

  • アプリケーションコードからの分析実行
  • before_actionチェーンの実行順序解析
  • メモリ展開リスクの検出(.to_a, .map, .each等の呼び出しタイミング)
  • スコープ適用タイミングの変更検出
  • Eager Loading(includes/preload)の影響分析

フェーズ2: ログ分析

分析内容

  • メモリ使用量のタイムライン分析
  • メモリスパイク検出(10MB以上の急増)
  • クエリパターン分析とN+1検出
  • スロークエリ検出(100ms以上)
  • 変更前後の比較(オプション)

フェーズ3: レポート生成

フェーズ1と2のの結果を統合し、Markdownレポートを生成します。

出力

# パフォーマンス分析レポート

## 総合評価
- リスクレベル: ⚠️ Critical
- OOMリスク: High
- 推奨アクション: マージ前に修正が必要

## Critical問題

### 1. メモリ展開タイミングの問題
**場所:** app/controllers/resources_controller.rb:45
**内容:** before_action内で.to_aが呼ばれているが、その時点でフィルタが未適用

**影響:**
- ローカル環境: 約500件のレコード → 約5MB
- 本番環境: 約50,000件のレコード → 約500MB
- OOMリスク: High

**推奨対応:**
1. フィルタリングをbefore_action内で適用
2. または、.to_a呼び出しをフィルタ適用後に移動

## ログ分析結果

### メモリ使用量
- 変更前: 20.0MB増加
- 変更後: 48.57MB増加
- 差分: +28.57MB (+142.9%)

### クエリ分析
- クエリ数: 80件 → 76件 (-4件)
- 実行時間: 150.0ms → 120.0ms (-30.0ms)
- N+1検出: 0件

## トレードオフ分析
クエリ回数と実行時間は改善しているが、メモリ使用量が大幅に増加している。
Eager Loadingの追加によりN+1は解消されたが、大量レコードの一括読み込みがメモリ圧迫の原因。

プロンプトの構成について

当初はモノリシックなプロンプトでしたが、以下の課題がありました。

  • プロンプトが大きすぎてタイムアウトが発生
  • 一部だけを改善するのが困難
  • デバッグが難しい

そこで、処理を複数の「Skill」に分割しました。
具体的には以下のような構成です。

.claude/
├── commands/
│   └── performance/
│       ├── analyze.md               # メインオーケストレーター
│       └── create-benchmark-spec.md # ベンチマーク生成オーケストレーター
└── skills/
    ├── performance-analyze-code/
    │   └── SKILL.md
    ├── performance-analyze-logs/
    │   └── SKILL.md
    ├── performance-analyze-report/
    │   └── SKILL.md
    ├── performance-benchmark-spec-analyze-pr/
    │   └── SKILL.md
    └── performance-benchmark-spec-generate-spec/
        └── SKILL.md

導入効果

データドリブンなレビューの実現

今まではレビューが知識量に依存していた部分もありましたが、定量データを用いて誰でも正確な分析ができるようになっただけではなく、自分では思いつかないような懸念なども気づくことができるようになりました。
具体的な数値とリスク評価により、レビューの説得力が向上しました。

知識の標準化

分析ノウハウをプロンプトに記述することで、チーム全体で共有可能になりました。
それだけでなく、AIが分析した内容そのものを議論することができるようになったため、より事実に基づいた議論と、高速に知見の吸収が行えるようになりました。

まとめ

本記事では、RSpecとAIを組み合わせることで、パフォーマンスチューニングの「計測」「分析」だけでなく、その事前段階の準備までをも自動化する取り組みについて紹介させていただきました。
特に、核心となる「テストコードをインターフェースとしてAIと連携する」というアプローチは、 テストコードという標準化されたフォーマットを通じて、AIが分析しやすい形でデータを提供することができました。
これは「AIファースト」のための効果的なアプローチだったのではないでしょうか。

パフォーマンスチューニングは、これまで「詳しい人に聞く」「経験に頼る」という属人的な作業でした。
ですが、今回紹介した仕組みにより、誰もがパフォーマンス問題を早期発見し、定量的なデータに基づいて改善できるようになります。

現に私のチームメンバーもすでに利用を開始してくれており、バンバンパフォーマンス改善に取り組んでくれています。
ざっと見たところですが、たまに過剰に指摘しているところこそあるものの、割と核心を突くフィードバックをAIが提供してくれているようです。
何よりもチームメンバーが「パフォーマンスチューニングが前よりも怖くなくなりました!」と言ってくれたことで、作ってよかったなあと感じることができました。

「推測するな、計測せよ」という名言はパフォーマンス改善の文脈でたまに耳にするフレーズですが、まさに今回は「AIを用いた計測」の足がかりを作ることができたんじゃないでしょうか。
今期、私たちGuardianグループは「NEXT CRE」を目標に、一部SRE的な働き方ができるよう、アプリケーションレイヤーだけでなくインフラレイヤーの知見も深めていこうとしています。

NEXT CREを掲げた図。New Era of Customer Reliability、Evolve to Empower the Team、X-functional Collaboration for Success、Tech-savvy Experts with SRE Mindsetの頭文字を取ってNEXT。

今回の取り組みによってGuardianグループのパフォーマンス改善への取り組みが進むとともに、皆さまにとっても参考になれば幸いです。