Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
俺の PHP プロファイラの話 PHP スクリプトで PHP 処理系のメモリをのぞき込む
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Infiniteloop
June 06, 2024
Programming
700
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
俺の PHP プロファイラの話 PHP スクリプトで PHP 処理系のメモリをのぞき込む
Infiniteloop
June 06, 2024
More Decks by Infiniteloop
See All by Infiniteloop
[新卒向け研修資料] テスト文字列に「うんこ」と入れるな(2025年版)
infiniteloop_inc
18
84k
心理的安全性を学び直し、 「いい組織とは何か?」を考えてみる
infiniteloop_inc
1
1.1k
ゼロからつくる 2D物理シミュレーション ~物理現象をコードに落とし込む方法~
infiniteloop_inc
1
1.5k
詫び石の裏側
infiniteloop_inc
0
950
[新卒向け研修資料] テスト文字列に「うんこ」と入れるな(2024年版)
infiniteloop_inc
7
36k
リファクタリングで実装が○○分短縮した話
infiniteloop_inc
0
310
ADRという考えを取り入れてみて
infiniteloop_inc
0
310
500万行のPHPプロジェクトにおけるログ出力の歩み
infiniteloop_inc
0
230
I ❤ Virtual Machines 仮想環境をより便利に使うツールたち
infiniteloop_inc
0
190
Other Decks in Programming
See All in Programming
ローカルLLMでどこまでコードが書けるか -拡張版 / How much code can be written on a local LLM Extended
kishida
12
4.4k
そのテスト、説明できますか?~LWテスト戦略FW~のご紹介
nakahara
0
160
Observability in Practice:Grafana 與 Edge Device SRE 的那些事
blueswen
0
170
Snowflake Summitでの新機能 CoCo / CoWork / snowflake-summit-2026-overall-what-new-coco
tatsuhiro
1
170
並列実装の現場、2ヶ月間実務でAIを使い倒したAIもPCも私も限界が近い
ming_ayami
0
130
さぁV100、メモリをお食べ・・・
nilpe
0
150
AI 時代のソフトウェア設計の学び方
masuda220
PRO
29
13k
Spec Driven Development | AI Summit Lisbon
danielsogl
PRO
0
200
A2UI という光を覗いてみる
satohjohn
1
150
軽量Java基盤の設計 DIコンテナに頼らない、長期保守と1秒起動の実現 JJUG CCC 2026 Spring
macha64
0
560
依存関係から依存物へ―Dependencyという言葉の歴史をひも解く
j_lee
0
130
セキュリティの専門家じゃなくてもできる。「セキュリティ意識」をアップデートして サプライチェーン攻撃への耐性を高めよう。
tk3fftk
5
910
Featured
See All Featured
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.1k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.8k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
440
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.4k
Groundhog Day: Seeking Process in Gaming for Health
codingconduct
0
210
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.3k
Mind Mapping
helmedeiros
PRO
1
260
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
49
10k
New Earth Scene 8
popppiees
3
2.4k
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
0
320
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1k
The Impact of AI in SEO - AI Overviews June 2024 Edition
aleyda
5
1.1k
Transcript
俺の PHP プロファイラの話 PHP スクリプトで PHP 処理系のメモリをのぞき込む 五十嵐 進士 /
sji / sj-i / @sji_ch 株式会社インフィニットループ
自己紹介 @sji_ch SNS 上のアイコンは GitHub が自動生成した奴
生まれも育ちも仙台
ふつうのサラリーマン 株式会社インフィニットループ仙台支社所属 スマホゲーのサーバサイドプログラマ
Agenda プロファイラとは 自作 PHP プロファイラ Reli の紹介 Reli の処理時間計測機能 Reli
のメモリ解析機能 Reli の内部構造 まとめ
プロファイラとは
プロファイラってこういうやつ 性能解析ツール 実行中のプログラムの挙動を収集 ボトルネックを見つけるのに使える
ボトルネック 80:20 の法則 ごく一部の処理がマシン資源の大部分を使う 実行時間やメモリ使用量など 一部を直すだけで大幅に改善しがち 瓶の首の細さが水の出る速度を決める
推測するな、計測せよ プロファイラのようなツールでの計測が要る 憶測でコードを直しているヒマはない 実際のリソース使用量を見る
当てずっぽうでは当たらない ボトルネックはシステム全体の中の一部 あてずっぽうで直すとハズレが多い ある程度以上の規模のシステムで改善点を突き 止めるのは当てずっぽうでは難しい 砂漠でゴマ粒を、haystack で needle を探すよ うなもの
膨大な時間をつぎ込めばできる、しかしその 時間はない
俺のプロファイラの話
Reli 2020 年頃から個人的に作っている PHP 製ツール 趣味でも仕事でも使ってる PHP の PHP による
PHP のためのプロファイラ 実行中のプログラムの処理内容やメモリ内容を確認できる CLI ツール 無修正の解析対象プログラムをプロセス外から解析 https://github.com/reliforp/reli-prof
デモ
4 年前にもタガヤスで話した 実は 4 年前のタガヤスでも同じネタで話した PHP で実行中のスクリプトの動作を下から覗き見る https://speakerdeck.com/sji/php-deshi-zhong-nosukuriputonodong-zuo-woxia-karasi-ki-ru
というかずっと同じネタをこすり続けてる ここ数年の技術トークのけっこうな割合がこれについての話な気がする PHP Conference Fukuoka 2019: PHPの関数実行とその計測 PHPerKaigi 2022: PHP
で PHP のプロファイラをつくろう PHPerKaigi 2023: 時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる PHP Conference Tokyo 2023: PHP で PHP のメモリプロファイラをつくろう PHPerKaigi 2024: PHP で読む楽しいコアダンプ
strrev('php-profiler') → reliforp-php → Reli for P(PHP) 元は php-profiler という名前
ライセンス問題から名前の変更が必要に 最適化のために取った処理系由来のコードが PHP ライセンス PHP ライセンスは「PHP hogehoge」のような名前を許可しない テキトーに選んだ PHP の文字列操作関数を元の名前へ適用した上で変形
Reli の機能: 実行内容取得
トレース取得 実行中のプロセスのコールトレースを取得 プロセス ID で対象を指定 今どの関数やメソッド・ファイル・行の処理を実行中か どこから呼ばれているか さらにそれがどこから呼ばれているか さらにそれがどこから呼ばれているか 「このスクリプト本当に動いてる?処理進んでそう?」を確認できる
composer install が遅いな、ちゃんと進んでるかな、などもわかる $ sudo php ./reli i:trace -p 2182685 0 time_nanosleep <internal>:-1 1 Reli\Lib\Loop\LoopMiddleware\NanoSleepMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddlew 2 Reli\Lib\Loop\LoopMiddleware\KeyboardCancelMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMi 3 Reli\Lib\Loop\LoopMiddleware\RetryOnExceptionMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/Loop 4 Reli\Lib\Loop\Loop::invoke /home/sji/work/reli/src/Lib/Loop/Loop.php:26 5 Reli\Command\Inspector\GetTraceCommand::execute /home/sji/work/reli/src/Command/Inspector/GetTraceCom 6 Symfony\Component\Console\Command\Command::run /home/sji/work/reli/vendor/symfony/console/Command/Com 7 Symfony\Component\Console\Application::doRunCommand /home/sji/work/reli/vendor/symfony/console/Applic 8 Symfony\Component\Console\Application::doRun /home/sji/work/reli/vendor/symfony/console/Application.p 9 Symfony\Component\Console\Application::run /home/sji/work/reli/vendor/symfony/console/Application.php 10 <main> /home/sji/work/reli/reli:45
daemon モードでのトレース取得 プロセス ID でなくコマンドラインの正規表現パターンで対象を指定 毎秒プロセスリストを監視し、条件にマッチするプロセスを見つけるたびアタッチ 対象の プロセス ID を決め打てる場合ばかりではない、ので必要
apache や fpm などのワーカプロセスは数リクエスト処理したら生え変わり、とかもある 「とにかく fpm のこの名前の pool のワーカ」といった指定が可能 Reli は複数のワーカプロセスを生やす 複数ターゲットから同時にトレースを取得可能 デフォルトでは 8 並列で待ち受け、設定で増やせる $ sudo php ./reli i:daemon -P "^/usr/sbin/httpd"
トレースを集めると嬉しいこと → ボトルネックの特定
よくあるプログラムは関数実行の連続 プログラムの実行は処理の連続 処理内容はいくつかの単位で区切れる ファイルや関数、行番号、VM 命令など ある処理を持つ部品が別の部品へ、の呼び出し の連続
サンプリングプロファイラの原理 だいたい等間隔でコールトレースを取得 長く実行される処理ほど引っかかる確率が高い 多くのサンプルを集めれば多くの時間を使って いる処理がわかる
speedscope でのコールトレースの集計・可視化 集めたコールトレースを speedscope で可視化 reli の出力を標準出力リダイレクトで集める reli の speedscope
変換機能を使って speedscope 用の JSON に変換 フレームグラフで遅い部分が視覚的に分かる ある処理の caller と callee それぞれを集計表示 URL の振り分け処理から呼ばれてる処理を見れば遅い URL が分かる DB アクセスのような末端処理を呼ぶ処理を見れば遅いクエリが分かる
speedscope のデモ
top-like モード 毎秒で top コマンドっぽく集計表示 挙動的には daemon と同様
参考情報 元ネタは phpspy phpspy の元ネタは rbspy python 用で py-spy というものも
Reli の機能: メモリ内容解析
PHP 処理系のメモリ管理概要 スクリプトで値が必要になったら処理系が自動で必要サイズの領域を確保 関数呼び出しでのローカル変数領域や new でのプロパティ領域など データの種類によっては参照カウントを処理系が管理 mod_php や fpm
の構成ではリクエストごとにほぼ全解放 普段はプログラマが意識しなくてよい
参照カウント 「何箇所で使われているか」のカウンタ 参照型の値は同じ領域への参照箇所が増えるたび参照カウントを 1 増やす オブジェクト、配列、文字列、リソース、PHP 参照 関数終了でのローカル変素破棄など、参照が減るたび参照カウントを 1 減らす
参照カウントが 0 になった領域は自動で解放 循環参照は M&S 風の GC で別途解放
リクエストごとのメモリ領域 PHP 処理系のメモリは ZendMM が管理 Zend Memory Manager ZendMM には
2 種類のメモリ領域 リクエストごとリセットされるメモリ領域 リクエストをまたいで残る永続メモリ領域 どちらも最終的には OS からもらう仮想メモリ領 域を使う 名前の Zend は Zeev と Andi の名前から memory_get_usage() などで取れるのはリクエス トごとの領域
普段の PHPer はメモリを意識しなくて済む メモリは値を使う時に勝手に裏で確保されるもの メモリは値が不要になったら勝手に裏で解放されるもの メモリはリクエストが終わったら勝手に裏で解放されるもの ほとんどのリクエストは短命、メモリリークをほとんど気にしなくてよい
長時間実行プロセスでは話が別 mod_php や fpm 以外の環境で PHP を動かすシーンも増えてきた RoadRunner や Swoole
などの AltFPM バッチ処理やジョブキューのワーカー 静的解析器など開発用の PHP ツール これらのプロセスは長時間生きたり、大量のデータを処理することが多い メモリリークが問題になることも どこにどれだけメモリを使っているか把握が必要
Reli のメモリ解析モード プロセス ID で指定したプロセスのメモリ内容を解析 どこでどんなサイズの何が参照されているかの情報を一通り取得・解析 JSON で構造化した内容にまとめてプロセスの状態をガッと出力 jq などの
JSON クエリでプロセスの状態をクエリできる
取得できる内容 処理系のコンパイル時の作業用領域 実行中のコールトレースと各コールフレームの ローカル変数 グローバル変数の領域 文字列、配列、オブジェクトの実体領域 各定数用の領域 各クラスの静的プロパティ領域 各関数の静的変数領域 各クラスや関数定義の管理情報
VM スタックサイズ 関数ごとのコンパイル後 VM 命令列 全オブジェクト表
JSON 出力の嬉しさ: クエリ可能性 定型のクエリで色々なプロセスのメモリ状態を解析できる 知識のスニペット化 処理系メモリ内のどこをどう見てたどれば使える情報を得られるか 気合があれば目 grep もギリできる 性能的には微妙なので将来は
RDB に突っ込みたい SQL の方がクエリもちょっと書きやすい、気がする
種類別にサイズの大きな領域ランキングを集計する例 cat memory_analyzed.json | jq .location_types_summary | jq -r '(["location_type",
"count", "memory_usag location_type | count | memory_usage ============= | ===== | ============ ZendStringMemoryLocation | 38045 | 1992633 ZendArrayTableMemoryLocation | 538 | 991352 ZendOpArrayBodyMemoryLocation | 1323 | 921840 ZendArrayTableOverheadMemoryLocation | 416 | 370896 ZendOpArrayHeaderMemoryLocation | 1356 | 325440 ZendClassEntryMemoryLocation | 179 | 88784 ZendArgInfosMemoryLocation | 1228 | 82080 RuntimeCacheMemoryLocation | 354 | 42976 ZendObjectMemoryLocation | 273 | 40744 ZendPropertyInfoMemoryLocation | 371 | 20776
インスタンスがメモリを多く食っている クラスのランキングを集計する例 cat memory_analyzed.json | jq .class_objects_summary | jq -r
'(["class_name", "count", "memory_usage"] class_name | count | memory_usage ========== | ===== | ============ Closure | 62 | 20336 DI\\Definition\\ObjectDefinition | 12 | 2016 Fiber | 5 | 1480 DI\\Definition\\Helper\\AutowireDefinitionHelper | 10 | 1200 Revolt\\EventLoop\\Internal\\SignalCallback | 7 | 952 DI\\Definition\\Reference | 12 | 864 Amp\\Internal\\FutureState | 6 | 816 Amp\\ByteStream\\ReadableResourceStream | 3 | 696 DI\\Definition\\ObjectDefinition\\MethodInjection | 9 | 648 Monolog\\Level | 8 | 576
サイズの大きな文字列を 参照元付きでランキング表示する例 cat $memory_analyzed_reli | jq '. as $root |
path(..|objects|select(."#type"=="StringContext"))| . as $ size | node_id | path ==== | ======= | ==== 93450252 | 14396 | context.call_frames.2.local_variables.serialized_messages.referenced.array_element 45285439 | 14377 | context.call_frames.2.local_variables.content.array_elements.0.value 168289 | 59466 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 72415 | 59264 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 37639 | 59200 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 34919 | 59160 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 32192 | 58802 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 26572 | 59044 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 26459 | 59154 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 26336 | 58754 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open
サイズの大きな配列を参照元付きでランキング表示する例 cat memory_analyzed.json | jq '. as $root | path(..|objects|select(."#type"=="ArrayHeaderContext"))|
. size | count | node_id | path ==== | ===== | ======= | ==== 319520 | 7937 | 42380 | context.class_table.jetbrains\\phpstormstub\\phpstormstubsmap.constants.CONS 222944 | 4919 | 27618 | context.class_table.jetbrains\\phpstormstub\\phpstormstubsmap.constants.FUNC 203616 | 3917 | 82513 | context.interned_strings 59424 | 1345 | 23577 | context.class_table.jetbrains\\phpstormstub\\phpstormstubsmap.constants.CLAS 54944 | 1205 | 19586 | context.class_table.composerautoloaderinitfa3d2d2c0bbb9c2389347803a32247de.s 1984 | 46 | 19254 | context.class_table.composerautoloaderinitfa3d2d2c0bbb9c2389347803a32247de.s 1248 | 31 | 23472 | context.class_table.composer\\autoload\\composerstaticinitfa3d2d2c0bbb9c2389 1248 | 31 | 1845 | context.call_frames.25.symbol_table.array_elements.__composer_autoload_files 1216 | 30 | 1754 | context.call_frames.25.symbol_table.array_elements._SERVER.value 800 | 17 | 620 | context.call_frames.7.local_variables.container.object_properties.fetchedDef
あるオブジェクトの参照元をすべて列挙する例 cat memory_analized.json | jq 'path(..|objects|select(."#reference_node_id"==3 or ."#node_id"==3))|join "context.call_frames.0.local_variables.project_analyzer" "context.call_frames.1.local_variables.project_analyzer"
"context.class_table.psalm\\internal\\analyzer\\projectanalyzer.static_properties.instance" "context.objects_store.17"
コールトレースの取得 cat memory_analyzed.json | jq -r '(["frame_no", "function", "line"] |
(., map(length*"="))),(path(.cont frame_no function line ======== ======== ==== 0 system 4 1 {closure}(/home/sji/work/oss/tmp/pdfparser_test/test.php:11-21) 20 2 Smalot\\PdfParser\\Font::uchr 150 3 Smalot\\PdfParser\\Font::loadTranslateTable 230 4 Smalot\\PdfParser\\Font::init 78 5 Smalot\\PdfParser\\Document::init 90 6 Smalot\\PdfParser\\Document::setObjects 316 7 Smalot\\PdfParser\\Parser::parseContent 122 8 Smalot\\PdfParser\\Parser::parseFile 90 9 <main>
特定のコールフレームの 特定のローカル変数の値を出力する例 cat memory_analyzed.json | jq '.context.call_frames."1".local_variables|{abc, def}' { "abc":
{ "#node_id": 142, "#type": "ScalarValueContext", "#value": 506895433343375 }, "def": { "#node_id": 143, "#type": "ScalarValueContext", "#value": 9958150 } }
コアダンプからの読み取り Reli のメモリ解析機能はコアダンプの読み取りにも対応 プロセス死亡時の PHP レベルでの状況を詳しく調べられる gdb も処理系のデバッグシンボルもいらない 生きてるプロセスも gcore
でごく短い間止めるだけで解析できる コアダンプ取得の時間しか要らない 生きてるプロセスの直接のメモリ解析は時間がかかり、プロダクションで使い辛い コアだけ吐いておけば後でじっくり根本原因を(現実的な手間の範囲で)追える トラブル対応でプロセスの再起動など急場しのぎの対応が要る場合も状況を保存できる その場にいない詳しい人が後で調べる、も可能
仕組み
ざっくりの概観 対象プロセスの重要構造体のアドレスを特定 処理系の ELF バイナリと procfs のメモリマップを解析 FFI でシステムコールを呼び別プロセスの処理系内のメモリを読む gdb
などのデバッガと同じようなことをやる 処理系内部のメモリレイアウトの知識を持って内部情報を解釈 FFI で処理系内の C 構造体定義を読み込む ポインタをたどり実行中の関数・VM命令のトレースやメモリ内容を収集
将来の展望 スレッドセーフ版処理系への対応度を上げる トレース取得のついででスポットでのメモリ解析を可能に より多くの状況で安定して動くように 高速化・省メモリ化 エラーや例外の発生時にのみ情報を取る機能 SQL でのプロセス状態のクエリ 別サーバへ取得した情報を集約可能に チュートリアルの拡充
おしまい