From a64fc188c2613ac654a963229628dc5962674e7b Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 11 Jan 2023 15:36:56 -0500 Subject: [PATCH] feat(profiling): Use co_qualname in python 3.11 The `get_frame_name` implementation works well for <3.11 but 3.11 introduced a `co_qualname` that works like our implementation of `get_frame_name` and handles some cases better. --- sentry_sdk/_compat.py | 1 + sentry_sdk/profiler.py | 105 +++++++++++++++++++++-------------------- tests/test_profiler.py | 37 ++++++++++----- 3 files changed, 80 insertions(+), 63 deletions(-) diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index f8c579e984..63f1b2e6a5 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -16,6 +16,7 @@ PY33 = sys.version_info[0] == 3 and sys.version_info[1] >= 3 PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7 PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10 +PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11 if PY2: import urlparse diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py index 81ba8f5753..5460cf57e9 100644 --- a/sentry_sdk/profiler.py +++ b/sentry_sdk/profiler.py @@ -24,7 +24,7 @@ from contextlib import contextmanager import sentry_sdk -from sentry_sdk._compat import PY33 +from sentry_sdk._compat import PY33, PY311 from sentry_sdk._types import MYPY from sentry_sdk.utils import ( filename_for_module, @@ -241,55 +241,60 @@ def extract_frame(frame, cwd): ) -def get_frame_name(frame): - # type: (FrameType) -> str - - # in 3.11+, there is a frame.f_code.co_qualname that - # we should consider using instead where possible - - f_code = frame.f_code - co_varnames = f_code.co_varnames - - # co_name only contains the frame name. If the frame was a method, - # the class name will NOT be included. - name = f_code.co_name - - # if it was a method, we can get the class name by inspecting - # the f_locals for the `self` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `self` if its an instance method - co_varnames - and co_varnames[0] == "self" - and "self" in frame.f_locals - ): - for cls in frame.f_locals["self"].__class__.__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except AttributeError: - pass - - # if it was a class method, (decorated with `@classmethod`) - # we can get the class name by inspecting the f_locals for the `cls` argument - try: - if ( - # the co_varnames start with the frame's positional arguments - # and we expect the first to be `cls` if its a class method - co_varnames - and co_varnames[0] == "cls" - and "cls" in frame.f_locals - ): - for cls in frame.f_locals["cls"].__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except AttributeError: - pass - - # nothing we can do if it is a staticmethod (decorated with @staticmethod) - - # we've done all we can, time to give up and return what we have - return name +if PY311: + + def get_frame_name(frame): + # type: (FrameType) -> str + return frame.f_code.co_qualname # type: ignore + +else: + + def get_frame_name(frame): + # type: (FrameType) -> str + + f_code = frame.f_code + co_varnames = f_code.co_varnames + + # co_name only contains the frame name. If the frame was a method, + # the class name will NOT be included. + name = f_code.co_name + + # if it was a method, we can get the class name by inspecting + # the f_locals for the `self` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `self` if its an instance method + co_varnames + and co_varnames[0] == "self" + and "self" in frame.f_locals + ): + for cls in frame.f_locals["self"].__class__.__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except AttributeError: + pass + + # if it was a class method, (decorated with `@classmethod`) + # we can get the class name by inspecting the f_locals for the `cls` argument + try: + if ( + # the co_varnames start with the frame's positional arguments + # and we expect the first to be `cls` if its a class method + co_varnames + and co_varnames[0] == "cls" + and "cls" in frame.f_locals + ): + for cls in frame.f_locals["cls"].__mro__: + if name in cls.__dict__: + return "{}.{}".format(cls.__name__, name) + except AttributeError: + pass + + # nothing we can do if it is a staticmethod (decorated with @staticmethod) + + # we've done all we can, time to give up and return what we have + return name MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds diff --git a/tests/test_profiler.py b/tests/test_profiler.py index 44474343ce..d30531339b 100644 --- a/tests/test_profiler.py +++ b/tests/test_profiler.py @@ -16,16 +16,17 @@ from sentry_sdk.tracing import Transaction -minimum_python_33 = pytest.mark.skipif( - sys.version_info < (3, 3), reason="Profiling is only supported in Python >= 3.3" -) +def requires_python_version(major, minor, reason=None): + if reason is None: + reason = "Requires Python{}.{}".format(major, minor) + return pytest.mark.skipif(sys.version_info < (major, minor), reason=reason) def process_test_sample(sample): return [(tid, (stack, stack)) for tid, stack in sample] -@minimum_python_33 +@requires_python_version(3, 3) def test_profiler_invalid_mode(teardown_profiling): with pytest.raises(ValueError): setup_profiler({"_experiments": {"profiler_mode": "magic"}}) @@ -126,7 +127,9 @@ def static_method(): ), pytest.param( GetFrame().instance_method_wrapped()(), - "wrapped", + "wrapped" + if sys.version_info < (3, 11) + else "GetFrame.instance_method_wrapped..wrapped", id="instance_method_wrapped", ), pytest.param( @@ -136,14 +139,17 @@ def static_method(): ), pytest.param( GetFrame().class_method_wrapped()(), - "wrapped", + "wrapped" + if sys.version_info < (3, 11) + else "GetFrame.class_method_wrapped..wrapped", id="class_method_wrapped", ), pytest.param( GetFrame().static_method(), - "GetFrame.static_method", + "static_method" + if sys.version_info < (3, 11) + else "GetFrame.static_method", id="static_method", - marks=pytest.mark.skip(reason="unsupported"), ), pytest.param( GetFrame().inherited_instance_method(), @@ -152,7 +158,9 @@ def static_method(): ), pytest.param( GetFrame().inherited_instance_method_wrapped()(), - "wrapped", + "wrapped" + if sys.version_info < (3, 11) + else "GetFrameBase.inherited_instance_method_wrapped..wrapped", id="instance_method_wrapped", ), pytest.param( @@ -162,14 +170,17 @@ def static_method(): ), pytest.param( GetFrame().inherited_class_method_wrapped()(), - "wrapped", + "wrapped" + if sys.version_info < (3, 11) + else "GetFrameBase.inherited_class_method_wrapped..wrapped", id="inherited_class_method_wrapped", ), pytest.param( GetFrame().inherited_static_method(), - "GetFrameBase.static_method", + "inherited_static_method" + if sys.version_info < (3, 11) + else "GetFrameBase.inherited_static_method", id="inherited_static_method", - marks=pytest.mark.skip(reason="unsupported"), ), ], ) @@ -255,7 +266,7 @@ def get_scheduler_threads(scheduler): return [thread for thread in threading.enumerate() if thread.name == scheduler.name] -@minimum_python_33 +@requires_python_version(3, 3) @pytest.mark.parametrize( ("scheduler_class",), [pytest.param(SleepScheduler, id="sleep scheduler")],