diff --git a/src/Profiling/Profiles.php b/src/Profiling/Profiles.php new file mode 100644 index 000000000..167e466a8 --- /dev/null +++ b/src/Profiling/Profiles.php @@ -0,0 +1,77 @@ +continuousProfiler = new ContinuousProfiler(); + } + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Starts the continuous profiler if the "profile_lifecycle" option is set to "manual" + * and the profiling session is sampled. + */ + public function start(): void + { + if (!$this->isManualLifecycle()) { + return; + } + + $this->continuousProfiler->start(); + } + + /** + * Stops the continuous profiler if the "profile_lifecycle" option is set to "manual" + * and sends the collected profile to Sentry. + */ + public function stop(): void + { + if (!$this->isManualLifecycle()) { + return; + } + + $this->continuousProfiler->stop(); + } + + /** + * Get the continuous profiler. + * + * @internal + */ + public function continuousProfiler(): ContinuousProfiler + { + return $this->continuousProfiler; + } + + private function isManualLifecycle(): bool + { + $client = SentrySdk::getCurrentHub()->getClient(); + + return $client !== null && $client->getOptions()->getProfileLifecycle() === 'manual'; + } +} diff --git a/src/functions.php b/src/functions.php index 6c1e1c8f5..4291b522a 100644 --- a/src/functions.php +++ b/src/functions.php @@ -12,6 +12,7 @@ use Sentry\Logs\Logs; use Sentry\Metrics\Metrics; use Sentry\Metrics\TraceMetrics; +use Sentry\Profiling\Profiles; use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; @@ -518,6 +519,14 @@ function traceMetrics(): TraceMetrics return TraceMetrics::getInstance(); } +/** + * Get the Sentry Profiles client. + */ +function profiler(): Profiles +{ + return Profiles::getInstance(); +} + /** * @deprecated use `traceMetrics` instead */ diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 6baae08ba..139ca5875 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -13,10 +13,12 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\EventId; +use Sentry\EventType; use Sentry\Integration\OTLPIntegration; use Sentry\MonitorConfig; use Sentry\MonitorSchedule; use Sentry\Options; +use Sentry\Profiling\Profiles; use Sentry\SentrySdk; use Sentry\Severity; use Sentry\State\Hub; @@ -47,6 +49,7 @@ use function Sentry\getOtlpTracesEndpointUrl; use function Sentry\getTraceparent; use function Sentry\init; +use function Sentry\profiler; use function Sentry\startContext; use function Sentry\startTransaction; use function Sentry\trace; @@ -859,4 +862,76 @@ public function testContinueTraceWhenOrgMatch(): void $this->assertSame('1', $dynamicSamplingContext->get('org_id')); }); } + + /** + * @requires extension excimer + */ + public function testProfilerStartAndStopCaptureProfileChunk(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'profile_lifecycle' => 'manual', + 'profile_session_sample_rate' => 1.0, + ])); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event): bool { + return $event->getType() === EventType::profileChunk() && $event->getProfileChunk() !== null; + })); + + SentrySdk::setCurrentHub(new Hub($client)); + + profiler()->start(); + + $this->assertTrue(Profiles::getInstance()->continuousProfiler()->isRunning()); + + self::busyWait(0.05); + + profiler()->stop(); + + $this->assertFalse(Profiles::getInstance()->continuousProfiler()->isRunning()); + } + + /** + * @requires extension excimer + */ + public function testProfilerStopDoesNotStopTraceLifecycleProfiler(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'profile_lifecycle' => 'trace', + 'profile_session_sample_rate' => 1.0, + ])); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event): bool { + return $event->getType() === EventType::profileChunk() && $event->getProfileChunk() !== null; + })); + + SentrySdk::setCurrentHub(new Hub($client)); + + $profiler = Profiles::getInstance()->continuousProfiler(); + $profiler->start(); + + self::busyWait(0.05); + + profiler()->stop(); + + $this->assertTrue($profiler->isRunning()); + + $profiler->stop(); + } + + private static function busyWait(float $seconds): void + { + $end = microtime(true) + $seconds; + + while (microtime(true) < $end) { + // Burn CPU so that Excimer collects samples + } + } }