Slide 1

Slide 1 text

PHP で PHP のプロファイラをつくろう 五十嵐 進士 / sji / sj-i / @sji_ch

Slide 2

Slide 2 text

自己紹介 @sji_ch SNS 上でのアイコンは GitHub が自動生成した奴

Slide 3

Slide 3 text

生まれも育ちも仙台

Slide 4

Slide 4 text

PHP カンファレンス仙台とかやった

Slide 5

Slide 5 text

ふつうのサラリーマン 株式会社インフィニットループ仙台支社所属 スマホゲーのサーバサイドプログラマ

Slide 6

Slide 6 text

二歳児の父 かわいい 絵本好き

Slide 7

Slide 7 text

WEB+DB PRESS の現 PHP 連載担当 昨年 6 月から WEB+DB PRESS の PHP 連載 だいたいわりと真面目な話をしてる

Slide 8

Slide 8 text

Agenda プロファイラとはなにか reli ( 旧 php-profiler) の紹介 どんなことができるのか 内部構造

Slide 9

Slide 9 text

プロファイラとはなにか

Slide 10

Slide 10 text

プロファイラってこういうやつ 性能解析ツール 実行中のプログラムの挙動を収集 ボトルネックを見つけるのに使える

Slide 11

Slide 11 text

ボトルネック 80:20 の法則 ごく一部の処理が実行時間の大部分 一部を直すだけで大幅に改善しがち 瓶の首の細さが水の出る速度を決める

Slide 12

Slide 12 text

推測するな、計測せよ 性能に大きく影響するコードはごく一部 あてずっぽうで直すとハズレが多いということ プロファイラのようなツールでの計測が要る

Slide 13

Slide 13 text

PHP にも色々なプロファイラ Xdebug Xhprof Tideways Blackfire NewRelic sample_prof phpspy

Slide 14

Slide 14 text

俺のプロファイラの話

Slide 15

Slide 15 text

reliforp/reli-prof PHP の PHP による PHPer のためのプロファイラ 2020 年頃から半分ギャグで作ってる phpspy の PHP 版のパクり 前は php-profiler という名前(後述)

Slide 16

Slide 16 text

デモ

Slide 17

Slide 17 text

最初に rbspy があった ruby で同様のことをやる rbspy という rust 製プログラム https://github.com/rbspy/rbspy

Slide 18

Slide 18 text

パクリで py-spy https://github.com/benfred/py-spy

Slide 19

Slide 19 text

パクリで phpspy https://github.com/adsr/phpspy

Slide 20

Slide 20 text

phpspy のパクリで reli 最初は php-profiler という名前だった 色々あって変わった https://github.com/reliforp/reli-prof

Slide 21

Slide 21 text

ざっくり言うと 実行中のスクリプトの動作をプロセス外から盗み見る 計測対象のプログラムは無修正でよい 拡張を組み込んだりしなくてよい 計測用コードを仕込んだりしなくてよい

Slide 22

Slide 22 text

何に使える? スクリプトの性能計測 手に入れたトレースを何らかの方法で集計すればよい トラブルシュート 「なにか知らんが無応答になった」みたいな時、どこで詰まってるかわかる

Slide 23

Slide 23 text

プロファイラの方式: 計測化(instrumentation) 型 オーバーヘッドの累積がつらい xdebug, xhprof, tideways などほとんどの奴 各関数呼び出しの前後で自動で時間取得 速く何度も実行される関数に弱い 計測オーバーヘッドの累積で不正確に 呼び出し関係の記録は正確

Slide 24

Slide 24 text

プロファイラの方式: サンプリング型 よくあるプログラムは関数呼び出しの繰り返し 「今何を実行中か」を大体等間隔でサンプリングして取得 adsr/phpspy 、nikic/sample_prof など一部の奴 VM の状態を定期的にのぞき見 速く何度も実行される関数に強い 呼び出し関係の記録は取りこぼしも reli はこっち

Slide 25

Slide 25 text

子プロセスを起動してトレース 子プロセスのトレースは権限いらず sudo なしで使える -o でプロファイラの出力だけリダイレクト $ ./reli i:trace -- php -r "fgets(STDIN 0 fgets :-1 1 :-1 0 fgets :-1 1 :-1 0 fgets :-1 1 :-1

Slide 26

Slide 26 text

他プロセスへアタッチ 権限があれば PID で任意プロセスへアタッチ可能 実行プロセスに CAP_SYS_PTRACE が必要 単に root でもいける $ sudo php ./reli i:trace -p 2182685

Slide 27

Slide 27 text

プロセス名で自動アタッチ 正規表現でプロセス名を指定 apache のワーカとかプロセス生え変わる奴向き デフォルトだと 8 並列で待受 ext-parallel が有効な環境ではマルチスレッド $ sudo php ./reli i:daemon -P "^/usr/sbin/ht

Slide 28

Slide 28 text

top-like モード 毎秒で top コマンドっぽく集計表示 挙動的には daemon と同様 $ sudo php ./reli i:top -P "^/usr/sbin/ht

Slide 29

Slide 29 text

実行中のオペコードを取得 PHP の VM 命令レベルでトレース PHP コード自体がボトルネックなら便利 I/O や標準関数がボトルネックなことの方が多い ./reli i:trace --template=phpspy_with_opcode -- php mandelbrot.php 0 ::ZEND_ASSIGN :-1 1 Mandelbrot::iterate /home/sji/work/test/mandelbrot.php:33:ZEND_ASSIG 2 Mandelbrot::__construct /home/sji/work/test/mandelbrot.php:12:ZEND_D 3 /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL

Slide 30

Slide 30 text

PHP テンプレート機能 出力テンプレートをPHP で整形可能 必要なら関数内の各行を別関数扱いにしたりもできる とりあえずJSON で吐いて後処理、とかもできる call_frames as $frame): ?> func: = $frame->getFullyQualifiedFunctionName() . PHP_EOL ?> file: = $frame->file_name . PHP_EOL ?> line: = $frame->getLineno() . PHP_EOL ?> = "\n" ?>

Slide 31

Slide 31 text

というか全体が PHP 製 PHP スクリプトの改善をするのはしばしば PHPer PHPer が好きに挙動をいじれるプロファイラという価値 かわりに速度は C 製の phpspy より遅い JIT はいくらか効く

Slide 32

Slide 32 text

Docker から使う いくつかのオプション設定で Docker からも apparmor 無効化 CAP_SYS_PTRACE のケーパビリティ追加 --pid=host でコンテナ外のプロセスが見える Windows の Docker Desktop で他のコンテナ内 PHP にア タッチ可能 $ docker pull reliforp/reli-prof $ docker run -it \ --security-opt="apparmor=unconfined" \ --cap-add=SYS_PTRACE \ --pid=host \ reliforp/reli-prof \ i:daemon -P "^/usr/sbin/httpd"

Slide 33

Slide 33 text

可視化 Flame Graph Search ic P.. Psalm\I.. Psalm\In.. PhpParser\Node.. Php.. Ph.. Psalm\Internal\Provider\StatementsPro.. P.. PhpParser.. PhpParser\Nod.. Psalm\In.. Psalm\Internal\Analyzer.. P.. P.. Psalm\Internal\Codebase\Scanner::scanFile Ph.. Psa.. Psalm\Internal\Provider\StatementsPro.. Ps.. PhpParser.. Ph.. Ps.. P.. PhpPa.. Psalm\Internal\Cli\Psalm::run Psal.. Psalm\Con.. PhpPa.. Psalm\Internal\Scanner\FileScanner::scan P.. Psalm\Internal\Codebase\Scanner::Psalm\Internal\Codebase\{closure} PhpParser\NodeTraverser::traverseArray PhpParser\ParserAbstract:.. Psalm\Internal\Analy.. Psalm\Internal\Provider\Statem.. Psalm\Cod.. Psalm\Codebase::scanFiles Psalm\Internal\Codebase\Scanner::scanFilePaths array_merge PhpParser\ParserAbst.. P.. Psalm\Internal\Analyzer\ProjectAnalyzer::check P.. Ph.. Psalm\Internal\Codebase\Scanner::scanFile Psalm\Internal\Scanner\FileScanner::scan Psalm\Internal\PhpVisito.. Php.. P.. Psalm\Config::visitComposerAutoloadFiles Psalm\Internal\Codebase\Scanner::scanFilePaths Ph.. PhpParser\NodeTraverser::traverseNode Psalm\Internal\Provider\Statem.. PhpParser\ParserAbstract.. Psalm\Internal\Codebase\Scanner::scanFiles Ph.. PhpPa.. P.. PhpP.. PhpParser\NodeTraverser::traverseArray PhpParse.. Psalm\Internal\Codebase\Scanner::scanFiles Psa.. P.. P.. Psalm.. Psalm\Internal\Analyzer.. Psalm\Internal\Analyzer\ProjectAnalyzer::visitAutoloadFiles Ph.. PhpParser\ParserAbstract::parse PhpParser\NodeTraverser::traverse Psalm\In.. Ph.. Ph.. Php.. Ph.. P.. PhpParser\.. Psal.. P.. PhpParser\No.. Psalm\Internal\PhpVisitor.. P.. Ph.. Ps.. Ps.. Psalm\In.. PhpParser\Nod.. Psalm\Internal\Codebase\Scanner::Psalm\Internal\Code.. PhpPa.. Psalm\Internal\PhpVisito.. Ph.. フレームグラフや speedscope 形式を吐ける これ以外の可視化方法にも徐々に対応していきたい perfetto とか call-grind とか

Slide 34

Slide 34 text

内部実装

Slide 35

Slide 35 text

達成すべきこと 外部プロセスのメモリ内のどこに何があるかを知る 外部プロセスのメモリ内容を覗き見る

Slide 36

Slide 36 text

/proc/ 下の内容を解釈 /proc//maps = 対象プロセスのメモリマップ 正規表現でパース PHP 処理系が居そうな領域を見つける % sudo cat /proc/10364/maps 56187c10d000-56187c20e000 r--p 00000000 08:07 1181323 /usr/sbin/p 56187c30d000-56187c6a9000 r-xp 00200000 08:07 1181323 /usr/sbin/p 56187c70d000-56187cef3000 r--p 00600000 08:07 1181323 /usr/sbin/p 56187d26a000-56187d30d000 r--p 00f5d000 08:07 1181323 /usr/sbin/p 56187d30d000-56187d314000 rw-p 01000000 08:07 1181323 /usr/sbin/p 56187d314000-56187d335000 rw-p 00000000 00:00 0 56187f100000-56187f2c6000 rw-p 00000000 00:00 0 [heap] 56187f100000-56187f2c6000 rw-p 00000000 00:00 0 [heap] 7f6b76c00000-7f6b76e00000 rw-p 00000000 00:00 0 7f6b76eb3000-7f6b76f34000 rw-p 00000000 00:00 0

Slide 37

Slide 37 text

ELF を解釈 ELF ファイルがシンボル情報を持つ ELF パーサを PHP で実装 PHP 処理系の VM 状態はグローバル変数(EG) に保持 EG をシンボル解決するとファイル上のオフセット オフセット + プロセスのベースアドレス = 目的アドレス ベースアドレスは /proc//maps から得る

Slide 38

Slide 38 text

FFI で process_vm_readv(2) を呼んでメモリのぞき見 呼び出しプロセスのアドレス空間の指定領域へ PID で指定さ れたプロセスのアドレス空間の指定領域をコピー プロセスの壁を越えてデータがコピーできる このために CAP_SYS_PTRACE が必要 PHP 7.4 で追加された FFI で呼び出し

Slide 39

Slide 39 text

FFI で ポインタを ZendEngine 内の構造体ポインタへキャスト PHP 処理系は C で書かれている C の構造体定義は FFI で読める process_vm_readv(2) で実行中の処理系内部データをコピー してきたバッファのポインタを PHP 構造体へのポインタにキ ャスト public function readAs( string $type, CData $cdata ): CastedCData { // 処理系のヘッダを読み込み $ffi = $this->loadHeader($this->php_version); return new CastedCData( $cdata, $ffi->cast($type, $cdata) ?? throw new CannotCastCDataException( 'cannot cast a C Data' ), ); }

Slide 40

Slide 40 text

各 C 言語構造体へのプロクシ CData は動的すぎて辛い型 PHP のクラス経由で各構造体へアクセスできるように 型付プロパティのunset でマジックメソッドが起動可能 CData へのアクセスは遅いのでマジックメソッドで lazy に final class ZendExecutorGlobals implements Dereferencable { /** @var Pointer|null */ public ?Pointer $current_execute_data; /** @param CastedCData $casted_cd public function __construct( private CastedCData $casted_cdata, ) { unset($this->current_execute_data); }

Slide 41

Slide 41 text

PHP の型としてのポインタ リモートプロセスから取得したポインタはリモートプロセス の中での仮想アドレスを指している 直接アクセスすると SEGV PHP 側でポインタ用の型を定義 process_vm_readv(2) を内部で呼び出すデリファレンサ経由 でしかアクセスできないように Psalm のジェネリクスでデリファレンス後は中身の型に /** * @template T of \PhpProfiler\Lib\Process\Pointer\Dereferen */ class Pointer { /** @param class-string $type */ public function __construct( public string $type, public int $address, public int $size, ) { } interface Dereferencer { /** * @template T of Dereferencable * @param Pointer $pointer * @return T */ public function deref(Pointer $pointer): mixed;

Slide 42

Slide 42 text

一応解説記事も書いたよ 『「別プロセスの PHP が今何をしているか」を実況するプロ グラムを PHP で作った』 https://qiita.com/sj-i/items/a29d54cfd83f230ddc3d

Slide 43

Slide 43 text

その他のこと

Slide 44

Slide 44 text

名前 元々は php-profiler というツール名だった 最適化にPHP ライセンスのコードが必要 PHP ライセンスでは派生プロジェクトがPHP と名乗るとダメ 雑にstrrev(php-profiler) するとreliforp-php reli for p(hp) → Reli

Slide 45

Slide 45 text

面白かったらスターを付けてね! スターが付くと「試してみようかな」という人も増える フィードバックが増えればより便利にできる 各種のフィードバック待ってます https://github.com/reliforp/reli-prof

Slide 46

Slide 46 text

まとめ

Slide 47

Slide 47 text

PHP ではわりともうなんでもできる

Slide 48

Slide 48 text

変なものを作りましょう!

Slide 49

Slide 49 text

おしまい