Share
Embed
PHP Performance Demystified - PHP UK 2026
Volker Dusch
February 26, 2026
Transcript
67 slides
-
About me ~20 years of PHP I work on web
applications with long life cycles Working at @Tideways on PHP Performance Tooling PHP 8.5 Release Manager Head of Engineering and "Full Stack" Engineer -
Intro Performance is a very wide topic From Database Tuning
over Premature Optimization to Distributed Systems and Observability -
Goals for Today I want to help you to: build
a mental model on how PHP executes measure more correctly inform your performance reasoning find bottlenecks -
PHP Script execution What happens when you run a PHP
script? We're skipping over the webserver and php-fpm php script.php https://tideways.com/profiler/blog/an-introduction-to-php-fpm-tuning -
PHP CLI SAPI strace php script.php Starts the PHP CLI
SAPI (Server Application Programming Interface) Allocate memory Set up signal handlers Load .ini files Load timezone data [...] Read the first php file -
Run the code Now the PHP VM executes the OPcodes
Fetch the next opline Execute the handler for this OPcode Calls into C functions if needed Clean up memory efficiently via ref counting Potentially the GC (Garbage Collector) runs When an include/require loads another file the compilation process starts again. -
OPcodes We can look at these OPcodes using OPcache php
-d opcache.enable_cli=1 -d opcache.opt_debug_level= 0x10000 = Before optimization 0x20000 = After optimization https://php.watch/articles/php-dump-opcodes -
OPcodes Example script <?php $foo = random_int(0,1); if (false) {
echo "Never"; } if ($foo) { echo "Yes"; } 1 2 3 4 5 6 7 8 9 10 11 $foo = random_int(0,1); <?php 1 2 3 4 if (false) { 5 echo "Never"; 6 } 7 8 if ($foo) { 9 echo "Yes"; 10 } 11 if (false) { echo "Never"; } <?php 1 2 $foo = random_int(0,1); 3 4 5 6 7 8 if ($foo) { 9 echo "Yes"; 10 } 11 if ($foo) { echo "Yes"; } <?php 1 2 $foo = random_int(0,1); 3 4 if (false) { 5 echo "Never"; 6 } 7 8 9 10 11 -
OPcodes opcache.opt_debug_level=0x10000 $_main: ; (lines=10, args=0, vars=1, tmps=2) ; (before
optimizer) 0000 INIT_FCALL 2 112 string("random_int") 0001 SEND_VAL int(0) 1 0002 SEND_VAL int(1) 2 0003 V1 = DO_ICALL 0004 ASSIGN CV0($foo) V1 0005 JMPZ bool(false) 0007 0006 ECHO string("Never") 0007 JMPZ CV0($foo) 0009 0008 ECHO string("Yes") 0009 RETURN int(1) 1 2 3 4 5 6 7 8 9 10 11 12 13 0000 INIT_FCALL 2 112 string("random_int") 0001 SEND_VAL int(0) 1 0002 SEND_VAL int(1) 2 0003 V1 = DO_ICALL 0004 ASSIGN CV0($foo) V1 $_main: 1 ; (lines=10, args=0, vars=1, tmps=2) 2 ; (before optimizer) 3 4 5 6 7 8 0005 JMPZ bool(false) 0007 9 0006 ECHO string("Never") 10 0007 JMPZ CV0($foo) 0009 11 0008 ECHO string("Yes") 12 0009 RETURN int(1) 13 0005 JMPZ bool(false) 0007 0006 ECHO string("Never") $_main: 1 ; (lines=10, args=0, vars=1, tmps=2) 2 ; (before optimizer) 3 0000 INIT_FCALL 2 112 string("random_int") 4 0001 SEND_VAL int(0) 1 5 0002 SEND_VAL int(1) 2 6 0003 V1 = DO_ICALL 7 0004 ASSIGN CV0($foo) V1 8 9 10 0007 JMPZ CV0($foo) 0009 11 0008 ECHO string("Yes") 12 0009 RETURN int(1) 13 0007 JMPZ CV0($foo) 0009 0008 ECHO string("Yes") 0009 RETURN int(1) $_main: 1 ; (lines=10, args=0, vars=1, tmps=2) 2 ; (before optimizer) 3 0000 INIT_FCALL 2 112 string("random_int") 4 0001 SEND_VAL int(0) 1 5 0002 SEND_VAL int(1) 2 6 0003 V1 = DO_ICALL 7 0004 ASSIGN CV0($foo) V1 8 0005 JMPZ bool(false) 0007 9 0006 ECHO string("Never") 10 11 12 13 -
OPcodes opcache.opt_debug_level=0x20000 $_main: ; (lines=8, args=0, vars=1, tmps=1) ; (after
optimizer) 0000 INIT_FCALL 2 112 string("random_int") 0001 SEND_VAL int(0) 1 0002 SEND_VAL int(1) 2 0003 V1 = DO_ICALL 0004 ASSIGN CV0($foo) V1 0005 JMPZ CV0($foo) 0007 0006 ECHO string("Yes") 0007 RETURN int(1) 1 2 3 4 5 6 7 8 9 10 11 0000 INIT_FCALL 2 112 string("random_int") 0001 SEND_VAL int(0) 1 0002 SEND_VAL int(1) 2 0003 V1 = DO_ICALL 0004 ASSIGN CV0($foo) V1 $_main: 1 ; (lines=8, args=0, vars=1, tmps=1) 2 ; (after optimizer) 3 4 5 6 7 8 0005 JMPZ CV0($foo) 0007 9 0006 ECHO string("Yes") 10 0007 RETURN int(1) 11 0005 JMPZ CV0($foo) 0007 0006 ECHO string("Yes") 0007 RETURN int(1) $_main: 1 ; (lines=8, args=0, vars=1, tmps=1) 2 ; (after optimizer) 3 0000 INIT_FCALL 2 112 string("random_int") 4 0001 SEND_VAL int(0) 1 5 0002 SEND_VAL int(1) 2 6 0003 V1 = DO_ICALL 7 0004 ASSIGN CV0($foo) V1 8 9 10 11 -
OPcache Compiled scripts are stored in Shared Memory (SHM) Apache
/ php-fpm master process Without OPcache the PHP Optimizer is not enabled. -
OPcache status Inspecting OPcache opcache.memory_consumption var_dump(opcache_get_status()); // Abbreviated output array(9)
{ 'opcache_enabled' => bool(true) 'cache_full' => bool(false) 'memory_usage' => array(4) { 'used_memory' => int(9167936) 'free_memory' => int(125049792) } 1 2 3 4 5 6 7 8 9 10 https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.memory- consumption -
Interned strings Immutable strings stored in a global memory table
opcache.interned_strings_buffer Interned strings are stored in the SHM They count against opcache.memory_consumption 'interned_strings_usage' => array(4) { 'buffer_size' => int(8388608) 'used_memory' => int(2593616) 'free_memory' => int(5794992) 'number_of_strings' => int(10358) } 1 2 3 4 5 6 https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.interned-strings- buffer -
Number of cached scripts Cached scripts don't need to be
compiled again opcache.max_accelerated_files Defaults to 10_000 Chances are you have more than 10k files these days 'opcache_statistics' => array(13) { 'num_cached_scripts' => int(0) // ... } 1 2 3 4 https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.max- accelerated-files -
Further reading APM tools like Tideways can audit and monitor
these settings for you https://tideways.com/profiler/blog/fine-tune-your-opcache-configuration-to-avoid- caching-surprises https://tideways.com/profiler/blog/should-you-use-opcache-preloading-in-your-php-app -
JIT Chances are you're not using the JIT Can be
worth it for long running scripts opcache.enable=1 opcache.enable_cli=1 # 8.0 - 8.3 size defaults to 0, turning off the JIT opcache.jit_buffer_size="100M" # 8.4+ defaults to disabled opcache.jit="tracing" var_dump(opcache_get_status()['jit']); array(7) { ["enabled"]=> bool(true) ["on"]=> bool(true) // [...] ["buffer_size"]=> int(104857584) } -
Quiz Which one is fastest? $foo = $bar = $baz
= "some string"; $a = "$foo-$bar-$baz"; // or $b = $foo . "-" . $bar . "-" . $baz; // or $c = sprintf("%s-%s-%s", $foo, $bar, $baz); 1 2 3 4 5 6 7 $a = "$foo-$bar-$baz"; $b = $foo . "-" . $bar . "-" . $baz; $c = sprintf("%s-%s-%s", $foo, $bar, $baz); $foo = $bar = $baz = "some string"; 1 2 3 // or 4 5 // or 6 7 -
Benchmark - PHP 8.3 $a = "$foo-$bar-$baz"; # Fastest $b
= $foo . "-" . $bar . "-" . $baz; # 40% slower $c = sprintf("%s-%s-%s", $foo, $bar, $baz); # 50% slower -
Benchmark - PHP 8.4 $a = "$foo-$bar-$baz"; # Fastest $b
= $foo . "-" . $bar . "-" . $baz; # Slower $c = sprintf("%s-%s-%s", $foo, $bar, $baz); # Fastest -
Explanation How did we measure this? How to apply our
understanding on how PHP works? What changed in 8.4? -
Benchmark - Script hyperfine -L function a,b,c 'php strings.php {function}'
$iterations = 10_000_000; $foo = $bar = $baz = "some string"; $function = $_SERVER['argv'][1](...); $function($iterations, $foo, $bar, $baz); function a($i, $foo, $bar, $baz) { while($i--) $a = "$foo-$bar-$baz"; } function b($i, $foo, $bar, $baz) { while($i--) $b = $foo . "-" . $bar . "-" . $baz; } function c($i, $foo, $bar, $baz) { while($i--) $c = \sprintf("%s-%s-%s", $foo, $bar, $baz); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $iterations = 10_000_000; $function = $_SERVER['argv'][1](...); 1 $foo = $bar = $baz = "some string"; 2 3 4 $function($iterations, $foo, $bar, $baz); 5 6 function a($i, $foo, $bar, $baz) { 7 while($i--) 8 $a = "$foo-$bar-$baz"; 9 } 10 function b($i, $foo, $bar, $baz) { 11 while($i--) 12 $b = $foo . "-" . $bar . "-" . $baz; 13 } 14 function c($i, $foo, $bar, $baz) { 15 while($i--) 16 $c = \sprintf("%s-%s-%s", $foo, $bar, $baz); 17 } 18 $a = "$foo-$bar-$baz"; $b = $foo . "-" . $bar . "-" . $baz; $c = \sprintf("%s-%s-%s", $foo, $bar, $baz); $iterations = 10_000_000; 1 $foo = $bar = $baz = "some string"; 2 3 $function = $_SERVER['argv'][1](...); 4 $function($iterations, $foo, $bar, $baz); 5 6 function a($i, $foo, $bar, $baz) { 7 while($i--) 8 9 } 10 function b($i, $foo, $bar, $baz) { 11 while($i--) 12 13 } 14 function c($i, $foo, $bar, $baz) { 15 while($i--) 16 17 } 18 -
String interpolation php -d opcache.enable_cli=1 \ -d opcache.opt_debug_level=0x10000 opcodes.php $foo
= $bar = $baz = "some string"; $a = "$foo-$bar-$baz"; 0000 T4 = ASSIGN CV2($baz) string("some string") 0001 T5 = ASSIGN CV1($bar) T4 0002 ASSIGN CV0($foo) T5 0003 T8 = ROPE_INIT 5 CV0($foo) 0004 T8 = ROPE_ADD 1 T8 string("-") 0005 T8 = ROPE_ADD 2 T8 CV1($bar) 0006 T8 = ROPE_ADD 3 T8 string("-") 0007 T7 = ROPE_END 4 T8 CV2($baz) 0008 ASSIGN CV3($a) T7 1 2 3 4 5 6 7 8 9 10 11 12 $foo = $bar = $baz = "some string"; 0000 T4 = ASSIGN CV2($baz) string("some string") 0001 T5 = ASSIGN CV1($bar) T4 0002 ASSIGN CV0($foo) T5 1 $a = "$foo-$bar-$baz"; 2 3 4 5 6 0003 T8 = ROPE_INIT 5 CV0($foo) 7 0004 T8 = ROPE_ADD 1 T8 string("-") 8 0005 T8 = ROPE_ADD 2 T8 CV1($bar) 9 0006 T8 = ROPE_ADD 3 T8 string("-") 10 0007 T7 = ROPE_END 4 T8 CV2($baz) 11 0008 ASSIGN CV3($a) T7 12 $a = "$foo-$bar-$baz"; 0003 T8 = ROPE_INIT 5 CV0($foo) 0004 T8 = ROPE_ADD 1 T8 string("-") 0005 T8 = ROPE_ADD 2 T8 CV1($bar) 0006 T8 = ROPE_ADD 3 T8 string("-") 0007 T7 = ROPE_END 4 T8 CV2($baz) $foo = $bar = $baz = "some string"; 1 2 3 0000 T4 = ASSIGN CV2($baz) string("some string") 4 0001 T5 = ASSIGN CV1($bar) T4 5 0002 ASSIGN CV0($foo) T5 6 7 8 9 10 11 0008 ASSIGN CV3($a) T7 12 -
String concatenation php -d opcache.enable_cli=1 \ -d opcache.opt_debug_level=0x10000 opcodes.php $foo
= $bar = $baz = "some string"; $b = $foo . "-" . $bar . "-" . $baz; // Same as before 0000 T4 = ASSIGN CV2($baz) string("some string") 0001 T5 = ASSIGN CV1($bar) T4 0002 ASSIGN CV0($foo) T5 // Different 0003 T7 = CONCAT CV0($foo) string("-") 0004 T8 = CONCAT T7 CV1($bar) 0005 T9 = CONCAT T8 string("-") 0006 T10 = CONCAT T9 CV2($baz) 0007 ASSIGN CV3($b) T10 1 2 3 4 5 6 7 8 9 10 11 12 13 $b = $foo . "-" . $bar . "-" . $baz; 0003 T7 = CONCAT CV0($foo) string("-") 0004 T8 = CONCAT T7 CV1($bar) 0005 T9 = CONCAT T8 string("-") 0006 T10 = CONCAT T9 CV2($baz) 0007 ASSIGN CV3($b) T10 $foo = $bar = $baz = "some string"; 1 2 3 // Same as before 4 0000 T4 = ASSIGN CV2($baz) string("some string") 5 0001 T5 = ASSIGN CV1($bar) T4 6 0002 ASSIGN CV0($foo) T5 7 // Different 8 9 10 11 12 13 -
sprintf - PHP 8.3 php -d opcache.enable_cli=1 \ -d opcache.opt_debug_level=0x10000
opcodes.php $foo = $bar = $baz = "some string"; $c = sprintf("%s-%s-%s", $foo, $bar, $baz); // Same as before 0000 T4 = ASSIGN CV2($baz) string("some string") 0001 T5 = ASSIGN CV1($bar) T4 0002 ASSIGN CV0($foo) T5 // Different 0003 INIT_FCALL 4 144 string("sprintf") 0004 SEND_VAL string("%s-%s-%s") 1 0005 SEND_VAR CV0($foo) 2 0006 SEND_VAR CV1($bar) 3 0007 SEND_VAR CV2($baz) 4 0008 V7 = DO_ICALL 0009 ASSIGN CV3($c) V7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $c = sprintf("%s-%s-%s", $foo, $bar, $baz); 0003 INIT_FCALL 4 144 string("sprintf") 0004 SEND_VAL string("%s-%s-%s") 1 0005 SEND_VAR CV0($foo) 2 0006 SEND_VAR CV1($bar) 3 0007 SEND_VAR CV2($baz) 4 0008 V7 = DO_ICALL 0009 ASSIGN CV3($c) V7 $foo = $bar = $baz = "some string"; 1 2 3 // Same as before 4 0000 T4 = ASSIGN CV2($baz) string("some string") 5 0001 T5 = ASSIGN CV1($bar) T4 6 0002 ASSIGN CV0($foo) T5 7 // Different 8 9 10 11 12 13 14 15 -
So what changed in 8.4? Let's take a look at
the \sprintf OPcodes with 8.4 -
sprintf opcodes in 8.4 php-8.4/sapi/cli/php -d zend_extension=opcache.so \ -d opcache.enable_cli=1
\ -d opcache.opt_debug_level=0x10000 sprintf.php // Same as always 0000 T4 = ASSIGN CV2($baz) string("some string") 0001 T5 = ASSIGN CV1($bar) T4 0002 ASSIGN CV0($foo) T5 // Just like string interpolation! 0003 T8 = ROPE_INIT 5 CV0($foo) 0004 T8 = ROPE_ADD 1 T8 string("-") 0005 T8 = ROPE_ADD 2 T8 CV1($bar) 0006 T8 = ROPE_ADD 3 T8 string("-") 0007 T7 = ROPE_END 4 T8 CV2($baz) 0008 ASSIGN CV3($c) T7 1 2 3 4 5 6 7 8 9 10 11 12 // Just like string interpolation! 0003 T8 = ROPE_INIT 5 CV0($foo) 0004 T8 = ROPE_ADD 1 T8 string("-") 0005 T8 = ROPE_ADD 2 T8 CV1($bar) 0006 T8 = ROPE_ADD 3 T8 string("-") 0007 T7 = ROPE_END 4 T8 CV2($baz) // Same as always 1 0000 T4 = ASSIGN CV2($baz) string("some string") 2 0001 T5 = ASSIGN CV1($bar) T4 3 0002 ASSIGN CV0($foo) T5 4 5 6 7 8 9 10 11 0008 ASSIGN CV3($c) T7 12 -
sprintf optimized out Where possible, sprintf calls will be "turned
into" interpolated strings Readability improvement with no performance cost // Our $c example sprintf("%s-%s-%s", $foo, $bar, $baz); // => "$foo-$bar-$baz"; -
Constraints Only for %s and %d placeholders No modifier, %02d
can't be optimized No optimization for other placeholders In namespaced code, you must use \sprintf Without the \ the sprintf function must be looked up in the local namespace at runtime use function sprintf; also works of course https://tideways.com/profiler/blog/new-in-php-8-4-engine-optimization-of-sprintf-to- string-interpolation -
Takeaways PHP gets faster and more optimized with each release
Write maintainable code and let PHP do optimizations If something in the engine is a bottleneck, investigate https://phpc.social/@pollita/114522260863354986 https://github.com/php/php-src/pull/18571 -
Optimization The hard parts Figuring out what's slow Deciding if
its worth optimizing Validating the results of a change Once an issue is identified, fixing it tends to be the easy, and fun, part of the process -
Validating results Can you reproduce the issue? Issues reproducible with
a single command are easier to tune and measure Systemic issues are tougher and need a different approach. We look at these later -
Benchmarking There are a lot of ways to measure runtime
Different abstraction layers offer different insights -
Micro benchmarks microtime() gettimeofday() microsecond resolution hrtime() clock_gettime(CLOCK_MONOTONIC[_RAW]) nanosecond resolution
$start = hrtime(true); sleep(1); // Your code here $time = hrtime(true) - $start; echo round($time / 1_000 / 1_000, 2), " ms", PHP_EOL; // 1005.08 ms -
Hyperfine My command-line benchmarking tool of choice Focus on human
readable output Great visualization of error bars and uncertainty Clearly communicates results Takes care of the hard parts of measuring Repeated runs Options for warmups Outlier detection https://github.com/sharkdp/hyperfine -
Comparing PHP versions hyperfine -L version 8.3,8.4 \ '/opt/php-{version}/sapi/cli/php -d
opcache.enable_cli=1 \ bench-minimal.php' $i = 10_000_000; while($i--) sprintf('last_ts_%s_%s', 'abc', 1); -
Comparing code changes hyperfine -L mode with_backslash,without_backslash \ '/opt/php-8.4/sapi/cli/php -d
opcache.enable_cli=1 \ bench.php {mode}' -
Comparing variants Hyperfine just runs a binary A simplified way
to test subsequent web requests: Load testing needs different tools! hyperfine -L url /variantA,/variantB \ 'curl -H "Content-Type: application/json" \ https://localhost/api/test{url}' -
Considerations Finding performance bottlenecks can be challenging Intuition, good tools
and lots of data can help Take a scientific and diligent approach Confirmation bias is hard to overcome -
Consider: OPcache Before PHP 8.5, it's possible the OPcache extension
isn’t loaded PHP will silently ignore the OPcache ini settings, invalidating the results php -m | grep -i opcache extension_loaded('zend opcache') || throw new \Exception('Missing OPcache'); ini_get('opcache.enabled') || throw new \Exception('OPcache not enabled'); ini_get('opcache.enabled_cli') || throw new \Exception('OPcache not enabled for CLI'); -
Consider: Caches Development setups usually have different cache settings than
production Warm or pregenerate your caches ensure they're used -
Consider: CPU turbo Most modern CPUs have a turbo for
the first X seconds they run, or until a temperature limit is reached So your first test might be faster than subsequent ones A --warmup might help -
Consider: PHP Process startup times PHP takes a while to
start up 40ms just to start up If your test only takes 100ms isolating changes is very hard and distorted root@ubuntu-4gb-nbg1-6:~ time php -r 'echo 1;' 1 real 0m0.038s user 0m0.015s sys 0m0.023s -
Sidenote: Why? sys time: root@ubuntu-4gb-nbg1-6:~ strace -c php -r 'echo
1;' 1% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 43.96 0.011745 21 545 3 newfstatat 14.99 0.004004 22 176 mmap 6.95 0.001857 20 91 5 openat 5.92 0.001581 16 97 read 5.00 0.001336 11 113 fstat 4.33 0.001156 13 86 close 3.63 0.000970 28 34 getdents64 3.22 0.000861 11 78 rt_sigaction 3.15 0.000841 21 40 mprotect 2.69 0.000719 23 31 munmap 2.57 0.000687 687 1 execve 1.04 0.000278 12 22 futex 0.92 0.000245 12 20 20 ioctl ------ ----------- ----------- --------- --------- ---------------- 100.00 0.026716 19 1379 29 total -
Sidenote: Other languages Is that a lot? Let's compare: Seems
reasonable root@ubuntu-4gb-nbg1-6:~ time python3 -c "print(1);" 1 real 0m0.050s user 0m0.040s sys 0m0.010s root@ubuntu-4gb-nbg1-6:~ time ruby -e "puts 1" 1 real 0m0.147s user 0m0.108s sys 0m0.039s -
Finding bottlenecks Profiling The two main approaches: Record every function
call: Possibly quite expensive and distorting, but the full picture Sampling: Look 100 or 1000 times per second at a process and see what it does at that moment -
Slow first test final class FooBarTest extends TestCase { public
static function provideTest() { yield 'test-case #1' => ['a']; yield 'test-case #2' => ['b']; yield 'test-case #3' => ['c']; } #[DataProvider('provideTest')] public function testTrue(): void { self::assertTrue(true); } } <testsuite name="FooBarTest::testTrue" [...] time="0.452475"> <testcase name="test-case #1" [...] time="0.445116"/> <testcase name="test-case #2" [...] time="0.003829"/> <testcase name="test-case #3" [...] time="0.003530"/> </testsuite> -
Narrowing down the issue Timing in Framework/TestCase --- a/src/Framework/TestCase.php (revision
ee4b9460f812c121abbed6c0 +++ b/src/Framework/TestCase.php (date 1745665479506) @@ -1270,10 +1270,15 @@ $capture = tmpfile(); $errorLogPrevious = ini_set('error_log', stream_get_meta_data($ca try { /** @phpstan-ignore method.dynamicName */ + var_dump("start"); + $start = hrtime(true); $testResult = $this->{$this->methodName}(...$testArguments); - + var_dump(hrtime(true) - $start); $errorLogOutput = stream_get_contents($capture); -
Validating ideas? Is it autoloading? --- a/src/Framework/TestCase.php (revision ee4b9460f812c121abbed6c0 +++
b/src/Framework/TestCase.php (date 1745665479506) @@ -1270,10 +1270,15 @@ $capture = tmpfile(); $errorLogPrevious = ini_set('error_log', stream_get_meta_data($ca + $x = \PHPUnit\Framework\Assert::isTrue(); + Success::success(); + try { /** @phpstan-ignore method.dynamicName */ + var_dump("start"); + $start = hrtime(true); $testResult = $this->{$this->methodName}(...$testArguments); - + var_dump(hrtime(true) - $start); $errorLogOutput = stream_get_contents($capture); -
Profilers Open Source: php-spx, Excimer, Xdebug, XHProf, php-profiler, ... Commercial:
Tideways, Sentry, Datadog, Blackfire, New Relic, Nightwatch, AppSignal https://tideways.com/the-6-best-php-profilers -
php-spx Simple Profiling eXtension Open Source Runs in your infra
Builtin web ui https://github.com/NoiseByNorthwest/php-spx -
SPX demos - Web UI SPX_ENABLED=1 SPX_BUILTINS=1 SPX_REPORT=full \ php
./phpunit --group small -
Summing up Try a profiler and look into a web
request of yours How much time do you spend in the DB, in caches, in PHP? What's the bottleneck? Make a change and benchmark the fix Confirm that it's faster What's the slowest /route/ in your system in total time spent? Route/Controller/Action -
Thank you I sometimes talk PHP on Mastodon @edorian.phpc.social Or
find me in the phpc Discord https://phpc.social/@edorian