Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling): Use co_qualname in python 3.11 #1831

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry_sdk/_compat.py
Expand Up @@ -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
Expand Down
97 changes: 51 additions & 46 deletions sentry_sdk/profiler.py
Expand Up @@ -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,
Expand Down Expand Up @@ -269,55 +269,60 @@ def extract_frame(frame, cwd):
)


def get_frame_name(frame):
# type: (FrameType) -> str
if PY311:

# in 3.11+, there is a frame.f_code.co_qualname that
# we should consider using instead where possible
def get_frame_name(frame):
# type: (FrameType) -> str
return frame.f_code.co_qualname # type: ignore
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


f_code = frame.f_code
co_varnames = f_code.co_varnames
else:

# 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
def get_frame_name(frame):
# type: (FrameType) -> str

# 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
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
Expand Down
35 changes: 23 additions & 12 deletions tests/test_profiler.py
Expand Up @@ -22,9 +22,11 @@
gevent = None


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)


requires_gevent = pytest.mark.skipif(gevent is None, reason="gevent not enabled")

Expand All @@ -33,6 +35,7 @@ def process_test_sample(sample):
return [(tid, (stack, stack)) for tid, stack in sample]


@requires_python_version(3, 3)
@pytest.mark.parametrize(
"mode",
[
Expand Down Expand Up @@ -146,7 +149,9 @@ def static_method():
),
pytest.param(
GetFrame().instance_method_wrapped()(),
"wrapped",
"wrapped"
if sys.version_info < (3, 11)
else "GetFrame.instance_method_wrapped.<locals>.wrapped",
id="instance_method_wrapped",
),
pytest.param(
Expand All @@ -156,14 +161,15 @@ def static_method():
),
pytest.param(
GetFrame().class_method_wrapped()(),
"wrapped",
"wrapped"
if sys.version_info < (3, 11)
else "GetFrame.class_method_wrapped.<locals>.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(),
Expand All @@ -172,7 +178,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.<locals>.wrapped",
id="instance_method_wrapped",
),
pytest.param(
Expand All @@ -182,14 +190,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.<locals>.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"),
),
],
)
Expand Down Expand Up @@ -275,7 +286,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",),
[
Expand Down