Skip to content

Commit

Permalink
feat(profiling): Use co_qualname in python 3.11
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Zylphrex committed Jan 11, 2023
1 parent 20c25f2 commit a64fc18
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 63 deletions.
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

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
37 changes: 24 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,17 @@ 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 +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.<locals>.wrapped",
id="instance_method_wrapped",
),
pytest.param(
Expand All @@ -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.<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 +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")],
Expand Down

0 comments on commit a64fc18

Please sign in to comment.