Skip to content

Commit 4875834

Browse files
committed
fix: Reduce idle CPU consumption of websocket server
1 parent 7426843 commit 4875834

File tree

2 files changed

+44
-32
lines changed

2 files changed

+44
-32
lines changed

rosbridge_server/scripts/rosbridge_websocket.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,19 @@
3434
from __future__ import annotations
3535

3636
import argparse
37+
import asyncio
38+
import signal
3739
import sys
40+
import threading
3841
import time
3942
from typing import TYPE_CHECKING, cast
4043

4144
import rclpy
4245
from rcl_interfaces.msg import ParameterDescriptor
46+
from rclpy.executors import SingleThreadedExecutor
4347
from rclpy.node import Node
4448
from rclpy.utilities import remove_ros_args
4549
from tornado.httpserver import HTTPServer
46-
from tornado.ioloop import IOLoop, PeriodicCallback
4750
from tornado.netutil import bind_sockets
4851
from tornado.web import Application
4952

@@ -53,14 +56,6 @@
5356
from tornado.routing import _RuleList
5457

5558

56-
def start_hook() -> None:
57-
IOLoop.instance().start()
58-
59-
60-
def shutdown_hook() -> None:
61-
IOLoop.instance().stop()
62-
63-
6459
SERVER_PARAMETERS = (
6560
# Server parameters
6661
("port", int, 9090, "Port to listen on for WebSocket connections."),
@@ -130,6 +125,7 @@ def __init__(self) -> None:
130125

131126
RosbridgeWebSocket.node_handle = self
132127
RosbridgeWebSocket.client_manager = ClientManager(self)
128+
RosbridgeWebSocket.event_loop = asyncio.get_event_loop()
133129

134130
self._handle_parameters()
135131

@@ -227,30 +223,42 @@ def _start_server(self) -> None:
227223
time.sleep(self.retry_startup_delay)
228224

229225

230-
def main() -> None:
231-
rclpy.init()
226+
async def async_main() -> None:
227+
rclpy.init(args=sys.argv, signal_handler_options=rclpy.signals.SignalHandlerOptions.NO)
228+
232229
node = RosbridgeWebsocketNode()
233230

234-
executor = rclpy.executors.SingleThreadedExecutor()
231+
executor = SingleThreadedExecutor()
235232
executor.add_node(node)
236233

237-
def spin_ros() -> None:
238-
if not rclpy.ok():
239-
shutdown_hook()
234+
spin_thread = threading.Thread(target=executor.spin)
235+
spin_thread.start()
236+
237+
loop = asyncio.get_running_loop()
238+
stop_event = asyncio.Event()
239+
signal_handled = False
240+
241+
def handle_signal() -> None:
242+
nonlocal signal_handled
243+
if signal_handled:
240244
return
241-
executor.spin_once(timeout_sec=0.01)
242-
243-
spin_callback = PeriodicCallback(spin_ros, 1)
244-
spin_callback.start()
245-
try:
246-
start_hook()
247-
node.destroy_node()
248-
rclpy.shutdown()
249-
except KeyboardInterrupt:
250245
print("Exiting due to SIGINT")
251-
finally:
252-
spin_callback.stop()
253-
shutdown_hook() # shutdown hook to stop the server
246+
stop_event.set()
247+
executor.shutdown()
248+
signal_handled = True
249+
250+
for sig in (signal.SIGINT, signal.SIGTERM):
251+
loop.add_signal_handler(sig, handle_signal)
252+
253+
await stop_event.wait()
254+
spin_thread.join()
255+
256+
node.destroy_node()
257+
rclpy.shutdown()
258+
259+
260+
def main() -> None:
261+
asyncio.run(async_main())
254262

255263

256264
if __name__ == "__main__":

rosbridge_server/src/rosbridge_server/websocket_handler.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,27 @@
3232

3333
from __future__ import annotations
3434

35+
import asyncio
3536
import sys
3637
import threading
3738
import traceback
3839
import uuid
3940
from collections import deque
40-
from functools import partial, wraps
41+
from functools import wraps
4142
from typing import TYPE_CHECKING, ClassVar, ParamSpec, TypeVar
4243

4344
from rclpy.node import Node
4445
from rosbridge_library.rosbridge_protocol import RosbridgeProtocol
4546
from rosbridge_library.util import bson
46-
from tornado.ioloop import IOLoop
4747
from tornado.iostream import StreamClosedError
4848
from tornado.websocket import WebSocketClosedError, WebSocketHandler
4949

5050
if TYPE_CHECKING:
51+
from asyncio.events import AbstractEventLoop
5152
from collections.abc import Callable
5253

5354
from .client_manager import ClientManager
5455

55-
_io_loop = IOLoop.instance()
56-
5756

5857
def _log_exception() -> None:
5958
"""Log the most recent exception to ROS."""
@@ -132,6 +131,9 @@ class RosbridgeWebSocket(WebSocketHandler):
132131
# Class variable to manage connected clients
133132
client_manager: ClassVar[ClientManager | None] = None
134133

134+
# Event loop to run the coroutines on
135+
event_loop: ClassVar[AbstractEventLoop | None] = None
136+
135137
# Node handle to pass to RosbridgeProtocol when opening a connection
136138
node_handle: ClassVar[Node | None] = None
137139

@@ -189,12 +191,14 @@ def on_close(self) -> None:
189191
self.incoming_queue.finish()
190192

191193
def send_message(self, message: bson.BSON | bytearray | str, compression: str = "none") -> None:
194+
cls = self.__class__
195+
192196
if isinstance(message, bson.BSON) or compression in ["cbor", "cbor-raw"]:
193197
binary = True
194198
else:
195199
binary = False
196200

197-
_io_loop.add_callback(partial(self.prewrite_message, message, binary))
201+
asyncio.run_coroutine_threadsafe(self.prewrite_message(message, binary), cls.event_loop)
198202

199203
async def prewrite_message(self, message: bson.BSON | bytearray | str, binary: bool) -> None:
200204
cls = self.__class__

0 commit comments

Comments
 (0)