Skip to content

Commit

Permalink
feat(profiling): Use co_qualname in python 3.11 (#1831)
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 authored Jan 19, 2023
1 parent 0714d9f commit 086e385
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 58 deletions.
1 change: 1 addition & 0 deletions sentry_sdk/_compat.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

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
Original file line number Diff line number Diff line change
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

0 comments on commit 086e385

Please sign in to comment.