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

PHP Performance Demystified

PHP Performance Demystified

Avatar for Volker Dusch

Volker Dusch

October 28, 2025
Tweet

More Decks by Volker Dusch

Other Decks in Technology

Transcript

  1. 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
  2. Intro Performance is a very wide topic From Database Tuning

    over Premature Optimization to Distributed Systems and Observability
  3. Today My goals for today. I want to help you:

    build a mental model on how PHP works measure more correctly by informing your performance reasoning find bottlenecks
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. OPcache OPcache stores compiled scripts in SHM SHared Memory, e.g.

    php-fpm master process Without OPcache the PHP Optimizer is not enabled. It's also where the JIT lives.
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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) }
  16. 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.
  17. 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
  18. 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
  19. Benchmark - PHP 8.4 $a = "$foo-$bar-$baz"; # Fastest $b

    = $foo . "-" . $bar . "-" . $baz; # Slower $c = sprintf("%s-%s-%s", $foo, $bar, $baz); # Fastest
  20. Explanation How did we measure this? How to apply our

    understanding on how PHP works? What changed in 8.4?
  21. 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
  22. 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
  23. 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
  24. 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
  25. So what changed in 8.4? Let's take a look at

    the \sprintf OPcodes with 8.4
  26. 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
  27. 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";
  28. 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
  29. Takeaways PHP gets faster and more optimized with each release

    Write maintainable code and let PHP do the hard work If something in the engine is a bottleneck, you can investigate https://phpc.social/@pollita/114522260863354986 https://github.com/php/php-src/pull/18571
  30. 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
  31. 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
  32. Benchmarking There are a lot of ways to measure runtime

    Different abstraction layers offer different insights
  33. 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
  34. 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
  35. 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);
  36. Comparing 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}'
  37. Considerations Finding performance bottlenecks needs intuition, or good tools and

    data Benchmarking is more science and diligence. Confirmation bias is hard to overcome
  38. 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');
  39. Consider: Caches Development setups usually have different cache settings than

    production Warm or pregenerate your caches ensure they're used
  40. 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
  41. 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
  42. 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
  43. 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
  44. Finding bottlenecks - Profiling 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
  45. 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>
  46. 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);
  47. 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);
  48. 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
  49. php-spx Simple Profiling eXtension Open Source Runs in your infra

    Builtin web ui https://github.com/NoiseByNorthwest/php-spx
  50. 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
  51. Thank you Slides will be at I sometimes talk PHP

    on Mastodon @edorian.phpc.social https://speakerdeck.com/edorian https://phpc.social/@edorian