lox/xhprof
XHProf profiling for Laravel. Easily profile requests and inspect performance data with middleware and configuration for enabling or disabling profiling in your application.
lox/xhprof is a Composer-packaged fork of Facebook's XHProf: the Composer side ships the userland library (xhprof_lib, xhprof_html), but the actual profiler is still a PHP extension (xhprof.so) that Composer cannot install for you. Plan for two steps, not one.
php -m | grep xhprof. If it's missing, you must build/install it (PECL or from source). Because this fork is archived, expect to be on an older PHP (7.x); on PHP 8 you'll almost certainly need tideways/xhprof or a maintained fork instead.composer require --dev lox/xhprof
php.ini (or verify it's writable at bootstrap):xhprof.output_dir = /tmp/xhprof
if (extension_loaded('xhprof')) {
xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY);
register_shutdown_function(function () {
$data = xhprof_disable();
$dir = ini_get('xhprof.output_dir') ?: sys_get_temp_dir();
$file = sprintf('%s/%s.%d.xhprof', rtrim($dir, '/'), uniqid('req', true), getmypid());
file_put_contents($file, serialize($data));
});
}
php -S 127.0.0.1:8080 -t vendor/lox/xhprof/xhprof_html
# then open http://127.0.0.1:8080/index.php?run=<run-id>&source=your-app
That UI is where you'll spend your time: flat profile, inclusive vs. exclusive wall time, call counts, memory, and the callgraph view.
Gate profiling behind an explicit switch. The classic pattern is a query param or env var checked before the framework bootstraps, so you capture the full request rather than just the controller:
// public/index.php, very top
if (getenv('XHPROF') || isset($_GET['_profile'])) {
xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY);
}
Wrap it in one service, not scattered calls. A small Profiler class that owns enable(), disable(), run naming, and persistence keeps you from double-enabling (XHProf warns and refuses) and gives you a single place to add tags like route, user_id, or git sha into the filename. Metadata in filenames makes the UI's run list navigable when you have hundreds of runs.
Profile CLI commands and queue jobs separately. XHProf is request-agnostic; wrapping artisan commands gives you the clearest signal for batch jobs and imports. For queue workers, enable/disable per job and persist at the end — do not leave it enabled across the worker loop.
Use sampling mode for anything long-running or shared. xhprof_sample_enable() / xhprof_sample_disable() gives you a statistical profile with far lower overhead and no per-function call counts. That's the mode to consider on staging, never the full flags.
Compare before/after with the diff view. This is the highest-leverage feature day-to-day: run the endpoint, apply an optimization, run it again, then open callgraph.php?run1=...&run2=... (or the diff link in the UI). Absolute time rarely moves the needle; the diff view shows whether your change actually shifted the hot path or just moved it.
Prune aggressively. Nothing deletes .xhprof files for you. A nightly find /tmp/xhprof -mtime +7 -delete, or a cap on file count, prevents a directory full of serialized arrays.
Framework integration is intentionally minimal. There's no service provider to register. The integration surface is: enable early, disable late, write a file, read it in the bundled UI. Resist the urge to wire it into production observability — it's a local optimization tool.
lox/xhprof is unmaintained. It will not build against modern PHP, and XHProf's internals (function name handling, zend_execute hooks) are exactly the kind of thing PHP minor releases break. Pin it, fork it internally if you must, and have tideways/xhprof, the Xdebug profiler, or SPX on your migration list. Do not build new workflow around it.composer require lox/xhprof succeeds, then every call to xhprof_enable() fatals because the .so isn't loaded. Check extension_loaded('xhprof') before every call site.xhprof.output_dir must exist and be writable by the FPM/CLI user. The default is /tmp; in containers that's often ephemeral and disappears on restart. Set it in php.ini rather than ini_set() at runtime if you hit permission or timing surprises.dot binary must be on PATH; without it the callgraph page renders broken images. This trips people up in slim Docker images.App\Service\Foo::bar), and very long names can be truncated. Grep the serialized data if a function seems to be missing.strlen, preg_match, PDO::query etc. mixed in. That's useful, but it means "top function" is often a builtin — read the whole call path, not the first row.output_dir and is a directory-listing-ish surface. Bind it to localhost or put it behind your existing dev-only auth; never expose it on a shared host.How can I help you explore Laravel packages today?