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

Profiler Laravel Package

yiisoft/profiler

Yii Profiler is a lightweight profiling component for Yii3 applications. It collects timing and memory usage data, supports custom checkpoints, and helps developers identify performance bottlenecks during development.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package with Composer and let its DI config wire itself into the container:

composer require yiisoft/profiler

The package ships a config/ directory (typically config/di.php / config/params.php) that registers Yiisoft\Profiler\ProfilerInterface → Yiisoft\Profiler\Profiler as a singleton. In a Yii3 application this is picked up automatically via the config plugin; in a standalone PSR-11 setup you must include those files yourself and make the ProfilerInterface service available wherever you inject it.

Look first at these files (they answer 90% of questions):

File Why
src/Profiler.php The start/stop/begin/end API and how messages are buffered
src/Message.php Shape of a recorded span (token, category, context, time, trace)
src/Target/TargetInterface.php + AbstractTarget.php How output sinks work, enabling/disabling
src/Target/FileTarget.php The default sink — where the files land and in what format
config/ Default service definitions and the profiler params block

Minimal working setup. Register a target so records actually go somewhere. The FileTarget is the pragmatic default — it dumps one file per request into a runtime directory that the Yii debug UI can read later:

// config/common.php or config/web.php
use Yiisoft\Profiler\Target\FileTarget;

return [
    FileTarget::class => [
        'setDirectory()' => ['@runtime/profiler'],
        'setEnabled()' => [true], // keep false in production
    ],
];

First use case. Wrap a single hot path you already suspect and dump the file target's output after a request:

final class OrderController
{
    public function __construct(private ProfilerInterface $profiler) {}

    public function create(Request $request): Response
    {
        $this->profiler->start('order.create', 'application', ['route' => 'order/create']);

        // ... validation, persistence, event dispatch ...

        $this->profiler->stop('order.create');

        return $response;
    }
}

Check @runtime/profiler/ for a timestamped file; each line is a JSON-encoded Message with token, category, time (seconds since request start), duration, and your context payload. That is enough to answer "which block is slow" on day one without any UI.

Implementation Patterns

Manual span wrapping (the default). start($token, $category, $context) / stop($token) is the primitive. Treat token as the name of the work and category as the subsystem (db, http, cache, queue, view). Consistent categories are what make file output greppable and aggregatable.

Nested / re-entrant work → begin() / end(). When the same logical operation can be entered recursively (a repository method calling itself through a decorator, a view partial rendered from another partial), use begin(), which appends a counter to the token and returns the concrete token string you must pass to end():

$token = $this->profiler->begin('db.query', 'db', ['sql' => $sql]);
try {
    return $this->pdo->query($sql);
} finally {
    $this->profiler->end($token);
}

start()/stop() use a flat token and will collide under recursion; begin()/end() exist precisely to avoid that.

Middleware for automatic instrumentation. Rather than sprinkling calls through controllers, wrap the request lifecycle in PSR-15 middleware. Start a span in process(), stop it in finally, and stash timing metadata in the context array. This gives you per-route wall-clock numbers for free and is the highest-leverage integration point in a Yii3 app.

Decorator pattern for infrastructure. Wrap PSR-18 HTTP clients, PSR-16 cache adapters, and DB connections in thin decorators that bracket each call with begin()/end(). This keeps profiling out of business logic entirely and makes it trivial to disable by swapping the binding back in the container.

final class ProfilingHttpClient implements ClientInterface
{
    public function __construct(
        private ClientInterface $inner,
        private ProfilerInterface $profiler,
    ) {}

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        $token = $this->profiler->begin('http.client', 'http', [
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
        ]);
        try {
            return $this->inner->sendRequest($request);
        } finally {
            $this->profiler->end($token);
        }
    }
}

Multiple targets for different audiences. TargetInterface is tiny — export(array $messages), plus enable/disable. Register the FileTarget for local debugging and your own target (log sink, OpenTelemetry exporter, statsd counter) alongside it. The profiler fans every flushed batch out to all enabled targets, so you can keep the file dump for humans and a metrics target for dashboards.

Environment gating. Keep the file target enabled only in dev/staging; either flip setEnabled() per environment in config or call disable() on the target at bootstrap. In production, either remove the targets (profiler becomes a cheap no-op collector) or keep only a sampled, low-cardinality metrics target.

Flush deliberately. Records accumulate in memory and are handed to targets on flush(). In a classic PHP-FPM request the destructor handles this for you. In long-running workers, cron daemons, or console commands that process many items, call $profiler->flush() at the end of each unit of work (each job, each iteration of a worker loop) so you get bounded memory and incremental output rather than one giant file at shutdown.

Gotchas and Tips

Mismatched tokens silently lose data. stop() with a token that was never started, or with a typo relative to the start() call, simply produces nothing. Wrap spans in try/finally — an exception thrown mid-block otherwise leaves the span open and skews every subsequent duration measurement.

start() vs begin() is not cosmetic. If you see a token recorded twice with overlapping durations and nonsensical results, you used start() in code that can recurse. Switch to begin() and always use its returned token for end().

Time units are seconds, not milliseconds. Message timing is a float of seconds (fractional). Multiply by 1000 before feeding dashboards or log parsers that assume milliseconds, or you will report a 200 ms query as 0.2 ms.

Trace capture is expensive. Collecting a backtrace per span is the single biggest cost of the profiler. Disable or reduce trace collection for anything you plan to run with the profiler enabled under load, and never enable it around a loop of thousands of iterations.

The buffer grows without bound in workers. Under RoadRunner, Swoole, or a queue worker, the profiler singleton lives for the life of the process. Without periodic flush(), you will see steadily climbing memory that looks like a leak in your application but is the message buffer. Flush per job, and consider disabling targets entirely in the worker entrypoint.

File target needs a writable, existing directory. FileTarget writes to the configured directory but does not necessarily create it, and it writes one file per flush tagged with the request/time. A missing or unwritable @runtime/profiler fails quietly from the profiler's point of view — you get no files and no error unless your logger happens to catch it. Verify the path and permissions first when "profiling isn't working."

There is no bundled viewer. The file format is consumable by the Yii debug tooling, but standalone you are reading JSON lines. Plan for that: either add the debug panel package, or write a 30-line script that reads a dump and prints a sorted table of duration by token. Most teams end up with the latter.

Low-activity package — pin and read the source. Last release was February 2023 with modest adoption. Treat the API as stable but unmaintained: pin the version in composer.json, and expect to consult src/Profiler.php rather than docs when behavior is unclear. Avoid building custom targets on undocumented internals of AbstractTarget; implement TargetInterface directly so upstream changes can't break you.

Categories are your only grouping dimension — use them strictly. Once you have a week of dumps, category is the field you'll filter on. Free-form categories ('misc', 'stuff', 'test') rot immediately; define a small enum of subsystem names up front.

Measure the profiler itself before trusting it. Start with it fully enabled on a local request, compare total request time against the sum of recorded spans, and note the gap. If the gap is large, your instrumentation overhead (or trace collection) is material and your production numbers will mislead you.

Extension point to remember: a custom TargetInterface implementation is the cleanest way to integrate with existing observability tooling — it receives the complete Message[] array at flush time, so you can convert spans to your metrics/tracing format in one place without touching application code.

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