RRelayer
Home/Development

Profiler#

When APP_ENV=dev, the request lifecycle is recorded by the container-bound profiler.

  • AppRouter records each dispatch event through ProfilingListener,

the framework-supplied DispatchListener. The /_profiler web view itself is also served by this listener — it intercepts the path in handleFrameworkRequest() so the profiler does not profile itself.

  • EtagStore / SessionStorage / Database / Authenticator are

swapped for traceable decorators, so cache.* / session.* / queries / auth events line up on the timeline.

  • Recordings are saved to var/cache/profiler.

In production (APP_ENV other than dev) Profiler resolves to NullProfiler, so there is no cost even if user code takes a Profiler dependency. The Traceable* classes are not even autoloaded. ProfilingListener is still wired in production, but with the NullProfiler its methods collapse to no-ops — measurement overhead is effectively nil.

Measuring your own code (measure())#

When you want your own work or a third-party library on the timeline, you can wrap the call site without writing a dedicated Traceable* decorator.

public function render(PageContext $ctx, Profiler $profiler): Element
{
    $rows = $profiler->measure('report', 'aggregate', fn () => $this->report->build());

    return <Table rows={$rows} />;
}

measure(string $collector, string $label, callable $fn): mixed runs $fn, returns its value as-is, and records only the elapsed time under $collector/$label (no manual stop() needed). If $fn throws, it records the span with an error payload and then rethrows the exception as-is.

  • It records time only. **Neither the arguments nor the return value

are recorded** (a generic wrapper cannot judge what is sensitive; if you want to keep a sanitized value, use start() + TraceSpan::stop() directly and pass only what is safe).

  • In production (NullProfiler) $fn still runs and the value/exception

passes straight through; only the recording is skipped — so you can keep the Profiler dependency at no cost.

Excluded paths#

Noisy paths can be excluded via an environment variable.

PROFILER_EXCLUDED_PATHS=/_profiler,/assets,/favicon.ico

Comma-separated, treated as prefix matches.

Clearing data#

Recordings accumulated in var/cache/profiler can be cleared with a dedicated command. Use it when you want to view /_profiler from a clean slate.

vendor/bin/relayer profiler:clear
  • It deletes only var/cache/profiler/*.json (the format the

profiler writes). The directory itself and any other files placed there are kept (the directory is recreated on the next dev request).

  • Idempotent and safe — if the cache directory is missing/empty it

finishes as a success with "nothing to delete." Re-running is always safe.

  • If a file cannot be deleted, it reports so and the exit code is 1

(no silent partial deletion). On success it is 0.

DispatchListener — dispatch extension#

The router observes its dispatch lifecycle through Polidog\Relayer\Router\Dispatch\DispatchListener. ProfilingListener is the framework's own implementation of the same hook; your app can register its own — audit logging, metrics, custom tracing, framework-managed URLs — without touching the dispatch core.

Register a service that implements DispatchListener and tag it with relayer.dispatch_listener:

services:
  App\Audit\RouteAuditListener:
    tags: ['relayer.dispatch_listener']

Two requirements: keep one bool "started" guard if you record state, and treat afterDispatch() as idempotent by contract — it may fire from both the finally block and the shutdown handler (the exit/die path). Hooks:

MethodFires when
setContainer(?ContainerInterface)The router receives a container.
setDocument(DocumentInterface)A document is pushed in via Relayer::boot()->setDocument(...).
handleFrameworkRequest(string $path): boolTop of run(). Returning true declares "I consumed this request" — before/afterDispatch and route matching are skipped (this is how /_profiler works).
beforeDispatch(string $url, string $method): boolJust before dispatch. Returning true is informational ("I actually started recording").
afterDispatch(int $status)End of dispatch. Must be idempotent (may be called twice — finally + shutdown).
onRouteMatch(RouteMatch) / onApiMatch(RouteMatch)Page / API route matched.
onNotFound()Dedicated 404 hook (not routed through onAbort).
onAbort(HttpException)Non-404 abort (4xx/5xx HttpException).
onAuthorizationFailure(AuthorizationException)Authorization failed.
onPageLoaded(string $pagePath, …) / onLayoutLoaded(string $filePath, …)After a page / layout is loaded.
onCacheApplied(Cache) / onCacheNotModified(Cache)Cache policy applied / 304 short-circuit (the latter is the last chance before exit).
startPsxCompile(string) / startPageRender(...)Return a ?TraceSpan for timing (the caller ?->stop(...)s it).

Tagged listeners are picked up by both the dispatcher.php dump (when routes:compile has produced it) and the RuntimeDispatcher fallback. Only the service ID is baked into the dump — constructor arguments are resolved through the live container per request, so listeners that depend on env-derived configuration are safe.

Listing routes#

Separately from the profiler, the discovered routes can be checked with a command.

vendor/bin/relayer routes
Last updated: 2026-05-21
Change history (3)
  • v0.21.0 TraceableAppRouter -> ProfilingListener; add DispatchListener extension section
  • Move to the new Development category
  • Created