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

Add experimental profiler under experiments.enable_profiling #1481

Merged
merged 41 commits into from Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
67a5572
Work-in-progress integration of profiler into WSGI
szokeasaurusrex Jun 27, 2022
e591c90
Deleted dead code
szokeasaurusrex Jun 28, 2022
1da4c76
Sentry-compatible JSON representation of profile
szokeasaurusrex Jun 29, 2022
cfa6fa9
Changed profile name to "main"
szokeasaurusrex Jun 30, 2022
be558ec
Added option for conditionally enabling profiling
szokeasaurusrex Jun 30, 2022
7e8e9cc
Adding transaction name to profile
szokeasaurusrex Jun 30, 2022
596f474
Save only the raw file name
szokeasaurusrex Jun 30, 2022
d4b309b
Merge branch 'master' into profiling
szokeasaurusrex Jul 6, 2022
d1755a1
Memory optimized, no overwriting profiles
szokeasaurusrex Jul 6, 2022
cff0127
Changed JSON output format
szokeasaurusrex Jul 8, 2022
7b97f69
Upload profile data to Sentry
szokeasaurusrex Jul 8, 2022
3b5f032
Stop printing profiles to JSON file locally
szokeasaurusrex Jul 8, 2022
8da6d10
Only save profile to event if profiling enabled
szokeasaurusrex Jul 11, 2022
78e6c1d
Fix bug: error when JSON generated no sample
szokeasaurusrex Jul 11, 2022
ccd2e45
Profile sent as separate envelope item
szokeasaurusrex Jul 11, 2022
68d77e3
Fix bug crashing program when no profile collected
szokeasaurusrex Jul 12, 2022
cb8ea79
Changed licensing notice in profiler.py
szokeasaurusrex Jul 13, 2022
ac41502
Merge branch 'master' into profiling
szokeasaurusrex Jul 15, 2022
5ba799f
Changes from Neel's code review feedback
szokeasaurusrex Jul 15, 2022
01599e8
Merge branch 'profiling' into profiling-new-data-format
szokeasaurusrex Jul 15, 2022
b418ac0
Code review changes for Neel
szokeasaurusrex Jul 15, 2022
2ace5fc
Deleted dead sample_weights method
szokeasaurusrex Jul 20, 2022
c475273
Fixed linter errors
szokeasaurusrex Jul 20, 2022
fb8989d
Fixed linter error that was missed in previous commit
szokeasaurusrex Jul 20, 2022
3797c39
Ran pre-commit hooks against the code
szokeasaurusrex Jul 20, 2022
9c84589
Fix MYPY errors
szokeasaurusrex Jul 20, 2022
d387cb3
Merge branch 'master' into profiling
sl0thentr0py Jul 21, 2022
fb58394
Added metadata to profile
szokeasaurusrex Jul 21, 2022
f8495a9
merge
szokeasaurusrex Jul 21, 2022
bc53830
Merge branch 'master' into profiling
sl0thentr0py Jul 22, 2022
b95f46c
enable_profiling flag moved to experiment
szokeasaurusrex Jul 22, 2022
8944fa6
Merge branch 'profiling' of https://github.com/getsentry/sentry-pytho…
szokeasaurusrex Jul 22, 2022
444de7f
Merge branch 'master' into profiling
szokeasaurusrex Jul 22, 2022
9d6a494
Added integration tests
szokeasaurusrex Jul 22, 2022
6c8fc38
Fixed thread ID call for Python 2.7
szokeasaurusrex Jul 25, 2022
1859525
Fixed MYPY error
szokeasaurusrex Jul 25, 2022
a93f10c
Add some comments, suppress missing import
szokeasaurusrex Jul 25, 2022
07e1f4b
Fixed Python2 compatibility issues
szokeasaurusrex Jul 25, 2022
d1ca7d9
Set version_code field to empty string
szokeasaurusrex Jul 25, 2022
c1f0b64
Move context manager to profiler
sl0thentr0py Jul 28, 2022
837c409
Factor out profiling hceck
sl0thentr0py Jul 28, 2022
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
56 changes: 29 additions & 27 deletions sentry_sdk/integrations/wsgi.py
@@ -1,4 +1,5 @@
import sys
import sentry_sdk.profiler as profiler

from sentry_sdk._functools import partial
from sentry_sdk.hub import Hub, _should_send_default_pii
Expand Down Expand Up @@ -109,34 +110,35 @@ def __call__(self, environ, start_response):
_wsgi_middleware_applied.set(True)
try:
hub = Hub(Hub.current)
with auto_session_tracking(hub, session_mode="request"):
with hub:
with capture_internal_exceptions():
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
scope._name = "wsgi"
scope.add_event_processor(
_make_wsgi_event_processor(
environ, self.use_x_forwarded_for
with profiler.Sampler(): # TODO: Check if profiling flag is set to True
with auto_session_tracking(hub, session_mode="request"):
with hub:
with capture_internal_exceptions():
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
scope._name = "wsgi"
scope.add_event_processor(
_make_wsgi_event_processor(
environ, self.use_x_forwarded_for
)
)
)

transaction = Transaction.continue_from_environ(
environ, op="http.server", name="generic WSGI request"
)

with hub.start_transaction(
transaction, custom_sampling_context={"wsgi_environ": environ}
):
try:
rv = self.app(
environ,
partial(
_sentry_start_response, start_response, transaction
),
)
except BaseException:
reraise(*_capture_exception(hub))

transaction = Transaction.continue_from_environ(
environ, op="http.server", name="generic WSGI request"
)

with hub.start_transaction(
transaction, custom_sampling_context={"wsgi_environ": environ}
):
try:
rv = self.app(
environ,
partial(
_sentry_start_response, start_response, transaction
),
)
except BaseException:
reraise(*_capture_exception(hub))
finally:
_wsgi_middleware_applied.set(False)

Expand Down
100 changes: 100 additions & 0 deletions sentry_sdk/profiler.py
@@ -0,0 +1,100 @@
"""
This file contains code from https://github.com/nylas/nylas-perftools, which is published under the following license:
szokeasaurusrex marked this conversation as resolved.
Show resolved Hide resolved

The MIT License (MIT)

Copyright (c) 2014 Nylas

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

import atexit
import os
import signal
import time
from sentry_sdk.utils import logger

def nanosecond_time():
return int(time.perf_counter() * 1e9)

class FrameData:
def __init__(self, frame):
self._function_name = frame.f_code.co_name
self._module = frame.f_globals['__name__']

# Depending on Python version, frame.f_code.co_filename either stores just the file name or the entire absolute path.
self._file_name = os.path.basename(frame.f_code.co_filename)
self._abs_path = os.path.abspath(frame.f_code.co_filename) # TODO: Must verify this will give us correct absolute paths in all cases!
self._line_number = frame.f_lineno

def __str__(self):
return f'{self._function_name}({self._module}) in {self._file_name}:{self._line_number}'

class StackSample:
def __init__(self, top_frame, profiler_start_time):
self._sample_time = nanosecond_time() - profiler_start_time
self._stack = []
self._add_all_frames(top_frame)

def _add_all_frames(self, top_frame):
frame = top_frame
while frame is not None:
self._stack.append(FrameData(frame))
frame = frame.f_back

def __str__(self):
return f'Time: {self._sample_time}; Stack: {[str(frame) for frame in reversed(self._stack)]}'

class Sampler(object):
"""
A simple stack sampler for low-overhead CPU profiling: samples the call
stack every `interval` seconds and keeps track of counts by frame. Because
this uses signals, it only works on the main thread.
"""
def __init__(self, interval=0.01):
self.interval = interval
self._stack_samples = []

def __enter__(self):
self.start()

def __exit__(self, *_):
self.stop()
print(self)

def start(self):
self._start_time = nanosecond_time()
self._stack_samples = []
try:
signal.signal(signal.SIGVTALRM, self._sample)
except ValueError:
logger.warn('Profiler failed to run because it was started from a non-main thread') # TODO: Does not print anything
szokeasaurusrex marked this conversation as resolved.
Show resolved Hide resolved
return

signal.setitimer(signal.ITIMER_VIRTUAL, self.interval)
atexit.register(self.stop)

def _sample(self, signum, frame):
self._stack_samples.append(StackSample(frame, self._start_time))
#print('j')
signal.setitimer(signal.ITIMER_VIRTUAL, self.interval)

def _format_frame(self, frame):
return '{}({})'.format(frame.f_code.co_name,
frame.f_globals.get('__name__'))

def samples(self):
return len(self._stack_samples)

def __str__(self):
return '\n'.join([str(sample) for sample in self._stack_samples])

def stop(self):
signal.setitimer(signal.ITIMER_VIRTUAL, 0)

def __del__(self):
self.stop()