Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP で PHP のプロファイラをつくろう

sji
April 11, 2022

PHP で PHP のプロファイラをつくろう

PHPerKaigi2022

sji

April 11, 2022
Tweet

Other Decks in Programming

Transcript

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

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

  3. 生まれも育ちも仙台

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

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

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

  7. WEB+DB PRESS の現 PHP 連載担当 昨年 6 月から WEB+DB PRESS

    の PHP 連載 だいたいわりと真面目な話をしてる
  8. Agenda プロファイラとはなにか reli ( 旧 php-profiler) の紹介 どんなことができるのか 内部構造

  9. プロファイラとはなにか

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

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

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

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

  14. 俺のプロファイラの話

  15. reliforp/reli-prof PHP の PHP による PHPer のためのプロファイラ 2020 年頃から半分ギャグで作ってる phpspy

    の PHP 版のパクり 前は php-profiler という名前(後述)
  16. デモ

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

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

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

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

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

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

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

    計測オーバーヘッドの累積で不正確に 呼び出し関係の記録は正確
  24. プロファイラの方式: サンプリング型 よくあるプログラムは関数呼び出しの繰り返し 「今何を実行中か」を大体等間隔でサンプリングして取得 adsr/phpspy 、nikic/sample_prof など一部の奴 VM の状態を定期的にのぞき見 速く何度も実行される関数に強い

    呼び出し関係の記録は取りこぼしも reli はこっち
  25. 子プロセスを起動してトレース 子プロセスのトレースは権限いらず sudo なしで使える -o でプロファイラの出力だけリダイレクト $ ./reli i:trace --

    php -r "fgets(STDIN 0 fgets <internal>:-1 1 <main> <internal>:-1 0 fgets <internal>:-1 1 <main> <internal>:-1 0 fgets <internal>:-1 1 <main> <internal>:-1
  26. 他プロセスへアタッチ 権限があれば PID で任意プロセスへアタッチ可能 実行プロセスに CAP_SYS_PTRACE が必要 単に root でもいける

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

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

    php ./reli i:top -P "^/usr/sbin/ht
  29. 実行中のオペコードを取得 PHP の VM 命令レベルでトレース PHP コード自体がボトルネックなら便利 I/O や標準関数がボトルネックなことの方が多い ./reli

    i:trace --template=phpspy_with_opcode -- php mandelbrot.php 0 <VM>::ZEND_ASSIGN <VM>:-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 <main> /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL
  30. PHP テンプレート機能 出力テンプレートをPHP で整形可能 必要なら関数内の各行を別関数扱いにしたりもできる とりあえずJSON で吐いて後処理、とかもできる <?php foreach ($call_trace->call_frames

    as $frame): ?> func: <?= $frame->getFullyQualifiedFunctionName() . PHP_EOL ?> file: <?= $frame->file_name . PHP_EOL ?> line: <?= $frame->getLineno() . PHP_EOL ?> <?php endforeach ?> <?= "\n" ?>
  31. というか全体が PHP 製 PHP スクリプトの改善をするのはしばしば PHPer PHPer が好きに挙動をいじれるプロファイラという価値 かわりに速度は C

    製の phpspy より遅い JIT はいくらか効く
  32. 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"
  33. 可視化 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.. <main> 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.. <main> Ph.. Ps.. Ps.. Psalm\In.. PhpParser\Nod.. Psalm\Internal\Codebase\Scanner::Psalm\Internal\Code.. PhpPa.. Psalm\Internal\PhpVisito.. Ph.. フレームグラフや speedscope 形式を吐ける これ以外の可視化方法にも徐々に対応していきたい perfetto とか call-grind とか
  34. 内部実装

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

  36. /proc/<pid> 下の内容を解釈 /proc/<pid>/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
  37. ELF を解釈 ELF ファイルがシンボル情報を持つ ELF パーサを PHP で実装 PHP 処理系の

    VM 状態はグローバル変数(EG) に保持 EG をシンボル解決するとファイル上のオフセット オフセット + プロセスのベースアドレス = 目的アドレス ベースアドレスは /proc/<pid>/maps から得る
  38. FFI で process_vm_readv(2) を呼んでメモリのぞき見 呼び出しプロセスのアドレス空間の指定領域へ PID で指定さ れたプロセスのアドレス空間の指定領域をコピー プロセスの壁を越えてデータがコピーできる このために

    CAP_SYS_PTRACE が必要 PHP 7.4 で追加された FFI で呼び出し
  39. 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' ), ); }
  40. 各 C 言語構造体へのプロクシ CData は動的すぎて辛い型 PHP のクラス経由で各構造体へアクセスできるように 型付プロパティのunset でマジックメソッドが起動可能 CData

    へのアクセスは遅いのでマジックメソッドで lazy に final class ZendExecutorGlobals implements Dereferencable { /** @var Pointer<ZendExecuteData>|null */ public ?Pointer $current_execute_data; /** @param CastedCData<zend_executor_globals> $casted_cd public function __construct( private CastedCData $casted_cdata, ) { unset($this->current_execute_data); }
  41. PHP の型としてのポインタ リモートプロセスから取得したポインタはリモートプロセス の中での仮想アドレスを指している 直接アクセスすると SEGV PHP 側でポインタ用の型を定義 process_vm_readv(2) を内部で呼び出すデリファレンサ経由

    でしかアクセスできないように Psalm のジェネリクスでデリファレンス後は中身の型に /** * @template T of \PhpProfiler\Lib\Process\Pointer\Dereferen */ class Pointer { /** @param class-string<T> $type */ public function __construct( public string $type, public int $address, public int $size, ) { } interface Dereferencer { /** * @template T of Dereferencable * @param Pointer<T> $pointer * @return T */ public function deref(Pointer $pointer): mixed;
  42. 一応解説記事も書いたよ 『「別プロセスの PHP が今何をしているか」を実況するプロ グラムを PHP で作った』 https://qiita.com/sj-i/items/a29d54cfd83f230ddc3d

  43. その他のこと

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

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

  46. まとめ

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

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

  49. おしまい