From 7afe0a3fb6677e52dd1116709b23291709b213d2 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:15:07 +0200 Subject: [PATCH] feat: implemented support for listening to ticks --- README.md | 25 ++++++++-- .../ProfileCouldNotRegisterTickHandler.php | 14 ++++++ src/Profile.php | 47 ++++++++++++++++++- src/Profiler.php | 7 ++- src/Profiling.php | 27 +++++++---- tests/ProfileTest.php | 24 ++++++++++ tests/ReadmeTest.php | 1 + 7 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 src/Exception/ProfileCouldNotRegisterTickHandler.php diff --git a/README.md b/README.md index 47d113d..1407d26 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Request a [profiler](./src/ProfilerInterface.php) as a dependency and call a `pr ```php namespace PetrKnap\Profiler; -function something(ProfilerInterface $profiler): string { +function doSomething(ProfilerInterface $profiler): string { // do something without profiling return $profiler->profile(function (): string { // do something @@ -46,8 +46,8 @@ It can be easily enabled, or disabled **through the DI**, which provides either ```php namespace PetrKnap\Profiler; -echo something(new Profiler()); -echo something(new NullProfiler()); +echo doSomething(new Profiler()); +echo doSomething(new NullProfiler()); ``` ### Cascade profiling @@ -59,7 +59,7 @@ namespace PetrKnap\Profiler; echo (new Profiler())->profile(function (ProfilerInterface $profiler): string { // do something before something - return something($profiler); + return doSomething($profiler); })->process(fn (ProfileInterface $profile) => printf( 'It took %.1f s to do something before something and something, there are %d children profiles.', $profile->getDuration(), @@ -67,6 +67,23 @@ echo (new Profiler())->profile(function (ProfilerInterface $profiler): string { )); ``` +### Tick listening + +For greater precision, you can use measurements at each `N` tick. +This will result in **very detailed code tracking**, which can degrade the performance of the monitored application. + +```php +declare(ticks=3); // this declaration is important (N=3) + +namespace PetrKnap\Profiler; + +$profiling = Profiling::start(listenToTicks: true); +doSomething(new NullProfiler()); +$profile = $profiling->finish(); + +printf('There are %d memory usage records.', count($profile->getMemoryUsages())); +``` + --- Run `composer require petrknap/profiler` to install it. diff --git a/src/Exception/ProfileCouldNotRegisterTickHandler.php b/src/Exception/ProfileCouldNotRegisterTickHandler.php new file mode 100644 index 0000000..c8cc6af --- /dev/null +++ b/src/Exception/ProfileCouldNotRegisterTickHandler.php @@ -0,0 +1,14 @@ + + */ + private array $memoryUsages = []; /** * @var array */ @@ -35,9 +42,11 @@ final class Profile implements ProcessableProfileInterface, ProfileWithOutputInt */ private Optional $outputOption; - public function __construct() - { + public function __construct( + bool $listenToTicks = self::DO_NOT_LISTEN_TO_TICKS, + ) { $this->state = ProfileState::Created; + $this->isListeningToTicks = $listenToTicks ? false : null; $this->timeBefore = OptionalFloat::empty(); $this->memoryUsageBefore = OptionalInt::empty(); $this->timeAfter = OptionalFloat::empty(); @@ -45,6 +54,11 @@ public function __construct() $this->outputOption = Optional::empty(); } + public function __destruct() + { + $this->unregisterTickHandler(); + } + public function getState(): ProfileState { return $this->state; @@ -62,6 +76,8 @@ public function start(): void $this->state = ProfileState::Started; $this->timeBefore = OptionalFloat::of(microtime(as_float: true)); $this->memoryUsageBefore = OptionalInt::of(memory_get_usage(real_usage: true)); + + $this->registerTickHandler(); } /** @@ -69,6 +85,8 @@ public function start(): void */ public function finish(): void { + $this->unregisterTickHandler(); + if ($this->state !== ProfileState::Started) { throw new Exception\ProfileCouldNotBeFinished(); } @@ -78,6 +96,30 @@ public function finish(): void $this->memoryUsageAfter = OptionalInt::of(memory_get_usage(real_usage: true)); } + /** + * @throws Exception\ProfileCouldNotRegisterTickHandler + */ + public function registerTickHandler(): void + { + if ($this->isListeningToTicks === false) { + register_tick_function([$this, 'tickHandler']) or throw new Exception\ProfileCouldNotRegisterTickHandler(); + $this->isListeningToTicks = true; + } + } + + public function unregisterTickHandler(): void + { + if ($this->isListeningToTicks === true) { + unregister_tick_function([$this, 'tickHandler']); + $this->isListeningToTicks = false; + } + } + + public function tickHandler(): void + { + $this->memoryUsages[sprintf(self::MICROTIME_FORMAT, microtime(as_float: true))] = memory_get_usage(real_usage: true); + } + /** * @throws Exception\ProfileCouldNotBeProcessed */ @@ -132,6 +174,7 @@ public function getMemoryUsages(bool $sortedByTime = self::SORTED_BY_TIME): arra return self::expandRecords( [ sprintf(self::MICROTIME_FORMAT, $this->timeBefore->orElseThrow()) => $this->memoryUsageBefore->orElseThrow(), + ...$this->memoryUsages, sprintf(self::MICROTIME_FORMAT, $this->timeAfter->orElseThrow()) => $this->memoryUsageAfter->orElseThrow(), ], $this->children, diff --git a/src/Profiler.php b/src/Profiler.php index ad05ac8..815a7af 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -6,9 +6,14 @@ /* final */class Profiler implements ProfilerInterface { + public function __construct( + private readonly bool $listenToTicks = Profile::DO_NOT_LISTEN_TO_TICKS, + ) { + } + public function profile(callable $callable): ProcessableProfileInterface & ProfileWithOutputInterface { - $profiling = Profiling::start(); + $profiling = Profiling::start($this->listenToTicks); $output = $callable(Profiling::createNestedProfiler($profiling)); /** @var Profile $profile */ $profile = $profiling->finish(); diff --git a/src/Profiling.php b/src/Profiling.php index 6664ab0..3a55e63 100644 --- a/src/Profiling.php +++ b/src/Profiling.php @@ -11,15 +11,17 @@ final class Profiling */ private function __construct( private readonly Profile $profile, + private readonly bool $listenToTicks, ) { } - public static function start(): self - { - $profile = new Profile(); + public static function start( + bool $listenToTicks = Profile::DO_NOT_LISTEN_TO_TICKS, + ): self { + $profile = new Profile($listenToTicks); $profile->start(); - return new self($profile); + return new self($profile, $listenToTicks); } /** @@ -41,13 +43,17 @@ public function finish(): ProfileInterface */ public static function createNestedProfiler(Profiling $profiling): ProfilerInterface { - return new class ($profiling->profile) extends Profiler { + return new class ($profiling->profile, $profiling->listenToTicks) extends Profiler { /** * @param Profile $parentProfile */ public function __construct( private readonly Profile $parentProfile, + bool $listenToTicks, ) { + parent::__construct( + listenToTicks: $listenToTicks, + ); } public function profile(callable $callable): ProcessableProfileInterface & ProfileWithOutputInterface @@ -56,10 +62,15 @@ public function profile(callable $callable): ProcessableProfileInterface & Profi throw new Exception\ParentProfileIsNotStarted(); } - $profile = parent::profile($callable); - $this->parentProfile->addChild($profile); + $this->parentProfile->unregisterTickHandler(); + try { + $profile = parent::profile($callable); + $this->parentProfile->addChild($profile); - return $profile; + return $profile; + } finally { + $this->parentProfile->registerTickHandler(); + } } }; } diff --git a/tests/ProfileTest.php b/tests/ProfileTest.php index 62df1d7..e1c7584 100644 --- a/tests/ProfileTest.php +++ b/tests/ProfileTest.php @@ -1,9 +1,11 @@ getChildren()); } + + #[DataProvider('dataListensToTicks')] + public function testListensToTicks(bool|null $shouldItListen): void + { + $profile = $shouldItListen === null ? new Profile() : new Profile(listenToTicks: $shouldItListen); + $profile->start(); + for ($i = 0; $i < 5; $i++) { + $i = (fn (int $i): int => $i)($i); + } + $profile->finish(); + + self::assertCount($shouldItListen === true ? 9 : 2, $profile->getMemoryUsages()); + } + + public static function dataListensToTicks(): array + { + return [ + 'default' => [null], + 'yes' => [true], + 'no' => [false], + ]; + } } diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php index 87897ba..d26f0f1 100644 --- a/tests/ReadmeTest.php +++ b/tests/ReadmeTest.php @@ -24,6 +24,7 @@ public static function getExpectedOutputsOfPhpExamples(): iterable 'long-term-profiling' => '', 'how-to-enable-disable-it' => 'It took 0.0 s to do something.' . 'something' . 'something', 'cascade-profiling' => 'It took 0.0 s to do something.' . 'It took 0.0 s to do something before something and something, there are 1 children profiles.' . 'something', + 'tick-listening' => 'There are 3 memory usage records.', ]; } }