Skip to content

Commit 1fa9b41

Browse files
authored
Merge pull request #1012 from plasma-umass/fix/windows-timer-thread-shutdown
Fix Windows timer thread shutdown (#991)
2 parents ca4a682 + 6fc26a7 commit 1fa9b41

File tree

1 file changed

+20
-7
lines changed

1 file changed

+20
-7
lines changed

scalene/scalene_signal_manager.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import signal
1212
import sys
1313
import threading
14-
import time
1514
from typing import Any, Generic, List, Optional, Set, TypeVar
1615

1716
from 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

Comments
 (0)