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

Xh Tool Laravel Package

phperf/xh-tool

phperf/xh-tool provides a command-line interface for XHProf profiling in PHP, making it easier to start profiles, save run data, and generate readable performance reports. Useful for diagnosing slow requests and identifying hot functions.

View on GitHub
Deep Wiki
Context7

Getting Started

phperf/xh-tool is not a profiler — it is a terminal viewer for run data produced by the xhprof extension. You can't "install it and get profiles"; the value only appears once you already have a pipeline that writes xhprof run files to disk. Plan for two installs, not one.

1. Get the extension producing data

pecl install xhprof          # or build github.com/phperf/xhprof from source
# php.ini
extension=xhprof.so
xhprof.output_dir=/tmp/xhprof

2. Capture a run — the smallest thing that works, e.g. an Artisan command you can point at any code path:

// app/Console/Commands/Profile.php  (skeleton)
xhprof_enable(XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS);

try {
    // the code you want to measure, e.g. Artisan::call($this->argument('cmd'))
} finally {
    $data = xhprof_disable();
    $dir  = storage_path('app/xhprof');
    @mkdir($dir, 0777, true);
    $file = $dir . '/' . getmypid() . '.' . uniqid('', true);
    file_put_contents($file, serialize($data));
    $this->line($file);
}

3. Install and view

composer global require phperf/xh-tool
xh-tool --help
xh-tool /path/to/storage/app/xhprof/<runfile>

Start with --help before anything else: the package is a single-purpose CLI, the README is thin, and the source under vendor/phperf/xh-tool/src is small enough to skim in five minutes. The flags you care about first are the ones selecting which column to sort by (wall time vs. CPU vs. memory) and a minimum-cost threshold, because an unfiltered tree of a Laravel request is hundreds of rows of framework internals.

First real use case: take your slowest endpoint or job, wrap it as above, dump one run, and read the tree from main() downward looking for the first node whose exclusive (self) cost is disproportionate to its inclusive cost. That's your hot spot. Don't bother wiring this into CI or production until you've done that loop by hand three or four times.

Implementation Patterns

Wrap capture in a reusable toggle, not in your controllers. A tiny service with enable()/disable(): string plus an env gate keeps profiling code out of the hot path:

if (config('profiling.enabled') && $request->hasHeader('X-Profile')) { ... }

Never enable it unconditionally — an accidental xhprof_enable() in production without a sampling strategy is a self-inflicted incident.

Keep run files timestamped and greppable. xh-tool doesn't manage history for you; ls -t storage/app/xhprof | head is the "recent runs" list. Prefix filenames with the route or command name (checkout.submit_<uniqid>) so you can find a run a week later without a database.

The before/after loop is the whole workflow. Profile → change one thing → profile again → compare the two trees. Because xhprof aggregates by caller→callee pair, the diff is usually obvious: one node's inclusive cost collapses while total drops by roughly the same amount. Beware comparing two runs with different workloads (cache cold vs. warm, different fixture data) — that produces more noise than most optimizations remove.

Read exclusive vs. inclusive correctly. Inclusive = function + everything it calls; exclusive/self = only the function's own body. A high inclusive with low exclusive means the cost lives in a child — keep descending. High exclusive on a leaf is where you actually optimize. main()'s inclusive cost is the wall time of the whole run, so it's only meaningful as a relative baseline.

Always pass the right flags or your columns are lies.

Flag Effect
XHPROF_FLAGS_CPU populates CPU columns (getrusage)
XHPROF_FLAGS_MEMORY populates memory columns (peak/alloc)
XHPROF_FLAGS_NO_BUILTINS drops strlen, preg_match, etc. — huge noise reduction
XHPROF_FLAGS_BUILTINS keep internals (use only when a builtin is the suspect, e.g. preg_* or serialize)

Integrate at the boundaries you already have. Eloquent observers, queue job middleware, and an Artisan command wrapping Artisan::call() give you profiling coverage for ~90% of real investigations without touching domain code. For HTTP, a middleware behind an env flag is enough — you don't need the xhgui web UI for a quick look, and that's precisely the niche xh-tool fills.

Escape hatch when the CLI fights you. If a run won't display, unserialize(file_get_contents($f)) in php -r and inspect the array directly — the format is ['caller==>callee' => ['ct'=>n,'wt'=>µs,'cpu'=>µs,'mu'=>bytes,'pmu'=>bytes], ...]. Half of "the tool is broken" turns out to be "the run file is empty" or "the extension was never loaded."

Pair it with xhgui only if you need history. xh-tool is a point-in-time inspector; if you find yourself wanting trend lines or a shared URL for the team, you've outgrown it and should switch the storage layer to xhgui rather than extending this tool.

Gotchas and Tips

It has been unmaintained since March 2018. Four stars, zero dependents, no issue triage. That's not automatically disqualifying for a local dev utility, but it means: no PHP 8 compatibility work, and its symfony/console constraint will likely conflict with any modern Laravel app. Install it globally or in a throwaway directory; do not add it to a project's require-dev and let Composer resolver chaos into your lock file. If install fails on PHP 8.x, run it under a PHP 7.4 Docker image rather than adding --ignore-platform-reqs to your real project.

The 2018 ecosystem matters for the data format too. xh-tool expects the classic xhprof array shape (main() as the root, caller==>callee keys). Profiles produced by forks — tideways_xhprof, upscaling/xhprof, or the {main}/main() root naming differences between versions — may not render. If the tree is empty or truncated, suspect a format mismatch before you suspect the tool. Sticking with phperf/xhprof end-to-end is the path of least resistance.

Zeros in the CPU or memory columns are a flag problem, not a tool bug. This is the single most common confusion: people enable with no flags and then wonder why everything shows 0 for CPU. Check the xhprof_enable() call first, always.

Sampling mode changes the numbers. Clock-sampled runs (via xhprof_sample_enable()) have much lower overhead but produce approximate, statistically noisy trees. Use full xhprof_enable() when you need exact call counts; use sampling when the overhead of profiling would distort the thing you're measuring.

Profiling changes what you measure. The extension adds per-call overhead, which disproportionately inflates functions that are called millions of times and are individually trivial. A function showing 30% CPU may show 15% uninstrumented. Optimize the top of the tree, then re-measure without the profiler to confirm the wall-clock win is real.

Recursion and repeated calls are aggregated. A recursive function appears as f==>f and its inclusive cost is meaningless in isolation — you're seeing the sum across all depths. Trust exclusive cost for recursive code.

Big trees need a threshold. A profile of a full Laravel request is thousands of nodes across Illuminate\*. Filter by minimum cost before you read anything, or you'll spend twenty minutes reading Container::make noise. XHPROF_FLAGS_NO_BUILTINS plus a threshold reduces the output to something a human can actually scan.

Extension point if you must extend it: the codebase is a thin formatter around the run array. If you need JSON output for a CI perf-budget check, forking and adding a printer format is a 50-line change — far more reliable than trying to parse the human-readable table with regex. Do that in a fork you vendor, not by depending on the abandoned package.

Keep it out of production, always. There is no sampling, no circuit breaker, and no kill switch built into this stack. Gate profiling behind an env var that defaults to off, and treat any config that enables it in prod as a bug in code review.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib