Skip to content

Commit e645712

Browse files
committed
Continuous Profiling
1 parent 5fc99eb commit e645712

File tree

13 files changed

+1032
-2
lines changed

13 files changed

+1032
-2
lines changed

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: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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 thread name
71+
*/
72+
public const THREAD_NAME = 'main';
73+
74+
/**
75+
* @var string The version of the profile format
76+
*/
77+
private const VERSION = '2';
78+
79+
/**
80+
* @var float|null The start time of the profile as a Unix timestamp with microseconds
81+
*/
82+
private $startTimeStamp;
83+
84+
/**
85+
* @var string|null The profiler ID
86+
*/
87+
private $profilerId;
88+
89+
/**
90+
* @var string|null The chunk ID (null = auto-generate)
91+
*/
92+
private $chunkId;
93+
94+
/**
95+
* @var array<int, \ExcimerLog> The data of the profile
96+
*/
97+
private $excimerLogs;
98+
99+
/**
100+
* @var Options|null
101+
*/
102+
private $options;
103+
104+
public function __construct(?Options $options = null)
105+
{
106+
$this->options = $options;
107+
}
108+
109+
public function setStartTimeStamp(?float $startTimeStamp): void
110+
{
111+
$this->startTimeStamp = $startTimeStamp;
112+
}
113+
114+
public function setProfilerId(?string $profilerId): void
115+
{
116+
$this->profilerId = $profilerId;
117+
}
118+
119+
public function setChunkId(string $chunkId): void
120+
{
121+
$this->chunkId = $chunkId;
122+
}
123+
124+
/**
125+
* @param array<int, \ExcimerLog> $excimerLogs
126+
*/
127+
public function setExcimerLogs($excimerLogs): void
128+
{
129+
$this->excimerLogs = $excimerLogs;
130+
}
131+
132+
/**
133+
* @return SentryV2Profile|null
134+
*/
135+
public function getFormattedData(Event $event): ?array
136+
{
137+
$frames = [];
138+
$frameHashMap = [];
139+
140+
$stacks = [];
141+
$stackHashMap = [];
142+
143+
$registerStack = static function (array $stack) use (&$stacks, &$stackHashMap): int {
144+
$stackHash = md5(serialize($stack));
145+
146+
if (\array_key_exists($stackHash, $stackHashMap) === false) {
147+
$stackHashMap[$stackHash] = \count($stacks);
148+
$stacks[] = $stack;
149+
}
150+
151+
return $stackHashMap[$stackHash];
152+
};
153+
154+
$samples = [];
155+
156+
$loggedStacks = $this->prepareStacks();
157+
foreach ($loggedStacks as $stack) {
158+
$stackFrames = [];
159+
160+
foreach ($stack['trace'] as $frame) {
161+
$absolutePath = $frame['file'];
162+
$lineno = $frame['line'];
163+
164+
$frameKey = "{$absolutePath}:{$lineno}";
165+
166+
$frameIndex = $frameHashMap[$frameKey] ?? null;
167+
168+
if ($frameIndex === null) {
169+
$file = $this->stripPrefixFromFilePath($this->options, $absolutePath);
170+
$module = null;
171+
172+
if (isset($frame['class'], $frame['function'])) {
173+
// Class::method
174+
$function = $frame['class'] . '::' . $frame['function'];
175+
$module = $frame['class'];
176+
} elseif (isset($frame['function'])) {
177+
// {closure}
178+
$function = $frame['function'];
179+
} else {
180+
// /index.php
181+
$function = $file;
182+
}
183+
184+
$frameHashMap[$frameKey] = $frameIndex = \count($frames);
185+
$frames[] = [
186+
'filename' => $file,
187+
'abs_path' => $absolutePath,
188+
'module' => $module,
189+
'function' => $function,
190+
'lineno' => $lineno,
191+
];
192+
}
193+
194+
$stackFrames[] = $frameIndex;
195+
}
196+
197+
$stackId = $registerStack($stackFrames);
198+
199+
$samples[] = [
200+
'stack_id' => $stackId,
201+
'thread_id' => self::THREAD_ID,
202+
'timestamp' => $this->startTimeStamp + $stack['timestamp'],
203+
];
204+
}
205+
206+
return [
207+
'profiler_id' => $this->profilerId,
208+
'chunk_id' => $this->chunkId ?? SentryUid::generate(),
209+
'platform' => 'php',
210+
'release' => $event->getRelease() ?? '',
211+
'environment' => $event->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT,
212+
'version' => self::VERSION,
213+
'profile' => [
214+
'frames' => $frames,
215+
'samples' => $samples,
216+
'stacks' => $stacks,
217+
'thread_metadata' => (object) [
218+
self::THREAD_ID => [
219+
'name' => self::THREAD_NAME,
220+
],
221+
],
222+
],
223+
'client_sdk' => [
224+
'name' => $event->getSdkIdentifier(),
225+
'version' => $event->getSdkVersion(),
226+
],
227+
];
228+
}
229+
230+
/**
231+
* This method is mainly used to be able to mock the ExcimerLog class in the tests.
232+
*
233+
* @return array<int, ExcimerLogStackEntry>
234+
*/
235+
private function prepareStacks(): array
236+
{
237+
$stacks = [];
238+
239+
foreach ($this->excimerLogs as $excimerLog) {
240+
foreach ($excimerLog as $stack) {
241+
if ($stack instanceof \ExcimerLogEntry) {
242+
$stacks[] = [
243+
'trace' => $stack->getTrace(),
244+
'timestamp' => $stack->getTimestamp(),
245+
];
246+
} else {
247+
/** @var ExcimerLogStackEntry $stack */
248+
$stacks[] = $stack;
249+
}
250+
}
251+
}
252+
253+
return $stacks;
254+
}
255+
}

0 commit comments

Comments
 (0)