sairahcaz/laravel-xhprof
A Laravel integration for Facebook's XHProf profiler that captures detailed request performance data. With minimal setup, enable profiling in your app, collect reports, and review them to identify bottlenecks and optimize slow endpoints. Useful for development and performance tuning.
laracraft-tech/laravel-xhprof wraps the XHProf (or Tideways XHProf) PHP extension so you can capture a function-level call graph for any Laravel request without hand-rolling xhprof_enable() calls.
1. Install the extension first — it is the real prerequisite.
pecl install xhprof # or tideways_xhprof
# then add extension=xhprof.so to your php.ini
php -m | grep -i xhprof # verify before touching composer
Herd/Valet/Docker users: this must be installed in the PHP-FPM container/SAPI, not just the CLI, or you will silently get zero profiles.
2. Install the package as a dev dependency.
composer require --dev laracraft-tech/laravel-xhprof
php artisan vendor:publish --tag=xhprof
3. Where to look first. Open config/xhprof.php and read it top to bottom before enabling anything. It holds the enable flag, the sample rate, the output directory (typically storage/app/xhprof), the server name used to tag runs, and the list of URLs to skip. The middleware class and the profiler service are the only two other files worth reading — the package is small.
4. Turn it on for one local request.
XHPROF_ENABLED=true
Hit a slow endpoint, then look in storage/app/xhprof for a new run file. Open it with an XHProf HTML UI, convert it to Speedscope JSON for a flame graph, or export as callgrind for QCachegrind.
The first use case is always the same: you have one endpoint that takes 800ms and no obvious culprit. Profile it once, sort the viewer by exclusive wall time, and you will usually find the answer in under a minute — an N+1 query, a synchronous HTTP call, or a collect() loop.
Scope profiling to a route group, not the whole app. Rather than registering the middleware globally, attach it to the specific routes you are investigating. This keeps overhead out of every other request and makes the output directory far easier to read.
Route::middleware(['xhprof'])->group(function () {
Route::get('/reports/export', ReportExportController::class);
});
Use the new needle-based skip list (v1.3.0) aggressively. PR #22 lets you skip URLs by substring needle. Add this to your published config for anything noisy:
'skip' => [
'_debugbar',
'telescope',
'horizon',
'livewire',
'/health',
'/up',
'webhooks/',
],
Skips are evaluated before profiling starts, so they save real CPU rather than just filtering output afterwards. This is the single highest-value config in the file.
Sampled production profiling. Set the sample rate low (a fraction of a percent to a few percent) in staging, pair it with server_name set per-host (e.g. staging-web-01) so you can tell runs apart, and leave it off entirely in production unless you have a disk-cleanup story. A handful of sampled runs from real traffic will find problems no local reproduction can.
Before/after refactor diffing. Profile the endpoint, save the run with a descriptive name, refactor, profile again, then compare in the viewer side by side. This is far more convincing in a code review than "it feels faster" — you can point at the exact function whose inclusive wall time dropped.
Targeted wrapping. For one-off investigations, the package's underlying profiler can be started and stopped around a specific block of code rather than the whole request. Use this when the interesting work happens in a queued job or an artisan command, both of which are invisible to HTTP middleware.
Reading the output. Sort by exclusive wall time to find the actual hot function; sort by inclusive to understand the call tree. Wall time includes I/O waits (DB, Redis, HTTP) — CPU time does not. When those two diverge wildly, you are looking at I/O, not at slow PHP.
XHPROF_ENABLED and config:cache. Once you run php artisan config:cache, your .env value is baked into the cached config. Flipping the env var afterwards does nothing until you re-cache. This burns everybody at least once — it is the number one reason "it isn't generating files."
Enable flag plus missing extension equals silence. If the extension is absent, you typically get no profiles and no loud error. If the directory stays empty, check php -m in the correct SAPI before debugging the package.
Sample rate semantics. Read the comment in the config file carefully: depending on version it is expressed as a divisor (1-in-N) rather than a percentage. Setting 1 expecting "1%" and getting "every request" is a memorable mistake on a busy staging box.
Needles are substrings, not regex and not globs. 'admin' will also skip /blog/admin-panel and /api/administrator. Order does not matter (first match wins, all matches skip), but specificity does — prefer /admin/ and _debugbar style needles with delimiters.
Output files accumulate forever. There is no automatic retention. A sampled production setup will happily fill storage/app/xhprof until the disk is full. Add a scheduled find ... -mtime +7 -delete or equivalent from day one.
Every profile includes framework boot. A meaningful fraction of each run is container resolution, service provider registration, and router matching. Filter the viewer down to your own namespace before drawing conclusions, or you will "optimise" the framework.
Octane and long-running workers. With Octane, config is loaded once per worker; a change to the enable flag or skip list needs a worker reload. Also verify that the profiler's start/stop actually brackets a single request rather than leaking state across requests in a resident worker.
Overhead is real and non-trivial. XHProf instruments every function call. Expect meaningful slowdown with XHPROF_ENABLED=true and a high sample rate — never benchmark response times with profiling active, and never A/B a performance change with the profiler running on one side.
On the v1.3.0 upgrade: both changes are additive — a new skip-needle option and Laravel 13.x compatibility. No breaking changes, no config keys removed, so composer update is safe. If you are on Laravel 13, this is the release that unblocks you; if you are on an older Laravel, there is nothing to do.
How can I help you explore Laravel packages today?