The Profiler and the Debug Toolbar
Read the dev toolbar on every page, open the full profiler for one request, and use the timeline to find the slow call.
/_profiler/{token} # also linked from the X-Debug-Token-Link response headerIn the dev environment Symfony records what each request did: the matched route, the controller, every log line, every query, the memory used and the time spent. The profiler is the web interface over those recordings. The debug toolbar is the strip injected at the bottom of HTML pages summarising the current one.
Reading the toolbar
Each segment is a link into the full profile: the status code and route, the controller that ran, counts of log entries and database queries, total time and peak memory. A red or yellow segment means something was logged or deprecated. Clicking any segment opens the profiler on that panel.
One request at a time
Every recorded request gets a token, and its profile lives at /_profiler/{token}. /_profiler lists the recent ones, which is how you inspect a request that produced no visible page — a form POST that redirected, or a JSON call. Responses also carry the profile URL in the X-Debug-Token-Link header, so curl -I is enough to find it.
Finding a slow call
The Performance panel draws a timeline of the request as nested bars: kernel boot, controller, template render, and anything measured inside them. Read it by width, not by name. A single wide bar is one slow operation; many narrow identical bars are a loop doing repeated work.
Development only
The profiler stores request contents, so it must never be enabled in production. It is installed as a dev dependency, through symfony/profiler-pack, and configured under when@dev.
Example
<?php
// src/Controller/ReportController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ReportController extends AbstractController
{
#[Route('/report', name: 'app_report', methods: ['GET'])]
public function show(): Response
{
$rows = [
['label' => 'signups', 'value' => 128],
['label' => 'cancellations', 'value' => 11],
];
// dump() comes from symfony/var-dumper. With symfony/debug-bundle
// installed, the value is collected and shown in the profiler's
// Debug panel rather than printed into the markup.
dump($rows);
$total = array_sum(array_column($rows, 'value'));
return new Response(sprintf('<p>Total: %d</p>', $total));
}
}When to use it
- Work out which route and controller served a page you did not write.
- Find the slow step in a page that takes two seconds, using the timeline widths.
- Inspect the profile of a form POST that redirected before you could look at it.
More examples
Turning the toolbar on, shorthand and longhand
Both toolbar forms are the same setting. The one to remember is intercept_redirects: set it to true and Symfony pauses on a redirect so you can open the profile of the request that caused it, instead of landing on the next page with the evidence gone.
# config/packages/web_profiler.yaml
# when@dev applies this block only in the dev environment.
when@dev:
web_profiler:
toolbar: true # shorthand
framework:
profiler:
only_exceptions: false
# The shorthand above expands to this, which is the form the
# configuration reference documents:
#
# when@dev:
# web_profiler:
# toolbar:
# enabled: true
# intercept_redirects: false
when@test:
web_profiler:
toolbar: false
framework:
profiler:
collect: falseFinding the profile of an API request
The standard way to profile anything that is not a web page: a JSON endpoint, a webhook receiver, a failed form POST. The token is unique to that request, so the profile you open is the one you just made and not a later one.
curl -I https://127.0.0.1:8000/status
# HTTP/2 200
# content-type: application/json
# x-debug-token: a1b2c3
# x-debug-token-link: https://127.0.0.1:8000/_profiler/a1b2c3
# Open that link in a browser for the full profile of that one request.
# Or list the most recent profiles, which includes requests that
# rendered nothing at all:
# https://127.0.0.1:8000/_profilerMeasuring one block so it appears on the timeline
Use this when the timeline shows one wide controller bar and you need to know which part of the method it was. Naming the sections splits that single bar into labelled ones; Stage 6 covers the measurement tooling in depth.
<?php
// src/Controller/ImportController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Stopwatch\Stopwatch;
class ImportController extends AbstractController
{
#[Route('/import', name: 'app_import', methods: ['GET'])]
public function run(): Response
{
// Built here with new because the service container is Stage 2.
// Events recorded on the Stopwatch the framework provides appear
// as named bars in the Performance panel.
$stopwatch = new Stopwatch();
$stopwatch->start('parse');
$lines = str_getcsv("a,b,c\n");
$event = $stopwatch->stop('parse');
return new Response(sprintf(
'<p>%d fields in %d ms</p>',
count($lines),
$event->getDuration(),
));
}
}
Discussion