1111import signal
1212import sys
1313import threading
14- import time
1514from typing import Any , Generic , List , Optional , Set , TypeVar
1615
1716from scalene .scalene_signals import ScaleneSignals , SignalHandlerFunction
@@ -53,6 +52,10 @@ def __init__(self) -> None:
5352 # Timer control for Windows
5453 self .timer_signals = True
5554
55+ # Events for interruptible sleep in Windows timer/memory threads
56+ self .__stop_event = threading .Event ()
57+ self .__mem_stop_event = threading .Event ()
58+
5659 # Store CPU signal handler for direct calling on Windows
5760 # (signal.raise_signal cannot be called from background threads)
5861 self .__cpu_signal_handler : Optional [SignalHandlerFunction ] = None
@@ -93,9 +96,9 @@ def stop_timer_thread(self) -> None:
9396 if sys .platform != "win32" :
9497 return
9598 self .timer_signals = False
99+ self .__stop_event .set ()
96100 if hasattr (self , "_ScaleneSignalManager__timer_thread" ) and self .__timer_thread :
97- # Give the thread a short time to finish its current iteration
98- self .__timer_thread .join (timeout = 0.1 )
101+ self .__timer_thread .join (timeout = 2.0 )
99102 self .__timer_thread = None
100103
101104 def enable_signals_win32 (
@@ -123,6 +126,7 @@ def enable_signals_win32(
123126 # The thread must be non-daemon so it can continue sampling while the main
124127 # thread is still running, even if main finishes early (short-running programs).
125128 # Cleanup is handled by stop_timer_thread() called from _disable_signals().
129+ self .__stop_event .clear ()
126130 self .__timer_thread = threading .Thread (
127131 target = lambda : self .windows_timer_loop (cpu_sampling_rate ), daemon = False
128132 )
@@ -135,6 +139,7 @@ def enable_signals_win32(
135139 self .__alloc_sigq_win = alloc_sigq
136140 self .__memcpy_sigq_win = memcpy_sigq
137141 self .__memory_polling_active = True
142+ self .__mem_stop_event .clear ()
138143 mem_thread = threading .Thread (
139144 target = self ._windows_memory_poll_loop , daemon = True
140145 )
@@ -156,23 +161,30 @@ def windows_timer_loop(self, cpu_sampling_rate: float) -> None:
156161 # Initial delay to let user code start executing before we begin sampling.
157162 # Without this, the first samples would be taken during Scalene's
158163 # initialization rather than during the user's actual code.
159- time .sleep (0.01 ) # 10ms initial delay
164+ if self .__stop_event .wait (0.01 ):
165+ return
160166
161167 while self .timer_signals :
162- # Call the CPU signal handler first, then sleep .
168+ # Call the CPU signal handler first, then wait .
163169 # This ensures we record samples even if the program exits quickly.
164170 if self .__cpu_signal_handler is not None :
165171 with contextlib .suppress (Exception ):
166172 self .__cpu_signal_handler (self .__signals .cpu_signal , None )
167- time .sleep (cpu_sampling_rate )
173+ # Use Event.wait() instead of time.sleep() so stop_timer_thread()
174+ # can interrupt the wait immediately by setting the event.
175+ if self .__stop_event .wait (cpu_sampling_rate ):
176+ break
168177
169178 def _windows_memory_poll_loop (self ) -> None :
170179 """For Windows, periodically poll for memory profiling data."""
171180 assert sys .platform == "win32"
172181 # Poll every 10ms for memory data
173182 poll_interval = 0.01
174183 while getattr (self , "_ScaleneSignalManager__memory_polling_active" , False ):
175- time .sleep (poll_interval )
184+ # Use Event.wait() instead of time.sleep() so
185+ # stop_windows_memory_polling() can interrupt immediately.
186+ if self .__mem_stop_event .wait (poll_interval ):
187+ break
176188 # Trigger malloc/free processing
177189 if (
178190 hasattr (self , "_ScaleneSignalManager__alloc_sigq_win" )
@@ -188,6 +200,7 @@ def _windows_memory_poll_loop(self) -> None:
188200
189201 def stop_windows_memory_polling (self ) -> None :
190202 """Stop the Windows memory polling thread."""
203+ self .__mem_stop_event .set ()
191204 if hasattr (self , "_ScaleneSignalManager__memory_polling_active" ):
192205 self .__memory_polling_active = False
193206
0 commit comments