Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

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

sji
April 11, 2022

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

PHPerKaigi2022

sji

April 11, 2022
Tweet

More Decks by sji

Other Decks in Programming

Transcript

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

    の PHP 連載 だいたいわりと真面目な話をしてる
  2. 子プロセスを起動してトレース 子プロセスのトレースは権限いらず 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
  3. 実行中のオペコードを取得 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
  4. 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" ?>
  5. 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"
  6. 可視化 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 とか
  7. /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
  8. ELF を解釈 ELF ファイルがシンボル情報を持つ ELF パーサを PHP で実装 PHP 処理系の

    VM 状態はグローバル変数(EG) に保持 EG をシンボル解決するとファイル上のオフセット オフセット + プロセスのベースアドレス = 目的アドレス ベースアドレスは /proc/<pid>/maps から得る
  9. 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' ), ); }
  10. 各 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); }
  11. 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;