A PHP report-generation request is slow; how do you set up Xdebug 3's profile mode to capture a cachegrind file for just that request?
answer
- profile starts with every request by default
- start_with_request=trigger
- XDEBUG_TRIGGER in GET, POST, cookie or env
- output_dir /tmp, cachegrind.out.%p
- compressed .gz output since 3.1
basics
~10 sSet xdebug.mode=profile with xdebug.start_with_request=trigger, then send XDEBUG_TRIGGER (query, POST, cookie or environment) with the report request; Xdebug writes a cachegrind.out file into xdebug.output_dir (default /tmp) for that request only.
solid answer
~30 sIn `php.ini` set `xdebug.mode=profile`, and also `xdebug.start_with_request=trigger`, because for profile mode the `default` value means `yes` and every request would be profiled. Restart PHP-FPM, then request the report with `?XDEBUG_TRIGGER=1`, a cookie or POST field of that name, or the `XDEBUG_TRIGGER` environment variable for a CLI run; `xdebug.trigger_value` can require a secret value. Xdebug writes the file into `xdebug.output_dir` (default `/tmp`, which systemd's private tmp may redirect), named by `xdebug.profiler_output_name` (default `cachegrind.out.%p`), gzip-compressed by default. The response carries an `X-Xdebug-Profile-Filename` header with the path. Open it in KCacheGrind, QCacheGrind (which needs `xdebug.use_compression=0`), Webgrind or PhpStorm.
code
bash · 9 lines# web request: profile only this one
curl -s -o /dev/null -D - 'https://app.test/reports/annual?XDEBUG_TRIGGER=1' \
| grep -i x-xdebug-profile-filename
# CLI run of the same report
XDEBUG_TRIGGER=1 php bin/generate-report.php --year=2025
# or, with no trigger configured at all, enable the mode for this run only
XDEBUG_MODE=profile php bin/generate-report.php --year=2025go deeper
Recall that profile mode writes cachegrind.out files into xdebug.output_dir, /tmp by default, and that XDEBUG_TRIGGER starts it for one request.
Explain why start_with_request must be trigger for profile mode, how the trigger is sent, and how %p naming and .gz compression affect finding the file.
Show you get a trustworthy profile: only profile mode enabled, a warm OPcache, unique file names per worker, a trigger_value secret, and comparing proportions rather than absolute times.
Decide where profiling happens at all: a production-like staging copy with Xdebug, or a sampling profiler that can stay on, given data sensitivity and overhead.
## What profile mode produces **Profile mode** makes Xdebug measure every function and method call of a request — time spent and memory used, both inside the function (**self** cost) and including its callees (**inclusive** cost) — and write the result as a **Cachegrind-compatible file**. Viewers such as KCacheGrind (Linux), QCacheGrind (Windows and macOS builds), Webgrind (a web front end) and PhpStorm read that format. Internal PHP functions appear with a `php::` prefix, and `include`/`require` calls appear with the included file name. Reading the call graph to find the culprit belongs to profiling technique; this answer is about getting a correct file for exactly the request you care about. ## Step 1: enable the mode, but not for every request ```ini zend_extension=xdebug xdebug.mode=profile xdebug.start_with_request=trigger xdebug.output_dir=/var/tmp/xdebug xdebug.profiler_output_name=cachegrind.out.%t.%R ``` The trap is `xdebug.start_with_request`. Its default value is `default`, which resolves **per mode**: `trigger` for `debug` and `trace`, but **`yes` for `profile`**. So `xdebug.mode=profile` on its own profiles every request that hits the server — every asset request routed through PHP, every health check — and fills the output directory. Setting `trigger` restricts it to requests that ask for it. Remember that `xdebug.mode` is read at process start, so restart PHP-FPM after editing. ## Step 2: send the trigger with the report request In trigger mode Xdebug looks for a variable named **`XDEBUG_TRIGGER`** in: - the query string or POST body (`/reports/annual?XDEBUG_TRIGGER=1`), - a cookie of that name (browser helper extensions set it for you), - the environment, for CLI runs: `XDEBUG_TRIGGER=1 php bin/generate-report.php`. `XDEBUG_PROFILE` is still accepted as a legacy profile-specific name. With `xdebug.trigger_value` empty, **any** value activates it; set `xdebug.trigger_value=SomeSecret` (or a comma-separated list since 3.1) and only a matching value does. For a CLI script you can skip the trigger entirely: `XDEBUG_MODE=profile php bin/generate-report.php` profiles that one run. ## Step 3: find the file | Setting | Default | Notes | |---|---|---| | `xdebug.output_dir` | `/tmp` | Must be writable by the PHP user | | `xdebug.profiler_output_name` | `cachegrind.out.%p` | `%p` is the process ID | | `xdebug.use_compression` | `true` (when built with zlib) | Adds `.gz` to the name | | `xdebug.profiler_append` | `0` | `1` appends to an existing file of the same name | Things that commonly go wrong: 1. **Private tmp.** Under systemd, PHP-FPM often gets a private `/tmp`, so the file lands in a directory like `/tmp/systemd-private-…-php-fpm.service-…/tmp`, not the `/tmp` you look in. 2. **Overwritten files.** A PHP-FPM worker serves many requests with the same PID, so with `cachegrind.out.%p` the next request that worker profiles overwrites the file. Use `%t` (timestamp), `%u` (microsecond timestamp), `%r` (random) or `%R` (request URI) for unique names. 3. **Compression.** Files end in `.gz`. KCacheGrind and PhpStorm read them; QCacheGrind does not, so set `xdebug.use_compression=0` for it. Compression cannot be combined with `profiler_append` — Xdebug falls back to an uncompressed file. 4. **Late `ini_set()`.** The profile file is created before the script runs, so changing `xdebug.output_dir` with `ini_set()` inside the script has no effect on it. `xdebug.profiler_output_name` cannot be changed with `ini_set()` at all. To learn the exact path, read the **`X-Xdebug-Profile-Filename`** response header that Xdebug adds to profiled requests, or call `xdebug_get_profiler_filename()`, which returns the name or `false` when no profile is being written. ## Step 4: keep the measurement honest - **Profile with only `profile` enabled.** Adding `debug` or `develop` adds their own overhead to the numbers. - **Expect absolute times to be inflated.** Instrumenting every call slows the request; the *proportions* between functions are what you compare. - **Warm OPcache first.** The first request after a restart also pays for compiling scripts; profile a repeat request. - **Watch the disk.** Profiles of large requests are big; clean the directory afterwards. ## When no file appears at all Work through the chain in order: 1. **Is the mode really `profile`?** The ini file must be one PHP reads at startup, and PHP-FPM must have been restarted; an `XDEBUG_MODE` variable in the service environment overrides the file. 2. **Did the trigger arrive?** A proxy or framework may drop an unknown query parameter or cookie; with a non-empty `xdebug.trigger_value`, the value must match one of the configured secrets. 3. **Can PHP write the directory?** `xdebug.output_dir` must be writable by the PHP-FPM pool user, not by you. 4. **Is it in a private tmp?** Look under the systemd private directory, or move `output_dir` elsewhere. Setting `xdebug.log` to a writable file makes Xdebug record its decisions — for example a warning when a trigger value did not match the configured secret — which answers most of these questions directly. Then open the file, sort the flat profile by inclusive and self cost, and follow the heaviest call path — a technique covered with PHP profiling itself.
- Two profiled requests handled by the same PHP-FPM worker left only one cachegrind file. Why?The default `xdebug.profiler_output_name` is `cachegrind.out.%p`, and `%p` is the process ID. A PHP-FPM worker keeps its PID across requests, so the second profile overwrote the first (`xdebug.profiler_append` is 0). Add `%t`, `%u`, `%r` or `%R` to the name to make each file unique.
- Why might a colleague's QCacheGrind refuse to open the file you sent?Since Xdebug 3.1, `xdebug.use_compression` defaults to true when Xdebug is built with zlib, so profiles are written as `cachegrind.out.<pid>.gz`. KCacheGrind and PhpStorm read gzip files, QCacheGrind does not. Decompress the file with `gunzip`, or set `xdebug.use_compression=0` before profiling.
saying these in an interview costs you the question
- xdebug.mode=profile alone only profiles requests that send a trigger
- The profile file is named after the script, so each URL gets its own file
- Setting xdebug.output_dir with ini_set() inside the script moves the profile file
- Absolute timings in the profile equal production timings
- Profiles are always plain text you can open in any cachegrind viewer