Profiler#
When APP_ENV=dev, the request lifecycle is recorded by the container-bound profiler.
AppRouterrecords each dispatch event throughProfilingListener,
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/Authenticatorare
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)$fnstill 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.icoComma-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:
| Method | Fires when |
|---|---|
setContainer(?ContainerInterface) | The router receives a container. |
setDocument(DocumentInterface) | A document is pushed in via Relayer::boot()->setDocument(...). |
handleFrameworkRequest(string $path): bool | Top 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): bool | Just 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 routesChange history (3)
- v0.21.0 TraceableAppRouter -> ProfilingListener; add DispatchListener extension section
- Move to the new Development category
- Created