Skip to content

Commit 0e14573

Browse files
committed
Continuous Profiling
1 parent 49a8065 commit 0e14573

13 files changed

Lines changed: 1006 additions & 2 deletions

File tree

src/Event.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Sentry\Context\OsContext;
88
use Sentry\Context\RuntimeContext;
99
use Sentry\Logs\Log;
10+
use Sentry\Profiles\ProfileChunk;
1011
use Sentry\Profiling\Profile;
1112
use Sentry\Tracing\Span;
1213

@@ -71,6 +72,11 @@ final class Event
7172
*/
7273
private $logs = [];
7374

75+
/**
76+
* @var ProfileChunk|null
77+
*/
78+
private $profileChunk;
79+
7480
/**
7581
* @var string|null The name of the server (e.g. the host name)
7682
*/
@@ -241,6 +247,11 @@ public static function createLogs(?EventId $eventId = null): self
241247
return new self($eventId, EventType::logs());
242248
}
243249

250+
public static function createProfileChunk(?EventId $eventId = null): self
251+
{
252+
return new self($eventId, EventType::profileChunk());
253+
}
254+
244255
/**
245256
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
246257
*/
@@ -445,6 +456,18 @@ public function setLogs(array $logs): self
445456
return $this;
446457
}
447458

459+
public function getProfileChunk(): ?ProfileChunk
460+
{
461+
return $this->profileChunk;
462+
}
463+
464+
public function setProfileChunk(?ProfileChunk $profileChunk): self
465+
{
466+
$this->profileChunk = $profileChunk;
467+
468+
return $this;
469+
}
470+
448471
/**
449472
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
450473
*/

src/EventType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public static function logs(): self
4747
return self::getInstance('log');
4848
}
4949

50+
public static function profileChunk(): self
51+
{
52+
return self::getInstance('profile_chunk');
53+
}
54+
5055
/**
5156
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
5257
*/

src/Profiles/ProfileChunk.php

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Profiles;
6+
7+
use Sentry\Event;
8+
use Sentry\Options;
9+
use Sentry\Util\PrefixStripper;
10+
use Sentry\Util\SentryUid;
11+
12+
/**
13+
* Type definition of the Sentry v2 profile format (continuous profiling).
14+
* All fields are none otpional.
15+
*
16+
* @see https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/
17+
*
18+
* @phpstan-type SentryProfileFrame array{
19+
* abs_path: string,
20+
* filename: string,
21+
* function: string,
22+
* module: string|null,
23+
* lineno: int|null,
24+
* }
25+
* @phpstan-type SentryV2Profile array{
26+
* profiler_id: string,
27+
* chunk_id: string,
28+
* platform: string,
29+
* release: string,
30+
* environment: string,
31+
* version: string,
32+
* profile: array{
33+
* frames: array<int, SentryProfileFrame>,
34+
* samples: array<int, array{
35+
* thread_id: string,
36+
* stack_id: int,
37+
* timestamp: float,
38+
* }>,
39+
* stacks: array<int, array<int, int>>,
40+
* },
41+
* client_sdk: array{
42+
* name: string,
43+
* version: string,
44+
* },
45+
* }
46+
* @phpstan-type ExcimerLogStackEntryTrace array{
47+
* file: string,
48+
* line: int,
49+
* class?: string,
50+
* function?: string,
51+
* closure_line?: int,
52+
* }
53+
* @phpstan-type ExcimerLogStackEntry array{
54+
* trace: array<int, ExcimerLogStackEntryTrace>,
55+
* timestamp: float
56+
* }
57+
*
58+
* @internal
59+
*/
60+
final class ProfileChunk
61+
{
62+
use PrefixStripper;
63+
64+
/**
65+
* @var string The thread ID
66+
*/
67+
public const THREAD_ID = '0';
68+
69+
/**
70+
* @var string The version of the profile format
71+
*/
72+
private const VERSION = '2';
73+
74+
/**
75+
* @var float The start time of the profile as a Unix timestamp with microseconds
76+
*/
77+
private $startTimeStamp;
78+
79+
/**
80+
* @var string The profiler ID
81+
*/
82+
private $profilerId;
83+
84+
/**
85+
* @var string|null The chunk ID (null = auto-generate)
86+
*/
87+
private $chunkId;
88+
89+
/**
90+
* @var array<int, \ExcimerLog> The data of the profile
91+
*/
92+
private $excimerLogs;
93+
94+
/**
95+
* @var Options|null
96+
*/
97+
private $options;
98+
99+
public function __construct(?Options $options = null)
100+
{
101+
$this->options = $options;
102+
}
103+
104+
public function setStartTimeStamp(float $startTimeStamp): void
105+
{
106+
$this->startTimeStamp = $startTimeStamp;
107+
}
108+
109+
public function setProfilerId(string $profilerId): void
110+
{
111+
$this->profilerId = $profilerId;
112+
}
113+
114+
public function setChunkId(string $chunkId): void
115+
{
116+
$this->chunkId = $chunkId;
117+
}
118+
119+
/**
120+
* @param array<int, \ExcimerLog> $excimerLogs
121+
*/
122+
public function setExcimerLogs($excimerLogs): void
123+
{
124+
$this->excimerLogs = $excimerLogs;
125+
}
126+
127+
/**
128+
* @return SentryV2Profile|null
129+
*/
130+
public function getFormattedData(Event $event): ?array
131+
{
132+
$frames = [];
133+
$frameHashMap = [];
134+
135+
$stacks = [];
136+
$stackHashMap = [];
137+
138+
$registerStack = static function (array $stack) use (&$stacks, &$stackHashMap): int {
139+
$stackHash = md5(serialize($stack));
140+
141+
if (\array_key_exists($stackHash, $stackHashMap) === false) {
142+
$stackHashMap[$stackHash] = \count($stacks);
143+
$stacks[] = $stack;
144+
}
145+
146+
return $stackHashMap[$stackHash];
147+
};
148+
149+
$samples = [];
150+
151+
$duration = 0;
152+
153+
$loggedStacks = $this->prepareStacks();
154+
foreach ($loggedStacks as $stack) {
155+
$stackFrames = [];
156+
157+
foreach ($stack['trace'] as $frame) {
158+
$absolutePath = $frame['file'];
159+
$lineno = $frame['line'];
160+
161+
$frameKey = "{$absolutePath}:{$lineno}";
162+
163+
$frameIndex = $frameHashMap[$frameKey] ?? null;
164+
165+
if ($frameIndex === null) {
166+
$file = $this->stripPrefixFromFilePath($this->options, $absolutePath);
167+
$module = null;
168+
169+
if (isset($frame['class'], $frame['function'])) {
170+
// Class::method
171+
$function = $frame['class'] . '::' . $frame['function'];
172+
$module = $frame['class'];
173+
} elseif (isset($frame['function'])) {
174+
// {closure}
175+
$function = $frame['function'];
176+
} else {
177+
// /index.php
178+
$function = $file;
179+
}
180+
181+
$frameHashMap[$frameKey] = $frameIndex = \count($frames);
182+
$frames[] = [
183+
'filename' => $file,
184+
'abs_path' => $absolutePath,
185+
'module' => $module,
186+
'function' => $function,
187+
'lineno' => $lineno,
188+
];
189+
}
190+
191+
$stackFrames[] = $frameIndex;
192+
}
193+
194+
$stackId = $registerStack($stackFrames);
195+
196+
$samples[] = [
197+
'stack_id' => $stackId,
198+
'thread_id' => self::THREAD_ID,
199+
'timestamp' => $this->startTimeStamp + $stack['timestamp'],
200+
];
201+
}
202+
203+
return [
204+
'profiler_id' => $this->profilerId,
205+
'chunk_id' => $this->chunkId ?? SentryUid::generate(),
206+
'platform' => 'php',
207+
'release' => $event->getRelease() ?? '',
208+
'environment' => $event->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT,
209+
'version' => self::VERSION,
210+
'profile' => [
211+
'frames' => $frames,
212+
'samples' => $samples,
213+
'stacks' => $stacks,
214+
],
215+
'client_sdk' => [
216+
'name' => $event->getSdkIdentifier(),
217+
'version' => $event->getSdkVersion(),
218+
],
219+
];
220+
}
221+
222+
/**
223+
* This method is mainly used to be able to mock the ExcimerLog class in the tests.
224+
*
225+
* @return array<int, ExcimerLogStackEntry>
226+
*/
227+
private function prepareStacks(): array
228+
{
229+
$stacks = [];
230+
231+
foreach ($this->excimerLogs as $excimerLog) {
232+
foreach ($excimerLog as $stack) {
233+
if ($stack instanceof \ExcimerLogEntry) {
234+
$stacks[] = [
235+
'trace' => $stack->getTrace(),
236+
'timestamp' => $stack->getTimestamp(),
237+
];
238+
} else {
239+
/** @var ExcimerLogStackEntry $stack */
240+
$stacks[] = $stack;
241+
}
242+
}
243+
}
244+
245+
return $stacks;
246+
}
247+
}

0 commit comments

Comments
 (0)