Skip to content

Commit b9e08e4

Browse files
committed
fix cleanup at shutdown
app tests in ngapp create many apps -> need clean shutdown
1 parent 896015a commit b9e08e4

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

webgpu/canvas.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,12 @@ def __init__(self, device, canvas, multisample_count=4):
147147
self.update_html_canvas(canvas)
148148

149149
def __del__(self):
150-
if self._resize_observer is not None:
151-
self._resize_observer.disconnect()
152-
if self._intersection_observer is not None:
153-
self._intersection_observer.disconnect()
150+
disconnect = getattr(self._resize_observer, "disconnect", None)
151+
if callable(disconnect):
152+
disconnect()
153+
disconnect = getattr(self._intersection_observer, "disconnect", None)
154+
if callable(disconnect):
155+
disconnect()
154156

155157
def update_html_canvas(self, html_canvas):
156158
"""Reconfigure the canvas with the current HTML canvas element. This is necessary when the HTML canvas element changes, disappears (e.g. when switching a tab) and appears again."""

webgpu/link/base.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,13 @@ class LinkBaseAsync(LinkBase):
475475
_callback_loop: asyncio.AbstractEventLoop
476476
_callback_queue: asyncio.Queue
477477
_callback_thread: threading.Thread
478+
_callback_task: asyncio.Task | None
478479

479480
def __init__(self):
480481
super().__init__()
481482
self._send_loop = asyncio.new_event_loop()
482483
self._callback_loop = asyncio.new_event_loop()
484+
self._callback_task = None
483485
self._callback_thread = threading.Thread(target=self._start_callback_thread, daemon=True)
484486
self._callback_thread.start()
485487

@@ -514,9 +516,11 @@ def _send_data(self, metadata, data, key=None):
514516
event = threading.Event()
515517
self._requests[request_id] = event, key
516518

519+
coro = self._send_async(data)
517520
try:
518-
asyncio.run_coroutine_threadsafe(self._send_async(data), self._send_loop)
521+
asyncio.run_coroutine_threadsafe(coro, self._send_loop)
519522
except RuntimeError:
523+
coro.close()
520524
# Event loop is closed — connection is dead, clean up and bail.
521525
if event:
522526
self._requests.pop(request_id, None)
@@ -547,7 +551,15 @@ async def handle_callbacks():
547551
self._callback_loop = asyncio.new_event_loop()
548552
asyncio.set_event_loop(self._callback_loop)
549553
self._callback_queue = asyncio.Queue()
550-
self._callback_loop.create_task(handle_callbacks())
551-
self._callback_loop.run_forever()
554+
self._callback_task = self._callback_loop.create_task(handle_callbacks())
555+
try:
556+
self._callback_loop.run_forever()
557+
finally:
558+
if self._callback_task is not None and not self._callback_task.done():
559+
self._callback_task.cancel()
560+
self._callback_loop.run_until_complete(
561+
asyncio.gather(self._callback_task, return_exceptions=True)
562+
)
563+
self._callback_loop.close()
552564
except Exception as e:
553565
print("exception in _start_callback_thread", e)

webgpu/link/websocket.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def __init__(self):
2222
self._start_handling_messages = threading.Event()
2323
self._send_loop = asyncio.new_event_loop()
2424

25-
self._websocket_thread = threading.Thread(target=self._connect)
25+
self._websocket_thread = threading.Thread(target=self._connect, daemon=True)
2626
self._websocket_thread.start()
2727

2828
def wait_for_server_running(self):
@@ -89,7 +89,7 @@ async def _websocket_handler(self, websocket, path=""):
8989
self._connection = websocket
9090
self._event_is_connected.set()
9191
async for message in websocket:
92-
thread = threading.Thread(target=self._on_message, args=(message,))
92+
thread = threading.Thread(target=self._on_message, args=(message,), daemon=True)
9393
thread.start()
9494
finally:
9595
self._connection = None
@@ -118,15 +118,37 @@ async def start_websocket():
118118
self._send_loop.run_until_complete(start_websocket())
119119
except Exception as e:
120120
print("exception in _start_websocket_server", e)
121+
finally:
122+
pending = [
123+
task for task in asyncio.all_tasks(self._send_loop)
124+
if not task.done()
125+
]
126+
for task in pending:
127+
task.cancel()
128+
if pending:
129+
self._send_loop.run_until_complete(
130+
asyncio.gather(*pending, return_exceptions=True)
131+
)
132+
self._send_loop.run_until_complete(self._send_loop.shutdown_asyncgens())
133+
self._send_loop.run_until_complete(self._send_loop.shutdown_default_executor())
134+
self._send_loop.close()
121135

122136
def stop(self):
123-
self._send_loop.call_soon_threadsafe(self._stop.set_result, None)
137+
try:
138+
if not self._stop.done():
139+
self._send_loop.call_soon_threadsafe(self._stop.set_result, None)
140+
except RuntimeError:
141+
pass # Event loop already closed
142+
if threading.current_thread() is not self._websocket_thread:
143+
self._websocket_thread.join(timeout=2)
124144

125145
# Stop the callback event loop so the _callback_thread exits.
126146
try:
127147
self._callback_loop.call_soon_threadsafe(self._callback_loop.stop)
128148
except RuntimeError:
129149
pass # Event loop already closed
150+
if threading.current_thread() is not self._callback_thread:
151+
self._callback_thread.join(timeout=1)
130152

131153
# Unblock any threads stuck waiting for websocket RPC responses.
132154
for rid, val in list(self._requests.items()):

webgpu/scene.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ def init(self, canvas):
9797
self.options.timestamp = time.time()
9898
self.options.update_buffers()
9999
for obj in self.render_objects:
100+
if not obj.active:
101+
continue
100102
try:
101103
obj._update_and_create_render_pipeline(self.options)
102104
except Exception as e:

0 commit comments

Comments
 (0)