Browse Source

feat(perf-issues): add mock consecutive db issue (#43047)

When `./bin/load-mock --load-performance-issue` is ran, a consecutive db
issue event is created, (you'll have to manually make changes to
performance_detection.py so the issue is actually created for now, see
https://github.com/getsentry/sentry/commit/d27bd3a79c64e89fc35840f5626abe8ab05c725e#diff-883e9299956c4c5306e433683add49bb03f94644f51db3a18589cca1d1a21034L320-R323).

This helps to more easily develop the UI, without triggering issues
yourself.
This is how it looks so far, some of the data (like offending span) will
need to be changed, but its a good start!

![image](https://user-images.githubusercontent.com/44422760/211664412-198f2b42-4264-4e27-9961-a6e4179ed16e.png)
Dominik Buszowiecki 2 years ago
parent
commit
b26399effb

+ 180 - 109
bin/load-mocks

@@ -900,124 +900,195 @@ def create_mock_transactions(
                     time.sleep(0.05)
 
     if load_performance_issues:
-        print(f"    > Loading performance issues data")  # NOQA
-        trace_id = uuid4().hex
-        transaction_user = generate_user()
-        frontend_root_span_id = uuid4().hex[:16]
-
-        n_plus_one_db_current_offset = timestamp
-        n_plus_one_db_duration = timedelta(milliseconds=100)
-
-        parent_span_id = uuid4().hex[:16]
-
-        source_span = {
-            "timestamp": (timestamp + n_plus_one_db_duration).timestamp(),
-            "start_timestamp": (timestamp + timedelta(milliseconds=10)).timestamp(),
-            "description": "SELECT `books_book`.`id`, `books_book`.`title`, `books_book`.`author_id` FROM `books_book` ORDER BY `books_book`.`id` DESC LIMIT 10",
-            "op": "db",
-            "parent_span_id": parent_span_id,
-            "span_id": uuid4().hex[:16],
-            "hash": "858fea692d4d93e8",
-        }
-
-        def make_repeating_span(duration):
-            nonlocal timestamp
-            nonlocal n_plus_one_db_current_offset
-            nonlocal n_plus_one_db_duration
-            n_plus_one_db_duration += timedelta(milliseconds=duration) + timedelta(milliseconds=1)
-            n_plus_one_db_current_offset = timestamp + n_plus_one_db_duration
-            return {
-                "timestamp": (
-                    n_plus_one_db_current_offset + timedelta(milliseconds=duration)
-                ).timestamp(),
-                "start_timestamp": (
-                    n_plus_one_db_current_offset + timedelta(milliseconds=1)
-                ).timestamp(),
-                "description": "SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21",
+
+        def load_performance_issues():
+            print(f"    > Loading performance issues data")  # NOQA
+            print(f"    > Loading n plus one issue")  # NOQA
+            load_n_plus_one_issue()
+            time.sleep(1.0)
+            print(f"    > Loading consecutive db issue")  # NOQA
+            load_consecutive_db_issue()
+
+        def load_n_plus_one_issue():
+            trace_id = uuid4().hex
+            transaction_user = generate_user()
+            frontend_root_span_id = uuid4().hex[:16]
+
+            n_plus_one_db_current_offset = timestamp
+            n_plus_one_db_duration = timedelta(milliseconds=100)
+
+            parent_span_id = uuid4().hex[:16]
+
+            source_span = {
+                "timestamp": (timestamp + n_plus_one_db_duration).timestamp(),
+                "start_timestamp": (timestamp + timedelta(milliseconds=10)).timestamp(),
+                "description": "SELECT `books_book`.`id`, `books_book`.`title`, `books_book`.`author_id` FROM `books_book` ORDER BY `books_book`.`id` DESC LIMIT 10",
                 "op": "db",
-                "span_id": uuid4().hex[:16],
                 "parent_span_id": parent_span_id,
-                "hash": "63f1e89e6a073441",
+                "span_id": uuid4().hex[:16],
+                "hash": "858fea692d4d93e8",
             }
 
-        repeating_spans = [make_repeating_span(200) for _ in range(10)]
-
-        parent_span = {
-            "timestamp": (
-                timestamp + n_plus_one_db_duration + timedelta(milliseconds=200)
-            ).timestamp(),
-            "start_timestamp": timestamp.timestamp(),
-            "description": "new",
-            "op": "django.view",
-            "parent_span_id": uuid4().hex[:16],
-            "span_id": parent_span_id,
-            "hash": "0f43fb6f6e01ca52",
-        }
-
-        create_sample_event(
-            project=backend_project,
-            platform="transaction",
-            transaction="/n_plus_one_db/backend/",
-            event_id=uuid4().hex,
-            user=transaction_user,
-            timestamp=timestamp + n_plus_one_db_duration + timedelta(milliseconds=300),
-            start_timestamp=timestamp,
-            trace=trace_id,
-            parent_span_id=frontend_root_span_id,
-            spans=[
-                parent_span,
-                source_span,
-            ]
-            + repeating_spans,
-        )
+            def make_repeating_span(duration):
+                nonlocal timestamp
+                nonlocal n_plus_one_db_current_offset
+                nonlocal n_plus_one_db_duration
+                n_plus_one_db_duration += timedelta(milliseconds=duration) + timedelta(
+                    milliseconds=1
+                )
+                n_plus_one_db_current_offset = timestamp + n_plus_one_db_duration
+                return {
+                    "timestamp": (
+                        n_plus_one_db_current_offset + timedelta(milliseconds=duration)
+                    ).timestamp(),
+                    "start_timestamp": (
+                        n_plus_one_db_current_offset + timedelta(milliseconds=1)
+                    ).timestamp(),
+                    "description": "SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21",
+                    "op": "db",
+                    "span_id": uuid4().hex[:16],
+                    "parent_span_id": parent_span_id,
+                    "hash": "63f1e89e6a073441",
+                }
+
+            repeating_spans = [make_repeating_span(200) for _ in range(10)]
+
+            parent_span = {
+                "timestamp": (
+                    timestamp + n_plus_one_db_duration + timedelta(milliseconds=200)
+                ).timestamp(),
+                "start_timestamp": timestamp.timestamp(),
+                "description": "new",
+                "op": "django.view",
+                "parent_span_id": uuid4().hex[:16],
+                "span_id": parent_span_id,
+                "hash": "0f43fb6f6e01ca52",
+            }
+
+            create_sample_event(
+                project=backend_project,
+                platform="transaction",
+                transaction="/n_plus_one_db/backend/",
+                event_id=uuid4().hex,
+                user=transaction_user,
+                timestamp=timestamp + n_plus_one_db_duration + timedelta(milliseconds=300),
+                start_timestamp=timestamp,
+                trace=trace_id,
+                parent_span_id=frontend_root_span_id,
+                spans=[
+                    parent_span,
+                    source_span,
+                ]
+                + repeating_spans,
+            )
+
+            time.sleep(1.0)
+
+            create_sample_event(
+                project=backend_project,
+                platform="transaction",
+                transaction="/file-io-main-thread/",
+                event_id=uuid4().hex,
+                user=transaction_user,
+                timestamp=timestamp + timedelta(milliseconds=300),
+                start_timestamp=timestamp,
+                trace=trace_id,
+                parent_span_id=frontend_root_span_id,
+                spans=[
+                    parent_span,
+                    {
+                        "timestamp": (timestamp + timedelta(milliseconds=200)).timestamp(),
+                        "start_timestamp": timestamp.timestamp(),
+                        "description": "1669031858711_file.txt (4.0 kB)",
+                        "op": "file.write",
+                        "span_id": uuid4().hex[:16],
+                        "parent_span_id": parent_span_id,
+                        "status": "ok",
+                        "data": {
+                            "blocked_ui_thread": True,
+                            "call_stack": [
+                                {
+                                    "function": "onClick",
+                                    "in_app": True,
+                                    "lineno": 2,
+                                    "module": "io.sentry.samples.android.MainActivity$$ExternalSyntheticLambda6",
+                                    "native": False,
+                                },
+                                {
+                                    "filename": "MainActivity.java",
+                                    "function": "lambda$onCreate$5$io-sentry-samples-android-MainActivity",
+                                    "in_app": True,
+                                    "lineno": 93,
+                                    "module": "io.sentry.samples.android.MainActivity",
+                                    "native": False,
+                                },
+                            ],
+                            "file.path": "/data/user/0/io.sentry.samples.android/files/1669031858711_file.txt",
+                            "file.size": 4010,
+                        },
+                    },
+                ],
+            )
 
-        time.sleep(1.0)
-
-        create_sample_event(
-            project=backend_project,
-            platform="transaction",
-            transaction="/file-io-main-thread/",
-            event_id=uuid4().hex,
-            user=transaction_user,
-            timestamp=timestamp + timedelta(milliseconds=300),
-            start_timestamp=timestamp,
-            trace=trace_id,
-            parent_span_id=frontend_root_span_id,
-            spans=[
-                parent_span,
+        def load_consecutive_db_issue():
+            transaction_user = generate_user()
+            trace_id = uuid4().hex
+            parent_span_id = uuid4().hex[:16]
+
+            parent_span = {
+                "timestamp": (timestamp + timedelta(milliseconds=300)).timestamp(),
+                "start_timestamp": timestamp.timestamp(),
+                "description": "new",
+                "op": "django.view",
+                "parent_span_id": uuid4().hex[:16],
+                "span_id": parent_span_id,
+                "hash": "0f43fb6f6e01ca52",
+            }
+
+            spans = [
                 {
-                    "timestamp": (timestamp + timedelta(milliseconds=200)).timestamp(),
-                    "start_timestamp": timestamp.timestamp(),
-                    "description": "1669031858711_file.txt (4.0 kB)",
-                    "op": "file.write",
+                    "timestamp": (timestamp + timedelta(milliseconds=1000)).timestamp(),
+                    "start_timestamp": (timestamp + timedelta(milliseconds=300)).timestamp(),
+                    "description": "SELECT `customer`.`id` FROM `customers` WHERE `customer`.`name` = 'customerName'",
+                    "op": "db",
+                    "parent_span_id": parent_span_id,
                     "span_id": uuid4().hex[:16],
+                    "hash": "858fea692d4d93e9",
+                },
+                {
+                    "timestamp": (timestamp + timedelta(milliseconds=2000)).timestamp(),
+                    "start_timestamp": (timestamp + timedelta(milliseconds=1000)).timestamp(),
+                    "description": "SELECT COUNT(*) FROM `customers`",
+                    "op": "db",
                     "parent_span_id": parent_span_id,
-                    "status": "ok",
-                    "data": {
-                        "blocked_ui_thread": True,
-                        "call_stack": [
-                            {
-                                "function": "onClick",
-                                "in_app": True,
-                                "lineno": 2,
-                                "module": "io.sentry.samples.android.MainActivity$$ExternalSyntheticLambda6",
-                                "native": False,
-                            },
-                            {
-                                "filename": "MainActivity.java",
-                                "function": "lambda$onCreate$5$io-sentry-samples-android-MainActivity",
-                                "in_app": True,
-                                "lineno": 93,
-                                "module": "io.sentry.samples.android.MainActivity",
-                                "native": False,
-                            },
-                        ],
-                        "file.path": "/data/user/0/io.sentry.samples.android/files/1669031858711_file.txt",
-                        "file.size": 4010,
-                    },
+                    "span_id": uuid4().hex[:16],
+                    "hash": "858fea692d4d93e7",
                 },
-            ],
-        )
+                {
+                    "timestamp": (timestamp + timedelta(milliseconds=3000)).timestamp(),
+                    "start_timestamp": (timestamp + timedelta(milliseconds=2000)).timestamp(),
+                    "description": "SELECT COUNT(*) FROM `items`",
+                    "op": "db",
+                    "parent_span_id": parent_span_id,
+                    "span_id": uuid4().hex[:16],
+                    "hash": "858fea692d4d93e6",
+                },
+            ]
+
+            create_sample_event(
+                project=backend_project,
+                platform="transaction",
+                transaction="/consecutive-db/",
+                event_id=uuid4().hex,
+                user=transaction_user,
+                timestamp=timestamp + timedelta(milliseconds=300),
+                start_timestamp=timestamp,
+                trace=trace_id,
+                parent_span_id=parent_span_id,
+                spans=[parent_span] + spans,
+            )
+
+        load_performance_issues()
 
 
 if __name__ == "__main__":

+ 2 - 0
src/sentry/types/issues.py

@@ -25,6 +25,7 @@ GROUP_CATEGORIES_CUSTOM_EMAIL = (GroupCategory.ERROR, GroupCategory.PERFORMANCE)
 
 GROUP_TYPE_TO_CATEGORY = {
     GroupType.ERROR: GroupCategory.ERROR,
+    GroupType.PERFORMANCE_CONSECUTIVE_DB_OP: GroupCategory.PERFORMANCE,
     GroupType.PERFORMANCE_SLOW_SPAN: GroupCategory.PERFORMANCE,
     GroupType.PERFORMANCE_RENDER_BLOCKING_ASSET_SPAN: GroupCategory.PERFORMANCE,
     GroupType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES: GroupCategory.PERFORMANCE,
@@ -36,6 +37,7 @@ GROUP_TYPE_TO_CATEGORY = {
 
 GROUP_TYPE_TO_TEXT = {
     GroupType.ERROR: "Error",
+    GroupType.PERFORMANCE_CONSECUTIVE_DB_OP: "Consecutive DB Queries",
     GroupType.PERFORMANCE_SLOW_SPAN: "Slow Span",
     GroupType.PERFORMANCE_RENDER_BLOCKING_ASSET_SPAN: "Render Blocking Asset Span",
     GroupType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES: "N+1 Query",

+ 14 - 8
src/sentry/utils/performance_issues/performance_detection.py

@@ -77,6 +77,7 @@ DETECTOR_TYPE_TO_GROUP_TYPE = {
 DETECTOR_TYPE_ISSUE_CREATION_TO_SYSTEM_OPTION = {
     DetectorType.N_PLUS_ONE_DB_QUERIES: "performance.issues.n_plus_one_db.problem-creation",
     DetectorType.N_PLUS_ONE_DB_QUERIES_EXTENDED: "performance.issues.n_plus_one_db_ext.problem-creation",
+    DetectorType.CONSECUTIVE_DB_OP: "performance.issues.consecutive_db.problem-creation",
     DetectorType.N_PLUS_ONE_API_CALLS: "performance.issues.n_plus_one_api_calls.problem-creation",
 }
 
@@ -820,8 +821,8 @@ class ConsecutiveDBSpanDetector(PerformanceDetector):
         self.consecutive_db_spans.append(span)
 
     def _validate_and_store_performance_problem(self):
-        independent_db_spans = self._find_independent_spans(self.consecutive_db_spans)
-        if not len(independent_db_spans):
+        self._set_independent_spans(self.consecutive_db_spans)
+        if not len(self.independent_db_spans):
             return
 
         exceeds_count_threshold = len(self.consecutive_db_spans) >= self.settings.get(
@@ -830,11 +831,11 @@ class ConsecutiveDBSpanDetector(PerformanceDetector):
         exceeds_span_duration_threshold = all(
             get_span_duration(span).total_seconds() * 1000
             > self.settings.get("span_duration_threshold")
-            for span in independent_db_spans
+            for span in self.independent_db_spans
         )
 
         exceeds_time_saved_threshold = self._calculate_time_saved(
-            independent_db_spans
+            self.independent_db_spans
         ) > self.settings.get("min_time_saved")
 
         if (
@@ -847,16 +848,20 @@ class ConsecutiveDBSpanDetector(PerformanceDetector):
     def _store_performance_problem(self) -> None:
         fingerprint = self._fingerprint()
         offender_span_ids = [span.get("span_id", None) for span in self.consecutive_db_spans]
+        query: str = self.independent_db_spans[0].get("description", None)
+
         self.stored_problems[fingerprint] = PerformanceProblem(
             fingerprint,
             "db",
-            "consecutive db",
-            GroupType.PERFORMANCE_CONSECUTIVE_DB_OP,
+            desc=query,  # TODO - figure out which query to use for description
+            type=GroupType.PERFORMANCE_CONSECUTIVE_DB_OP,
             cause_span_ids=None,
             parent_span_ids=None,
             offender_span_ids=offender_span_ids,
         )
 
+        self._reset_variables()
+
     def _sum_span_duration(self, spans: list[Span]) -> int:
         "Given a list of spans, find the sum of the span durations in milliseconds"
         sum = 0
@@ -864,7 +869,7 @@ class ConsecutiveDBSpanDetector(PerformanceDetector):
             sum += get_span_duration(span).total_seconds() * 1000
         return sum
 
-    def _find_independent_spans(self, spans: list[Span]) -> list[Span]:
+    def _set_independent_spans(self, spans: list[Span]):
         """
         Given a list of spans, checks if there is at least a single span that is independent of the rest.
         To start, we are just checking for a span in a list of consecutive span without a WHERE clause
@@ -879,7 +884,7 @@ class ConsecutiveDBSpanDetector(PerformanceDetector):
                 and not CONTAINS_PARAMETER_REGEX.search(query)
             ):
                 independent_spans.append(span)
-        return independent_spans
+        self.independent_db_spans = independent_spans
 
     def _calculate_time_saved(self, independent_spans: list[Span]) -> float:
         """
@@ -914,6 +919,7 @@ class ConsecutiveDBSpanDetector(PerformanceDetector):
 
     def _reset_variables(self) -> None:
         self.consecutive_db_spans = []
+        self.independent_db_spans = []
 
     def _is_db_query(self, span: Span) -> bool:
         op: str = span.get("op", "") or ""

+ 4 - 6
tests/sentry/utils/performance_issues/test_consecutive_db_detector.py

@@ -50,7 +50,7 @@ class ConsecutiveDbDetectorTest(unittest.TestCase):
             PerformanceProblem(
                 fingerprint="1-GroupType.PERFORMANCE_CONSECUTIVE_DB_OP-e6a9fc04320a924f46c7c737432bb0389d9dd095",
                 op="db",
-                desc="consecutive db",
+                desc="SELECT `order`.`id` FROM `books_author`",
                 type=GroupType.PERFORMANCE_CONSECUTIVE_DB_OP,
                 parent_span_ids=None,
                 cause_span_ids=None,
@@ -94,9 +94,7 @@ class ConsecutiveDbDetectorTest(unittest.TestCase):
         spans = [modify_span_start(span, span_duration * spans.index(span)) for span in spans]
         event = create_event(spans)
 
-        detector = ConsecutiveDBSpanDetector(self.settings, event)
-        run_detector_on_data(detector, event)
-        problems = list(detector.stored_problems.values())
+        problems = self.find_problems(event)
 
         assert problems == []
 
@@ -149,7 +147,7 @@ class ConsecutiveDbDetectorTest(unittest.TestCase):
             PerformanceProblem(
                 fingerprint="1-GroupType.PERFORMANCE_CONSECUTIVE_DB_OP-0700523cc3ca755e447329779e50aeb19549e74f",
                 op="db",
-                desc="consecutive db",
+                desc="SELECT `books_book`.`id`, `books_book`.`title`, `books_book`.`author_id` FROM `books_book` ORDER BY `books_book`.`id` ASC LIMIT 1",
                 type=GroupType.PERFORMANCE_CONSECUTIVE_DB_OP,
                 parent_span_ids=None,
                 cause_span_ids=None,
@@ -205,7 +203,7 @@ class ConsecutiveDbDetectorTest(unittest.TestCase):
             PerformanceProblem(
                 fingerprint="1-GroupType.PERFORMANCE_CONSECUTIVE_DB_OP-e6a9fc04320a924f46c7c737432bb0389d9dd095",
                 op="db",
-                desc="consecutive db",
+                desc="SELECT COUNT(*) FROM `products`",
                 type=GroupType.PERFORMANCE_CONSECUTIVE_DB_OP,
                 parent_span_ids=None,
                 cause_span_ids=None,