Skip to Content
Course content

144: Profiling PHP with Xdebug

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably been there: a page that used to load in 200ms is suddenly taking three seconds. Your first instinct is likely to start guessing. You think, "Maybe it's the database query in the header," or "Maybe that new loop in the service layer is the culprit." When we're in this mindset, we tend to treat performance tuning like a game of Minesweeper, clicking around the code hoping to find the bomb.

The 'Guess and Check' Timer Method

The most common naive approach I see is peppering the code with microtime(true) calls. You drop a timer at the start of a controller, another one after the service call, and another after the template renders. You dump the differences to the log and say, "Okay, the service layer took 2.1 seconds, so the problem is in there."

$start = microtime(true);
$this->reportService->generate();
$end = microtime(true);
error_log("Report took: " . ($end - $start) . " seconds");

I'll be honest—I did this for years. It feels productive because you're getting numbers, but it's actually a blunt instrument. This method only tells you which block of code is slow, not why it's slow. If generate() is slow, is it because of one massive SQL query? Is it because it's calling a small helper function 10,000 times in a loop? Is it a slow preg_replace_callback? You're still guessing; you've just narrowed the search area. Plus, you're polluting your codebase with timing logic that you'll inevitably forget to remove before your next commit.

Letting Xdebug Map the Entire Call Stack

The professional way to handle this is through profiling. Instead of measuring a few specific points, we let Xdebug record every single function call, how long each took, and how many times it was invoked. This produces a "cachegrind" file—a dense map of your application's execution path.

To do this, you don't change a single line of your PHP code. Instead, you configure your php.ini to enable the profiler. I usually set xdebug.mode=profile and xdebug.start_with_request=trigger. Using the "trigger" option is key; it means Xdebug won't profile every single request (which would kill your machine), but only the ones where you specifically ask for it—usually by adding XDEBUG_PROFILE to your browser's cookies or using a browser extension.

Once you run the slow page, Xdebug drops a file into your xdebug.output_dir. Now, you don't read this file in a text editor. You open it in a tool like WebGrind or QCacheGrind. Suddenly, you aren't guessing anymore. You can see a call graph that shows you exactly where the time is leaking. You might discover that your "fast" helper function is actually taking 0.001ms, but because it's being called 50,000 times inside a loop, it's consuming 50ms of your budget. That's a discovery you'd almost never make with manual timers.

The Cost of Total Visibility

Now, there's a trade-off here that we need to be clear about: profiling is expensive. When Xdebug records every function call, it adds massive overhead to the execution. Your three-second page might take fifteen seconds to load while the profiler is running. This is why you never leave profiling enabled in a production environment. If you do, your server will likely crawl to a halt under the weight of writing those massive profile files to disk.

The trade-off is essentially "Execution Speed vs. Insight." When you're in the "Guess and Check" phase, you have execution speed but zero insight. When you're profiling, you sacrifice speed to get absolute clarity. In my experience, spending ten minutes setting up a profile is always faster than spending three hours hunting for a bottleneck by trial and error.




📋 Practical Task

Hunting the Bottleneck in the Invoice Generator

You have been handed a legacy InvoiceGenerator class that is performing terribly. The script processes a list of 500 invoices, calculating taxes and formatting currency for each line item. To the naked eye, the code looks fine, but the page load is unacceptable.

Your Goal: Use Xdebug profiling to identify the specific function causing the slowdown.

  1. Enable xdebug.mode=profile in your local PHP environment.
  2. Run the provided generate_invoices.php script with the Xdebug profile trigger enabled.
  3. Open the resulting cachegrind.out.* file in WebGrind or a similar analysis tool.
  4. Identify which function has the highest "Self Cost" (the time spent inside the function itself, not including its children).
  5. Optimize that specific function (e.g., moving a heavy calculation outside of a loop or replacing a slow regex) and run the profiler again to verify the "Self Cost" has dropped.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.