Skip to content

Commit a80a05f

Browse files
authored
feat(replays): Improve index page query performance (#45098)
The /replays index page slows down when presented with large datasets. Given a large enough dataset some queries will OOM. By keeping fewer values in memory and making various performance optimizations to the query's structure we improve the performance of the query by 5x for our largest customers. - Uses non-unique counting for the `count_errors` field. - Breaking change from previous design. - Reduces memory usage. - Removes `urls_sorted` dependency for `count_urls` and replaces with simple count. - Improves `activity` performance. - Reduces memory usage. - For ALL grouped scalar fields we have replaced `groupUniqArray` with `groupArray(1)`. This is faster (no unique requirements) and more memory efficient (we only ever have one value in memory). - `project_id` is no longer a grouped scalar value. It is included in the GROUP BY clause. - Fixed an error where trace and error ids were filtering against the column rather than the aggregation.
1 parent 1498406 commit a80a05f

File tree

5 files changed

+59
-66
lines changed

5 files changed

+59
-66
lines changed

src/sentry/replays/post_process.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def generate_normalized_output(
2525
"""For each payload in the response strip "agg_" prefixes."""
2626
for item in response:
2727
item["id"] = item.pop("replay_id", None)
28-
item["project_id"] = item.pop("projectId", None)
28+
item["project_id"] = str(item["project_id"])
2929
item["trace_ids"] = item.pop("traceIds", [])
3030
item["error_ids"] = item.pop("errorIds", [])
3131
item["environment"] = item.pop("agg_environment", None)

src/sentry/replays/query.py

Lines changed: 49 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from snuba_sdk import (
88
Column,
99
Condition,
10+
CurriedFunction,
1011
Entity,
1112
Function,
1213
Granularity,
@@ -153,7 +154,7 @@ def query_replays_dataset(
153154
*having,
154155
],
155156
orderby=sorting,
156-
groupby=[Column("replay_id")],
157+
groupby=[Column("project_id"), Column("replay_id")],
157158
granularity=Granularity(3600),
158159
**query_options,
159160
),
@@ -308,17 +309,22 @@ def _grouped_unique_values(
308309
)
309310

310311

311-
def _grouped_unique_scalar_value(
312-
column_name: str, alias: Optional[str] = None, aliased: bool = True
312+
def take_first_from_aggregation(
313+
column_name: str,
314+
alias: Optional[str] = None,
315+
aliased: bool = True,
313316
) -> Function:
314-
"""Returns the first value of a unique array.
317+
"""Returns the first value encountered in an aggregated array.
315318
316319
E.g.
317-
[1, 2, 2, 3, 3, 3, null] => [1, 2, 3] => 1
320+
[1, 2, 2, 3, 3, 3, null] => [1] => 1
318321
"""
319322
return Function(
320323
"arrayElement",
321-
parameters=[_grouped_unique_values(column_name), 1],
324+
parameters=[
325+
CurriedFunction("groupArray", initializers=[1], parameters=[Column(column_name)]),
326+
1,
327+
],
322328
alias=alias or column_name if aliased else None,
323329
)
324330

@@ -383,11 +389,11 @@ class ReplayQueryConfig(QueryConfig):
383389
releases = ListField()
384390
release = ListField(query_alias="releases")
385391
dist = String()
386-
error_ids = ListField(query_alias="error_ids", is_uuid=True)
387-
error_id = ListField(query_alias="error_ids", is_uuid=True)
388-
trace_ids = ListField(query_alias="trace_ids", is_uuid=True)
389-
trace_id = ListField(query_alias="trace_ids", is_uuid=True)
390-
trace = ListField(query_alias="trace_ids", is_uuid=True)
392+
error_ids = ListField(query_alias="errorIds")
393+
error_id = ListField(query_alias="errorIds")
394+
trace_ids = ListField(query_alias="traceIds")
395+
trace_id = ListField(query_alias="traceIds")
396+
trace = ListField(query_alias="traceIds")
391397
urls = ListField(query_alias="urls_sorted")
392398
url = ListField(query_alias="urls_sorted")
393399
user_id = String(field_alias="user.id", query_alias="user_id")
@@ -421,8 +427,8 @@ class ReplayQueryConfig(QueryConfig):
421427
started_at = String(is_filterable=False)
422428
finished_at = String(is_filterable=False)
423429
# Dedicated url parameter should be used.
424-
project_id = String(query_alias="projectId", is_filterable=False)
425-
project = String(query_alias="projectId", is_filterable=False)
430+
project_id = String(query_alias="project_id", is_filterable=False)
431+
project = String(query_alias="project_id", is_filterable=False)
426432

427433

428434
# Pagination.
@@ -467,20 +473,8 @@ def _activity_score():
467473
# score = (count_errors * 25 + pagesVisited * 5 ) / 10;
468474
# score = Math.floor(Math.min(10, Math.max(1, score)));
469475

470-
error_weight = Function(
471-
"multiply",
472-
parameters=[Column("count_errors"), 25],
473-
)
474-
pages_visited_weight = Function(
475-
"multiply",
476-
parameters=[
477-
Function(
478-
"length",
479-
parameters=[Column("urls_sorted")],
480-
),
481-
5,
482-
],
483-
)
476+
error_weight = Function("multiply", parameters=[Column("count_errors"), 25])
477+
pages_visited_weight = Function("multiply", parameters=[Column("count_urls"), 5])
484478

485479
combined_weight = Function(
486480
"plus",
@@ -549,10 +543,10 @@ def _activity_score():
549543
"urls": ["urls_sorted", "agg_urls"],
550544
"url": ["urls_sorted", "agg_urls"],
551545
"count_errors": ["count_errors"],
552-
"count_urls": ["count_urls", "urls_sorted", "agg_urls"],
546+
"count_urls": ["count_urls"],
553547
"count_segments": ["count_segments"],
554548
"is_archived": ["is_archived"],
555-
"activity": ["activity", "count_errors", "urls_sorted", "agg_urls"],
549+
"activity": ["activity", "count_errors", "count_urls"],
556550
"user": ["user_id", "user_email", "user_name", "user_ip"],
557551
"os": ["os_name", "os_version"],
558552
"browser": ["browser_name", "browser_version"],
@@ -582,18 +576,7 @@ def _activity_score():
582576

583577
QUERY_ALIAS_COLUMN_MAP = {
584578
"replay_id": _strip_uuid_dashes("replay_id", Column("replay_id")),
585-
"replay_type": _grouped_unique_scalar_value(column_name="replay_type", alias="replay_type"),
586-
"project_id": Function(
587-
"toString",
588-
parameters=[_grouped_unique_scalar_value(column_name="project_id", alias="agg_pid")],
589-
alias="projectId",
590-
),
591-
"platform": _grouped_unique_scalar_value(column_name="platform"),
592-
"agg_environment": _grouped_unique_scalar_value(
593-
column_name="environment", alias="agg_environment"
594-
),
595-
"releases": _grouped_unique_values(column_name="release", alias="releases", aliased=True),
596-
"dist": _grouped_unique_scalar_value(column_name="dist"),
579+
"project_id": Column("project_id"),
597580
"trace_ids": Function(
598581
"arrayMap",
599582
parameters=[
@@ -637,13 +620,13 @@ def _activity_score():
637620
),
638621
"count_segments": Function("count", parameters=[Column("segment_id")], alias="count_segments"),
639622
"count_errors": Function(
640-
"uniqArray",
641-
parameters=[Column("error_ids")],
623+
"sum",
624+
parameters=[Function("length", parameters=[Column("error_ids")])],
642625
alias="count_errors",
643626
),
644627
"count_urls": Function(
645-
"length",
646-
parameters=[Column("urls_sorted")],
628+
"sum",
629+
parameters=[Function("length", parameters=[Column("urls")])],
647630
alias="count_urls",
648631
),
649632
"is_archived": Function(
@@ -652,29 +635,31 @@ def _activity_score():
652635
alias="isArchived",
653636
),
654637
"activity": _activity_score(),
655-
"user_id": _grouped_unique_scalar_value(column_name="user_id"),
656-
"user_email": _grouped_unique_scalar_value(column_name="user_email"),
657-
"user_name": _grouped_unique_scalar_value(column_name="user_name"),
638+
"releases": _grouped_unique_values(column_name="release", alias="releases", aliased=True),
639+
"replay_type": take_first_from_aggregation(column_name="replay_type", alias="replay_type"),
640+
"platform": take_first_from_aggregation(column_name="platform"),
641+
"agg_environment": take_first_from_aggregation(
642+
column_name="environment", alias="agg_environment"
643+
),
644+
"dist": take_first_from_aggregation(column_name="dist"),
645+
"user_id": take_first_from_aggregation(column_name="user_id"),
646+
"user_email": take_first_from_aggregation(column_name="user_email"),
647+
"user_name": take_first_from_aggregation(column_name="user_name"),
658648
"user_ip": Function(
659649
"IPv4NumToString",
660-
parameters=[
661-
_grouped_unique_scalar_value(
662-
column_name="ip_address_v4",
663-
aliased=False,
664-
)
665-
],
650+
parameters=[take_first_from_aggregation(column_name="ip_address_v4", aliased=False)],
666651
alias="user_ip",
667652
),
668-
"os_name": _grouped_unique_scalar_value(column_name="os_name"),
669-
"os_version": _grouped_unique_scalar_value(column_name="os_version"),
670-
"browser_name": _grouped_unique_scalar_value(column_name="browser_name"),
671-
"browser_version": _grouped_unique_scalar_value(column_name="browser_version"),
672-
"device_name": _grouped_unique_scalar_value(column_name="device_name"),
673-
"device_brand": _grouped_unique_scalar_value(column_name="device_brand"),
674-
"device_family": _grouped_unique_scalar_value(column_name="device_family"),
675-
"device_model": _grouped_unique_scalar_value(column_name="device_model"),
676-
"sdk_name": _grouped_unique_scalar_value(column_name="sdk_name"),
677-
"sdk_version": _grouped_unique_scalar_value(column_name="sdk_version"),
653+
"os_name": take_first_from_aggregation(column_name="os_name"),
654+
"os_version": take_first_from_aggregation(column_name="os_version"),
655+
"browser_name": take_first_from_aggregation(column_name="browser_name"),
656+
"browser_version": take_first_from_aggregation(column_name="browser_version"),
657+
"device_name": take_first_from_aggregation(column_name="device_name"),
658+
"device_brand": take_first_from_aggregation(column_name="device_brand"),
659+
"device_family": take_first_from_aggregation(column_name="device_family"),
660+
"device_model": take_first_from_aggregation(column_name="device_model"),
661+
"sdk_name": take_first_from_aggregation(column_name="sdk_name"),
662+
"sdk_version": take_first_from_aggregation(column_name="sdk_version"),
678663
"tk": Function("groupArrayArray", parameters=[Column("tags.key")], alias="tk"),
679664
"tv": Function("groupArrayArray", parameters=[Column("tags.value")], alias="tv"),
680665
}

src/sentry/replays/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def serialize(self, obj: ReplayRecordingSegment, attrs, user):
1717
VALID_FIELD_SET = {
1818
"id",
1919
"title",
20-
"projectId",
20+
"project_id",
2121
"errorIds",
2222
"traceIds",
2323
"urls",

tests/sentry/replays/test_organization_replay_index.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def test_get_replays(self):
6868
# error_ids=[uuid.uuid4().hex, replay1_id], # duplicate error-id
6969
urls=["http://localhost:3000/"], # duplicate urls are okay
7070
tags={"test": "world", "other": "hello"},
71+
error_ids=[],
7172
)
7273
)
7374

@@ -476,6 +477,10 @@ def test_get_replays_user_filters(self):
476477
seq2_timestamp,
477478
project.id,
478479
replay1_id,
480+
user_id=None,
481+
user_name=None,
482+
user_email=None,
483+
ipv4=None,
479484
os_name=None,
480485
os_version=None,
481486
browser_name=None,
@@ -485,6 +490,7 @@ def test_get_replays_user_filters(self):
485490
device_family=None,
486491
device_model=None,
487492
tags={"a": "n", "b": "o"},
493+
error_ids=[],
488494
)
489495
)
490496

tests/sentry/replays/test_project_replay_details.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def test_get_replay_schema(self):
109109
segment_id=1,
110110
trace_ids=[trace_id_2],
111111
urls=["http://www.sentry.io/"],
112+
error_ids=[],
112113
)
113114
)
114115
self.store_replays(
@@ -119,6 +120,7 @@ def test_get_replay_schema(self):
119120
segment_id=2,
120121
trace_ids=[trace_id_2],
121122
urls=["http://localhost:3000/"],
123+
error_ids=[],
122124
)
123125
)
124126

0 commit comments

Comments
 (0)