Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/Profiling/ContinuousProfiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ final class ContinuousProfiler
private $sampled;

/**
* @var array<string, true> The span IDs of the sampled root spans the profiler is running for
* @var array<string, string> The runtime context IDs of the sampled root spans the profiler is running for, keyed by span ID
*/
private $activeRootSpans = [];

Expand Down Expand Up @@ -145,7 +145,7 @@ public function startForRootSpan(string $spanId): void
$this->start();

if ($this->profiler !== null) {
$this->activeRootSpans[$spanId] = true;
$this->activeRootSpans[$spanId] = SentrySdk::getCurrentRuntimeContext()->getId();
}
}

Expand All @@ -162,6 +162,26 @@ public function stopForRootSpan(string $spanId): void
}
}

/**
* Releases the root spans of an ending runtime context that were never finished.
*/
public function stopForRuntimeContext(string $runtimeContextId): void
{
$released = false;

foreach ($this->activeRootSpans as $spanId => $rootSpanRuntimeContextId) {
if ($rootSpanRuntimeContextId === $runtimeContextId) {
unset($this->activeRootSpans[$spanId]);

$released = true;
}
}

if ($released && $this->activeRootSpans === []) {
$this->stop();
}
}

public function stop(): void
{
if ($this->profiler === null) {
Expand Down
3 changes: 3 additions & 0 deletions src/State/RuntimeContextManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Sentry\ErrorHandler;
use Sentry\Profiling\Profiles;
use Sentry\Tracing\PropagationContext;

/**
Expand Down Expand Up @@ -122,6 +123,8 @@ public function endContext(?int $timeout = null): void
return;
}

Profiles::getInstance()->continuousProfiler()->stopForRuntimeContext($runtimeContext->getId());

Comment thread
cursor[bot] marked this conversation as resolved.
$logger = $this->getLoggerFromHub($runtimeContext->getHub());

$this->flushRuntimeContextResources($runtimeContext, $timeout, $logger);
Expand Down
116 changes: 115 additions & 1 deletion tests/SentrySdkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Sentry\State\Scope;
use Sentry\Tracing\Span;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\TransactionContext;
use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;

Expand Down Expand Up @@ -140,7 +141,7 @@ public function testWithContextResetsSpanAndTransactionAcrossInvocations(): void
SentrySdk::init();

SentrySdk::withContext(function (): void {
$transaction = SentrySdk::getCurrentHub()->startTransaction(new \Sentry\Tracing\TransactionContext('request-1'));
$transaction = SentrySdk::getCurrentHub()->startTransaction(new TransactionContext('request-1'));
SentrySdk::getCurrentHub()->setSpan($transaction);

$this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan());
Expand Down Expand Up @@ -442,6 +443,110 @@ public function testFlushSendsCollectedProfileSamplesAndKeepsProfiling(): void
$this->assertCount(2, StubTransport::$events);
}

/**
* @requires extension excimer
*/
public function testEndContextStopsProfilerForUnfinishedRootSpans(): void
{
StubTransport::$events = [];

self::initWithProfileLifecycleTrace();

$profiler = Profiles::getInstance()->continuousProfiler();

SentrySdk::startContext();
SentrySdk::getCurrentHub()->startTransaction(TransactionContext::make());

$this->assertTrue($profiler->isRunning());

self::busyWait(0.05);

SentrySdk::endContext();

$this->assertFalse($profiler->isRunning());
$this->assertCount(1, StubTransport::$events);
$this->assertNotNull(StubTransport::$events[0]->getProfileChunk());
}

/**
* @requires extension excimer
*/
public function testEndContextKeepsProfilerRunningForRootSpansOfOtherContexts(): void
{
self::initWithProfileLifecycleTrace();

$profiler = Profiles::getInstance()->continuousProfiler();
$transaction = SentrySdk::getCurrentHub()->startTransaction(TransactionContext::make());

SentrySdk::startContext();
SentrySdk::getCurrentHub()->startTransaction(TransactionContext::make());
SentrySdk::endContext();

$this->assertTrue($profiler->isRunning());

$transaction->finish();

$this->assertFalse($profiler->isRunning());
}

/**
* @requires extension excimer
*/
public function testEndContextKeepsProfilerRunningWhileAnotherExecutionSharesTheContext(): void
{
$storage = new StubRuntimeContextStorage();
SentrySdk::setRuntimeContextStorage($storage);

self::initWithProfileLifecycleTrace();

$profiler = Profiles::getInstance()->continuousProfiler();

SentrySdk::startContext();
SentrySdk::getCurrentHub()->startTransaction(TransactionContext::make());

// A child coroutine of the request shares its context and ends first
$storage->share('child');
$storage->switchTo('child');

SentrySdk::endContext();

$this->assertTrue($profiler->isRunning());

$storage->switchTo('default');

SentrySdk::endContext();

$this->assertFalse($profiler->isRunning());
}

/**
* @requires extension excimer
*/
public function testWithContextKeepsProfilerRunningWhileAnotherExecutionSharesTheContext(): void
{
$storage = new StubRuntimeContextStorage();
SentrySdk::setRuntimeContextStorage($storage);

self::initWithProfileLifecycleTrace();

$profiler = Profiles::getInstance()->continuousProfiler();

SentrySdk::withContext(static function () use ($storage): void {
SentrySdk::getCurrentHub()->startTransaction(TransactionContext::make());

// A detached child coroutine shares the context and outlives the callback
$storage->share('child');
});

$this->assertTrue($profiler->isRunning());

$storage->switchTo('child');

SentrySdk::endContext();

$this->assertFalse($profiler->isRunning());
}

public function testFlushFlushesClientTransport(): void
{
/** @var ClientInterface&MockObject $client */
Expand Down Expand Up @@ -602,6 +707,15 @@ private function getCurrentScopeTag(string $key): ?string
return $value;
}

private static function initWithProfileLifecycleTrace(): void
{
SentrySdk::init()->bindClient(new Client(new Options([
'traces_sample_rate' => 1.0,
'profile_lifecycle' => 'trace',
'profile_session_sample_rate' => 1.0,
]), StubTransport::getInstance()));
}

private static function busyWait(float $seconds): void
{
$end = microtime(true) + $seconds;
Expand Down
20 changes: 19 additions & 1 deletion tests/StubRuntimeContextStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
use Sentry\State\RuntimeContextStorageInterface;

/**
* Models independently selectable execution slots and abandoned execution cleanup.
* Models independently selectable execution slots, contexts shared between executions and
* abandoned execution cleanup.
*/
final class StubRuntimeContextStorage implements RuntimeContextStorageInterface
{
Expand Down Expand Up @@ -37,6 +38,11 @@ public function remove(): ?RuntimeContext
$runtimeContext = $this->get();
unset($this->contexts[$this->execution]);

// A shared context is retained until the last execution sharing it releases it
if ($runtimeContext !== null && \in_array($runtimeContext, $this->contexts, true)) {
return null;
}

return $runtimeContext;
}

Expand All @@ -45,6 +51,18 @@ public function switchTo(string $execution): void
$this->execution = $execution;
}

/**
* Shares the context of the current execution with the given one, such as a child coroutine.
*/
public function share(string $execution): void
{
$runtimeContext = $this->get();

if ($runtimeContext !== null) {
$this->contexts[$execution] = $runtimeContext;
}
}

public function release(string $execution): void
{
unset($this->contexts[$execution]);
Expand Down
Loading