Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks
#laravel #performance #profiling #blackfire #xdebug

Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks

3 min read Mohamed Said Mohamed Said

Why Guessing Is Expensive

Most performance work starts with a hunch: "It must be the N+1 query" or "The cache is probably cold." Hunches waste hours. Profilers give you a call graph with wall-clock time, CPU time, memory delta, and I/O — so you fix the right thing first.

This article covers two complementary tools:

  • Xdebug — free, always available, great for local deep-dives with a GUI like PHPStorm or KCachegrind.
  • Blackfire — commercial, CI-friendly, built for continuous performance testing with assertions.

Xdebug: Callgrind Profiles Locally

Install Xdebug 3 and add to php.ini:

[xdebug]
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
xdebug.profiler_output_name=cachegrind.out.%p.%r

Trigger a profile for a single request by appending ?XDEBUG_PROFILE=1 or setting the cookie. For a CLI job:

XDEBUG_MODE=profile php artisan queue:work --once

Open the resulting cachegrind.out.* file in KCachegrind (Linux) or QCachegrind (macOS). Sort by Self Cost to find functions that consume time without delegating — these are your actual hot paths, not just callers.

Reading the Call Graph

A common surprise: PDOStatement::execute shows up with 40 % self cost because a Blade partial triggers 60 lazy-loaded relations. The call graph makes the chain obvious:

View::render
  └─ BlogPost::author()      ← repeated 50×
       └─ PDOStatement::execute

Fix it with with('author') on the controller query, re-profile, confirm the cost drops.


Blackfire: Continuous Performance Assertions

Blackfire's killer feature is not the flame graph — it's assertions in CI. You write a .blackfire.yaml at the project root:

tests:
  "Homepage loads fast":
    path: /
    assertions:
      - "main.wall_time < 200ms"
      - "metrics.sql.queries.count < 10"
      - "metrics.http.requests.count == 1"

Push to a branch, run blackfire run php artisan blackfire:test (or the GitHub Action), and the build fails if a regression sneaks in. This is performance-as-code.

Profiling a Specific Artisan Command

blackfire run php artisan import:products --limit=100

Blackfire uploads the trace to its dashboard. Filter by exclusive time to find the single most expensive function call. A real example: json_decode inside a loop consuming 18 % of wall time because a 2 MB payload was decoded once per row instead of once per batch.

Instrumenting Custom Code

For long jobs, add manual probes so Blackfire can segment the timeline:

use Blackfire\Client;
use Blackfire\Profile\Configuration;

$blackfire = new Client();
$probe = $blackfire->createProbe((new Configuration())->setTitle('Batch import'));

foreach ($chunks as $chunk) {
    $this->processChunk($chunk);
}

$blackfire->endProbe($probe);

This gives you a named segment in the timeline rather than one undifferentiated blob.


Combining Both Tools Effectively

| Scenario | Tool | |---|---| | Local deep-dive, no account needed | Xdebug + KCachegrind | | CI regression gate | Blackfire assertions | | Profiling a queue job in staging | Blackfire run | | Memory leak hunt | Xdebug memory snapshots | | Comparing two implementations | Blackfire comparison view |

A practical workflow: use Xdebug locally to understand what is slow, fix it, then write a Blackfire assertion so it never regresses.


Takeaways

  • Never optimise without a profiler — wall-clock logs lie about where time is actually spent.
  • Xdebug callgrind + KCachegrind is zero-cost and reveals the full PHP call graph.
  • Blackfire assertions in CI make performance a first-class build constraint.
  • Sort by exclusive/self time, not inclusive, to find the real culprit.
  • Instrument long-running jobs with Blackfire probes to segment the timeline.
  • Fix one bottleneck at a time and re-profile; compound fixes mask each other's impact.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use Xdebug profiling in production?
Avoid it. Xdebug profiling adds significant overhead (often 2–5×) and writes large files to disk. Use it locally or in a dedicated staging environment. For production profiling, Blackfire's agent has much lower overhead and is designed for live traffic sampling.
Q02 How do Blackfire assertions differ from just checking response time in a test?
Blackfire assertions inspect the internal call graph — SQL query count, HTTP sub-requests, memory allocations, and specific function call counts — not just the final response time. This means you can catch a regression like an extra 20 queries even if the total wall time stays under your threshold due to a fast database in CI.
Q03 Does Xdebug profiling work with Laravel Octane?
Partially. Because Octane reuses worker processes, Xdebug's per-request profiler output can mix traces across requests. The safest approach is to profile with Octane disabled locally, or use Blackfire which has explicit Octane support via its agent.

Continue reading

More Articles

View all