Skip to content

Commit dbce322

Browse files
authored
feat(flamegraphs): Support functions flamegraphs for continuous profi… (#80822)
…ling Previously, when generating flamegraphs from functions, it was only capable of using transaction profiles. This adds support for generating flamegraphs for a function using continuous profiles.
1 parent 985a324 commit dbce322

File tree

2 files changed

+78
-29
lines changed

2 files changed

+78
-29
lines changed

src/sentry/profiles/flamegraph.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ class ProfilerMeta:
295295
thread_id: str
296296
start: float
297297
end: float
298-
transaction_id: str
298+
transaction_id: str | None = None
299299

300300
def as_condition(self) -> Condition:
301301
return And(
@@ -341,14 +341,13 @@ def get_profile_candidates(self) -> ProfileCandidates:
341341
raise NotImplementedError
342342

343343
def get_profile_candidates_from_functions(self) -> ProfileCandidates:
344-
# TODO: continuous profiles support
345344
max_profiles = options.get("profiling.flamegraph.profile-set.size")
346345

347346
builder = ProfileFunctionsQueryBuilder(
348347
dataset=Dataset.Functions,
349348
params={},
350349
snuba_params=self.snuba_params,
351-
selected_columns=["project.id", "timestamp", "unique_examples()"],
350+
selected_columns=["project.id", "timestamp", "all_examples()"],
352351
query=self.query,
353352
limit=max_profiles,
354353
config=QueryBuilderConfig(
@@ -365,22 +364,34 @@ def get_profile_candidates_from_functions(self) -> ProfileCandidates:
365364
results = builder.process_results(results)
366365

367366
transaction_profile_candidates: list[TransactionProfileCandidate] = []
367+
profiler_metas: list[ProfilerMeta] = []
368368

369369
for row in results["data"]:
370370
project = row["project.id"]
371-
for example in row["unique_examples()"]:
371+
for example in row["all_examples()"]:
372372
if len(transaction_profile_candidates) > max_profiles:
373373
break
374-
transaction_profile_candidates.append(
375-
{
376-
"project_id": project,
377-
"profile_id": example,
378-
}
379-
)
374+
if "profile_id" in example:
375+
transaction_profile_candidates.append(
376+
{
377+
"project_id": project,
378+
"profile_id": example["profile_id"],
379+
}
380+
)
381+
elif "profiler_id" in example:
382+
profiler_metas.append(
383+
ProfilerMeta(
384+
project_id=project,
385+
profiler_id=example["profiler_id"],
386+
thread_id=example["thread_id"],
387+
start=example["start"],
388+
end=example["end"],
389+
)
390+
)
380391

381392
return {
382393
"transaction": transaction_profile_candidates,
383-
"continuous": [],
394+
"continuous": self.get_chunks_for_profilers(profiler_metas),
384395
}
385396

386397
def get_profile_candidates_from_transactions(self) -> ProfileCandidates:
@@ -499,17 +510,19 @@ def get_chunks_for_profilers(
499510
if start > profiler_meta.end or end < profiler_meta.start:
500511
continue
501512

502-
continuous_profile_candidates.append(
503-
{
504-
"project_id": profiler_meta.project_id,
505-
"profiler_id": profiler_meta.profiler_id,
506-
"chunk_id": row["chunk_id"],
507-
"thread_id": profiler_meta.thread_id,
508-
"start": str(int(profiler_meta.start * 1.0e9)),
509-
"end": str(int(profiler_meta.end * 1.0e9)),
510-
"transaction_id": profiler_meta.transaction_id,
511-
}
512-
)
513+
candidate: ContinuousProfileCandidate = {
514+
"project_id": profiler_meta.project_id,
515+
"profiler_id": profiler_meta.profiler_id,
516+
"chunk_id": row["chunk_id"],
517+
"thread_id": profiler_meta.thread_id,
518+
"start": str(int(profiler_meta.start * 1e9)),
519+
"end": str(int(profiler_meta.end * 1e9)),
520+
}
521+
522+
if profiler_meta.transaction_id is not None:
523+
candidate["transaction_id"] = profiler_meta.transaction_id
524+
525+
continuous_profile_candidates.append(candidate)
513526

514527
return continuous_profile_candidates
515528

tests/sentry/api/endpoints/test_organization_profiling_profiles.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def setUp(self):
121121
self.login_as(user=self.user)
122122
self.url = reverse(self.endpoint, args=(self.organization.slug,))
123123
self.ten_mins_ago = before_now(minutes=10)
124+
self.hour_ago = before_now(hours=1).replace(minute=0, second=0, microsecond=0)
124125

125126
def do_request(self, query, features=None, compat=True, **kwargs):
126127
if features is None:
@@ -404,40 +405,65 @@ def test_queries_profile_candidates_from_profiles(self):
404405

405406
assert profiles_snql_request.dataset == Dataset.Profiles.value
406407

408+
@patch("sentry.profiles.flamegraph.bulk_snuba_queries", wraps=bulk_snuba_queries)
407409
@patch("sentry.search.events.builder.base.raw_snql_query", wraps=raw_snql_query)
408410
@patch("sentry.api.endpoints.organization_profiling_profiles.proxy_profiling_service")
409411
def test_queries_profile_candidates_from_functions_with_data(
410412
self,
411413
mock_proxy_profiling_service,
412414
mock_raw_snql_query,
415+
mock_bulk_snuba_queries,
413416
):
414-
data = load_data("transaction", timestamp=self.ten_mins_ago)
417+
data = load_data("transaction", timestamp=self.hour_ago)
415418
data["transaction"] = "foo"
416419
profile_id = uuid4().hex
417420
data.setdefault("contexts", {}).setdefault("profile", {})["profile_id"] = profile_id
418421

419-
stored = self.store_functions(
422+
stored_1 = self.store_functions(
420423
[
421424
{
422-
"self_times_ns": [100],
425+
"self_times_ns": [100_000_000],
423426
"package": "foo",
424427
"function": "bar",
425428
"in_app": True,
426429
},
427430
],
428431
project=self.project,
429432
transaction=data,
433+
timestamp=self.hour_ago,
430434
)
435+
stored_2 = self.store_functions_chunk(
436+
[
437+
{
438+
"self_times_ns": [100_000_000],
439+
"package": "foo",
440+
"function": "bar",
441+
"thread_id": "1",
442+
"in_app": True,
443+
},
444+
],
445+
project=self.project,
446+
timestamp=self.hour_ago,
447+
)
448+
449+
chunk = {
450+
"project_id": self.project.id,
451+
"profiler_id": stored_2["profiler_id"],
452+
"chunk_id": stored_2["chunk_id"],
453+
"start_timestamp": self.hour_ago.isoformat(),
454+
"end_timestamp": (self.hour_ago + timedelta(microseconds=100_000)).isoformat(),
455+
}
456+
457+
mock_bulk_snuba_queries.return_value = [{"data": [chunk]}]
431458

432459
mock_proxy_profiling_service.return_value = HttpResponse(status=200)
433460

434-
fingerprint = stored["functions"][0]["fingerprint"]
461+
fingerprint = stored_1["functions"][0]["fingerprint"]
435462

436463
response = self.do_request(
437464
{
438465
"project": [self.project.id],
439466
"dataSource": "functions",
440-
"query": "transaction:foo",
441467
"fingerprint": str(fingerprint),
442468
},
443469
)
@@ -457,7 +483,6 @@ def test_queries_profile_candidates_from_functions_with_data(
457483
)
458484
in snql_request.query.where
459485
)
460-
assert Condition(Column("transaction_name"), Op.EQ, "foo") in snql_request.query.where
461486

462487
mock_proxy_profiling_service.assert_called_once_with(
463488
method="POST",
@@ -469,7 +494,18 @@ def test_queries_profile_candidates_from_functions_with_data(
469494
"profile_id": profile_id,
470495
},
471496
],
472-
"continuous": [],
497+
"continuous": [
498+
{
499+
"project_id": self.project.id,
500+
"profiler_id": stored_2["profiler_id"],
501+
"chunk_id": stored_2["chunk_id"],
502+
"thread_id": "1",
503+
"start": str(int(self.hour_ago.timestamp() * 1e9)),
504+
"end": str(
505+
int((self.hour_ago + timedelta(microseconds=100_000)).timestamp() * 1e9)
506+
),
507+
},
508+
],
473509
},
474510
)
475511

0 commit comments

Comments
 (0)