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 1 commit
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
105 changes: 55 additions & 50 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 @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

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


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
Expand Down
35 changes: 22 additions & 13 deletions tests/test_profiler.py
Expand Up @@ -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"}})
Expand Down Expand Up @@ -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.<locals>.wrapped",
id="instance_method_wrapped",
),
pytest.param(
Expand All @@ -136,14 +139,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 @@ -152,7 +156,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 @@ -162,14 +168,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 @@ -255,7 +264,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")],
Expand Down