# phpspy phpspy is a low-overhead sampling profiler for PHP. It works with non-ZTS PHP 7.0+ with CLI, Apache, and FPM SAPIs on x86_64 Linux 3.2+. Experimental support for aarch64 (arm64) is also included[^9]. [![Build Status](https://github.com/adsr/phpspy/actions/workflows/phpspy_test.yml/badge.svg)](https://github.com/adsr/phpspy/actions/workflows/phpspy_test.yml) ### Demos You can profile PHP scripts: ![child](https://i.imgur.com/QE8iJLA.gif) You can attach to running PHP processes: ![attach cli](https://i.imgur.com/HubBqo0.gif) ![attach httpd](https://i.imgur.com/KVY3KXq.gif) It has a top-like mode: ![top](https://i.imgur.com/Y0NJJ1C.gif) It can collect request info, memory usage, and variables: ![advanced](https://i.imgur.com/nlUfCWT.gif) You can also use it to make flamegraphs like this: ![FlameGraph example](https://i.imgur.com/e6P9Arp.png) All with no changes to your application and minimal overhead. ### Synopsis $ git clone https://github.com/adsr/phpspy.git Cloning into 'phpspy'... ... $ cd phpspy $ make ... $ sudo ./phpspy --limit=1000 --pid=$(pgrep -n httpd) >traces ... $ ./stackcollapse-phpspy.pl flame.svg $ google-chrome flame.svg # View flame.svg in browser ### Build options $ make # Use built-in structs $ # or $ USE_ZEND=1 make ... # Use Zend structs (requires PHP development headers) ### Usage $ ./phpspy -h Usage: phpspy [options] -p phpspy [options] -P phpspy [options] [--] Options: -h, --help Show this help -p, --pid= Trace PHP process at `pid` -P, --pgrep= Concurrently trace processes that match pgrep `args` (see also `-T`) -T, --threads= Set number of threads to use with `-P` (default: 16) -s, --sleep-ns= Sleep `ns` nanoseconds between traces (see also `-H`) (default: 10101010) -H, --rate-hz= Trace `hz` times per second (see also `-s`) (default: 99) -V, --php-version= Set PHP version (default: auto; supported: 70 71 72 73 74 80 81 82 83 84 85 86) -l, --limit= Limit total number of traces to capture (approximate limit in pgrep mode) (default: 0; 0=unlimited) -i, --time-limit-ms= Stop tracing after `ms` milliseconds (second granularity in pgrep mode) (default: 0; 0=unlimited) -n, --max-depth= Keep frames from the leaf stack frame (default: -1; -1=off) -N, --max-depth-outer= Keep frames from the root stack frame (default: -1; -1=off) -r, --request-info= Set request info parts to capture (q=query c=cookie u=uri p=path capital=negation) (default: QCUP; none) -m, --memory-usage Capture peak and current memory usage with each trace (requires target PHP process to have debug symbols) -o, --output= Write phpspy output to `path`. If a `%d` is present, replace it with a thread id. Useful in `-P` mode. (default: -; -=stdout) -O, --child-stdout= Write child stdout to `path`. Replace `%d` with child pid. (default: phpspy.%d.out; -=stdout) -E, --child-stderr= Write child stderr to `path` Replace `%d` with child pid. (default: phpspy.%d.err; -=stderr) --addr-executor-globals= Set address of executor_globals in hex (default: 0; 0=find dynamically) --addr-sapi-globals= Set address of sapi_globals in hex (default: 0; 0=find dynamically) -1, --single-line Output in single-line mode -b, --buffer-size= Set max bytes per write to `size`. Traces larger than this are split into chunks, each ending with a `# trace_id = .` record. In `-P` mode, if writing to stdout, setting this above `PIPE_BUF` (4096) may lead to interleaved writes, unless `--event-handler-opts m` is specified. (default: 4096) -f, --filter= Filter output by POSIX regex (default: none) -F, --filter-negate= Same as `-f` except negated -d, --verbose-fields= Set verbose output fields (p=pid t=timestamp capital=negation) (default: PT; none) -c, --continue-on-error Attempt to continue tracing after encountering an error -q, --quiet Suppress errors and warnings on stderr --libname-awk-patt= Awk pattern to match name of PHP lib (default: libphp[78]?) -#, --comment= Ignored; intended for self-documenting commands -@, --nothing Ignored -v, --version Print phpspy version and exit Experimental options: --event-handler= Set event handler (fout, callgrind) (default: fout) --event-handler-opts= Set event handler options (fout: m=use mutex to prevent interlaced writes on stdout in `-P` mode) -S, --pause-process Pause process while reading stacktrace (unsafe for production!) -e, --peek-var= Peek at the contents of the var located at `varspec`, which has the format: @: @:- e.g., xyz@/path/to.php:10-20 -D, --peek-pdo Peek at the SQL and arguments of PDO queries. Emits varpeek events. -g, --peek-global= Peek at the contents of a global var located at `glospec`, which has the format: . where is one of: post|get|cookie|server|files|globals e.g., server.REQUEST_TIME -t, --top Show dynamic top-like output ### Output format Each trace is a sequence of records delimited by newlines (or tabs in `-1` mode), followed by a newline. Frame records look like ` :`. Metadata records start with a hash (`#`). If a trace exceeds `-b` bytes, it is split into multiple chunks at record boundaries. Each chunk ends with a `# trace_id = .` metadata record where `` is a process-wide counter, `` is a 0-based index, and `` is either an exclamation point (`!`) for the last chunk, or a tilde (`~`) for intermediate chunks. If a single record is too large to fit in a chunk, or if a whole trace exceeds 4 megabytes, the trace is truncated. That record and every record after it are dropped, and the trace ends with a final chunk containing only a `# truncated = 1` metadata record. ### Example (variable peek) $ sudo ./phpspy -e 'i@/var/www/test/lib/test.php:12' -p $(pgrep -n httpd) | grep varpeek # varpeek i@/var/www/test/lib/test.php:12 = 42 # varpeek i@/var/www/test/lib/test.php:12 = 42 # varpeek i@/var/www/test/lib/test.php:12 = 43 # varpeek i@/var/www/test/lib/test.php:12 = 44 ... ### Example (pgrep daemon mode) $ sudo ./phpspy -H1 -T4 -P '-x php' 0 proc_open :-1 1 system_with_timeout /home/adam/php-src/run-tests.php:1137 2 run_test /home/adam/php-src/run-tests.php:1937 3 run_all_tests /home/adam/php-src/run-tests.php:1215 4
/home/adam/php-src/run-tests.php:986 ... ^C main_pgrep finished gracefully ### Example (httpd) $ sudo ./phpspy -p $(pgrep -n httpd) 0 Memcached::get :-1 1 Cache_MemcachedToggleable::get /foo/bar/lib/Cache/MemcachedToggleable.php:26 2 Cache_Memcached::get /foo/bar/lib/Cache/Memcached.php:251 3 IpDb_CacheBase::getFromCache /foo/bar/lib/IpDb/CacheBase.php:165 4 IpDb_CacheBase::get /foo/bar/lib/IpDb/CacheBase.php:107 5 IpDb_CacheBase::contains /foo/bar/lib/IpDb/CacheBase.php:70 6 IpDb_Botnet::has /foo/bar/lib/IpDb/Botnet.php:32 7 Security_Rule_IpAddr::__construct /foo/bar/lib/Security/Rule/IpAddr.php:53 8 Security_Rule_HttpRequestContext::initVariables /foo/bar/lib/Security/Rule/HttpRequestContext.php:22 9 Security_Rule_Context::__construct /foo/bar/lib/Security/Rule/Context.php:44 10 Security_Rule_Engine::getContextByName /foo/bar/lib/Security/Rule/Engine.php:225 11 Security_Rule_Engine::getContextByLocation /foo/bar/lib/Security/Rule/Engine.php:210 12 Security_Rule_Engine::evaluateActionRules /foo/bar/lib/Security/Rule/Engine.php:116 13
/foo/bar/lib/bootstrap/api.php:49 14
/foo/bar/htdocs/v3/public.php:5 ... ### Example (cli child) $ ./phpspy -- php -r 'usleep(1000000);' 0 usleep :-1 1
:-1 0 usleep :-1 1
:-1 0 usleep :-1 1
:-1 0 usleep :-1 1
:-1 0 usleep :-1 1
:-1 0 usleep :-1 1
:-1 ... main_pid: pid 2830271 exited ### Example (cli attach) $ php -r 'sleep(10);' & [1] 28586 $ sudo ./phpspy -p 28586 0 sleep :-1 1
:-1 ... ### Example (docker) $ docker build . -t phpspy $ docker run -it --cap-add SYS_PTRACE phpspy:latest ./phpspy/phpspy -V73 -r -- php -r 'sleep(1);' 0 sleep :-1 1
:-1 ... ### Known bugs * phpspy may not work with a chrooted mod_php process whose binary lives inside overlayfs. (See [#109][8].) * Tracing on aarch64 (arm64) will fail for PHP 8.2 and below. ### See also * [rbspy][0] for Ruby, the original inspiration for phpspy * [py-spy][1] for Python * [Xdebug profiler][2], instrumented profiler * [reli-prof][3], similar to phpspy but pure PHP * [sample_prof][4] * [php-trace][5] * [Blackfire][6], commercial * [Tideways][7], commercial ### TODO * See `grep -ri todo` * See https://github.com/adsr/phpspy/issues [0]: https://github.com/rbspy/rbspy [1]: https://github.com/benfred/py-spy [2]: http://www.xdebug.org/docs/profiler [3]: https://github.com/reliforp/reli-prof [4]: https://github.com/nikic/sample_prof [5]: https://github.com/krakjoe/trace [6]: https://blackfire.io/ [7]: https://tideways.io/ [8]: https://github.com/adsr/phpspy/issues/109 [^9]: Currently the aarch64 structs are only valid for PHP 8.3+.