sandermuller / stopwatch
Stopwatch to measure execution times (profile code) for Laravel and PHP projects
Requires
- php: ^8.3
- illuminate/collections: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- nesbot/carbon: ^2|^3.10.3
- spatie/laravel-package-tools: ^1.92.7
- symfony/var-dumper: ^7.3.3|^8.0
Requires (Dev)
- fakerphp/faker: ^1.24
- illuminate/database: ^11.0|^12.0|^13.0
- laravel/pint: ^1.24
- maximebf/debugbar: ^3.5
- nunomaduro/collision: ^8.8.2
- orchestra/testbench: ^9.6|^10.6|^11.0
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^2.1.25
- phpstan/phpstan-deprecation-rules: ^2.0.3
- phpstan/phpstan-strict-rules: ^2.0.6
- rector/rector: ^2.1.7
- rector/type-perfect: ^2.1
- roave/security-advisories: dev-latest
- spaze/phpstan-disallowed-calls: ^4.6
- symplify/phpstan-extensions: ^12.0.1
- tomasvotruba/cognitive-complexity: ^1.0
- tomasvotruba/type-coverage: ^2.0.2
Suggests
- barryvdh/laravel-debugbar: Display stopwatch checkpoints as a timeline in Debugbar
This package is auto-updated.
Last update: 2026-03-24 16:36:01 UTC
README
A lightweight profiler for PHP and Laravel. Add checkpoints to your code, measure closures, track queries and memory, and see where time is spent. Output as HTML, Server-Timing headers, log entries, or Debugbar timelines.
Requires PHP 8.3+
Installation
You can install the package via composer:
composer require sandermuller/stopwatch
Optionally publish the config file:
php artisan vendor:publish --tag=stopwatch-config
Configuration
All settings can be configured via environment variables or the config/stopwatch.php file:
| Setting | Env Variable | Default | Description |
|---|---|---|---|
enabled |
STOPWATCH_ENABLED |
true |
Disable to make all calls no-ops with near-zero overhead |
output |
STOPWATCH_OUTPUT |
silent |
Default output mode (silent, log, stderr, dump) |
log_level |
STOPWATCH_LOG_LEVEL |
debug |
Log level when output is log |
slow_threshold |
STOPWATCH_SLOW_THRESHOLD |
50 |
Highlight checkpoints slower than this (ms) |
track_queries |
STOPWATCH_TRACK_QUERIES |
false |
Auto-track query count and duration per checkpoint |
track_memory |
STOPWATCH_TRACK_MEMORY |
false |
Auto-track memory usage per checkpoint |
notify_threshold |
STOPWATCH_NOTIFY_THRESHOLD |
null |
Notify via channels if total duration exceeds this (ms) |
mail.to |
STOPWATCH_MAIL_TO |
null |
Recipient address for MailChannel notifications |
mail.subject |
STOPWATCH_MAIL_SUBJECT |
null |
Email subject (defaults to duration if not set) |
Usage
Checkpoints
stopwatch()->checkpoint('First checkpoint'); stopwatch()->checkpoint('Second checkpoint'); stopwatch()->lap('Third checkpoint'); // alias for checkpoint()
Calling checkpoint() auto-starts the stopwatch if it hasn't been started yet. You can also start it explicitly with stopwatch()->start(). Note that start() resets any existing checkpoints, use it to begin a fresh measurement.
You can attach metadata to any checkpoint:
stopwatch()->checkpoint('Query executed', ['table' => 'users', 'rows' => 42]);
Output each checkpoint
Configure where each checkpoint is emitted using outputTo():
use SanderMuller\Stopwatch\StopwatchOutput; stopwatch()->outputTo(StopwatchOutput::Log)->start(); stopwatch()->checkpoint('First checkpoint'); // Automatically logged stopwatch()->checkpoint('Second checkpoint'); // Automatically logged
Available output modes:
| Mode | Description |
|---|---|
StopwatchOutput::Silent |
Collect only, render later (default) |
StopwatchOutput::Log |
Send to Laravel log |
StopwatchOutput::Stderr |
Write to stderr |
StopwatchOutput::Dump |
Use Laravel's dump() |
You can override the output for a single checkpoint:
stopwatch()->checkpoint('Debug this', output: StopwatchOutput::Dump);
Or use the log() shortcut to send a single checkpoint to the log:
stopwatch()->log('Query executed'); stopwatch()->log('Query executed', level: 'warning');
Measure a closure
Wrap a closure to automatically create a checkpoint after execution. Auto-starts the stopwatch if needed.
$result = stopwatch()->measure('Heavy computation', function () { return doExpensiveWork(); });
Query tracking
Automatically track the number of database queries and their total duration between each checkpoint. Requires illuminate/database.
stopwatch()->withQueryTracking()->start(); User::all(); stopwatch()->checkpoint('Load users'); // Checkpoint includes: 1q / 2.3ms Order::where('status', 'pending')->get(); stopwatch()->checkpoint('Load orders'); // Checkpoint includes: 1q / 1.5ms
Can also be enabled via config (STOPWATCH_TRACK_QUERIES=true).
Memory tracking
Track memory usage changes between each checkpoint:
stopwatch()->withMemoryTracking()->start(); $data = loadLargeDataset(); stopwatch()->checkpoint('Load data'); // Checkpoint includes: +2.4MB
In the HTML output, memory is shown as a compact delta badge with full details on hover (current usage, delta, peak). In plain-text output (toStderr, toLog), the delta is included inline. Can also be enabled via config (STOPWATCH_TRACK_MEMORY=true).
Both tracking methods can be combined:
stopwatch()->withQueryTracking()->withMemoryTracking()->start();
Write a full report
Write all checkpoints and the total duration to stderr or your log:
stopwatch()->checkpoint('Validation'); stopwatch()->checkpoint('DB inserts'); // Write to stderr stopwatch()->toStderr('Profile:'); // Or write to the log stopwatch()->toLog('Profile:', level: 'info');
Conditional notifications
Get notified when a request or operation exceeds a time threshold. Notifications are dispatched when the stopwatch finishes:
stopwatch()->notifyIfSlowerThan(500); stopwatch()->checkpoint('Fetch order'); stopwatch()->checkpoint('Generate PDF'); stopwatch()->checkpoint('Upload to S3'); stopwatch()->finish(); // notifications dispatch here if total >= 500ms
The threshold is also checked on implicit finishes (render(), toArray(), toLog(), toStderr()), and also accepts CarbonInterval:
stopwatch()->notifyIfSlowerThan(CarbonInterval::seconds(2));
The threshold and channels can be configured entirely via config/env:
STOPWATCH_NOTIFY_THRESHOLD=500
This pairs well with the middleware. Every request that exceeds the threshold will trigger a notification automatically.
Or set it programmatically in a service provider:
// AppServiceProvider::boot() stopwatch()->notifyIfSlowerThan(500);
Configure which channels are used in config/stopwatch.php:
'notification_channels' => [ \SanderMuller\Stopwatch\Notifications\LogChannel::class, ],
Email notifications
Add MailChannel to receive an email with the stopwatch's HTML report when a threshold is exceeded:
'notification_channels' => [ \SanderMuller\Stopwatch\Notifications\LogChannel::class, \SanderMuller\Stopwatch\Notifications\MailChannel::class, ],
Configure the recipient in your .env:
STOPWATCH_MAIL_TO=dev-team@example.com STOPWATCH_MAIL_SUBJECT="Slow request detected" # optional
Or bind the channel with constructor arguments:
$this->app->bind(MailChannel::class, fn () => new MailChannel( to: 'dev-team@example.com', subject: 'Slow request', ));
Custom notification channels
Create your own channel by implementing StopwatchNotificationChannel:
use SanderMuller\Stopwatch\Notifications\StopwatchNotificationChannel; use SanderMuller\Stopwatch\Stopwatch; class SlackChannel implements StopwatchNotificationChannel { public function notify(Stopwatch $stopwatch): void { Slack::message("Slow request: {$stopwatch->totalRunDurationReadable()}"); } }
Register it in your config:
'notification_channels' => [ \SanderMuller\Stopwatch\Notifications\LogChannel::class, \App\Stopwatch\SlackChannel::class, ],
Or set channels at runtime:
stopwatch()->notifyUsing([new SlackChannel()]);
Render as HTML
Render an HTML report with the total execution time, each checkpoint, and the time between them. Slow checkpoints are highlighted.
stopwatch()->checkpoint('First checkpoint'); stopwatch()->checkpoint('Second checkpoint'); // Render the output {{ stopwatch()->render() }}
Or use the Blade directive:
@stopwatch
Laravel Debugbar
If you have barryvdh/laravel-debugbar installed, checkpoint timings automatically appear as a timeline tab in Debugbar with a duration badge.
Server-Timing header
Add a Server-Timing HTTP header to your responses so you can inspect checkpoint timings in the browser's DevTools Network tab.
Register the middleware to automatically add the header whenever the stopwatch has been started:
// bootstrap/app.php use SanderMuller\Stopwatch\StopwatchMiddleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware) { $middleware->append(StopwatchMiddleware::class); }) // ...
By default the middleware is passive, it only adds the Server-Timing header if the stopwatch was started somewhere in your code (e.g. via stopwatch()->start() or stopwatch()->checkpoint()). Requests where the stopwatch is never started will not have the header.
To auto-start the stopwatch on every request, use StopwatchMiddleware::autoStart():
$middleware->append(StopwatchMiddleware::autoStart());
Or add the header manually without the middleware:
return response('OK') ->header('Server-Timing', stopwatch()->toServerTiming());
Manually stop the stopwatch
You can manually stop the stopwatch to freeze the timing. It will also stop automatically when output is rendered (e.g. render(), toArray(), toStderr()).
stopwatch()->checkpoint('First checkpoint'); // Stop the stopwatch stopwatch()->stop(); // Do something else you don't want to measure // Finally render the output {{ stopwatch()->render() }}
You can get the total duration as a string with stopwatch()->toString() (e.g. "116ms").
Enable / disable at runtime
Enable or disable the stopwatch at runtime. When disabled, all calls become no-ops:
stopwatch()->disable(); stopwatch()->checkpoint('Skipped'); // no-op stopwatch()->enable();
Serialization
Convert the stopwatch data to an array or JSON:
$data = stopwatch()->toArray(); $json = stopwatch()->toJson();
Debugging
stopwatch()->dump(); // dump the stopwatch instance stopwatch()->dd(); // dump and die
Without Laravel
You can use the stopwatch without the Laravel helper by creating instances directly:
$stopwatch = \SanderMuller\Stopwatch\Stopwatch::new(); $stopwatch->start(); $stopwatch->checkpoint('Done'); echo $stopwatch->toString();
The stopwatch() helper is not available outside Laravel. Query tracking requires illuminate/database and a Laravel application. Config-based setup and notification channel resolution from class strings also require the Laravel container.
License
MIT
