Просмотр исходного кода

feat(perf-issues): Deobfuscate call stack in file io issues (#42026)

- If the event includes a debug_meta for proguard, retrieve the proguard
data and use it to deobfuscate the call stack for fingerprinting
- Also updates blocked_ui_thread to blocked_main_thread
- Closes #42075
William Mak 2 лет назад
Родитель
Сommit
45b0edbcd2

+ 295 - 0
fixtures/events/performance_problems/file-io-on-main-thread-with-obfuscation.json

@@ -0,0 +1,295 @@
+{
+  "event_id": "8ab6e6ebbaa5458687143fe0e19a1d8d",
+  "project": 5428559,
+  "release": "io.sentry.samples.android@1.1.0+2",
+  "dist": "2",
+  "platform": "java",
+  "message": "",
+  "datetime": "2022-11-25T22:43:58.519693+00:00",
+  "tags": [
+    [
+      "device",
+      "Android SDK built for x86"
+    ],
+    [
+      "device.family",
+      "Android"
+    ],
+    [
+      "environment",
+      "release"
+    ],
+    [
+      "isSideLoaded",
+      "true"
+    ],
+    [
+      "level",
+      "info"
+    ],
+    [
+      "os",
+      "Android 10"
+    ],
+    [
+      "os.name",
+      "Android"
+    ],
+    [
+      "os.rooted",
+      "no"
+    ],
+    [
+      "dist",
+      "2"
+    ],
+    [
+      "release",
+      "io.sentry.samples.android@1.1.0+2"
+    ],
+    [
+      "user",
+      "id:0f38dda2-6ef7-4d9a-b7f4-6bcc80b85b2e"
+    ],
+    [
+      "transaction",
+      "MainActivity.add_attachment"
+    ]
+  ],
+  "_metrics": {
+    "bytes.ingested.event": 3552,
+    "bytes.stored.event": 4685
+  },
+  "breadcrumbs": {
+    "values": [
+      {
+        "timestamp": 1669416139.706,
+        "type": "navigation",
+        "category": "ui.lifecycle",
+        "level": "info",
+        "data": {
+          "screen": "MainActivity",
+          "state": "created"
+        }
+      },
+      {
+        "timestamp": 1669416139.9,
+        "type": "navigation",
+        "category": "ui.lifecycle",
+        "level": "info",
+        "data": {
+          "screen": "MainActivity",
+          "state": "started"
+        }
+      },
+      {
+        "timestamp": 1669416139.902,
+        "type": "session",
+        "category": "app.lifecycle",
+        "level": "info",
+        "data": {
+          "state": "start"
+        }
+      },
+      {
+        "timestamp": 1669416139.903,
+        "type": "navigation",
+        "category": "app.lifecycle",
+        "level": "info",
+        "data": {
+          "state": "foreground"
+        }
+      },
+      {
+        "timestamp": 1669416139.903,
+        "type": "navigation",
+        "category": "ui.lifecycle",
+        "level": "info",
+        "data": {
+          "screen": "MainActivity",
+          "state": "resumed"
+        }
+      },
+      {
+        "timestamp": 1669416238.454,
+        "type": "user",
+        "category": "ui.click",
+        "level": "info",
+        "data": {
+          "view.class": "k.e",
+          "view.id": "add_attachment"
+        }
+      }
+    ]
+  },
+  "breakdowns": {
+    "span_ops": {
+      "total.time": {
+        "value": 17.19904,
+        "unit": "millisecond"
+      }
+    }
+  },
+  "contexts": {
+    "app": {
+      "app_start_time": "2022-11-25T22:42:19.465Z",
+      "app_identifier": "io.sentry.samples.android",
+      "app_name": "Sentry sample",
+      "app_version": "1.1.0",
+      "app_build": "2",
+      "permissions": {
+        "ACCESS_NETWORK_STATE": "granted",
+        "CAMERA": "not_granted",
+        "INTERNET": "granted",
+        "READ_EXTERNAL_STORAGE": "not_granted",
+        "READ_PHONE_STATE": "not_granted",
+        "WRITE_EXTERNAL_STORAGE": "not_granted"
+      },
+      "type": "app"
+    },
+    "device": {
+      "name": "Android SDK built for x86",
+      "family": "Android",
+      "model": "Android SDK built for x86",
+      "model_id": "QSR1.190920.001",
+      "orientation": "portrait",
+      "manufacturer": "Google",
+      "brand": "google",
+      "screen_density": 2.625,
+      "screen_dpi": 420,
+      "simulator": true,
+      "boot_time": "2022-11-25T21:17:00.121Z",
+      "timezone": "Europe/Vienna",
+      "archs": [
+        "x86"
+      ],
+      "id": "0f38dda2-6ef7-4d9a-b7f4-6bcc80b85b2e",
+      "language": "en",
+      "locale": "en_US",
+      "screen_height_pixels": 1794,
+      "screen_width_pixels": 1080,
+      "type": "device"
+    },
+    "os": {
+      "name": "Android",
+      "version": "10",
+      "build": "QSR1.190920.001",
+      "kernel_version": "4.14.112+",
+      "rooted": false,
+      "type": "os"
+    },
+    "trace": {
+      "trace_id": "e4ab5e05af08473886eb06690f358fbf",
+      "span_id": "e00f26281ba24d3f",
+      "op": "ui.action.click",
+      "status": "ok",
+      "exclusive_time": 49.000024,
+      "client_sample_rate": 1,
+      "hash": "1ff9a18d6a6b09a8",
+      "type": "trace"
+    }
+  },
+  "culprit": "MainActivity.add_attachment",
+  "debug_meta": {
+    "images": [
+      {
+        "uuid": "467ade76-6d0b-11ed-a1eb-0242ac120002",
+        "type": "proguard"
+      }
+    ]
+  },
+  "environment": "release",
+  "grouping_config": {
+    "enhancements": "eJybzDRxY3J-bm5-npWRgaGlroGxrpHxxEkT1-Zm5usVp-aVFFXqaWlNZAQAKGsOFg",
+    "id": "newstyle:2019-10-29"
+  },
+  "hashes": [],
+  "ingest_path": [
+    {
+      "version": "22.11.0",
+      "public_key": "XE7QiyuNlja9PZ7I9qJlwQotzecWrUIN91BAO7Q5R38"
+    }
+  ],
+  "key_id": "1336851",
+  "level": "info",
+  "location": "MainActivity.add_attachment",
+  "logger": "",
+  "metadata": {
+    "location": "MainActivity.add_attachment",
+    "title": "MainActivity.add_attachment"
+  },
+  "nodestore_insert": 1669416243.421129,
+  "received": 1669416242.917276,
+  "sdk": {
+    "name": "sentry.java.android.timber",
+    "version": "6.8.0",
+    "packages": [
+      {
+        "name": "maven:io.sentry:sentry",
+        "version": "6.8.0"
+      },
+      {
+        "name": "maven:io.sentry:sentry-android-core",
+        "version": "6.8.0"
+      },
+      {
+        "name": "maven:io.sentry:sentry-android-ndk",
+        "version": "6.8.0"
+      },
+      {
+        "name": "maven:io.sentry:sentry-android-timber",
+        "version": "6.8.0"
+      }
+    ]
+  },
+  "span_grouping_config": {
+    "id": "default:2022-10-27"
+  },
+  "spans": [
+    {
+      "timestamp": 1669416238.520199,
+      "start_timestamp": 1669416238.503,
+      "exclusive_time": 17.19904,
+      "description": "1669416238502_file.txt (4.0 kB)",
+      "op": "file.write",
+      "span_id": "25cec4d31c2f4acb",
+      "parent_span_id": "e00f26281ba24d3f",
+      "trace_id": "e4ab5e05af08473886eb06690f358fbf",
+      "status": "ok",
+      "data": {
+        "blocked_main_thread": true,
+        "call_stack": [
+          {
+            "filename": "R8$$SyntheticClass",
+            "function": "a",
+            "in_app": true,
+            "lineno": 69,
+            "module": "org.a.b.g$a",
+            "native": false
+          }
+        ],
+        "file.path": "/data/user/0/io.sentry.samples.android/files/1669416238502_file.txt",
+        "file.size": 4010
+      },
+      "hash": "67650ed457dbcced"
+    }
+  ],
+  "start_timestamp": 1669416238.454,
+  "timestamp": 1669416238.519693,
+  "title": "MainActivity.add_attachment",
+  "transaction": "MainActivity.add_attachment",
+  "transaction_info": {
+    "source": "component"
+  },
+  "type": "transaction",
+  "user": {
+    "id": "0f38dda2-6ef7-4d9a-b7f4-6bcc80b85b2e",
+    "ip_address": "86.56.213.87",
+    "geo": {
+      "country_code": "AT",
+      "city": "Hoersching",
+      "region": "Austria"
+    }
+  },
+  "version": "7"
+}

+ 1 - 1
fixtures/events/performance_problems/file-io-on-main-thread.json

@@ -250,7 +250,7 @@
       "trace_id": "b2a33f3f79fe4a7c8de3426725a045cb",
       "status": "ok",
       "data": {
-        "blocked_ui_thread": true,
+        "blocked_main_thread": true,
         "call_stack": [
           {
             "function": "onClick",

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

@@ -13,10 +13,11 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
 from urllib.parse import urlparse
 
 import sentry_sdk
+from symbolic import ProguardMapper  # type: ignore
 
 from sentry import features, nodestore, options, projectoptions
 from sentry.eventstore.models import Event
-from sentry.models import Organization, Project, ProjectOption
+from sentry.models import Organization, Project, ProjectDebugFile, ProjectOption
 from sentry.types.issues import GROUP_TYPE_TO_TEXT, GroupType
 from sentry.utils import metrics
 from sentry.utils.event_frames import get_sdk_name
@@ -1357,6 +1358,51 @@ class FileIOMainThreadDetector(PerformanceDetector):
         self.most_recent_start_time = {}
         self.most_recent_hash = {}
         self.stored_problems = {}
+        self.mapper = None
+        self._prepare_deobfuscation()
+
+    def _prepare_deobfuscation(self):
+        event = self._event
+        if "debug_meta" in event:
+            images = event["debug_meta"].get("images", [])
+            project_id = event.get("project")
+            if not isinstance(images, list):
+                return
+            if project_id is not None:
+                project = Project.objects.get_from_cache(id=project_id)
+            else:
+                return
+
+            for image in images:
+                if image.get("type") == "proguard":
+                    uuid = image.get("uuid")
+                    dif_paths = ProjectDebugFile.difcache.fetch_difs(
+                        project, [uuid], features=["mapping"]
+                    )
+                    debug_file_path = dif_paths.get(uuid)
+                    if debug_file_path is None:
+                        return
+
+                    mapper = ProguardMapper.open(debug_file_path)
+                    if not mapper.has_line_info:
+                        return
+                    self.mapper = mapper
+                    return
+
+    def _deobfuscate_module(self, module: str) -> str:
+        if self.mapper is not None:
+            return self.mapper.remap_class(module)
+        else:
+            return module
+
+    def _deobfuscate_function(self, frame):
+        if self.mapper is not None and "module" in frame and "function" in frame:
+            functions = self.mapper.remap_frame(
+                frame["module"], frame["function"], frame.get("lineno") or 0
+            )
+            return ".".join([func.method for func in functions])
+        else:
+            return frame.get("function", "")
 
     def visit_span(self, span: Span):
         if self._is_file_io_on_main_thread(span):
@@ -1378,12 +1424,12 @@ class FileIOMainThreadDetector(PerformanceDetector):
                 )
 
     def _fingerprint(self, span) -> str:
-        call_stack = ".".join(
-            [
-                f"{item.get('module', '')}.{item.get('function', '')}"
-                for item in span.get("data", {}).get("call_stack", [])
-            ]
-        ).encode("utf8")
+        call_stack_strings = []
+        for item in span.get("data", {}).get("call_stack", []):
+            module = self._deobfuscate_module(item.get("module", ""))
+            function = self._deobfuscate_function(item)
+            call_stack_strings.append(f"{module}.{function}")
+        call_stack = ".".join(call_stack_strings).encode("utf8")
         hashed_stack = hashlib.sha1(call_stack).hexdigest()
         return f"1-{GroupType.PERFORMANCE_FILE_IO_MAIN_THREAD}-{hashed_stack}"
 
@@ -1392,7 +1438,7 @@ class FileIOMainThreadDetector(PerformanceDetector):
         if data is None:
             return False
         # doing is True since the value can be any type
-        return data.get("blocked_ui_thread", False) is True
+        return data.get("blocked_main_thread", False) is True
 
 
 # Reports metrics and creates spans for detection

+ 43 - 2
tests/sentry/utils/performance_issues/test_file_io_on_main_thread_detector.py

@@ -1,23 +1,47 @@
-import unittest
+import hashlib
+from io import BytesIO
+from zipfile import ZipFile
 
 import pytest
 
+from sentry.models import create_files_from_dif_zip
+from sentry.testutils import TestCase
 from sentry.testutils.performance_issues.event_generators import EVENTS
 from sentry.testutils.silo import region_silo_test
+from sentry.types.issues import GroupType
 from sentry.utils.performance_issues.performance_detection import (
     FileIOMainThreadDetector,
     get_detection_settings,
     run_detector_on_data,
 )
 
+PROGUARD_SOURCE = b"""\
+# compiler: R8
+# compiler_version: 2.0.74
+# min_api: 16
+# pg_map_id: 5b46fdc
+# common_typos_disable
+# {"id":"com.android.tools.r8.mapping","version":"1.0"}
+org.slf4j.helpers.Util$ClassContextSecurityManager -> org.a.b.g$a:
+    65:65:void <init>() -> <init>
+    67:67:java.lang.Class[] getClassContext() -> a
+    69:69:java.lang.Class[] getExtraClassContext() -> a
+    65:65:void <init>(org.slf4j.helpers.Util$1) -> <init>
+"""
+
 
 @region_silo_test
 @pytest.mark.django_db
-class NPlusOneAPICallsDetectorTest(unittest.TestCase):
+class NPlusOneAPICallsDetectorTest(TestCase):
     def setUp(self):
         super().setUp()
         self.settings = get_detection_settings()
 
+    def create_proguard(self, uuid):
+        with ZipFile(BytesIO(), "w") as f:
+            f.writestr(f"proguard/{uuid}.txt", PROGUARD_SOURCE)
+            create_files_from_dif_zip(f, project=self.project)
+
     def test_gives_problem_correct_title(self):
         event = EVENTS["file-io-on-main-thread"]
 
@@ -25,3 +49,20 @@ class NPlusOneAPICallsDetectorTest(unittest.TestCase):
         run_detector_on_data(detector, event)
         problem = list(detector.stored_problems.values())[0]
         assert problem.title == "File IO on Main Thread"
+
+    def test_file_io_with_proguard(self):
+        event = EVENTS["file-io-on-main-thread-with-obfuscation"]
+        event["project"] = self.project.id
+
+        uuid = event["debug_meta"]["images"][0]["uuid"]
+        self.create_proguard(uuid)
+
+        detector = FileIOMainThreadDetector(self.settings, event)
+        run_detector_on_data(detector, event)
+        problem = list(detector.stored_problems.values())[0]
+        call_stack = b"org.slf4j.helpers.Util$ClassContextSecurityManager.getExtraClassContext"
+        hashed_stack = hashlib.sha1(call_stack).hexdigest()
+        assert (
+            problem.fingerprint == f"1-{GroupType.PERFORMANCE_FILE_IO_MAIN_THREAD}-{hashed_stack}"
+        )
+        assert problem.title == "File IO on Main Thread"

+ 1 - 1
tests/sentry/utils/performance_issues/test_performance_detection.py

@@ -1008,7 +1008,7 @@ class PerformanceDetectionTest(unittest.TestCase):
 
     def test_does_not_detect_file_io_main_thread(self):
         file_io_event = EVENTS["file-io-on-main-thread"]
-        file_io_event["spans"][0]["data"]["blocked_ui_thread"] = False
+        file_io_event["spans"][0]["data"]["blocked_main_thread"] = False
         sdk_span_mock = Mock()
 
         _detect_performance_problems(file_io_event, sdk_span_mock)