{"id":36328086,"url":"https://github.com/sandermuller/stopwatch","last_synced_at":"2026-04-26T21:01:10.618Z","repository":{"id":245228327,"uuid":"817629000","full_name":"SanderMuller/Stopwatch","owner":"SanderMuller","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-21T08:22:38.000Z","size":162,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-21T10:24:22.213Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SanderMuller.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2024-06-20T06:10:35.000Z","updated_at":"2026-04-21T08:22:42.000Z","dependencies_parsed_at":"2025-01-05T14:28:32.343Z","dependency_job_id":"86417386-e8a3-414f-bef1-cdce5f5a1658","html_url":"https://github.com/SanderMuller/Stopwatch","commit_stats":null,"previous_names":["sandermuller/stopwatch"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/SanderMuller/Stopwatch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2FStopwatch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2FStopwatch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2FStopwatch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2FStopwatch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SanderMuller","download_url":"https://codeload.github.com/SanderMuller/Stopwatch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SanderMuller%2FStopwatch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32312505,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T19:15:34.056Z","status":"ssl_error","status_checked_at":"2026-04-26T19:15:15.467Z","response_time":129,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-11T11:56:20.439Z","updated_at":"2026-04-26T21:01:10.609Z","avatar_url":"https://github.com/SanderMuller.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Stopwatch for PHP \u0026 Laravel\n\n[![Latest Version on Packagist](https://img.shields.io/packagist/v/sandermuller/stopwatch.svg?style=flat-square)](https://packagist.org/packages/sandermuller/stopwatch)\n[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/sandermuller/stopwatch/run-tests.yml?branch=main\u0026label=tests\u0026style=flat-square)](https://github.com/sandermuller/stopwatch/actions/workflows/run-tests.yml)\n[![GitHub PHPStan Action Status](https://img.shields.io/github/actions/workflow/status/sandermuller/stopwatch/phpstan.yml?branch=main\u0026label=phpstan\u0026style=flat-square)](https://github.com/sandermuller/stopwatch/actions?query=workflow%3Aphpstan+branch%3Amain)\n[![Total Downloads](https://img.shields.io/packagist/dt/sandermuller/stopwatch.svg?style=flat-square)](https://packagist.org/packages/sandermuller/stopwatch)\n[![License](https://img.shields.io/github/license/sandermuller/stopwatch.svg?style=flat-square)](LICENSE)\n[![Laravel Compatibility](https://badge.laravel.cloud/badge/sandermuller/stopwatch?style=flat)](https://packagist.org/packages/sandermuller/stopwatch)\n\nA 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.\n\n**Requires PHP 8.3+**\n\n## Installation\n\nYou can install the package via composer:\n\n```bash\ncomposer require sandermuller/stopwatch\n```\n\nOptionally publish the config file:\n\n```bash\nphp artisan vendor:publish --tag=stopwatch-config\n```\n\n## Configuration\n\nAll settings can be configured via environment variables or the `config/stopwatch.php` file:\n\n| Setting            | Env Variable                 | Default  | Description                                              |\n|--------------------|------------------------------|----------|----------------------------------------------------------|\n| `enabled`          | `STOPWATCH_ENABLED`          | `true`   | Disable to make all calls no-ops with near-zero overhead |\n| `output`           | `STOPWATCH_OUTPUT`           | `silent` | Default output mode (`silent`, `log`, `stderr`, `dump`)  |\n| `log_level`        | `STOPWATCH_LOG_LEVEL`        | `debug`  | Log level when output is `log`                           |\n| `slow_threshold`   | `STOPWATCH_SLOW_THRESHOLD`   | `50`     | Highlight checkpoints slower than this (ms)              |\n| `track_queries`    | `STOPWATCH_TRACK_QUERIES`    | `false`  | Auto-track query count and duration per checkpoint       |\n| `track_memory`     | `STOPWATCH_TRACK_MEMORY`     | `false`  | Auto-track memory usage per checkpoint                   |\n| `notify_threshold` | `STOPWATCH_NOTIFY_THRESHOLD` | `null`   | Notify via channels if total duration exceeds this (ms)  |\n| `mail.to`          | `STOPWATCH_MAIL_TO`          | `null`   | Recipient address for `MailChannel` notifications        |\n| `mail.subject`     | `STOPWATCH_MAIL_SUBJECT`     | `null`   | Email subject (defaults to duration if not set)          |\n\n## Usage\n\n### Checkpoints\n\n```php\nstopwatch()-\u003echeckpoint('First checkpoint');\nstopwatch()-\u003echeckpoint('Second checkpoint');\nstopwatch()-\u003elap('Third checkpoint'); // alias for checkpoint()\n```\n\nCalling `checkpoint()` auto-starts the stopwatch if it hasn't been started yet. You can also start it explicitly with `stopwatch()-\u003estart()`. Note that `start()` resets any existing checkpoints, use it to begin a fresh measurement.\n\nYou can attach metadata to any checkpoint:\n\n```php\nstopwatch()-\u003echeckpoint('Query executed', ['table' =\u003e 'users', 'rows' =\u003e 42]);\n```\n\n### Output each checkpoint\n\nConfigure where each checkpoint is emitted using `outputTo()`:\n\n```php\nuse SanderMuller\\Stopwatch\\StopwatchOutput;\n\nstopwatch()-\u003eoutputTo(StopwatchOutput::Log)-\u003estart();\n\nstopwatch()-\u003echeckpoint('First checkpoint');  // Automatically logged\nstopwatch()-\u003echeckpoint('Second checkpoint'); // Automatically logged\n```\n\nAvailable output modes:\n\n| Mode                      | Description                          |\n|---------------------------|--------------------------------------|\n| `StopwatchOutput::Silent` | Collect only, render later (default) |\n| `StopwatchOutput::Log`    | Send to Laravel log                  |\n| `StopwatchOutput::Stderr` | Write to stderr                      |\n| `StopwatchOutput::Dump`   | Use Laravel's `dump()`               |\n\nYou can override the output for a single checkpoint:\n\n```php\nstopwatch()-\u003echeckpoint('Debug this', output: StopwatchOutput::Dump);\n```\n\nOr use the `log()` shortcut to send a single checkpoint to the log:\n\n```php\nstopwatch()-\u003elog('Query executed');\nstopwatch()-\u003elog('Query executed', level: 'warning');\n```\n\n### Measure a closure\n\nWrap a closure to automatically create a checkpoint after execution. Auto-starts the stopwatch if needed.\n\n```php\n$result = stopwatch()-\u003emeasure('Heavy computation', function () {\n    return doExpensiveWork();\n});\n```\n\n### Query tracking\n\nAutomatically track the number of database queries and their total duration between each checkpoint. Requires `illuminate/database`.\n\n```php\nstopwatch()-\u003ewithQueryTracking()-\u003estart();\n\nUser::all();\nstopwatch()-\u003echeckpoint('Load users');\n// Checkpoint includes: 1q / 2.3ms\n\nOrder::where('status', 'pending')-\u003eget();\nstopwatch()-\u003echeckpoint('Load orders');\n// Checkpoint includes: 1q / 1.5ms\n```\n\nCan also be enabled via config (`STOPWATCH_TRACK_QUERIES=true`).\n\n### Memory tracking\n\nTrack memory usage changes between each checkpoint:\n\n```php\nstopwatch()-\u003ewithMemoryTracking()-\u003estart();\n\n$data = loadLargeDataset();\nstopwatch()-\u003echeckpoint('Load data');\n// Checkpoint includes: +2.4MB\n```\n\nIn 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`).\n\nBoth tracking methods can be combined:\n\n```php\nstopwatch()-\u003ewithQueryTracking()-\u003ewithMemoryTracking()-\u003estart();\n```\n\n### Write a full report\n\nWrite all checkpoints and the total duration to stderr or your log:\n\n```php\nstopwatch()-\u003echeckpoint('Validation');\nstopwatch()-\u003echeckpoint('DB inserts');\n\n// Write to stderr\nstopwatch()-\u003etoStderr('Profile:');\n\n// Or write to the log\nstopwatch()-\u003etoLog('Profile:', level: 'info');\n```\n\n### Conditional notifications\n\nGet notified when a request or operation exceeds a time threshold. Notifications are dispatched when the stopwatch finishes:\n\n```php\nstopwatch()-\u003enotifyIfSlowerThan(500);\n\nstopwatch()-\u003echeckpoint('Fetch order');\nstopwatch()-\u003echeckpoint('Generate PDF');\nstopwatch()-\u003echeckpoint('Upload to S3');\n\nstopwatch()-\u003efinish(); // notifications dispatch here if total \u003e= 500ms\n```\n\nThe threshold is also checked on implicit finishes (`render()`, `toArray()`, `toLog()`, `toStderr()`), and also accepts `CarbonInterval`:\n\n```php\nstopwatch()-\u003enotifyIfSlowerThan(CarbonInterval::seconds(2));\n```\n\nThe threshold and channels can be configured entirely via config/env:\n\n```env\nSTOPWATCH_NOTIFY_THRESHOLD=500\n```\n\nThis pairs well with the middleware. Every request that exceeds the threshold will trigger a notification automatically.\n\nOr set it programmatically in a service provider:\n\n```php\n// AppServiceProvider::boot()\nstopwatch()-\u003enotifyIfSlowerThan(500);\n```\n\nConfigure which channels are used in `config/stopwatch.php`:\n\n```php\n'notification_channels' =\u003e [\n    \\SanderMuller\\Stopwatch\\Notifications\\LogChannel::class,\n],\n```\n\n#### Email notifications\n\nAdd `MailChannel` to receive an email with the stopwatch's HTML report when a threshold is exceeded:\n\n```php\n'notification_channels' =\u003e [\n    \\SanderMuller\\Stopwatch\\Notifications\\LogChannel::class,\n    \\SanderMuller\\Stopwatch\\Notifications\\MailChannel::class,\n],\n```\n\nConfigure the recipient in your `.env`:\n\n```env\nSTOPWATCH_MAIL_TO=dev-team@example.com\nSTOPWATCH_MAIL_SUBJECT=\"Slow request detected\"  # optional\n```\n\nOr bind the channel with constructor arguments:\n\n```php\n$this-\u003eapp-\u003ebind(MailChannel::class, fn () =\u003e new MailChannel(\n    to: 'dev-team@example.com',\n    subject: 'Slow request',\n));\n```\n\n#### Custom notification channels\n\nCreate your own channel by implementing `StopwatchNotificationChannel`:\n\n```php\nuse SanderMuller\\Stopwatch\\Notifications\\StopwatchNotificationChannel;\nuse SanderMuller\\Stopwatch\\Stopwatch;\n\nclass SlackChannel implements StopwatchNotificationChannel\n{\n    public function notify(Stopwatch $stopwatch): void\n    {\n        Slack::message(\"Slow request: {$stopwatch-\u003etotalRunDurationReadable()}\");\n    }\n}\n```\n\nRegister it in your config:\n\n```php\n'notification_channels' =\u003e [\n    \\SanderMuller\\Stopwatch\\Notifications\\LogChannel::class,\n    \\App\\Stopwatch\\SlackChannel::class,\n],\n```\n\nOr set channels at runtime:\n\n```php\nstopwatch()-\u003enotifyUsing([new SlackChannel()]);\n```\n\n### Render as HTML\n\nRender an HTML report with the total execution time, each checkpoint, and the time between them. Slow checkpoints are highlighted.\n\n```php\nstopwatch()-\u003echeckpoint('First checkpoint');\nstopwatch()-\u003echeckpoint('Second checkpoint');\n\n// Render the output\n{{ stopwatch()-\u003erender() }}\n```\n\nOr use the Blade directive:\n\n```blade\n@stopwatch\n```\n\n![rendered-stopwatch.png](rendered-stopwatch.png)\n\nThe card is self-contained — all styles are inline so it drops into any host page (or email body) without picking up surrounding CSS. It includes:\n\n- **Smart duration formatting** that scales the unit so long profiles read clearly: `3.4ms`, `143ms`, `1.25s`, `1m 5s`. Available as a public helper too: `Stopwatch::formatDuration(1247)`.\n- **Slow severity tiers.** Checkpoints over the slow threshold get a tiered red signal — light (1×–2×), medium (2×–5×), heavy (5×+) — so you can tell a barely-slow row from a way-too-slow one at a glance.\n- **Overview bar** at the top with one colored segment per checkpoint, sized by share of total. Hovering a row cross-highlights its segment, and vice versa.\n- **Hover tooltip** per row with the full label, timestamp, delta vs cumulative, share, query and memory metrics.\n- **Footer totals** showing the cumulative query count, query time, and memory delta when the corresponding tracking is enabled.\n- **Copy as Markdown** button (clipboard icon, header) that copies a Markdown summary table to the clipboard — paste it into a chat with an AI assistant or a bug report. Available programmatically too: `stopwatch()-\u003etoMarkdown()`.\n- **Empty state** when no checkpoints have been recorded.\n\n#### Light + dark mode\n\nThe card respects `prefers-color-scheme` automatically, and includes a built-in toggle button (sun/moon, in the header) that lets users override the theme. The choice persists in `localStorage` under the `sw-theme` key. Pages that disallow JavaScript fall back to the system preference and the toggle is hidden.\n\n#### Custom CSS overrides\n\nThe card root is `.sw-stopwatch`. All themable surfaces are exposed as CSS variables (e.g. `--sw-bg`, `--sw-text`, `--sw-border`, `--sw-hover-bg`, `--sw-tip-bg`). To re-skin without forking the renderer, override these on `.sw-stopwatch` (or its `[data-theme=\"dark\"]` variant) in your application stylesheet.\n\n#### Print\n\nA `@media print` rule strips shadows, drops the toggle button and tooltips, expands the card to full width, and disables the bar grow-in animation, so PDF exports of an HTML profile look clean.\n\n### Laravel Debugbar\n\nIf you have [barryvdh/laravel-debugbar](https://github.com/barryvdh/laravel-debugbar) installed, checkpoint timings automatically appear as a timeline tab in Debugbar with a duration badge.\n\n### Server-Timing header\n\nAdd a `Server-Timing` HTTP header to your responses so you can inspect checkpoint timings in the browser's DevTools Network tab.\n\nRegister the middleware to automatically add the header whenever the stopwatch has been started:\n\n```php\n// bootstrap/app.php\nuse SanderMuller\\Stopwatch\\StopwatchMiddleware;\n\nreturn Application::configure(basePath: dirname(__DIR__))\n    -\u003ewithMiddleware(function (Middleware $middleware) {\n        $middleware-\u003eappend(StopwatchMiddleware::class);\n    })\n    // ...\n```\n\nBy 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()-\u003estart()` or `stopwatch()-\u003echeckpoint()`). Requests where the stopwatch is never started will not have the header.\n\nTo auto-start the stopwatch on every request, use `StopwatchMiddleware::autoStart()`:\n\n```php\n$middleware-\u003eappend(StopwatchMiddleware::autoStart());\n```\n\nOr add the header manually without the middleware:\n\n```php\nreturn response('OK')\n    -\u003eheader('Server-Timing', stopwatch()-\u003etoServerTiming());\n```\n\n### Manually stop the stopwatch\n\nYou can manually stop the stopwatch to freeze the timing. It will also stop automatically when output is rendered (e.g. `render()`, `toArray()`, `toStderr()`).\n\n```php\nstopwatch()-\u003echeckpoint('First checkpoint');\n\n// Stop the stopwatch\nstopwatch()-\u003estop();\n\n// Do something else you don't want to measure\n\n// Finally render the output\n{{ stopwatch()-\u003erender() }}\n```\n\nYou can get the total duration as a string with `stopwatch()-\u003etoString()` (e.g. `\"116ms\"`).\n\n### Enable / disable at runtime\n\nEnable or disable the stopwatch at runtime. When disabled, all calls become no-ops:\n\n```php\nstopwatch()-\u003edisable();\n\nstopwatch()-\u003echeckpoint('Skipped'); // no-op\n\nstopwatch()-\u003eenable();\n```\n\n### Serialization\n\nConvert the stopwatch data to an array or JSON:\n\n```php\n$data = stopwatch()-\u003etoArray();\n$json = stopwatch()-\u003etoJson();\n```\n\n### Debugging\n\n```php\nstopwatch()-\u003edump(); // dump the stopwatch instance\nstopwatch()-\u003edd();   // dump and die\n```\n\n### Without Laravel\n\nYou can use the stopwatch without the Laravel helper by creating instances directly:\n\n```php\n$stopwatch = \\SanderMuller\\Stopwatch\\Stopwatch::new();\n$stopwatch-\u003estart();\n$stopwatch-\u003echeckpoint('Done');\necho $stopwatch-\u003etoString();\n```\n\nThe `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.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsandermuller%2Fstopwatch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsandermuller%2Fstopwatch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsandermuller%2Fstopwatch/lists"}