104
104
},
105
105
)
106
106
107
+ try :
108
+ from gevent .monkey import is_module_patched # type: ignore
109
+ except ImportError :
110
+
111
+ def is_module_patched (* args , ** kwargs ):
112
+ # type: (*Any, **Any) -> bool
113
+ # unable to import from gevent means no modules have been patched
114
+ return False
115
+
107
116
108
117
_scheduler = None # type: Optional[Scheduler]
109
118
@@ -128,11 +137,31 @@ def setup_profiler(options):
128
137
129
138
frequency = 101
130
139
131
- profiler_mode = options ["_experiments" ].get ("profiler_mode" , SleepScheduler .mode )
132
- if profiler_mode == SleepScheduler .mode :
133
- _scheduler = SleepScheduler (frequency = frequency )
140
+ if is_module_patched ("threading" ) or is_module_patched ("_thread" ):
141
+ # If gevent has patched the threading modules then we cannot rely on
142
+ # them to spawn a native thread for sampling.
143
+ # Instead we default to the GeventScheduler which is capable of
144
+ # spawning native threads within gevent.
145
+ default_profiler_mode = GeventScheduler .mode
146
+ else :
147
+ default_profiler_mode = ThreadScheduler .mode
148
+
149
+ profiler_mode = options ["_experiments" ].get ("profiler_mode" , default_profiler_mode )
150
+
151
+ if (
152
+ profiler_mode == ThreadScheduler .mode
153
+ # for legacy reasons, we'll keep supporting sleep mode for this scheduler
154
+ or profiler_mode == "sleep"
155
+ ):
156
+ _scheduler = ThreadScheduler (frequency = frequency )
157
+ elif profiler_mode == GeventScheduler .mode :
158
+ try :
159
+ _scheduler = GeventScheduler (frequency = frequency )
160
+ except ImportError :
161
+ raise ValueError ("Profiler mode: {} is not available" .format (profiler_mode ))
134
162
else :
135
163
raise ValueError ("Unknown profiler mode: {}" .format (profiler_mode ))
164
+
136
165
_scheduler .setup ()
137
166
138
167
atexit .register (teardown_profiler )
@@ -445,6 +474,11 @@ def __init__(self, frequency):
445
474
# type: (int) -> None
446
475
self .interval = 1.0 / frequency
447
476
477
+ self .sampler = self .make_sampler ()
478
+
479
+ self .new_profiles = deque () # type: Deque[Profile]
480
+ self .active_profiles = set () # type: Set[Profile]
481
+
448
482
def __enter__ (self ):
449
483
# type: () -> Scheduler
450
484
self .setup ()
@@ -462,50 +496,6 @@ def teardown(self):
462
496
# type: () -> None
463
497
raise NotImplementedError
464
498
465
- def start_profiling (self , profile ):
466
- # type: (Profile) -> None
467
- raise NotImplementedError
468
-
469
- def stop_profiling (self , profile ):
470
- # type: (Profile) -> None
471
- raise NotImplementedError
472
-
473
-
474
- class ThreadScheduler (Scheduler ):
475
- """
476
- This abstract scheduler is based on running a daemon thread that will call
477
- the sampler at a regular interval.
478
- """
479
-
480
- mode = "thread"
481
- name = None # type: Optional[str]
482
-
483
- def __init__ (self , frequency ):
484
- # type: (int) -> None
485
- super (ThreadScheduler , self ).__init__ (frequency = frequency )
486
-
487
- self .sampler = self .make_sampler ()
488
-
489
- # used to signal to the thread that it should stop
490
- self .event = threading .Event ()
491
-
492
- # make sure the thread is a daemon here otherwise this
493
- # can keep the application running after other threads
494
- # have exited
495
- self .thread = threading .Thread (name = self .name , target = self .run , daemon = True )
496
-
497
- self .new_profiles = deque () # type: Deque[Profile]
498
- self .active_profiles = set () # type: Set[Profile]
499
-
500
- def setup (self ):
501
- # type: () -> None
502
- self .thread .start ()
503
-
504
- def teardown (self ):
505
- # type: () -> None
506
- self .event .set ()
507
- self .thread .join ()
508
-
509
499
def start_profiling (self , profile ):
510
500
# type: (Profile) -> None
511
501
profile .active = True
@@ -515,10 +505,6 @@ def stop_profiling(self, profile):
515
505
# type: (Profile) -> None
516
506
profile .active = False
517
507
518
- def run (self ):
519
- # type: () -> None
520
- raise NotImplementedError
521
-
522
508
def make_sampler (self ):
523
509
# type: () -> Callable[..., None]
524
510
cwd = os .getcwd ()
@@ -600,14 +586,99 @@ def _sample_stack(*args, **kwargs):
600
586
return _sample_stack
601
587
602
588
603
- class SleepScheduler ( ThreadScheduler ):
589
+ class ThreadScheduler ( Scheduler ):
604
590
"""
605
- This scheduler uses time.sleep to wait the required interval before calling
606
- the sampling function .
591
+ This scheduler is based on running a daemon thread that will call
592
+ the sampler at a regular interval .
607
593
"""
608
594
609
- mode = "sleep"
610
- name = "sentry.profiler.SleepScheduler"
595
+ mode = "thread"
596
+ name = "sentry.profiler.ThreadScheduler"
597
+
598
+ def __init__ (self , frequency ):
599
+ # type: (int) -> None
600
+ super (ThreadScheduler , self ).__init__ (frequency = frequency )
601
+
602
+ # used to signal to the thread that it should stop
603
+ self .event = threading .Event ()
604
+
605
+ # make sure the thread is a daemon here otherwise this
606
+ # can keep the application running after other threads
607
+ # have exited
608
+ self .thread = threading .Thread (name = self .name , target = self .run , daemon = True )
609
+
610
+ def setup (self ):
611
+ # type: () -> None
612
+ self .thread .start ()
613
+
614
+ def teardown (self ):
615
+ # type: () -> None
616
+ self .event .set ()
617
+ self .thread .join ()
618
+
619
+ def run (self ):
620
+ # type: () -> None
621
+ last = time .perf_counter ()
622
+
623
+ while True :
624
+ if self .event .is_set ():
625
+ break
626
+
627
+ self .sampler ()
628
+
629
+ # some time may have elapsed since the last time
630
+ # we sampled, so we need to account for that and
631
+ # not sleep for too long
632
+ elapsed = time .perf_counter () - last
633
+ if elapsed < self .interval :
634
+ time .sleep (self .interval - elapsed )
635
+
636
+ # after sleeping, make sure to take the current
637
+ # timestamp so we can use it next iteration
638
+ last = time .perf_counter ()
639
+
640
+
641
+ class GeventScheduler (Scheduler ):
642
+ """
643
+ This scheduler is based on the thread scheduler but adapted to work with
644
+ gevent. When using gevent, it may monkey patch the threading modules
645
+ (`threading` and `_thread`). This results in the use of greenlets instead
646
+ of native threads.
647
+
648
+ This is an issue because the sampler CANNOT run in a greenlet because
649
+ 1. Other greenlets doing sync work will prevent the sampler from running
650
+ 2. The greenlet runs in the same thread as other greenlets so when taking
651
+ a sample, other greenlets will have been evicted from the thread. This
652
+ results in a sample containing only the sampler's code.
653
+ """
654
+
655
+ mode = "gevent"
656
+ name = "sentry.profiler.GeventScheduler"
657
+
658
+ def __init__ (self , frequency ):
659
+ # type: (int) -> None
660
+
661
+ # This can throw an ImportError that must be caught if `gevent` is
662
+ # not installed.
663
+ from gevent .threadpool import ThreadPool # type: ignore
664
+
665
+ super (GeventScheduler , self ).__init__ (frequency = frequency )
666
+
667
+ # used to signal to the thread that it should stop
668
+ self .event = threading .Event ()
669
+
670
+ # Using gevent's ThreadPool allows us to bypass greenlets and spawn
671
+ # native threads.
672
+ self .pool = ThreadPool (1 )
673
+
674
+ def setup (self ):
675
+ # type: () -> None
676
+ self .pool .spawn (self .run )
677
+
678
+ def teardown (self ):
679
+ # type: () -> None
680
+ self .event .set ()
681
+ self .pool .join ()
611
682
612
683
def run (self ):
613
684
# type: () -> None
0 commit comments