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.

Maintainers

Package info

github.com/konradmichalik/typo3-request-profiler

Type:typo3-cms-extension

pkg:composer/konradmichalik/typo3-request-profiler

Statistics

Installs: 418

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

0.2.0 2026-06-17 14:53 UTC

This package is auto-updated.

Last update: 2026-06-17 14:56:29 UTC


README

Extension icon

TYPO3 extension typo3_request_profiler

Packagist Packagist Downloads Supported PHP Versions CGL Coverage Tests License

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

Packagist Packagist Downloads

composer require --dev konradmichalik/typo3-request-profiler

TER

TER version TER downloads

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).