hoa/bench
hoa/bench is a lightweight PHP benchmarking library from the Hoa project. It helps measure execution time, compare code snippets, and profile performance with a simple API, making it easy to add repeatable benchmarks to PHP scripts and tests.
hoa/bench is a standalone micro-benchmarking library, not a Laravel package. There is no service provider, no facade, no config file, and no auto-discovery. You get one class you instantiate yourself, and you read the timings back by casting it to a string.
Install it as a dev-only dependency:
composer require --dev hoa/bench
If Composer fights you, pin the last release explicitly (2017-01-11 / ^2.x) and remember it drags in the abandoned hoa/consistency, hoa/visitor, hoa/stream and hoa/protocol packages with it.
First use, in php artisan tinker — put a stopwatch around two competing implementations:
$bench = new Hoa\Bench\Bench();
$bench->start('strtotime');
for ($i = 0; $i < 100000; $i++) { strtotime('2024-01-01'); }
$bench->stop('strtotime');
$bench->start('carbon');
for ($i = 0; $i < 100000; $i++) { \Illuminate\Support\Carbon::parse('2024-01-01'); }
$bench->stop('carbon');
echo $bench;
Where to look first:
vendor/hoa/bench/README.md — the nested-points example is the whole mental model.vendor/hoa/bench/Bench.php — start(), stop(), getPoint().vendor/hoa/bench/Point.php — the accessors (getTime(), aggregate stats) actually available to you. Trust this file over any blog post; the API surface is small and the docblocks are the spec.Your first real use case should be a one-off: proving which of two Eloquent query shapes, two serializers, or two cache key strategies is faster, before you touch production code. That is where a 5-line tool beats firing up Telescope.
Bind it, don't new it everywhere. If you use it in more than one place, register a non-singleton binding in a local-only provider:
// app/Providers/BenchServiceProvider.php
public function register(): void
{
$this->app->bind(\Hoa\Bench\Bench::class, fn () => new \Hoa\Bench\Bench());
}
public function shouldBeRegistered(): bool
{
return $this->app->isLocal();
}
Bind (not singleton) on purpose — see the Octane gotcha below.
Time named phases in a service. The library's real advantage over microtime() is that repeated start('db')/stop('db') calls with the same ID aggregate, and the point object exposes sum/min/max/average/variance. That makes "how long do my 47 queries in this request actually take, and what's the worst one" a one-liner instead of a manual array.
foreach ($queries as $q) {
$bench->start('query');
$result = $q->run();
$bench->stop('query');
}
Nest points for phase breakdowns. Starting a point while another is open makes it a child, so a single echo gives you tree-shaped output (bootstrap → handler → render) rather than a flat list. Wrap in try/finally so an exception doesn't leave a point dangling.
Middleware for request-level totals:
public function handle($request, Closure $next)
{
$bench = app(\Hoa\Bench\Bench::class);
$bench->start('request');
try {
return $next($request);
} finally {
$bench->stop('request');
Log::debug('request timing', ['report' => (string) $bench]);
}
}
Log it, don't append it to the response — and guard the middleware registration with $this->app->isLocal().
Artisan command with a --bench flag is the cleanest integration point: php artisan report:generate --bench prints the report to the console instead of leaking timing into HTTP responses.
Migration pattern: wrap the two calls you actually use (start/stop + a report() string) behind your own tiny interface. When you swap to symfony/stopwatch later — and you probably will — it's a one-file change, since Stopwatch has essentially the same start($name) / stop($name) shape.
hoa/* stack; keep it out of production require and out of anything you ship to a client.try/finally per point.getPoint('typo') won't give you a helpful error; you'll just see a missing row and wonder why. Grep your string literals — most "it's not measuring" bugs are a typo'd ID, not a library bug.echo $bench is HTML-flavoured by default. Don't interpolate it straight into a JSON response or an Artisan info() line without checking the output first; capture the cast to string into a log channel instead.microtime-class, not hrtime. Fine for millisecond-scale work; don't draw conclusions from sub-microsecond deltas, and always run the loop long enough (100k iterations, not 10) that cold-start and opcache warm-up wash out.memory_get_peak_usage().hoa/visitor) means customising the report format is more ceremony than writing your own foreach over getPoint(). Just iterate the points and format it yourself.composer audit will flag the transitive hoa/* packages. Since this is require-dev, the honest answer is "it's a local profiling tool, not shipped code" — but note it in your README so the next developer doesn't panic.hoa/bench earns a place only for quick, throwaway comparative benchmarks where its aggregate statistics are convenient — treat it as a scratchpad tool, not infrastructure.How can I help you explore Laravel packages today?