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.
Build vs. buy — and the real decision is what to build. The timer itself is a commodity: MIT-licensed, zero dependencies, ~a dozen classes, no config, no I/O. Buying it costs nothing. What no one sells is the instrumentation contract around it — event naming taxonomy, section conventions, threshold policy, output format. Stopwatch has no reporter, no sink, no serialization contract, so every team that adopts it writes the same thin wrapper. This package lets us make one deliberate decision — standardize a Profiler wrapper and facade — instead of letting ten squads improvise four dialects of db.query that can never be joined. That wrapper is the product; Stopwatch is a dependency of it.
Roadmap: a bounded performance-reliability track, not an observability program. The package supports a 2–3 sprint initiative with a clear end state: phase timing on the paths that carry the SLO (checkout, search ranking, auth), a pre-merge performance gate on the two most latency-sensitive paths, and JSON-lines output into an existing log channel. No new infrastructure, no data migration, no schema change, no backfill.
Prioritization of where to instrument first — queue jobs and Artisan commands, then HTTP middleware. This is the decision the package most directly informs. APM coverage is weakest on background jobs and console commands and the ROI is highest there; middleware timing is largely already covered by whatever APM we run. Instrumenting everything at once is the classic failure mode.
Feature-level decisions it accelerates: which code paths get phase boundaries (fetch → transform → persist), whether a given job class gets a duration budget, and whether staging requests are sampled for endpoint profiling. It also forces an explicit non-goal list — this is not an APM replacement, not a micro-benchmarking tool (durations are integer milliseconds; sub-millisecond work floors to zero), and not a flame-graph profiler. Debugbar and Telescope cover the dev-time introspection job better; we should not rebuild them.
Scope discipline worth naming up front: the package does nothing until someone writes start()/stop(). If we are not prepared to make manual instrumentation a team norm with a named owner, this initiative should not start — the tool is not at fault, the adoption model is.
Adopt when:
vendor/ transitively (verify with composer why symfony/stopwatch before adding a direct constraint) — the cost is a version pin, not an install.Look elsewhere when:
scoped rather than singleton and reset per unit of work.start/stop calls. Adoption will stall regardless of package quality.Two operational caveats to resolve before committing to a constraint: the listed last release date is future-dated relative to a normal review cadence — treat it as a pipeline artifact and verify against Packagist and the GitHub tag list. And floor at Symfony 7.2+ (for Stopwatch::ROOT and getLastPeriod(), both real ergonomics wins), matching the Symfony major the framework already resolves; v8 is gated on a PHP 8.4 upgrade. Do not cite the dependents: 0 figure as adoption evidence — it is a metadata artifact of the Symfony monorepo split.
To executives: This is a one-week, one-line-rollback investment that buys us accountability on the handful of code paths that carry revenue. It adds no infrastructure, no vendor contract, no data-egress surface, and no compliance finding — MIT-licensed, zero runtime dependencies, maintained under Symfony governance. The deliverable is not "a profiler"; it is a shared standard for how we measure and a gate that stops latency regressions before they ship. Total ask is roughly 5–8 engineering days across two phases plus one named owner. The dominant risk is not breakage — it is false confidence from instrumentation nobody acts on, which we mitigate by tying every measurement to a threshold and capping ourselves at two blocking gates.
To engineering: It is ~a dozen classes, no I/O, no container coupling, droppable at any layer. Highest-value hooks in ROI order: queue job middleware (no base-class tax), an Artisan --profile flag, then sampled HTTP middleware, then three to five targeted service methods. Bind it as a scoped container singleton and put a thin Profiler facade in front of it — application code should never touch Stopwatch directly, and that single decision is what keeps the OpenTelemetry exit ramp open. Two API footguns to design around now: getDuration() returns integer milliseconds, and calling start() on an already-started event throws a LogicException that surfaces as a 500 the first time re-entrant code hits it. The wrapper absorbs both.
The ask, stated plainly: approve Phase 1 (provider, facade, job middleware, --profile flag, log channel) and Phase 2 (threshold config, test helper, two blocking CI gates), assign an owner for the naming taxonomy and gate-failure policy, and agree that the Stopwatch binding is scoped, not singleton. Everything else — how many paths we instrument, how strict the budgets are — is a reversible config change we can tune without a second approval.
How can I help you explore Laravel packages today?