Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Xhprof Laravel Package

laracraft-tech/laravel-xhprof

Laravel XHProf is an easy profiling package that helps you find performance bottlenecks in your Laravel apps. Enable it, browse your app, then inspect detailed XHProf or Tideways call graphs through a built-in web UI to see exactly where time is spent.

View on GitHub
Deep Wiki
Context7

Getting Started

Step 0 — verify the extension before anything else. This is the single step that decides whether the package is viable for you:

php -m | grep -Ei 'xhprof|tideways'
# and again with the CLI binary that actually runs your queue workers:
php -i | grep -i 'xhprof\|tideways'

You need it enabled in both the web SAPI (php-fpm/mod_php) and CLI. If the extension isn't present and you don't control the base image, stop here — the package will install cleanly and profile nothing.

Step 1 — install and scaffold.

composer require laracraft-tech/laravel-xhprof
php artisan vendor:publish --provider="LaracraftTech\LaravelXhprof\LaravelXhprofServiceProvider"
php artisan migrate

Open the published config/xhprof.php first — that's where enabled, sample rate, storage driver, output path, and skip rules live. Confirm the exact key names against your installed version rather than trusting a blog post; several keys have been renamed across 1.x.

Step 2 — register the middleware, early. In bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php (10), put it at the top of the global stack:

->withMiddleware(function (Middleware $middleware) {
    $middleware->prepend(\LaracraftTech\LaravelXhprof\Middleware\XhprofMiddleware::class);
})

Prepending matters: a profiler registered after StartSession/SubstituteBindings will attribute boot and routing cost to the wrong bucket.

Step 3 — gate it to non-production.

XHPROF_ENABLED=true
XHPROF_SAMPLE_RATE=1

Set XHPROF_ENABLED=false (or simply don't set it) in .env.production. Never rely on "we'll remember to turn it off."

Step 4 — first use case: profile one known-slow route. Hit your slowest real endpoint once in local, then open the bundled viewer (or query the runs table directly) and sort by exclusive wall time. You're looking for the top three functions by self-time, not the biggest blobs in the tree. This one exercise usually settles the "is it the database or is it us?" question in under ten minutes.

Acceptance test for the setup: a profile row must exist after (a) an HTTP request and (b) an Artisan command. If either is missing, you have a silent no-op, not a working profiler.

Implementation Patterns

On-demand profiling in staging via request toggle. Rather than sampling everything, leave the global middleware disabled and enable per-request with the query parameter or header the package supports:

curl -H 'X-Xhprof-Enabled: 1' https://staging.example.com/checkout
# or: https://staging.example.com/checkout?xhprof=1

This is the pattern that keeps staging usable for the rest of the team — no table growth, no overhead on everyone else's traffic, and profiling becomes a deliberate act with an owner.

Skip noise with URL needles (v1.3.0+). Health checks, webhooks, Horizon/Telescope dashboards, and admin panels otherwise dominate your runs table. Add needles for those paths in the config's skip list; matching is substring-based, so a short needle like horizon covers the whole namespace.

Cover the non-HTTP half. In most Laravel apps the real cost is imports, nightly batches, and notification fan-out. Register the package's job/command listeners (check the published config and provider for whether they're automatic or opt-in in your version), then run one representative job locally and confirm a row lands. This is where you get the answers query-log tooling can't give you: serialization cost, framework boot overhead, container resolution.

Storage: database vs disk. Database is the default and is convenient for browsing, but it creates a retention obligation and a blob column. For high-volume staging, switch to disk storage in config and put the output directory under storage/, then treat the directory like log files — rotate or wipe.

Prune before you scale up:

// App\Console\Kernel schedule
$schedule->call(fn () => DB::table('xhprof_runs')
    ->where('created_at', '<', now()->subDays(7))->delete())->daily();

Before/after regression triage. When a deploy "feels slower," profile the same route on the previous tag and the new one with the same seeded dataset and the same sample rate. Compare top exclusive-time functions, not overall wall time — total time drifts with data volume and machine load, but the shape of the call tree is stable. That comparison is the artifact you attach to the incident.

Performance assertions in CI. The package is callable from code, which is what turns an NFR into a merge gate. Wrap the hot path in a test:

// Verify the exact start/stop API against your installed version first.
$profiler->start();
$this->post('/import', $payload);
$profile = $profiler->stop();
$this->assertLessThan(300, $profile->wallTime());

Keep these assertions on a dedicated, seeded fixture set and run them on a fixed machine — wall-clock assertions on shared CI runners flake.

Use events as release markers. The ProfileSaved event is the natural hook for annotating runs with a git SHA, environment, or incident ID, and for shipping profiles to cold storage without forking the package. A one-line listener that writes the commit hash into the run record makes "which deploy changed this?" a query instead of a bisect.

Reading profiles properly. Sort by exclusive time to find offenders; use inclusive time to confirm a parent frame is the aggregate of its children. Always check call count alongside time — an N+1 shows up as a function called 400 times with tiny individual cost, which no wall-time ranking surfaces on its own.

Gotchas and Tips

Silent no-op is the default failure mode. Nothing errors when xhprof_enable() never fires. Bake the two-part acceptance test (request + command produces a row) into your pilot, or you'll "profile" for a week and conclude the app is fine.

The CLI SAPI is the one people forget. Web works, workers don't, and the failure looks like a package bug. Check php -m with the same binary your queue supervisor invokes — not the one in your shell alias.

Middleware ordering. Prepending is correct; appending silently misattributes routing, session, and container-boot time. Sanity-check by profiling a route you already know is slow — if the obvious culprit doesn't appear near the top, your ordering is wrong.

Long-running workers leak profiler state. In a Horizon/queue-worker context, an exception mid-job can leave the profiler running into the next job, producing corrupted or merged profiles. Ensure stop() is in a finally, and consider profiling only specific jobs rather than all of them via the listener.

Schema drift on upgrade. v1.0.10 made id the primary key and dropped idcount; v1.0.6 fixed MySQL blob length. If you're adopting onto an older database, publish the current migration and migrate fresh rather than trusting an old xhprof_runs table. Expect the upgrade to be a small manual step, not a composer update.

Overhead is real, sampling is the lever. At XHPROF_SAMPLE_RATE=1 you're paying for every request. On staging under load testing, drop to a low rate or switch to on-demand only — otherwise the profiler becomes part of what you're measuring.

Pin the version and vendor it. ~235 stars, effectively zero dependents, maintenance driven by Laravel-major compatibility PRs. "laracraft-tech/laravel-xhprof": "1.3.*" in composer.json, and keep a fork as a known one-day fallback. This is fine for internal tooling; don't let it become load-bearing infrastructure.

Permissions bite on the disk driver. The output directory is written by whichever user runs php-fpm and the queue worker; they're often different. chown/chmod both, or you'll get empty profiles from cron only.

Extension version matters for Tideways users. If you're on tideways_xhprof rather than xhprof, set the driver/extension in config explicitly and confirm the package version supports it — the function names differ (tideways_xhprof_enable vs xhprof_enable).

No APM, and don't let it drift into one. No RUM, no distributed tracing, no trends, no alerting. Enable → profile → fix → retire, then turn it off. If the team starts asking for week-over-week graphs, that's the signal to scope a real observability purchase, not to grow this table.

Profile a fixed dataset. Comparing two runs with different record counts produces a confident, wrong conclusion. Seed a deterministic fixture (e.g. 1,000 orders) and re-use it for every before/after.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
adachsoft/cli-process-tester
adachsoft/changelog-linter
adachsoft/collection
adachsoft/php-code-style
adachsoft/api-version-bundle
cgv/localized-resource