├── composer.json ├── CHANGELOG.md ├── README.md ├── LICENSE ├── StopwatchPeriod.php ├── Section.php ├── Stopwatch.php └── StopwatchEvent.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/stopwatch", 3 | "type": "library", 4 | "description": "Provides a way to profile code", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/service-contracts": "^2.5|^3" 21 | }, 22 | "autoload": { 23 | "psr-4": { "Symfony\\Component\\Stopwatch\\": "" }, 24 | "exclude-from-classmap": [ 25 | "/Tests/" 26 | ] 27 | }, 28 | "minimum-stability": "dev" 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.2 5 | --- 6 | 7 | * Add method `getLastPeriod()` to `StopwatchEvent` 8 | * Add `getRootSectionEvents()` method and `ROOT` constant to `Stopwatch` 9 | 10 | 5.2 11 | --- 12 | 13 | * Add `name` argument to the `StopWatchEvent` constructor, accessible via a new `StopwatchEvent::getName()` 14 | 15 | 5.0.0 16 | ----- 17 | 18 | * Removed support for passing `null` as 1st (`$id`) argument of `Section::get()` method, pass a valid child section identifier instead. 19 | 20 | 4.4.0 21 | ----- 22 | 23 | * Deprecated passing `null` as 1st (`$id`) argument of `Section::get()` method, pass a valid child section identifier instead. 24 | 25 | 3.4.0 26 | ----- 27 | 28 | * added the `Stopwatch::reset()` method 29 | * allowed to measure sub-millisecond times by introducing an argument to the 30 | constructor of `Stopwatch` 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stopwatch Component 2 | =================== 3 | 4 | The Stopwatch component provides a way to profile code. 5 | 6 | Getting Started 7 | --------------- 8 | 9 | ```bash 10 | composer require symfony/stopwatch 11 | ``` 12 | 13 | ```php 14 | use Symfony\Component\Stopwatch\Stopwatch; 15 | 16 | $stopwatch = new Stopwatch(); 17 | 18 | // optionally group events into sections (e.g. phases of the execution) 19 | $stopwatch->openSection(); 20 | 21 | // starts event named 'eventName' 22 | $stopwatch->start('eventName'); 23 | 24 | // ... run your code here 25 | 26 | // optionally, start a new "lap" time 27 | $stopwatch->lap('foo'); 28 | 29 | // ... run your code here 30 | 31 | $event = $stopwatch->stop('eventName'); 32 | 33 | $stopwatch->stopSection('phase_1'); 34 | ``` 35 | 36 | Resources 37 | --------- 38 | 39 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 40 | * [Report issues](https://github.com/symfony/symfony/issues) and 41 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 42 | in the [main Symfony repository](https://github.com/symfony/symfony) 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /StopwatchPeriod.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Stopwatch; 13 | 14 | /** 15 | * Represents a Period for an Event. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class StopwatchPeriod 20 | { 21 | private int|float $start; 22 | private int|float $end; 23 | private int $memory; 24 | 25 | /** 26 | * @param int|float $start The relative time of the start of the period (in milliseconds) 27 | * @param int|float $end The relative time of the end of the period (in milliseconds) 28 | * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision 29 | */ 30 | public function __construct(int|float $start, int|float $end, bool $morePrecision = false) 31 | { 32 | $this->start = $morePrecision ? (float) $start : (int) $start; 33 | $this->end = $morePrecision ? (float) $end : (int) $end; 34 | $this->memory = memory_get_usage(true); 35 | } 36 | 37 | /** 38 | * Gets the relative time of the start of the period in milliseconds. 39 | */ 40 | public function getStartTime(): int|float 41 | { 42 | return $this->start; 43 | } 44 | 45 | /** 46 | * Gets the relative time of the end of the period in milliseconds. 47 | */ 48 | public function getEndTime(): int|float 49 | { 50 | return $this->end; 51 | } 52 | 53 | /** 54 | * Gets the time spent in this period in milliseconds. 55 | */ 56 | public function getDuration(): int|float 57 | { 58 | return $this->end - $this->start; 59 | } 60 | 61 | /** 62 | * Gets the memory usage in bytes. 63 | */ 64 | public function getMemory(): int 65 | { 66 | return $this->memory; 67 | } 68 | 69 | public function __toString(): string 70 | { 71 | return \sprintf('%.2F MiB - %d ms', $this->getMemory() / 1024 / 1024, $this->getDuration()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Section.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Stopwatch; 13 | 14 | /** 15 | * Stopwatch section. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class Section 20 | { 21 | /** 22 | * @var StopwatchEvent[] 23 | */ 24 | private array $events = []; 25 | 26 | private ?string $id = null; 27 | 28 | /** 29 | * @var Section[] 30 | */ 31 | private array $children = []; 32 | 33 | /** 34 | * @param float|null $origin Set the origin of the events in this section, use null to set their origin to their start time 35 | * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision 36 | */ 37 | public function __construct( 38 | private ?float $origin = null, 39 | private bool $morePrecision = false, 40 | ) { 41 | } 42 | 43 | /** 44 | * Returns the child section. 45 | */ 46 | public function get(string $id): ?self 47 | { 48 | foreach ($this->children as $child) { 49 | if ($id === $child->getId()) { 50 | return $child; 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | /** 58 | * Creates or re-opens a child section. 59 | * 60 | * @param string|null $id Null to create a new section, the identifier to re-open an existing one 61 | */ 62 | public function open(?string $id): self 63 | { 64 | if (null === $id || null === $session = $this->get($id)) { 65 | $session = $this->children[] = new self(microtime(true) * 1000, $this->morePrecision); 66 | } 67 | 68 | return $session; 69 | } 70 | 71 | public function getId(): ?string 72 | { 73 | return $this->id; 74 | } 75 | 76 | /** 77 | * Sets the session identifier. 78 | * 79 | * @return $this 80 | */ 81 | public function setId(string $id): static 82 | { 83 | $this->id = $id; 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Starts an event. 90 | */ 91 | public function startEvent(string $name, ?string $category): StopwatchEvent 92 | { 93 | if (!isset($this->events[$name])) { 94 | $this->events[$name] = new StopwatchEvent($this->origin ?: microtime(true) * 1000, $category, $this->morePrecision, $name); 95 | } 96 | 97 | return $this->events[$name]->start(); 98 | } 99 | 100 | /** 101 | * Checks if the event was started. 102 | */ 103 | public function isEventStarted(string $name): bool 104 | { 105 | return isset($this->events[$name]) && $this->events[$name]->isStarted(); 106 | } 107 | 108 | /** 109 | * Stops an event. 110 | * 111 | * @throws \LogicException When the event has not been started 112 | */ 113 | public function stopEvent(string $name): StopwatchEvent 114 | { 115 | if (!isset($this->events[$name])) { 116 | throw new \LogicException(\sprintf('Event "%s" is not started.', $name)); 117 | } 118 | 119 | return $this->events[$name]->stop(); 120 | } 121 | 122 | /** 123 | * Stops then restarts an event. 124 | * 125 | * @throws \LogicException When the event has not been started 126 | */ 127 | public function lap(string $name): StopwatchEvent 128 | { 129 | return $this->stopEvent($name)->start(); 130 | } 131 | 132 | /** 133 | * Returns a specific event by name. 134 | * 135 | * @throws \LogicException When the event is not known 136 | */ 137 | public function getEvent(string $name): StopwatchEvent 138 | { 139 | if (!isset($this->events[$name])) { 140 | throw new \LogicException(\sprintf('Event "%s" is not known.', $name)); 141 | } 142 | 143 | return $this->events[$name]; 144 | } 145 | 146 | /** 147 | * Returns the events from this section. 148 | * 149 | * @return StopwatchEvent[] 150 | */ 151 | public function getEvents(): array 152 | { 153 | return $this->events; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Stopwatch.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Stopwatch; 13 | 14 | use Symfony\Contracts\Service\ResetInterface; 15 | 16 | // Help opcache.preload discover always-needed symbols 17 | class_exists(Section::class); 18 | 19 | /** 20 | * Stopwatch provides a way to profile code. 21 | * 22 | * @author Fabien Potencier 23 | */ 24 | class Stopwatch implements ResetInterface 25 | { 26 | public const ROOT = '__root__'; 27 | 28 | /** 29 | * @var Section[] 30 | */ 31 | private array $sections; 32 | 33 | /** 34 | * @var Section[] 35 | */ 36 | private array $activeSections; 37 | 38 | /** 39 | * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision 40 | */ 41 | public function __construct( 42 | private bool $morePrecision = false, 43 | ) { 44 | $this->reset(); 45 | } 46 | 47 | /** 48 | * @return Section[] 49 | */ 50 | public function getSections(): array 51 | { 52 | return $this->sections; 53 | } 54 | 55 | /** 56 | * Creates a new section or re-opens an existing section. 57 | * 58 | * @param string|null $id The id of the session to re-open, null to create a new one 59 | * 60 | * @throws \LogicException When the section to re-open is not reachable 61 | */ 62 | public function openSection(?string $id = null): void 63 | { 64 | $current = end($this->activeSections); 65 | 66 | if (null !== $id && null === $current->get($id)) { 67 | throw new \LogicException(\sprintf('The section "%s" has been started at an other level and cannot be opened.', $id)); 68 | } 69 | 70 | $this->start('__section__.child', 'section'); 71 | $this->activeSections[] = $current->open($id); 72 | $this->start('__section__'); 73 | } 74 | 75 | /** 76 | * Stops the last started section. 77 | * 78 | * The id parameter is used to retrieve the events from this section. 79 | * 80 | * @see getSectionEvents() 81 | * 82 | * @throws \LogicException When there's no started section to be stopped 83 | */ 84 | public function stopSection(string $id): void 85 | { 86 | $this->stop('__section__'); 87 | 88 | if (1 == \count($this->activeSections)) { 89 | throw new \LogicException('There is no started section to stop.'); 90 | } 91 | 92 | $this->sections[$id] = array_pop($this->activeSections)->setId($id); 93 | $this->stop('__section__.child'); 94 | } 95 | 96 | /** 97 | * Starts an event. 98 | */ 99 | public function start(string $name, ?string $category = null): StopwatchEvent 100 | { 101 | return end($this->activeSections)->startEvent($name, $category); 102 | } 103 | 104 | /** 105 | * Checks if the event was started. 106 | */ 107 | public function isStarted(string $name): bool 108 | { 109 | return end($this->activeSections)->isEventStarted($name); 110 | } 111 | 112 | /** 113 | * Stops an event. 114 | */ 115 | public function stop(string $name): StopwatchEvent 116 | { 117 | return end($this->activeSections)->stopEvent($name); 118 | } 119 | 120 | /** 121 | * Stops then restarts an event. 122 | */ 123 | public function lap(string $name): StopwatchEvent 124 | { 125 | return end($this->activeSections)->stopEvent($name)->start(); 126 | } 127 | 128 | /** 129 | * Returns a specific event by name. 130 | */ 131 | public function getEvent(string $name): StopwatchEvent 132 | { 133 | return end($this->activeSections)->getEvent($name); 134 | } 135 | 136 | /** 137 | * Gets all events for a given section. 138 | * 139 | * @return StopwatchEvent[] 140 | */ 141 | public function getSectionEvents(string $id): array 142 | { 143 | return isset($this->sections[$id]) ? $this->sections[$id]->getEvents() : []; 144 | } 145 | 146 | /** 147 | * Gets all events for the root section. 148 | * 149 | * @return StopwatchEvent[] 150 | */ 151 | public function getRootSectionEvents(): array 152 | { 153 | return $this->sections[self::ROOT]->getEvents() ?? []; 154 | } 155 | 156 | /** 157 | * Resets the stopwatch to its original state. 158 | */ 159 | public function reset(): void 160 | { 161 | $this->sections = $this->activeSections = [self::ROOT => new Section(null, $this->morePrecision)]; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /StopwatchEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Stopwatch; 13 | 14 | /** 15 | * Represents an Event managed by Stopwatch. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class StopwatchEvent 20 | { 21 | /** 22 | * @var StopwatchPeriod[] 23 | */ 24 | private array $periods = []; 25 | 26 | private float $origin; 27 | private string $category; 28 | 29 | /** 30 | * @var float[] 31 | */ 32 | private array $started = []; 33 | 34 | private string $name; 35 | 36 | /** 37 | * @param float $origin The origin time in milliseconds 38 | * @param string|null $category The event category or null to use the default 39 | * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision 40 | * @param string|null $name The event name or null to define the name as default 41 | */ 42 | public function __construct( 43 | float $origin, 44 | ?string $category = null, 45 | private bool $morePrecision = false, 46 | ?string $name = null, 47 | ) { 48 | $this->origin = $this->formatTime($origin); 49 | $this->category = \is_string($category) ? $category : 'default'; 50 | $this->name = $name ?? 'default'; 51 | } 52 | 53 | /** 54 | * Gets the category. 55 | */ 56 | public function getCategory(): string 57 | { 58 | return $this->category; 59 | } 60 | 61 | /** 62 | * Gets the origin in milliseconds. 63 | */ 64 | public function getOrigin(): float 65 | { 66 | return $this->origin; 67 | } 68 | 69 | /** 70 | * Starts a new event period. 71 | * 72 | * @return $this 73 | */ 74 | public function start(): static 75 | { 76 | $this->started[] = $this->getNow(); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Stops the last started event period. 83 | * 84 | * @return $this 85 | * 86 | * @throws \LogicException When stop() is called without a matching call to start() 87 | */ 88 | public function stop(): static 89 | { 90 | if (!\count($this->started)) { 91 | throw new \LogicException('stop() called but start() has not been called before.'); 92 | } 93 | 94 | $this->periods[] = new StopwatchPeriod(array_pop($this->started), $this->getNow(), $this->morePrecision); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Checks if the event was started. 101 | */ 102 | public function isStarted(): bool 103 | { 104 | return (bool) $this->started; 105 | } 106 | 107 | /** 108 | * Stops the current period and then starts a new one. 109 | * 110 | * @return $this 111 | */ 112 | public function lap(): static 113 | { 114 | return $this->stop()->start(); 115 | } 116 | 117 | /** 118 | * Stops all non already stopped periods. 119 | */ 120 | public function ensureStopped(): void 121 | { 122 | while (\count($this->started)) { 123 | $this->stop(); 124 | } 125 | } 126 | 127 | /** 128 | * Gets all event periods. 129 | * 130 | * @return StopwatchPeriod[] 131 | */ 132 | public function getPeriods(): array 133 | { 134 | return $this->periods; 135 | } 136 | 137 | /** 138 | * Gets the last event period. 139 | */ 140 | public function getLastPeriod(): ?StopwatchPeriod 141 | { 142 | if ([] === $this->periods) { 143 | return null; 144 | } 145 | 146 | return $this->periods[array_key_last($this->periods)]; 147 | } 148 | 149 | /** 150 | * Gets the relative time of the start of the first period in milliseconds. 151 | */ 152 | public function getStartTime(): int|float 153 | { 154 | if (isset($this->periods[0])) { 155 | return $this->periods[0]->getStartTime(); 156 | } 157 | 158 | if ($this->started) { 159 | return $this->started[0]; 160 | } 161 | 162 | return 0; 163 | } 164 | 165 | /** 166 | * Gets the relative time of the end of the last period in milliseconds. 167 | */ 168 | public function getEndTime(): int|float 169 | { 170 | $count = \count($this->periods); 171 | 172 | return $count ? $this->periods[$count - 1]->getEndTime() : 0; 173 | } 174 | 175 | /** 176 | * Gets the duration of the events in milliseconds (including all periods). 177 | */ 178 | public function getDuration(): int|float 179 | { 180 | $periods = $this->periods; 181 | $left = \count($this->started); 182 | 183 | for ($i = $left - 1; $i >= 0; --$i) { 184 | $periods[] = new StopwatchPeriod($this->started[$i], $this->getNow(), $this->morePrecision); 185 | } 186 | 187 | $total = 0; 188 | foreach ($periods as $period) { 189 | $total += $period->getDuration(); 190 | } 191 | 192 | return $total; 193 | } 194 | 195 | /** 196 | * Gets the max memory usage of all periods in bytes. 197 | */ 198 | public function getMemory(): int 199 | { 200 | $memory = 0; 201 | foreach ($this->periods as $period) { 202 | if ($period->getMemory() > $memory) { 203 | $memory = $period->getMemory(); 204 | } 205 | } 206 | 207 | return $memory; 208 | } 209 | 210 | /** 211 | * Return the current time relative to origin in milliseconds. 212 | */ 213 | protected function getNow(): float 214 | { 215 | return $this->formatTime(microtime(true) * 1000 - $this->origin); 216 | } 217 | 218 | /** 219 | * Formats a time. 220 | */ 221 | private function formatTime(float $time): float 222 | { 223 | return round($time, 1); 224 | } 225 | 226 | /** 227 | * Gets the event name. 228 | */ 229 | public function getName(): string 230 | { 231 | return $this->name; 232 | } 233 | 234 | public function __toString(): string 235 | { 236 | return \sprintf('%s/%s: %.2F MiB - %d ms', $this->getCategory(), $this->getName(), $this->getMemory() / 1024 / 1024, $this->getDuration()); 237 | } 238 | } 239 | --------------------------------------------------------------------------------