「本番環境で突然レスポンスが遅くなった」「特定のAPIエンドポイントだけ異常に重い」「メモリリークの原因がわからない」——パフォーマンス問題の解決は、SESエンジニアが現場で最も頻繁に直面する課題の一つです。
**Gemini CLIを使えば、パフォーマンスプロファイリングの結果を分析し、ボトルネックの特定から最適化コードの生成まで、一連の作業を大幅に効率化できます。**プロファイラの出力を読み解く専門知識がなくても、AIがデータを解釈して具体的な改善提案を行ってくれます。
本記事では、Gemini CLIを活用したパフォーマンスプロファイリングと最適化の実践手法を、Node.js/Python/Goのマルチランタイムで解説します。

パフォーマンスプロファイリングの基礎知識
プロファイリングの3つの軸
パフォーマンス問題は大きく3つの軸で分析します。
| 軸 | 指標 | ツール例 |
|---|---|---|
| CPU | 実行時間、CPU使用率 | perf, pprof, V8 CPU Profiler |
| メモリ | ヒープサイズ、GC頻度 | heapdump, memray, pprof |
| I/O | レイテンシ、スループット | strace, tcpdump, jaeger |
Gemini CLIがプロファイリングに向く理由
- プロファイラ出力の自然言語解釈:フレームグラフやヒープスナップショットの数値データを読み解き、問題箇所を特定
- コンテキスト理解:コードベース全体を把握した上で、ボトルネック箇所のリファクタリングを提案
- マルチランタイム対応:Node.js、Python、Go、Javaなど多様なプロファイラ形式に対応
- 最適化パターンの提案:キャッシュ導入、クエリ最適化、アルゴリズム改善などの具体的なコード生成
Gemini CLIの基本的な使い方を理解していることを前提に進めます。
Node.js アプリケーションのプロファイリング
CPU プロファイリング
Node.jsのビルトインプロファイラとGemini CLIを組み合わせて、CPU集中処理のボトルネックを特定します。
# 1. CPU プロファイルの取得
node --prof app.js
# → isolate-0xXXXXXXXX-v8.log が生成される
# 2. プロファイルの加工
node --prof-process isolate-*.log > profile.txt
# 3. Gemini CLIで分析
gemini "以下のNode.js CPUプロファイルを分析してください。
ボトルネックになっている関数を特定し、
最適化の提案をコード付きで行ってください。
$(cat profile.txt)"
実践例:JSONパースのボトルネック
gemini "以下のプロファイル結果から、JSON処理がボトルネックであることがわかりました。
大量のJSONをパースするこのコードを最適化してください:
$(cat src/services/data-processor.ts)
プロファイル結果(上位5関数):
1. JSON.parse - 35.2% (ticks: 4521)
2. processRecords - 22.1% (ticks: 2836)
3. validateSchema - 15.3% (ticks: 1964)
4. transformData - 12.7% (ticks: 1630)
5. writeOutput - 8.4% (ticks: 1078)"
Gemini CLIが生成する最適化コード:
// 最適化前:全データを一括パース
async function processLargeJSON(filePath: string): Promise<ProcessedData[]> {
const raw = await fs.readFile(filePath, 'utf-8');
const data = JSON.parse(raw); // ← ボトルネック:大量メモリ消費 + ブロッキング
return data.map(record => transformRecord(record));
}
// 最適化後:ストリーミングパース + Worker Threads
import { createReadStream } from 'fs';
import { pipeline } from 'stream/promises';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import os from 'os';
const NUM_WORKERS = Math.max(1, os.cpus().length - 1);
async function processLargeJSON(filePath: string): Promise<ProcessedData[]> {
const results: ProcessedData[] = [];
const batch: RawRecord[] = [];
const BATCH_SIZE = 1000;
await pipeline(
createReadStream(filePath),
parser(),
streamArray(),
async function* (source) {
for await (const { value } of source) {
batch.push(value);
if (batch.length >= BATCH_SIZE) {
const processed = await processBatchInWorker(batch.splice(0));
results.push(...processed);
yield;
}
}
// 残りのバッチを処理
if (batch.length > 0) {
const processed = await processBatchInWorker(batch);
results.push(...processed);
}
},
);
return results;
}
function processBatchInWorker(batch: RawRecord[]): Promise<ProcessedData[]> {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, {
workerData: { batch },
});
worker.on('message', resolve);
worker.on('error', reject);
});
}
// Worker Thread側
if (!isMainThread && parentPort) {
const { batch } = workerData;
const results = batch.map(transformRecord);
parentPort.postMessage(results);
}
メモリリーク検出
# ヒープスナップショットの取得
node --inspect app.js &
# Chrome DevToolsでヒープスナップショットを取得 → heapdump.json
# Gemini CLIで分析
gemini "以下のNode.jsヒープスナップショットのサマリーを分析してください。
メモリリークの原因となっている可能性のあるオブジェクトを特定し、
修正方法を提案してください。
ヒープサマリー:
- 総ヒープサイズ: 512MB(起動時128MB → 24時間後512MB)
- 最大オブジェクト:
1. (string) - 180MB, 2,340,000個
2. (array) - 95MB, 450,000個
3. EventEmitter - 45MB, 120,000個
4. Timer - 32MB, 80,000個
5. Socket - 28MB, 15,000個
EventEmitter のリテイナーツリー:
- root → Server → _connections → Socket → EventEmitter"
Gemini CLIの分析結果例:
// メモリリークの原因と修正
// 問題1: イベントリスナーの解除忘れ
class ConnectionManager {
private connections = new Map<string, Socket>();
addConnection(id: string, socket: Socket) {
// ❌ リスナーが蓄積し続ける
// socket.on('data', this.handleData);
// socket.on('error', this.handleError);
// ✅ 修正:接続クローズ時にリスナーを解除
const dataHandler = (data: Buffer) => this.handleData(id, data);
const errorHandler = (err: Error) => this.handleError(id, err);
socket.on('data', dataHandler);
socket.on('error', errorHandler);
socket.once('close', () => {
socket.removeListener('data', dataHandler);
socket.removeListener('error', errorHandler);
this.connections.delete(id);
});
this.connections.set(id, socket);
}
}
// 問題2: タイマーのクリア忘れ
class HealthChecker {
private timers: NodeJS.Timeout[] = [];
start() {
// ❌ タイマーが追加され続ける
// this.timers.push(setInterval(this.check, 5000));
// ✅ 修正:既存タイマーをクリアしてから設定
this.stop();
this.timers.push(setInterval(() => this.check(), 5000));
}
stop() {
this.timers.forEach(clearInterval);
this.timers = [];
}
}
Python アプリケーションのプロファイリング
cProfile + Gemini CLI
# cProfileで実行プロファイルを取得
python -m cProfile -o profile.prof app.py
# テキスト形式で出力
python -c "
import pstats
p = pstats.Stats('profile.prof')
p.sort_stats('cumulative')
p.print_stats(30)
" > profile_stats.txt
# Gemini CLIで分析
gemini "以下のPython cProfileの結果を分析し、
上位のボトルネックを特定してください。
各ボトルネックに対する具体的な最適化コードを提供してください。
$(cat profile_stats.txt)"
Django/FastAPIのN+1クエリ検出
gemini "以下のDjango ORMコードにN+1クエリ問題がないか分析してください。
SQLクエリログも添付します。
コード:
$(cat views/api.py)
SQLクエリログ(1リクエストで発行されたクエリ):
$(cat django_query_log.txt)"
N+1問題の典型的な修正:
# ❌ N+1クエリ問題
def get_orders(request):
orders = Order.objects.all() # 1クエリ
result = []
for order in orders:
items = order.items.all() # N回クエリ!
customer = order.customer # さらにN回!
result.append({
'id': order.id,
'customer_name': customer.name,
'items': [{'name': i.name, 'price': i.price} for i in items],
})
return JsonResponse(result, safe=False)
# ✅ Gemini CLIが提案する修正
def get_orders(request):
orders = Order.objects.select_related(
'customer' # JOINで1クエリに
).prefetch_related(
'items' # 2クエリ目でまとめて取得
).all()
result = []
for order in orders:
result.append({
'id': order.id,
'customer_name': order.customer.name, # キャッシュ済み
'items': [
{'name': i.name, 'price': i.price}
for i in order.items.all() # プリフェッチ済み
],
})
return JsonResponse(result, safe=False)
memray によるメモリプロファイリング
# memrayでメモリプロファイルを取得
python -m memray run --output profile.bin app.py
# テキスト形式で出力
python -m memray stats profile.bin > memray_stats.txt
# Gemini CLIで分析
gemini "以下のPython memrayメモリプロファイルを分析してください。
メモリ使用量が多い箇所を特定し、最適化方法を提案してください。
$(cat memray_stats.txt)"
Go アプリケーションのプロファイリング
pprof + Gemini CLI
# Goアプリにpprofエンドポイントを追加
# import _ "net/http/pprof"
# CPU プロファイルの取得
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof
# テキスト形式で出力
go tool pprof -text cpu.prof > pprof_cpu.txt
# Gemini CLIで分析
gemini "以下のGo pprofのCPUプロファイルを分析してください。
ホットパスを特定し、最適化の提案をGoコード付きで行ってください。
$(cat pprof_cpu.txt)"
ゴルーチンリーク検出
# ゴルーチンプロファイル取得
curl http://localhost:6060/debug/pprof/goroutine?debug=1 > goroutines.txt
gemini "以下のGoゴルーチンダンプを分析してください。
ゴルーチンリークの兆候がないか確認し、
リークしている場合は原因と修正方法を提案してください。
$(cat goroutines.txt)"
// ❌ ゴルーチンリーク
func processItems(items []Item) {
for _, item := range items {
go func(it Item) {
ch := make(chan Result)
go fetchData(it, ch)
// chを読まないとゴルーチンがリーク!
if it.Priority > 5 {
return // ← ここでリーク発生
}
result := <-ch
saveResult(result)
}(item)
}
}
// ✅ Gemini CLIが提案する修正
func processItems(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // 同時実行数制限
for _, item := range items {
item := item
g.Go(func() error {
if item.Priority <= 5 {
return nil // 早期リターンでもリークしない
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
result, err := fetchDataWithContext(ctx, item)
if err != nil {
return fmt.Errorf("fetch failed for %s: %w", item.ID, err)
}
return saveResult(result)
})
}
return g.Wait()
}
データベースクエリの最適化
スロークエリ分析
# MySQLスロークエリログの分析
gemini "以下のMySQLスロークエリログを分析してください。
各クエリの問題点を特定し、インデックス追加やクエリ書き換えの提案をしてください。
$(cat mysql-slow.log | head -200)"
EXPLAIN結果の解釈
gemini "以下のMySQLのEXPLAIN結果を解釈してください。
フルテーブルスキャンやファイルソートが発生している箇所を特定し、
最適なインデックス設計とクエリ修正を提案してください。
EXPLAIN結果:
$(mysql -e 'EXPLAIN SELECT o.*, c.name FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = \"pending\"
ORDER BY o.created_at DESC
LIMIT 100' mydb)"
Gemini CLIが提案するインデックスとクエリの最適化:
-- 提案1: 複合インデックスの追加
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC);
-- 提案2: カバリングインデックス化
CREATE INDEX idx_orders_covering
ON orders(status, created_at DESC, customer_id);
-- 提案3: クエリの書き換え(サブクエリ化でJOINの範囲を限定)
SELECT o.*, c.name
FROM (
SELECT * FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC
LIMIT 100
) o
JOIN customers c ON o.customer_id = c.id;
継続的パフォーマンスモニタリング
パフォーマンスバジェットの設定
Gemini CLIを使って、CI/CDパイプラインにパフォーマンスバジェットを組み込みます。
gemini "以下のパフォーマンスメトリクスに基づいて、
GitHub Actionsのパフォーマンスバジェットチェックワークフローを生成してください。
バジェット:
- API応答時間: p95 < 200ms
- メモリ使用量: < 512MB
- バンドルサイズ: < 500KB (gzip)
- Lighthouseスコア: > 90
- データベースクエリ数: < 10/リクエスト"
パフォーマンスレグレッション検出
# .github/workflows/perf-check.yml
name: Performance Regression Check
on:
pull_request:
paths: ['src/**']
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Run Benchmarks
run: npx vitest bench --reporter=json > bench-results.json
- name: Compare with Baseline
run: |
# メインブランチのベースラインと比較
if [ -f bench-baseline.json ]; then
node scripts/compare-bench.js bench-baseline.json bench-results.json > comparison.txt
# 10%以上の性能劣化があれば警告
REGRESSION=$(cat comparison.txt | grep "REGRESSION" | wc -l)
if [ "$REGRESSION" -gt 0 ]; then
echo "::warning::パフォーマンスレグレッション検出"
fi
fi
SES現場でのパフォーマンス改善提案テンプレート
## パフォーマンス改善報告書
### 調査結果
- 問題: API応答時間が平均2.5秒(SLA: 500ms以内)
- 原因: N+1クエリ × 非効率なJSONシリアライゼーション
- 影響: 日間2,000リクエストに対してタイムアウト率12%
### 改善内容
1. select_related/prefetch_relatedの適用 → クエリ数 245 → 3
2. JSONストリーミングレスポンス → メモリ使用量 60%削減
3. Redis キャッシュ層の追加 → キャッシュヒット率 85%
### 効果
- 応答時間: 2.5秒 → 120ms(95%改善)
- メモリ使用量: 800MB → 320MB
- タイムアウト率: 12% → 0.1%
Gemini CLIでのデバッグ手法やモニタリング設定も合わせて参照してください。
まとめ|Gemini CLIでパフォーマンス改善を加速
パフォーマンスプロファイリングは専門的な知識が求められる領域ですが、Gemini CLIを活用することで:
- プロファイラ出力の自動解釈でボトルネック特定を迅速化
- 最適化コードの自動生成で修正工数を大幅削減
- N+1クエリやメモリリークを自動検出し、修正パターンを提案
- データベースのEXPLAIN結果を解釈し、最適なインデックスを設計
- 継続的パフォーマンスモニタリングをCI/CDに組み込み
SES現場では、パフォーマンス改善はクライアントへの価値提供として非常に評価されます。Gemini CLIを活用して、定量的な改善を実現しましょう。
Gemini CLIの活用法をさらに深く学びたい方は、Gemini CLI完全攻略シリーズをご覧ください。最新のテクニックを随時更新しています。