4
4
from datetime import datetime
5
5
from typing import Any , Literal , NotRequired , TypedDict
6
6
7
+ from rest_framework .request import Request as HttpRequest
7
8
from snuba_sdk import (
8
9
And ,
9
10
Column ,
19
20
Storage ,
20
21
)
21
22
22
- from sentry import options
23
+ from sentry import features , options
23
24
from sentry .search .eap .types import EAPResponse , SearchResolverConfig
24
25
from sentry .search .events .builder .discover import DiscoverQueryBuilder
25
26
from sentry .search .events .builder .profile_functions import ProfileFunctionsQueryBuilder
@@ -85,11 +86,13 @@ class FlamegraphExecutor:
85
86
def __init__ (
86
87
self ,
87
88
* ,
89
+ request : HttpRequest ,
88
90
snuba_params : SnubaParams ,
89
91
data_source : Literal ["functions" , "transactions" , "profiles" , "spans" ],
90
92
query : str ,
91
93
fingerprint : int | None = None ,
92
94
):
95
+ self .request = request
93
96
self .snuba_params = snuba_params
94
97
self .data_source = data_source
95
98
self .query = query
@@ -165,12 +168,14 @@ def get_profile_candidates_from_functions(self) -> ProfileCandidates:
165
168
max_profiles - len (transaction_profile_candidates ), 0
166
169
)
167
170
171
+ continuous_profile_candidates , _ = self .get_chunks_for_profilers (
172
+ profiler_metas ,
173
+ max_continuous_profile_candidates ,
174
+ )
175
+
168
176
return {
169
177
"transaction" : transaction_profile_candidates ,
170
- "continuous" : self .get_chunks_for_profilers (
171
- profiler_metas ,
172
- max_continuous_profile_candidates ,
173
- ),
178
+ "continuous" : continuous_profile_candidates ,
174
179
}
175
180
176
181
def get_profile_candidates_from_transactions (self ) -> ProfileCandidates :
@@ -196,23 +201,25 @@ def get_profile_candidates_from_transactions(self) -> ProfileCandidates:
196
201
max_profiles - len (transaction_profile_candidates ), 0
197
202
)
198
203
204
+ continuous_profile_candidates , _ = self .get_chunks_for_profilers (
205
+ [
206
+ ProfilerMeta (
207
+ project_id = row ["project.id" ],
208
+ profiler_id = row ["profiler.id" ],
209
+ thread_id = row ["thread.id" ],
210
+ start = row ["precise.start_ts" ],
211
+ end = row ["precise.finish_ts" ],
212
+ transaction_id = row ["id" ],
213
+ )
214
+ for row in results ["data" ]
215
+ if row ["profiler.id" ] is not None and row ["thread.id" ]
216
+ ],
217
+ max_continuous_profile_candidates ,
218
+ )
219
+
199
220
return {
200
221
"transaction" : transaction_profile_candidates ,
201
- "continuous" : self .get_chunks_for_profilers (
202
- [
203
- ProfilerMeta (
204
- project_id = row ["project.id" ],
205
- profiler_id = row ["profiler.id" ],
206
- thread_id = row ["thread.id" ],
207
- start = row ["precise.start_ts" ],
208
- end = row ["precise.finish_ts" ],
209
- transaction_id = row ["id" ],
210
- )
211
- for row in results ["data" ]
212
- if row ["profiler.id" ] is not None and row ["thread.id" ]
213
- ],
214
- max_continuous_profile_candidates ,
215
- ),
222
+ "continuous" : continuous_profile_candidates ,
216
223
}
217
224
218
225
def get_transactions_based_candidate_query (
@@ -264,9 +271,11 @@ def get_transactions_based_candidate_query(
264
271
265
272
def get_chunks_for_profilers (
266
273
self , profiler_metas : list [ProfilerMeta ], limit : int
267
- ) -> list [ContinuousProfileCandidate ]:
274
+ ) -> tuple [list [ContinuousProfileCandidate ], float ]:
275
+ total_duration = 0.0
276
+
268
277
if len (profiler_metas ) == 0 :
269
- return []
278
+ return [], total_duration
270
279
271
280
chunk_size = options .get ("profiling.continuous-profiling.chunks-query.size" )
272
281
queries = [
@@ -304,6 +313,8 @@ def get_chunks_for_profilers(
304
313
"end" : str (int (profiler_meta .end * 1e9 )),
305
314
}
306
315
316
+ total_duration += profiler_meta .end - profiler_meta .start
317
+
307
318
if profiler_meta .transaction_id is not None :
308
319
candidate ["transaction_id" ] = profiler_meta .transaction_id
309
320
@@ -317,7 +328,7 @@ def get_chunks_for_profilers(
317
328
continue
318
329
break
319
330
320
- return continuous_profile_candidates
331
+ return continuous_profile_candidates , total_duration
321
332
322
333
def _create_chunks_query (self , profiler_metas : list [ProfilerMeta ]) -> Query :
323
334
assert profiler_metas , "profiler_metas cannot be empty"
@@ -480,22 +491,38 @@ def get_profile_candidates_from_profiles(self) -> ProfileCandidates:
480
491
]
481
492
482
493
continuous_profile_candidates : list [ContinuousProfileCandidate ] = []
494
+ continuous_duration = 0.0
483
495
484
496
# If there are continuous profiles attached to transactions, we prefer those as
485
497
# the active thread id gives us more user friendly flamegraphs than without.
486
498
if profiler_metas :
487
- continuous_profile_candidates = self .get_chunks_for_profilers (
499
+ continuous_profile_candidates , continuous_duration = self .get_chunks_for_profilers (
488
500
profiler_metas , max_continuous_profile_candidates
489
501
)
490
502
503
+ seen_chunks = {
504
+ (candidate ["profiler_id" ], candidate ["chunk_id" ])
505
+ for candidate in continuous_profile_candidates
506
+ }
507
+
508
+ always_use_direct_chunks = features .has (
509
+ "organizations:profiling-flamegraph-always-use-direct-chunks" ,
510
+ self .snuba_params .organization ,
511
+ actor = self .request .user ,
512
+ )
513
+
491
514
# If we still don't have any continuous profile candidates, we'll fall back to
492
515
# directly using the continuous profiling data
493
- if not continuous_profile_candidates :
494
- total_duration = 0.0
495
-
516
+ if not continuous_profile_candidates or always_use_direct_chunks :
517
+ total_duration = continuous_duration if always_use_direct_chunks else 0.0
496
518
max_duration = options .get ("profiling.continuous-profiling.flamegraph.max-seconds" )
497
519
498
520
for row in continuous_profile_results ["data" ]:
521
+
522
+ # Make sure to dedupe profile chunks so we don't reuse chunks
523
+ if (row ["profiler_id" ], row ["chunk_id" ]) in seen_chunks :
524
+ continue
525
+
499
526
start = datetime .fromisoformat (row ["start_timestamp" ]).timestamp ()
500
527
end = datetime .fromisoformat (row ["end_timestamp" ]).timestamp ()
501
528
@@ -535,22 +562,25 @@ def get_profile_candidates_from_spans(self) -> ProfileCandidates:
535
562
max_continuous_profile_candidates = max (
536
563
max_profiles - len (transaction_profile_candidates ), 0
537
564
)
565
+
566
+ continuous_profile_candidates , _ = self .get_chunks_for_profilers (
567
+ [
568
+ ProfilerMeta (
569
+ project_id = row ["project.id" ],
570
+ profiler_id = row ["profiler.id" ],
571
+ thread_id = row ["thread.id" ],
572
+ start = row ["precise.start_ts" ],
573
+ end = row ["precise.finish_ts" ],
574
+ )
575
+ for row in results ["data" ]
576
+ if row ["profiler.id" ] is not None and row ["thread.id" ]
577
+ ],
578
+ max_continuous_profile_candidates ,
579
+ )
580
+
538
581
return {
539
582
"transaction" : transaction_profile_candidates ,
540
- "continuous" : self .get_chunks_for_profilers (
541
- [
542
- ProfilerMeta (
543
- project_id = row ["project.id" ],
544
- profiler_id = row ["profiler.id" ],
545
- thread_id = row ["thread.id" ],
546
- start = row ["precise.start_ts" ],
547
- end = row ["precise.finish_ts" ],
548
- )
549
- for row in results ["data" ]
550
- if row ["profiler.id" ] is not None and row ["thread.id" ]
551
- ],
552
- max_continuous_profile_candidates ,
553
- ),
583
+ "continuous" : continuous_profile_candidates ,
554
584
}
555
585
556
586
def get_spans_based_candidates (self , query : str | None , limit : int ) -> EAPResponse :
0 commit comments