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
- typo3/cms-core: ^13.4 || ^14.0
- typo3/cms-frontend: ^13.4 || ^14.0
Requires (Dev)
- composer/class-map-generator: ^1.0
- eliashaeussler/version-bumper: ^4.0
- phpunit/phpcov: ^9.0 || ^10.0 || ^11.0 || ^13.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.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-06-17 14:56:29 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 only in a Development context (Environment::getContext()->isDevelopment()). It registers no middleware and collects no data in production.
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.
Warning
This package is in early development stage and may change significantly in the future. I am working steadily to release a stable version as soon as possible.
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
Supports
| Version | TYPO3 | PHP |
|---|---|---|
| 0.x | 13-14 | 8.2-8.5 |
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_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). |
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.
💡 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 | Full request URI. |
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).