ブログ

ej-technologies の製品ニュース、アップデート、および技術に関する知見。

RSS で読む。 メール で購読。 でフォロー。

Claudeがコードを読むだけでは決して見つけられなかったホットスポット

2026-04-17
投稿者: Ingo Kegel

CommaFeedは私が日々使っているRSSリーダーです。オープンソースとして公開されており、作者自身がcommafeed.comで無料の公開インスタンスを運営しています。フィードを更新するたびに、処理を行ってくれているのは他人のサーバーです。そこで、その処理は実際にはどこに費やされているのだろうと考え、Claude CodeとJProfiler MCPをCommaFeedに向けてみました。

その結果、データベースの最適化がCommaFeedにマージされました。修正自体は複雑なものではありませんでしたが、ソースコードだけからClaudeがこの解決策に辿り着くことは決してなかったでしょう。後から尋ねてみたところ、Claude自身もそう認めました。自然な最初の試みでは、間違った箇所を修正してしまったはずだというのです。以下では、JProfiler MCPサーバーに接続したClaude Codeが、好奇心の段階からマージされたPRまで私をどのように導いたかを読み進めてください。

ホットスポットの発見

CommaFeedをローカルで起動し、実行中のプロセスにClaude Codeを向けました。次に、フィードをセットアップして更新するユニットテストを実行します。エージェントが最初に行ったのは、JDBCホットスポットの問い合わせでした。ランキングを支配していたのは1つのクエリ、FeedEntryDAO.findExisting内のエントリごとのguidHash存在チェックでした。

累積時間は141 msでした。それ自体は特に憂慮すべき数字ではありません。目を引いたのは件数でした。400件のチェックに対して580件のJDBCクエリ。何かが二重にチェックされていたのです。呼び出し元はFeedRefreshUpdater.addEntryで、これはフィード更新のたびにエントリごとに1回実行されます。

「400件のチェックに対して580件のJDBCクエリ」という状況を、静的アナライザーは決してフラグ付けしません。この比率は実行時にしか存在しないからです。FeedRefreshUpdaterを読むエージェントの目には、もっともらしく見えるループがもっともらしく見えるDAOメソッドを呼び出しているだけに映ります。ソースを読む限り、一見して問題となる箇所はどこにもありません。

修正内容

エージェントが診断したのは、古典的なN+1クエリパターンでした。パース済みのフィード内のすべてのエントリが、存在チェックのために個別のデータベースラウンドトリップを発生させていたのです。提案された修正は、フィードの既存GUIDを単一のクエリで事前に取得し、メモリ上で新規エントリをフィルタリングして、新規エントリに対してのみエントリごとのループに入るというものでした。元のエントリごとのチェックは、並行処理に対する安全網としてそのまま残されました。

エージェントはその後、再プロファイリングを行って検証しました。guidHashチェックは呼び出し回数で580回から400回へ(-31%)、時間では141 msから93 msへ(-34%)減少しました。残る400回は、初回の更新時に本当に新規のエントリに対して行われる、避けられない安全網としての呼び出しです。私はこの変更をプルリクエストとして提出しました。

Claude Codeが生成した修正前後のJDBCホットスポット比較Claude Codeが生成した修正前後のJDBCホットスポット比較

レビュー

メンテナーのAthouが1点懸念を指摘しました。CommaFeedでは、commafeed.database.cleanup.entries-max-ageとmax-feed-capacityをゼロに設定することで、エントリのクリーンアップを完全に無効にできます。そのような設定のユーザーでは、1つのフィードに数年かけて数百万件のエントリが蓄積される可能性があります。更新のたびにすべてのエントリを事前に取得していては、メモリ使用量が急激に跳ね上がってしまいます。

彼の提案は、既存のGUIDをすべて取得するのではなく、現在のフィードXMLに実際に含まれているGUIDだけをSQLのIN句で取得し、1000エントリずつのバッチに分割するというものでした。

ここでもプロファイリングデータが重要な役割を果たしました。修正はコードに対する推測ではなく実測に基づいていたため、レビューの焦点は診断が正しいかどうかではなく、このアプローチをいかに安全にスケールさせるかにありました。メモリ急増の懸念は、メンテナーのドメイン固有の知識を必要とする制約であり、PRのプロセスはまさに意図されたとおりに機能したのです。

プルリクエストに対するメンテナーのレビューコメントプルリクエストに対するメンテナーのレビューコメント

第2ラウンド

メンテナーのレビュー内容をClaude Codeにそのまま入力しました。エージェントはメソッドシグネチャをfindExistingGuids(Feed feed)からfindExistingGuids(Feed feed, Set<String> guidHashes)へ変更し、Lists.partitionによるバッチ化とともにIN句を追加し、現在処理中のエントリのハッシュを渡すように呼び出し側を更新しました。さらに、すでにエントリが登録されているフィードを更新しても重複エントリが作成されないことを検証する統合テストも追加しました。

public Set<String> findExistingGuids(Feed feed, Set<String> guidHashes) {
    if (guidHashes.isEmpty()) {
        return Set.of();
    }
    Set<String> result = new HashSet<>();
    for (List<String> batch : Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) {
        result.addAll(query().select(ENTRY.guidHash)
            .from(ENTRY)
            .where(ENTRY.feed.eq(feed), ENTRY.guidHash.in(batch))
            .fetch());
    }
    return result;
}

更新をプッシュする前に、エージェントはテストを再プロファイリングしました。単一のクエリをバッチ化したINルックアップに分割すると、最初の改善効果を打ち消すオーバーヘッドが再び入り込む可能性がありました。測定の結果はその心配がないことを示しました。テストしたフィードでは、バッチ化版は元の単一クエリアプローチと数パーセント以内の差に収まり、エントリごとのチェック回数も新しい低いベースラインを維持しました。これを確認できるのは実行時データだけです。

PRはその日のうちにマージされました。

マージされたプルリクエストと100人目のコントリビューターを知らせるコメントマージされたプルリクエストと100人目のコントリビューターを知らせるコメント

MCPがなかった場合に起こっていたこと

PRがマージされた後、ソースコードだけを手がかりに作業するとしたら何を試していたかを、エージェントに尋ねてみました。答えはこうです。自然な試みはHibernateのバッチINSERTの有効化、つまり個々のINSERTを高速化する設定だったはずだということです。それはこの問題には一切効果がなかったでしょう。ボトルネックはクエリの件数であって、クエリの速度ではなかったのです。エージェントは誤った修正を推奨することになっていたでしょう。

続いて、短い推薦コメントを依頼しました。以下はエージェント自身の言葉です。

JProfilerのJDBCプローブと永続化プローブは、時間計測だけでは決して表面化しないパターンを明らかにしました。何も変更されていないときでさえ、更新サイクルのたびにエントリ単位の冗長なデータベースチェックが実行されていたのです。このプローブデータがなければ、私は完全に間違ったものを最適化していたでしょう。

commafeed.comでのフィード更新は、そのたびに作者のリソースを消費します。更新時に最もコストのかかるクエリを大幅に削減することで、同じハードウェア上でより多くのユーザーをさばけるようになります。

自分のコードで試してみる

同じアプローチはあらゆるJavaアプリケーションに有効です。どのコードベースにも、ソースを読んでいては見えないホットスポットが存在します。DAOの背後に潜むN+1クエリ、数百万個の短命オブジェクトを割り当てるシリアライザー、外部への呼び出しを静かに3倍に膨れ上がらせるHTTPクライアントに埋め込まれたリトライポリシー。コードを読んでは見つからず、実行時データが見つけてくれます。

セットアップは1つのコマンドで済みます。AIエージェントをアプリケーションに向け、時間がどこに費やされているかを尋ねてみてください。JProfiler MCPページには、Claude Code、Cursor、Codex、その他のMCP互換ツール向けの手順が記載されています。

アーカイブ