@@ -64,18 +64,15 @@ def nanosecond_time():
6464_scheduler = None # type: Optional[_Scheduler]
6565
6666
67- def setup_profiler (buffer_secs = 60 , frequency = 101 ):
68- # type: (int, int ) -> None
67+ def setup_profiler (options ):
68+ # type: (Dict[str, Any] ) -> None
6969
7070 """
71- This method sets up the application so that it can be profiled.
72- It MUST be called from the main thread. This is a limitation of
73- python's signal library where it only allows the main thread to
74- set a signal handler.
75-
7671 `buffer_secs` determines the max time a sample will be buffered for
7772 `frequency` determines the number of samples to take per second (Hz)
7873 """
74+ buffer_secs = 60
75+ frequency = 101
7976
8077 global _sample_buffer
8178 global _scheduler
@@ -86,11 +83,19 @@ def setup_profiler(buffer_secs=60, frequency=101):
8683 # a capcity of `buffer_secs * frequency`.
8784 _sample_buffer = _SampleBuffer (capacity = buffer_secs * frequency )
8885
89- _scheduler = _Scheduler (frequency = frequency )
86+ profiler_mode = options ["_experiments" ].get ("profiler_mode" , _SigprofScheduler .mode )
87+ if profiler_mode == _SigprofScheduler .mode :
88+ _scheduler = _SigprofScheduler (frequency = frequency )
89+ elif profiler_mode == _SigalrmScheduler .mode :
90+ _scheduler = _SigalrmScheduler (frequency = frequency )
91+ elif profiler_mode == _SleepScheduler .mode :
92+ _scheduler = _SleepScheduler (frequency = frequency )
93+ elif profiler_mode == _EventScheduler .mode :
94+ _scheduler = _EventScheduler (frequency = frequency )
95+ else :
96+ raise ValueError ("Unknown profiler mode: {}" .format (profiler_mode ))
97+ _scheduler .setup ()
9098
91- # This setups a process wide signal handler that will be called
92- # at an interval to record samples.
93- signal .signal (signal .SIGPROF , _sample_stack )
9499 atexit .register (teardown_profiler )
95100
96101
@@ -100,32 +105,18 @@ def teardown_profiler():
100105 global _sample_buffer
101106 global _scheduler
102107
108+ if _scheduler is not None :
109+ _scheduler .teardown ()
110+
103111 _sample_buffer = None
104112 _scheduler = None
105113
106- # setting the timer with 0 will stop will clear the timer
107- signal .setitimer (signal .ITIMER_PROF , 0 )
108-
109- # put back the default signal handler
110- signal .signal (signal .SIGPROF , signal .SIG_DFL )
111114
112-
113- def _sample_stack (_signal_num , _frame ):
114- # type: (int, Frame) -> None
115+ def _sample_stack (* args , ** kwargs ):
116+ # type: (*Any, **Any) -> None
115117 """
116118 Take a sample of the stack on all the threads in the process.
117- This handler is called to handle the signal at a set interval.
118-
119- See https://www.gnu.org/software/libc/manual/html_node/Alarm-Signals.html
120-
121- This is not based on wall time, and you may see some variances
122- in the frequency at which this handler is called.
123-
124- Notably, it looks like only threads started using the threading
125- module counts towards the time elapsed. It is unclear why that
126- is the case right now. However, we are able to get samples from
127- threading._DummyThread if this handler is called as a result of
128- another thread (e.g. the main thread).
119+ This should be called at a regular interval to collect samples.
129120 """
130121
131122 assert _sample_buffer is not None
@@ -298,33 +289,240 @@ def slice_profile(self, start_ns, stop_ns):
298289
299290
300291class _Scheduler (object ):
292+ mode = "unknown"
293+
301294 def __init__ (self , frequency ):
302295 # type: (int) -> None
303296 self ._lock = threading .Lock ()
304297 self ._count = 0
305298 self ._interval = 1.0 / frequency
306299
300+ def setup (self ):
301+ # type: () -> None
302+ raise NotImplementedError
303+
304+ def teardown (self ):
305+ # type: () -> None
306+ raise NotImplementedError
307+
307308 def start_profiling (self ):
308309 # type: () -> bool
309310 with self ._lock :
310- # we only need to start the timer if we're starting the first profile
311- should_start_timer = self ._count == 0
312311 self ._count += 1
313-
314- if should_start_timer :
315- signal .setitimer (signal .ITIMER_PROF , self ._interval , self ._interval )
316- return should_start_timer
312+ return self ._count == 1
317313
318314 def stop_profiling (self ):
319315 # type: () -> bool
320316 with self ._lock :
321- # we only need to stop the timer if we're stoping the last profile
322- should_stop_timer = self ._count == 1
323317 self ._count -= 1
318+ return self ._count == 0
319+
320+
321+ class _ThreadScheduler (_Scheduler ):
322+ """
323+ This abstract scheduler is based on running a daemon thread that will call
324+ the sampler at a regular interval.
325+ """
326+
327+ mode = "thread"
328+
329+ def __init__ (self , frequency ):
330+ # type: (int) -> None
331+ super (_ThreadScheduler , self ).__init__ (frequency )
332+ self .event = threading .Event ()
333+
334+ def setup (self ):
335+ # type: () -> None
336+ pass
337+
338+ def teardown (self ):
339+ # type: () -> None
340+ pass
341+
342+ def start_profiling (self ):
343+ # type: () -> bool
344+ if super (_ThreadScheduler , self ).start_profiling ():
345+ # make sure to clear the event as we reuse the same event
346+ # over the lifetime of the scheduler
347+ self .event .clear ()
348+
349+ # make sure the thread is a daemon here otherwise this
350+ # can keep the application running after other threads
351+ # have exited
352+ thread = threading .Thread (target = self .run , daemon = True )
353+ thread .start ()
354+ return True
355+ return False
356+
357+ def stop_profiling (self ):
358+ # type: () -> bool
359+ if super (_ThreadScheduler , self ).stop_profiling ():
360+ # make sure the set the event here so that the thread
361+ # can check to see if it should keep running
362+ self .event .set ()
363+ return True
364+ return False
365+
366+ def run (self ):
367+ # type: () -> None
368+ raise NotImplementedError
369+
370+
371+ class _SleepScheduler (_ThreadScheduler ):
372+ """
373+ This scheduler uses time.sleep to wait the required interval before calling
374+ the sampling function.
375+ """
376+
377+ mode = "sleep"
378+
379+ def run (self ):
380+ # type: () -> None
381+ while True :
382+ if self .event .is_set ():
383+ break
384+ time .sleep (self ._interval )
385+ _sample_stack ()
386+
387+
388+ class _EventScheduler (_ThreadScheduler ):
389+ """
390+ This scheduler uses threading.Event to wait the required interval before
391+ calling the sampling function.
392+ """
393+
394+ mode = "event"
324395
325- if should_stop_timer :
326- signal .setitimer (signal .ITIMER_PROF , 0 )
327- return should_stop_timer
396+ def run (self ):
397+ # type: () -> None
398+ while True :
399+ if self .event .is_set ():
400+ break
401+ self .event .wait (timeout = self ._interval )
402+ _sample_stack ()
403+
404+
405+ class _SignalScheduler (_Scheduler ):
406+ """
407+ This abstract scheduler is based on UNIX signals. It sets up a
408+ signal handler for the specified signal, and the matching itimer in order
409+ for the signal handler to fire at a regular interval.
410+
411+ See https://www.gnu.org/software/libc/manual/html_node/Alarm-Signals.html
412+ """
413+
414+ mode = "signal"
415+
416+ @property
417+ def signal_num (self ):
418+ # type: () -> signal.Signals
419+ raise NotImplementedError
420+
421+ @property
422+ def signal_timer (self ):
423+ # type: () -> int
424+ raise NotImplementedError
425+
426+ def setup (self ):
427+ # type: () -> None
428+ """
429+ This method sets up the application so that it can be profiled.
430+ It MUST be called from the main thread. This is a limitation of
431+ python's signal library where it only allows the main thread to
432+ set a signal handler.
433+ """
434+
435+ # This setups a process wide signal handler that will be called
436+ # at an interval to record samples.
437+ try :
438+ signal .signal (self .signal_num , _sample_stack )
439+ except ValueError :
440+ raise ValueError (
441+ "Signal based profiling can only be enabled from the main thread."
442+ )
443+
444+ # Ensures that system calls interrupted by signals are restarted
445+ # automatically. Otherwise, we may see some strage behaviours
446+ # such as IOErrors caused by the system call being interrupted.
447+ signal .siginterrupt (self .signal_num , False )
448+
449+ def teardown (self ):
450+ # type: () -> None
451+
452+ # setting the timer with 0 will stop will clear the timer
453+ signal .setitimer (self .signal_timer , 0 )
454+
455+ # put back the default signal handler
456+ signal .signal (self .signal_num , signal .SIG_DFL )
457+
458+ def start_profiling (self ):
459+ # type: () -> bool
460+ if super (_SignalScheduler , self ).start_profiling ():
461+ signal .setitimer (self .signal_timer , self ._interval , self ._interval )
462+ return True
463+ return False
464+
465+ def stop_profiling (self ):
466+ # type: () -> bool
467+ if super (_SignalScheduler , self ).stop_profiling ():
468+ signal .setitimer (self .signal_timer , 0 )
469+ return True
470+ return False
471+
472+
473+ class _SigprofScheduler (_SignalScheduler ):
474+ """
475+ This scheduler uses SIGPROF to regularly call a signal handler where the
476+ samples will be taken.
477+
478+ This is not based on wall time, and you may see some variances
479+ in the frequency at which this handler is called.
480+
481+ This has some limitations:
482+ - Only the main thread counts towards the time elapsed. This means that if
483+ the main thread is blocking on a sleep() or select() system call, then
484+ this clock will not count down. Some examples of this in practice are
485+ - When using uwsgi with multiple threads in a worker, the non main
486+ threads will only be profiled if the main thread is actively running
487+ at the same time.
488+ - When using gunicorn with threads, the main thread does not handle the
489+ requests directly, so the clock counts down slower than expected since
490+ its mostly idling while waiting for requests.
491+ """
492+
493+ mode = "sigprof"
494+
495+ @property
496+ def signal_num (self ):
497+ # type: () -> signal.Signals
498+ return signal .SIGPROF
499+
500+ @property
501+ def signal_timer (self ):
502+ # type: () -> int
503+ return signal .ITIMER_PROF
504+
505+
506+ class _SigalrmScheduler (_SignalScheduler ):
507+ """
508+ This scheduler uses SIGALRM to regularly call a signal handler where the
509+ samples will be taken.
510+
511+ This is based on real time, so it *should* be called close to the expected
512+ frequency.
513+ """
514+
515+ mode = "sigalrm"
516+
517+ @property
518+ def signal_num (self ):
519+ # type: () -> signal.Signals
520+ return signal .SIGALRM
521+
522+ @property
523+ def signal_timer (self ):
524+ # type: () -> int
525+ return signal .ITIMER_REAL
328526
329527
330528def _should_profile (transaction , hub ):
0 commit comments