konradmichalik / typo3-request-profiler
Dev-only TYPO3 frontend request profiler that records SQL queries, N+1 patterns, cache state and timing as compact JSON profiles for AI coding assistants.
Package info
github.com/konradmichalik/typo3-request-profiler
Type:typo3-cms-extension
pkg:composer/konradmichalik/typo3-request-profiler
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- doctrine/dbal: ^3.9 || ^4.0
- psr/event-dispatcher: ^1.0
- psr/http-message: ^1.0 || ^2.0
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
- psr/log: ^3.0.2
- typo3/cms-core: ^13.4 || ^14.0
- typo3/cms-frontend: ^13.4 || ^14.0
Requires (Dev)
- composer/class-map-generator: ^1.7.3
- eliashaeussler/version-bumper: ^4.0.3
- phpunit/phpcov: ^9.0 || ^10.0 || ^11.0 || ^13.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0 || ^13.0
- typo3/cms-base-distribution: ^13.4 || ^14.0
- typo3/cms-lowlevel: ^13.4 || ^14.0
- typo3/testing-framework: ^8.2 || ^9.5
This package is auto-updated.
Last update: 2026-07-08 07:24:25 UTC
README
TYPO3 extension typo3_request_profiler
A dev-only TYPO3 frontend request profiler. It instruments live frontend requests and writes one compact JSON profile per request — SQL queries, N+1 patterns, cache state, and timing — to var/log/profiles/{request_id}.json.
Important
This extension is active by default only in a Development context (Environment::getContext()->isDevelopment()). Outside Development it stays off and collects no data unless explicitly opted in via TYPO3_REQUEST_PROFILER_FORCE=1 (intended for staging, never real production — see Configuration).
The profiler is a thin, standalone collector with no external dependencies. It is inspired by the Symfony Profiler — and by some of the metrics the TYPO3 Admin Panel surfaces — but records them as compact, machine-readable JSON instead of an interactive panel.
What it captures per request:
- Wall-clock and SQL timing, peak memory usage, included PHP file count
- Full query count + top slow queries + N+1 duplicate detection
- Cache hit/miss state with disabled reasons
- Log activity per request (count by level + noisiest components)
- Optional call-site origin (
Class::method (file:line)) for every flagged query
🔥 Installation
Requirements
- TYPO3 13.4 LTS & 14.0+
- PHP 8.2+
- Doctrine DBAL 3.x or 4.x
Composer
composer require --dev konradmichalik/typo3-request-profiler
TER
Download the zip file from TYPO3 extension repository (TER).
⚙️ Configuration
The profiler is controlled entirely via environment variables:
| Variable | Default | Effect |
|---|---|---|
TYPO3_REQUEST_PROFILER |
(on) | Set to 0 to disable profiling for a request/process. |
TYPO3_REQUEST_PROFILER_FORCE |
(off) | Set to 1 to enable profiling outside the Development context (e.g. staging). Must be set deliberately, never in real production. |
TYPO3_REQUEST_PROFILER_MIN_MS |
0 |
Only persist requests whose total time exceeds this threshold (ms). |
TYPO3_REQUEST_PROFILER_KEEP |
50 |
Number of most-recent profiles to retain; older files are pruned automatically. |
TYPO3_REQUEST_PROFILER_TRACE |
(off) | Set to 1 to capture the calling Class::method (file:line) for each query (added as origin to slow_queries/duplicate_queries). |
TYPO3_REQUEST_PROFILER_EVENTS |
(off) | Set to 1 to time dispatched PSR-14 events and add an events section (count + the most expensive event classes). |
Note
TYPO3_REQUEST_PROFILER_FORCE is a deployment-level switch evaluated in ext_localconf.php and therefore cached — it is not a live toggle. Changing it requires a cache flush to take effect. This is in contrast to the per-request TYPO3_REQUEST_PROFILER=0, which short-circuits the middleware on each request and needs no cache flush.
Tip
TYPO3_REQUEST_PROFILER_TRACE=1 uses debug_backtrace per query and is therefore opt-in for performance. No bound parameter values are ever captured — only the call site.
Tip
TYPO3_REQUEST_PROFILER_EVENTS=1 wraps the core PSR-14 dispatcher and measures every dispatched event. Dispatch happens very frequently, so the per-event timing is opt-in. When off, events are dispatched without any measurement and the events section is omitted. Event timing follows the same activation gate as the rest of the profiler, so it also works on staging together with TYPO3_REQUEST_PROFILER_FORCE=1.
💡 Profile Format
Each request produces one JSON file at var/log/profiles/{request_id}.json:
{
"schemaVersion": 1,
"token": "<RequestId>",
"time": "2026-06-15T10:00:00+00:00",
"method": "GET",
"url": "https://example.ddev.site/",
"status": 200,
"page": { "id": 1, "type": 0 },
"cache": { "hit": false, "cacheable": false, "disabled_reasons": ["&no_cache=1 query parameter was given"] },
"timing": { "total_ms": 142.5 },
"memory": { "peak_mb": 16.1 },
"php": { "included_files": 432 },
"queries": { "count": 101, "total_ms": 38.2 },
"slow_queries": [
{ "sql": "SELECT * FROM pages WHERE slug = ? ORDER BY slug desc", "ms": 12.4 }
],
"duplicate_queries": [
{ "sql": "SELECT COUNT(*) FROM tt_content WHERE pid = ? AND deleted = ?", "count": 100, "total_ms": 31.4 }
],
"log": {
"count": 3,
"by_level": { "warning": 2, "notice": 1 },
"top_components": [
{ "component": "TYPO3.CMS.Core.Authentication.BackendUserAuthentication", "count": 2 }
]
},
"events": {
"count": 142,
"total_ms": 12.3,
"top": [
{ "event": "TYPO3\\CMS\\Core\\Cache\\Event\\CacheFlushEvent", "count": 100, "total_ms": 8.1 }
]
}
}
Note
The log section only appears when the request produced log entries. Only the level and component are recorded — never the message body — so no user data leaks into the profile.
Note
The events section only appears when TYPO3_REQUEST_PROFILER_EVENTS=1.
Profile schema
The artifact carries an explicit, versioned schema contract via the top-level
schemaVersion field. It is written first so it is immediately visible in every file.
Top-level fields (always present):
| Field | Type | Description |
|---|---|---|
schemaVersion |
int | Schema contract version of the artifact (currently 1). |
token |
string | Request identifier; also the file name. |
time |
string | Request time as ISO 8601 (date('c')). |
method |
string | HTTP request method. |
url |
string | Request URI with masked query values (?q=?&page=?) — parameter names are kept, values are never persisted (they regularly carry search terms, e-mail addresses or one-time tokens). |
status |
int | HTTP response status code. |
Section keys (key = Section::name(); each appears only when the section is enabled and produced data):
| Key | Shape |
|---|---|
page |
{ id, type } |
cache |
{ hit, cacheable, disabled_reasons[] } |
timing |
{ total_ms } |
memory |
{ peak_mb } |
php |
{ included_files } |
queries |
{ count, total_ms } |
slow_queries |
[{ sql, ms, origin? }] |
duplicate_queries |
[{ sql, count, total_ms, origin? }] |
log |
{ count, by_level{}, top_components[{ component, count }] } |
events |
{ count, total_ms, top[{ event, count, total_ms }] } |
Note
schemaVersion is incremented only when field names or shapes change in a breaking way. Additive changes keep the same version.
Reading profiles
KonradMichalik\Typo3RequestProfiler\Profiling\ProfileReader is the supported, framework-agnostic read API for these artifacts — external consumers should use it instead of re-implementing the glob/sort/json_decode logic:
| Method | Returns |
|---|---|
all() |
All profiles, newest first. |
latest(int $limit = 10) |
The $limit newest profiles, newest first. |
byToken(string $token) |
A single profile by its token, or null if unknown. |
The reader is directory-based and carries no framework dependency — its constructor takes the profiles directory (new ProfileReader($directory)). On the TYPO3 side, that directory is ProfileWriter::defaultDirectory() (the same source the writer persists to). Its public signature is kept stable as a contract for consumers.
🧑💻 Contributing
Please have a look at CONTRIBUTING.md.
⭐ License
This project is licensed under GNU General Public License 2.0 (or later).