Stars: 328
Forks: 8
Pull Requests: 251
Issues: 42
Watchers: 7
Last Updated: 2023-09-12 15:29:03
A sampling profiler for PHP written in PHP, which reads information about running PHP VM from outside of the process.
License: MIT License
Languages: PHP, C, Makefile, Dockerfile, Perl
Reli is a sampling profiler (or a VM state inspector) written in PHP. It can read information about running PHP script from outside of the process. It's a stand alone CLI tool, so target programs don't need any modifications. The former name of this tool was sj-i/php-profiler.
It's implemented by using following techniques:
If you have a bit of extra CPU resource, the overhead of this software would be negligible.
Reli is heavily inspired by adsr/phpspy.
The main difference between the two is that reli is written in almost pure PHP while phpspy is written in C. In profiling, there are cases you want to customize how and what information to get. If customizability for PHP developers matters, you can use this software at the cost of performance. (Although, we hope the cost is not too big.)
Additionally, reli can find VM state from ZTS interpreters. For example, in the daemon mode, traces of threads started via ext-parallel are automatically retrieved. Currently this cannot be done with phpspy only. Reli also provides functionality to only get the address of EG from targets, so you can use actual profiling with phpspy if you want, even when the target is ZTS.
Other features of reli that phpspy does not currently have include:
There is no particular reason why these features cannot be implemented on the phpspy side, so it may be possible to do them on phpspy in the future.
On the other hand, there are a few things that phpspy can do but reli cannot yet.
Much of what can be done with phpspy will be done with reli in the future.
On targeting ZTS, the target process must load libpthread.so, and also you must have unstripped binary of the interpreter and the libpthread.so, to find EG from the TLS.
composer create-project reliforp/reli-prof
cd reli
./reli
git clone [email protected]:reliforp/reli-prof.git
cd reli
composer install
./reli
./reli inspector:trace --help
Description:
periodically get call trace from an outer process or thread
Usage:
inspector:trace [options] [--] [<cmd> [<args>...]]
Arguments:
cmd command to execute as a target: either pid (via -p/--pid) or cmd must be specified
args command line arguments for cmd
Options:
-p, --pid=PID process id
-d, --depth[=DEPTH] max depth
-s, --sleep-ns[=SLEEP-NS] nanoseconds between traces (default: 1000 * 1000 * 10)
-r, --max-retries[=MAX-RETRIES] max retries on contiguous errors of read (default: 10)
-S, --stop-process[=STOP-PROCESS] stop the target process while reading its trace (default: off)
--php-regex[=PHP-REGEX] regex to find the php binary loaded in the target process
--libpthread-regex[=LIBPTHREAD-REGEX] regex to find the libpthread.so loaded in the target process
--php-version[=PHP-VERSION] php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
--php-path[=PHP-PATH] path to the php binary (only needed in tracing chrooted ZTS target)
--libpthread-path[=LIBPTHREAD-PATH] path to the libpthread.so (only needed in tracing chrooted ZTS target)
-t, --template[=TEMPLATE] template name (phpspy|phpspy_with_opcode|json_lines) (default: phpspy)
-o, --output=OUTPUT path to write output from this tool (default: stdout)
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
./reli inspector:daemon --help
Description:
concurrently get call traces from processes whose command-lines match a given regex
Usage:
inspector:daemon [options]
Options:
-P, --target-regex=TARGET-REGEX regex to find target processes which have matching command-line (required)
-T, --threads[=THREADS] number of workers (default: 8)
-d, --depth[=DEPTH] max depth
-s, --sleep-ns[=SLEEP-NS] nanoseconds between traces (default: 1000 * 1000 * 10)
-r, --max-retries[=MAX-RETRIES] max retries on contiguous errors of read (default: 10)
-S, --stop-process[=STOP-PROCESS] stop the target process while reading its trace (default: off)
--php-regex[=PHP-REGEX] regex to find the php binary loaded in the target process
--libpthread-regex[=LIBPTHREAD-REGEX] regex to find the libpthread.so loaded in the target process
--php-version[=PHP-VERSION] php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
--php-path[=PHP-PATH] path to the php binary (only needed in tracing chrooted ZTS target)
--libpthread-path[=LIBPTHREAD-PATH] path to the libpthread.so (only needed in tracing chrooted ZTS target)
-t, --template[=TEMPLATE] template name (phpspy|phpspy_with_opcode|json_lines) (default: phpspy)
-o, --output=OUTPUT path to write output from this tool (default: stdout)
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
./reli inspector:top --help
Description:
show an aggregated view of traces in real time in a form similar to the UNIX top command.
Usage:
inspector:top [options]
Options:
-P, --target-regex=TARGET-REGEX regex to find target processes which have matching command-line (required)
-T, --threads[=THREADS] number of workers (default: 8)
-d, --depth[=DEPTH] max depth
-s, --sleep-ns[=SLEEP-NS] nanoseconds between traces (default: 1000 * 1000 * 10)
-r, --max-retries[=MAX-RETRIES] max retries on contiguous errors of read (default: 10)
-S, --stop-process[=STOP-PROCESS] stop the target process while reading its trace (default: off)
--php-regex[=PHP-REGEX] regex to find the php binary loaded in the target process
--libpthread-regex[=LIBPTHREAD-REGEX] regex to find the libpthread.so loaded in the target process
--php-version[=PHP-VERSION] php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
--php-path[=PHP-PATH] path to the php binary (only needed in tracing chrooted ZTS target)
--libpthread-path[=LIBPTHREAD-PATH] path to the libpthread.so (only needed in tracing chrooted ZTS target)
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
./reli inspector:eg --help
Description:
get EG address from an outer process or thread
Usage:
inspector:eg_address [options] [--] [<cmd> [<args>...]]
Arguments:
cmd command to execute as a target: either pid (via -p/--pid) or cmd must be specified
args command line arguments for cmd
Options:
-p, --pid=PID process id
--php-regex[=PHP-REGEX] regex to find the php binary loaded in the target process
--libpthread-regex[=LIBPTHREAD-REGEX] regex to find the libpthread.so loaded in the target process
--php-version[=PHP-VERSION] php version (auto|v7[0-4]|v8[01]) of the target (default: auto)
--php-path[=PHP-PATH] path to the php binary (only needed in tracing chrooted ZTS target)
--libpthread-path[=LIBPTHREAD-PATH] path to the libpthread.so (only needed in tracing chrooted ZTS target)
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
$ ./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
<press q to exit>
...
$ 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/LoopMiddleware/NanoSleepMiddleware.php:33
2 Reli\Lib\Loop\LoopMiddleware\KeyboardCancelMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/KeyboardCancelMiddleware.php:39
3 Reli\Lib\Loop\LoopMiddleware\RetryOnExceptionMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/RetryOnExceptionMiddleware.php:37
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/GetTraceCommand.php:133
6 Symfony\Component\Console\Command\Command::run /home/sji/work/reli/vendor/symfony/console/Command/Command.php:291
7 Symfony\Component\Console\Application::doRunCommand /home/sji/work/reli/vendor/symfony/console/Application.php:979
8 Symfony\Component\Console\Application::doRun /home/sji/work/reli/vendor/symfony/console/Application.php:299
9 Symfony\Component\Console\Application::run /home/sji/work/reli/vendor/symfony/console/Application.php:171
10 <main> /home/sji/work/reli/reli:45
0 time_nanosleep <internal>:-1
1 Reli\Lib\Loop\LoopMiddleware\NanoSleepMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/NanoSleepMiddleware.php:33
2 Reli\Lib\Loop\LoopMiddleware\KeyboardCancelMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/KeyboardCancelMiddleware.php:39
3 Reli\Lib\Loop\LoopMiddleware\RetryOnExceptionMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddleware/RetryOnExceptionMiddleware.php:37
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/GetTraceCommand.php:133
6 Symfony\Component\Console\Command\Command::run /home/sji/work/reli/vendor/symfony/console/Command/Command.php:291
7 Symfony\Component\Console\Application::doRunCommand /home/sji/work/reli/vendor/symfony/console/Application.php:979
8 Symfony\Component\Console\Application::doRun /home/sji/work/reli/vendor/symfony/console/Application.php:299
9 Symfony\Component\Console\Application::run /home/sji/work/reli/vendor/symfony/console/Application.php:171
10 <main> /home/sji/work/reli/reli:45
<press q to exit>
...
The executing process must have the CAP_SYS_PTRACE capability. (Usually run as root is enough.)
$ sudo php ./reli i:daemon -P "^/usr/sbin/httpd"
The executing process must have the CAP_SYS_PTRACE capability. (Usually run as root is enough.)
$ sudo php ./reli i:eg -p 2183131
0x555ae7825d80
The executing process must have the CAP_SYS_PTRACE capability. (Usually run as root is enough.)
If a user wants to profile a really CPU-bound application, then he or she wouldn't only want to know what line is slow, but what opcode is. In such cases, use --template=phpspy_with_opcode
with inspector:trace
or inspector:daemon
.
$ sudo php ./reli i:trace --template=phpspy_with_opcode -p <pid of the target process or thread>
The output would be like the following.
0 <VM>::ZEND_ASSIGN <VM>:-1
1 Mandelbrot::iterate /home/sji/work/test/mandelbrot.php:33:ZEND_ASSIGN
2 Mandelbrot::__construct /home/sji/work/test/mandelbrot.php:12:ZEND_DO_FCALL
3 <main> /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL
0 <VM>::ZEND_ASSIGN <VM>:-1
1 Mandelbrot::iterate /home/sji/work/test/mandelbrot.php:30:ZEND_ASSIGN
2 Mandelbrot::__construct /home/sji/work/test/mandelbrot.php:12:ZEND_DO_FCALL
3 <main> /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL
The currently executing opcode becomes the first frame of the callstack. So visualizations of the trace like flamegraph can show the usage of opcodes.
For informational purposes, executing opcodes are also added to each end of the call frames. Except for the first frame, opcodes for function calls such as ZEND_DO_FCALL should appear there.
If JIT is enabled at the target process, this information may be slightly inaccurate.
$ docker pull reliforp/reli-prof
$ docker run -it --security-opt="apparmor=unconfined" --cap-add=SYS_PTRACE --pid=host reliforp/reli-prof i:trace -p <pid of the target process or thread>
$ ./reli i:trace -o traces -- php ./vendor/bin/psalm.phar --no-cache
$ ./reli c:flamegraph <traces >flame.svg
$ google-chrome flame.svg
The generated flamegraph below visualizes traces from the execution of the psalm command.
$ sudo php ./reli i:trace -p <pid of the target process or thread> >traces
$ ./reli c:speedscope <traces >profile.speedscope.json
$ speedscope profile.speedscope.json
See #101.
$ ./reli c:callgrind <traces >callgrind.out
$ kcachegrind callgrind.out
If your PHP binary uses a non-standard binary name that does not end with /php
, use the --php-regex
option to specify the name of the executable (or shared object) that contains the PHP interpreter.
The -S
option will give you better results. Using this option stops the execution of the target process for a moment at every sampling, but the trace obtained will be more accurate. If you don't stop the VMs from running when profiling CPU-heavy programs such as benchmarking programs, you may misjudge the bottleneck, because you will miss more VM states that transition very quickly and are not detected well.
Try to specify --libpthread-regex="libc.so"
as an option.
First, try cat /proc/<pid>/maps
to check the memory map of the target PHP process. If the first module does not indicate the location of the PHP binary and looks like an anonymous region, try to specify --php-regex="^$"
as an option.
We would like to achieve the following 5 goals through this project.
This product includes the Zend Engine, freely available at
http://www.zend.com
"Reli" means nothing, though you are free to think of this tool as a something reliable, or religious, or relishable, or whatever other reli-s as you like.
Originally the name of this tool was just "php-profiler". Due to a licensing problem (#175), this simple good name had to be changed.
So we applied a randomly chosen string manipulation function to the original name. strrev('php-profiler')
results to 'reliforp-php'
, and it can be read as "reli for p(php)".
Thus the name of this tool is "Reli for PH*" now. And you can also call it just "Reli".