aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

ROSでrosbridge_serverの処理速度改善

はじめに

ソリューションプロフェッショナルグループのみよしです。
ROSソリューションの担当をしています。

今回はROSでrosbridge_serverの処理速度改善を試した結果についてご紹介します。

intdash ROS Bridge

弊社が提供する DX Functions遠隔制御のユースケースとして、ROSを利用したロボット開発が挙げられます。
これらのユースケースに対して、弊社ではROSメッセージの遠隔リアルタイムデータ伝送を行うintdash Bridge / intdash ROS2Bridgeというプロダクトを提供しています。
intdash Bridge / intdash ROS2Bridgeを使うことで、遠隔地のROS1 / ROS2空間をつなぎ、ROSのメッセージをやり取りすることによる遠隔制御やモニタリングなどのユースケースが実現できます。

弊社の過去Blogでも、intdash Bridge / intdash ROS2Bridge、rosbridge_serverに関してのご紹介をしてきました。

tech.aptpod.co.jp tech.aptpod.co.jp tech.aptpod.co.jp

intdash Bridge / intdash ROS2Bridgeは、intdash Edge Agentを介してintdash Serverに接続することが可能です。
(下記は一例です)

ROS Bridge Server

これらの弊社製品とは別にrosbridge_suiteのrosbridge_serverを使用する場合もあります。 rosbridge_serverは、rosbridge protocolを使用するクライアントであれば、接続することが可能です。
rosbridge protocolを使用するクライアントからrosbridge_serverへ接続するので、LANやVPN等でつながっている、もしくはrosbridge_serverが動く環境でグローバルIPを有している場合に使用可能です。
(下記は一例です)

rosbridge_serverの通信方式として、ROS1では、TCP・UDP・WebSocketが提供されています。

  • rosbridge_tcp
  • rosbridge_udp
  • rosbridge_websocket

また、ROS2では、現在、WebSocketのみ提供されています。 *1

  • rosbridge_websocket

高頻度データに対する問題点

過去のROS環境向けの開発でも、rosbridge_serverを度々使用していますが、高頻度のデータでは処理速度が遅くなり、その影響がrosbridge_serverの後段にあるノードやクライアント側にも生じるという問題がありました。
例えば、下記計測で行なっている環境(条件)では、テスト用クライアントでs.send()の後にtime.sleep()をして、送信する間隔を調整した場合、time.sleep()が0.0001secよりsleep時間が短くなると、rosbridge_serverを起動している側で行うrostopic echoの表示が明らかに遅れ、クライアント側でも送信待ち状態が生じます。
(rosbridge_serverで受信するメッセージ(サイズや要素数)によって、送信する間隔がどれぐらいであれば影響を受けないかは異なります。)

ROS(Melodic)でrosbridge_tcpの速度改善

rosbridge_tcp

最初に結論を述べると、Melodic(Python2.7)ではProtocolクラスのincoming()内で、文字列の連結を + から join() とすることで処理速度が改善できました。
Pythonの文字列連結については、こちらの “String Concatenation”に記載されています。

RosBridgeTcpSocketクラスのhandle()内で、recv()したデータの処理が終わるまで次recv()が行なわれないので、一度にrecv()するデータのサイズが大きくなると、recv()後に行なう処理で時間が掛かる為、遅くなります。
構成として

  • SocketServerのTCPServerを使用
  • TCPServerでrequest_queue_size=5と設定
  • RosbridgeTcpSocketでincoming_buffer=65536byte(64k)と設定

となっているので、MAX 64k x 5 = 320k 分のデータが溜まる可能性があります。
incoming_bufferは起動時にパラメーターとして変更可能です。

改善策

rosbridge_library/src/rosbridge_library/protocol.py で文字列の連結をしている箇所を変更します。(下記diff参照)
rosbridge_tcpが動く環境のスペックによってはincoming_bufferの値を小さくします。

diff --git a/rosbridge_library/src/rosbridge_library/protocol.py b/rosbridge_library/src/rosbridge_library/protocol.py
index c9424ad..3ca2c47 100644
--- a/rosbridge_library/src/rosbridge_library/protocol.py
+++ b/rosbridge_library/src/rosbridge_library/protocol.py
@@ -124,10 +124,11 @@ class Protocol:
         message_string -- the wire-level message sent by the client
 
         """
-        if self.bson_only_mode:
-            self.buffer.extend(message_string)
-        else:
-            self.buffer = self.buffer + str(message_string)
+        if message_string != "":
+            if self.bson_only_mode:
+                self.buffer.extend(message_string)
+            else:
+                self.buffer = ''.join([self.buffer, str(message_string)])
         msg = None
 
         # take care of having multiple JSON-objects in receiving buffer

計測と結果

実際に、改善策の効果を計測します。
計測の方法として

  • DockerのContainerで動かすrosbridge_serverに対して、Hostからテスト用のクライアントでメッセージ 100,000 件を送信する。
  • rosbridge_serverで受信したメッセージはpublishされるので、rostopic echoで表示する。

ということを行ないます。

環境

DockerのContainerを使用(下記イメージを使用) *2

$ docker pull ros:melodic-robot
計測する時間
  • メッセージ送信開始(クライアント起動)から、メッセージ送信終了(クライアント終了)まで。
  • メッセージ送信開始(クライアント起動)から、rosbridge_serverを起動している側(DockerのContainer)でrostopic echoを行い、送信したメッセージの表示が終了するまで。
テスト用のクライアント (test_client_tcp.py)
#!/usr/bin/env python3

import sys
import socket
import time
import json

def advertise_msg(topic, type):
    return json.dumps(
        dict(op='advertise', topic=topic, type=type)).encode('utf-8')

def publisher_msg(topic, data):
    return json.dumps(dict(op='publish', topic=topic, msg=data)).encode('utf-8')

def publish(host=None, port=None, topic=None):
    if not host or not port or not topic:
        print('invalid params host={0} port={1} topic=({2}, {3})'.format(
            host, port, topic['name'], topic['type']))
    try:
        s = socket.socket()
        s.connect((host, port))
        s.send(advertise_msg(topic=topic['name'], type=topic['type']))
        time.sleep(0.5)

        cnt_max = 100000
        for cnt in range(cnt_max):
            data = dict(data='hello, {0}, {1}'.format(cnt, time.time()))
            s.send(publisher_msg(topic['name'], data))
            print(data)
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(e)
    finally:
        s.close()

if __name__ == '__main__':
    host = '127.0.0.1'
    port = 9090
    topic = {
        'name': '/chatter',
        'type': 'std_msgs/String'
    }
    if len(sys.argv) > 1:
        host = sys.argv[1]
    publish(host, port, topic)
手順

DockerのContainerでrosbridge_serverを起動する。

incoming_bufferの設定なし
$ roslaunch rosbridge_server rosbridge_tcp.launch

incoming_buffer=1024
$ roslaunch rosbridge_server rosbridge_tcp.launch incoming_buffer:=1024

DockerのContainerでrostopic echoを行なう。

$ rostopic echo /chatter

Hostでテスト用のクライアントを起動する。

$ python3 test_client_tcp.py 172.17.0.2
結果 - incoming_bufferの設定なし
文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了
変更なし 01h 12m 14.94s 02h 45m 22.59s
変更あり 00h 01m 30.16s 00h 04m 15.50s
結果 - incoming_buffer=1024
文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了
変更なし 00h 03m 26.07s 00h 03m 37.23s
変更あり 00h 00m 13.99s 00h 00m 16.06s

文字列の連結をしている箇所の変更で大きく変わりました。
計測した環境がDockerのContainerで非力な為、incoming_bufferを小さくすることで更に効果が得らました。

ROS(Noetic)でrosbridge_tcpを試す

先のMelodicで行なった変更を試してみました。
テスト用のクライアント、手順はMelodicと同様です。

環境

DockerのContainerを使用(下記イメージを使用)

$ docker pull ros:noetic-robot
結果 - incoming_bufferの設定なし
文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了
変更なし 00h 01m 59.37s 00h 04m 15.79s
変更あり 00h 01m 28.01s 00h 04m 09.26s
結果 - incoming_buffer=1024
文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了
変更なし 00h 00m 10.53s 00h 00m 11.87s
変更あり 00h 00m 10.45s 00h 00m 11.90s

Melodicでの結果とは異なり、文字列の連結をしている箇所の変更をしても大した違いは見られませんでした。
rosbridge_suiteはPythonで作られているので、PythonのVersionの違いが影響していると思われます。(MelodicはPython2.7、NoeticはPython3)
incoming_bufferを小さくすることについては、効果が得られました。

rosbridge_websocketを試す

構成として

  • autobahnのWebSocketServerFactoryを使用
  • TornadoのWebSocketHandlerを使用

となっています。

環境

DockerのContainerを使用

テスト用のクライアント (test_client_ws.py)
#!/usr/bin/env python3

import sys
import json
import time

import asyncio
from autobahn.asyncio.websocket import (WebSocketClientProtocol,
                                        WebSocketClientFactory)

class TestClientProtocol(WebSocketClientProtocol):
    def onOpen(self):
        self._sendDict({
            'op': 'advertise',
            'topic': '/chatter',
            'type': 'std_msgs/String',
        })
        time.sleep(0.5)
        self._sendDictLoop()

    def _sendDict(self, msg_dict):
        msg = json.dumps(msg_dict).encode('utf-8')
        self.sendMessage(msg)

    cnt = 0
    cnt_max = 100000

    def _sendDictLoop(self):
        data = 'hello, {0} {1}'.format(self.cnt, time.time())
        msg_dict = {
                'op': 'publish',
                'topic': '/chatter',
                'msg': { 'data': data }
            }
        msg = json.dumps(msg_dict).encode('utf-8')
        self.sendMessage(msg)

        print(data)

        self.cnt += 1
        if self.cnt >= self.cnt_max:
            return

        self.factory.loop.call_later(0, self._sendDictLoop)

    def onMessage(self, payload, binary):
        self.__class__.received.append(payload)

if __name__ == '__main__':
    host = '127.0.0.1'
    port = 9090
    if len(sys.argv) > 1:
        host = sys.argv[1]
    url = 'ws://{0}:{1}'.format(host, port)
    factory = WebSocketClientFactory(url)
    factory.protocol = TestClientProtocol
    loop = asyncio.get_event_loop()
    coro = loop.create_connection(factory, host, port)
    loop.run_until_complete(coro)
    loop.run_forever()
    loop.close()
手順

DockerのContainerでrosbridge_serverを起動する。

$ roslaunch rosbridge_server rosbridge_websocket.launch

DockerのContainerでrostopic echoを行なう。

$ rostopic echo /chatter

Hostでテスト用のクライアントを起動する。

$ python3 test_client_tcp.py 172.17.0.2
結果 - Melodic
文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了
WebSocket 変更なし 00h 00m 05.18s 00h 00m 16.20s
WebSocket 変更あり 00h 00m 04.99s 00h 00m 15.50s
TCP 変更あり(incoming_buffer=1024) 00h 00m 13.99s 00h 00m 16.06s
結果 - Noetic
文字列の連結 メッセージ送信開始~メッセージ送信終了 メッセージ送信開始~rostopic echoの表示終了
WebSocket 変更なし 00h 00m 04.63s 00h 00m 06.17s
WebSocket 変更あり 00h 00m 04.49s 00h 00m 06.05s
TCP 変更あり(incoming_buffer=1024) 00h 00m 10.45s 00h 00m 11.90s

WebSocketが、文字列の連結をしている箇所の変更あり・なしに関わらず速いのは、常に1メッセージずつ処理を行なっているからだと思います。

まとめ

本記事では、Melodicでのrosbridge_serverの処理速度改善と、NoeticやWebSocketとの比較を行いました。

rosbridge_tcpを使用する場合、Melodicでは文字列の連結をしている箇所を変更することで大きく処理速度を改善できました。

同様の変更をNoeticで行っても、大きな効果は得られませんでしたが、それはPythonのVersionの違いによるものだと思われます。
rosbridge_tcpが動く環境のスペックによって、incoming_bufferを小さくすることはMelodic・Noeticともに効果はありました。(潤沢なスペックの場合、大きな効果は得られない可能性があります。)
文字列の連結をしている箇所の変更は、rosbridge_tcp・rosbridge_websocketともに使用している箇所ですが、変更なしのMelodicでもrosbridge_websocketが速いのは、常に1メッセージずつ処理を行なうからだと思います。
可能であるなら、rosbridge_websocketを使用するのが良いと思います。

弊社ではROSコミュニティへの貢献もしていきたいと思っております。

上記も含めて弊社製品、またはROSに関連した開発に興味を持って頂けた方は是非、こちらの弊社採用ページもご覧ください。

製品に関するお問い合わせはこちらへ!

www.aptpod.co.jp

最後までご覧いただきありがとうございました。

*1:v1.1.1(2021-12-09)のリリースでTCP・UDPは削除されました。

*2:HostのCPUはIntel(R) Core(TM) i7-10870H CPU @ 2.20GHz、メモリは64Gです。