symfony/stopwatch
Symfony Stopwatch profiles PHP code: start and stop named events, group them into sections, and record lap times to measure where execution time goes. Install via Composer.
Architecture fit
Integration feasibility
composer require symfony/stopwatch. Laravel 11/12 already pulls Symfony components, so the package is likely already in vendor/ transitively (notably via Debugbar's timeline). Verify with composer why symfony/stopwatch before adding a direct constraint — the risk is not installation, it is version locking against the Symfony major already pinned by the framework.dependents: 0 figure is a metadata artifact, not a signal. Stopwatch ships through the Symfony monorepo split and is consumed indirectly across the ecosystem. Do not present that number to stakeholders as adoption evidence, in either direction.start / stop / lap, openSection / stopSection, getEvent, getSectionEvents, plus StopwatchEvent::getDuration(), getMemory(), getPeriods(). Symfony 7.2 added Stopwatch::ROOT and getLastPeriod(), which meaningfully improve ergonomics — a reason to floor at 7.2+ rather than 6.4 LTS.getDuration() returns integer milliseconds (sub-millisecond work floors to zero — fine for endpoint/query profiling, disqualifying for micro-benchmarks), and start() on an already-started event throws LogicException, which surfaces as a 500 the first time re-entrant code or double-applied middleware hits it.composer.json. This is a five-minute check that protects the credibility of the whole proposal.Technical risk
lap() inside a loop over 100k iterations and the StopwatchEvent period array becomes the memory problem it was meant to detect. Instrumentation not tied to a threshold, a CI gate, or a dashboard is theater.db.query, db_query, query:users — un-joinable, un-aggregable. That is a governance cost we pay later at roughly five times the price.start/stop calls. If the team will not add them, the package fails regardless of its quality, and no amount of wrapper tooling fixes it.Key questions
scoped, not singleton?Stack fit
Highest-value insertion points in a Laravel application, in ROI order:
| Priority | Surface | Why | Effort |
|---|---|---|---|
| 1 | Queue jobs (job middleware) | Weakest existing APM coverage; clean hook with no base-class tax | ~1 day |
| 2 | Artisan commands (--profile flag) |
Named phases (fetch / transform / persist) map naturally to sections | ~0.5 day |
| 3 | HTTP middleware | One outer event plus named sub-events for DB, cache, outbound HTTP — sampled, staging-first | ~1.5 days |
| 4 | Targeted service methods | The 3–5 paths carrying the SLO: checkout, search ranking, auth | Per-path |
$this->app->scoped(Stopwatch::class)) and expose a thin Profiler facade that owns the naming taxonomy, feature-flag gating, and the serializer. Application code must never touch Stopwatch directly — that is the single architectural decision that keeps the exit ramp to OTel or a vendor APM open.Migration path
^7.2|^8.0 constraint aligned to the framework; confirm the release-date anomaly against Packagist.ProfilerServiceProvider, Profiler facade, ProfilingJobMiddleware (Laravel job middleware is the cleanest hook — no inheritance, no base-class tax), an Artisan --profile flag, and a JSON-lines log channel.Compatibility
ROOT and getLastPeriod()) and treat v8 as gated on a PHP 8.4 upgrade. Do not let v8's PHP 8.4 requirement silently force an unrelated platform upgrade into this initiative's scope.Sequencing
Maintenance
Support
getDuration() returned 0 (integer-ms flooring), and why a 500 appeared from a double start(). Pre-empt all four in a one-page README; this eliminates most of the tail.Scaling
lap() call count. Cap laps, or forbid laps inside loops over large datasets, in the wrapper.Failure modes
| Failure | Blast radius | Mitigation |
|---|---|---|
Double start() → LogicException |
500 on a previously healthy path | ensureStarted() in the wrapper; static analysis rule against direct Stopwatch use |
| Octane binding not reset | Memory growth; corrupted timings | scoped() binding + per-unit-of-work reset test |
Unbounded lap() in loops |
Memory pressure proportional to iteration count | Wrapper cap; code review checklist |
Integer-ms flooring to 0 |
Teams conclude the path is free; micro-optimization chases nothing | Document unit and precision limits in the README |
| Un-joinable event names | Dashboards unusable; gate thresholds meaningless | Taxonomy owned by one named person before rollout |
| Flaky timing gate | Merge friction; initiative loses credibility | Gate only two paths; generous initial budget; tune down over sprints |
| Orphaned wrapper after owner moves | Rot, then silent removal | Ownership assigned at approval; quarterly review on the calendar |
Ramp-up
Success criteria to agree before kickoff
If those four are not agreed up front, the honest recommendation is to adopt Stopwatch informally as a developer tool with no wrapper, no gate, and no roadmap commitment — and revisit only when a regression actually escapes.
How can I help you explore Laravel packages today?