symfony/stopwatch
Symfony Stopwatch provides lightweight code profiling: start/stop named events, record laps, and group timings into sections/phases. Useful for measuring execution time and memory across parts of an application, with simple API and Composer install.
Installation:
composer require symfony/stopwatch
No additional configuration is required—Stopwatch is dependency-free.
First Use Case: Profile a controller method or service to identify bottlenecks:
use Symfony\Component\Stopwatch\Stopwatch;
public function index(Stopwatch $stopwatch)
{
$event = $stopwatch->start('process_order');
// Simulate work
$this->orderService->process();
$event->stop();
// Log or display results
$duration = $event->getDuration();
logger()->info("Order processing took {$duration}ms", ['event' => $event->getName()]);
}
Where to Look First:
Stopwatch and StopwatchEvent classes.Stopwatch in controllers/services.dd($event) to inspect timing data (duration, memory, laps).Granular Profiling in Controllers:
public function show(Stopwatch $stopwatch)
{
$stopwatch->start('fetch_user_data');
$user = $this->userRepository->find($id);
$stopwatch->stop('fetch_user_data');
$stopwatch->start('render_view');
return view('user.show', compact('user'));
$stopwatch->stop('render_view');
}
Middleware Profiling:
public function handle($request, Closure $next, Stopwatch $stopwatch)
{
$event = $stopwatch->start('auth_middleware');
$response = $next($request);
$event->stop();
return $response;
}
CLI Command Benchmarking:
protected function execute(InputInterface $input, OutputInterface $output, Stopwatch $stopwatch)
{
$event = $stopwatch->start('import_users');
$this->importUsers();
$event->stop();
$output->writeln(sprintf("Import took %dms", $event->getDuration()));
}
Nested Sections for Phased Workflows:
$stopwatch->openSection('checkout_flow');
$stopwatch->start('validate_cart');
// ... validate
$stopwatch->stop('validate_cart');
$stopwatch->start('process_payment');
// ... process
$stopwatch->stop('process_payment');
$stopwatch->stopSection('checkout_flow');
Debugbar::addMeasure('stopwatch', $event->getDuration(), 'ms');
if ($event->getDuration() > 500) {
logger()->warning("Slow event: {$event->getName()}", $event->toArray());
}
public function testOrderProcessing()
{
$stopwatch = new Stopwatch();
$event = $stopwatch->start('process_order');
$this->app->make(OrderService::class)->process();
$event->stop();
$this->assertLessThan(100, $event->getDuration());
}
$this->app->bind('profiler', Stopwatch::class);
Memory Leaks:
stop() or let events go out of scope:
// Bad: Holds reference indefinitely
$event = $stopwatch->start('event');
// ... long-running code
// Forgot to stop!
try-finally or Laravel’s finally in middleware/controllers.Lap Timing Quirks:
lap()) do not reset the event timer. They record elapsed time since the event started:
$stopwatch->start('event');
$stopwatch->lap('first_step'); // Records time from start()
sleep(1);
$stopwatch->lap('second_step'); // Records cumulative time
$stopwatch->stop('event');
Section Nesting:
$stopwatch->openSection('outer');
$stopwatch->openSection('inner');
// ... code
$stopwatch->stopSection('outer'); // Throws if 'inner' is still open!
PHP 8.1+ Requirement:
^7.0:
composer require symfony/stopwatch:^7.0
composer.json constraints to avoid accidental upgrades.Event Naming Collisions:
$stopwatch->start('event');
$stopwatch->stop('event'); // First event
$stopwatch->start('event'); // Overwrites!
$stopwatch->stop('event'); // Second event (first lost)
$eventName = "OrderService::process_{$order->id}";
dd($event->toArray());
// Outputs:
// [
// 'name' => 'eventName',
// 'duration' => 123.45, // ms
// 'memory' => 1024, // bytes
// 'startTime' => 1234567890,
// 'laps' => [...],
// ]
$stopwatch = new Stopwatch(true); // Enable memory
public function boot()
{
$this->app->resolving(Stopwatch::class, function ($stopwatch) {
$stopwatch->getClient()->listen(function (StopwatchEvent $event) {
logger()->debug("Stopwatch: {$event->getName()}", $event->toArray());
});
});
}
Custom Event Classes:
Extend StopwatchEvent to add metadata:
class CustomEvent extends StopwatchEvent
{
public function __construct(string $name, float $startTime, bool $memory = false)
{
parent::__construct($name, $startTime, $memory);
$this->setData(['custom_field' => 'value']);
}
}
Register with Stopwatch:
$stopwatch = new Stopwatch();
$stopwatch->getClient()->setEventClass(CustomEvent::class);
Storage Backends: Persist events to a database or cache:
$stopwatch->getClient()->listen(function (StopwatchEvent $event) {
cache()->put("stopwatch:{$event->getName()}", $event->toArray(), now()->addHours(1));
});
Laravel Telescope Integration: Publish Stopwatch data to Telescope:
Telescope::addData([$event->getName() => $event->toArray()]);
Conditional Profiling: Disable Stopwatch in production:
$stopwatch = app()->environment('local') ? new Stopwatch() : new class {
public function start($name) { return $this; }
public function stop($name) { return $this; }
};
How can I help you explore Laravel packages today?