ROS2をもっと良くしたいではnanosleepの起床レイテンシを測定していました。
今回は、ROS2で使用されているタイマーの解析と、その確認として行った実験の結果を示し、ROS2のタイマーがシステムクロックの調整に影響を受けることをご紹介します。
rcl_wait の解析
ROS2のクロックには、System Time (システムクロック), ROS Time, Steady Time (ハードウェアクロック) の3つがありますが、タイマーコールバック使用する Steady Time のケースを調査しました。
タイマーコールバックが使用するクロックは rclcpp/timer.hpp で初期化している通り、Steady Time となります。
一方で、Node::now()で使用されるクロックは rclcpp/node_clock.cpp で初期化している通り ROS Time を使用しており、 /clock トピックへの publish がない場合は System Time として振る舞います。
ココらへんの設計については ROS 2 Design | Clock and Time が詳しいです。
rcl_waitとはコールバックの実行後、次にコールバック実行まで待つ関数です。
rcl_waitを解析したところ、就寝から起床までの処理は次のようになっていることが分かりました。
- セットされている全てのタイマーの内、直近の起床時刻までの就寝期間を計算(ハードウェアクロック)
- 就寝期間をrmw_wait内でstd::condition_variable::wait_for()のタイムアウト引数として渡す
- システムクロック上の現在時刻と就寝期間から次の起床時刻を計算
- タイムアウト付きのfutexシステムコールで設定したシステムクロックで設定した時刻まで就寝
- タイムアウトまたはメッセージ受信で起床
- 起床時刻を過ぎたタイマーのコールバックを実行
注目すべきは、起床時刻の計算がハードウェアクロックで計算されている一方で、実際の起床時刻はソフトウェアクロックが使われている点です。
これでは、ハードウェアクロックを使用しているにも関わらず、ソフトウェアクロックの調整にタイマーコールバックが影響を受けてしまいます。
実験による解析結果の確認
解析した結果を確認するために、システム時刻を途中で変更させる実験をしました。
create_wall_timerで1秒毎起床してタイマーコールバックを実行するノードを作成し、
- 実行途中でシステム時刻を5秒進めたケース
- 実行途中でシステム時刻を5秒戻したケース
を測定しました。
比較用に、ハードウェアクロックで就寝・起床をしたケースと、システムクロックで就寝・起床したケースも別々に測定しました。
図はハードウェアクロック・システムクロック・ROS2タイマーコールバックそれぞれの起床タイミングを示しています。
上図はシステムクロックを進めたケース、下図はシステムクロックを戻したケースの結果です。
横軸はハードウェアクロックで、観測者から見る時刻に対応します。
システムクロックを5秒進めたケースでは、システムクロックは直ちに5回連続して起床しましたが、タイマーコールバックには影響無く、しっかり1秒毎起床しました。
システムクロックを進めたケースでは、futexシステムコールの起床後、ハードウェアクロックで期限切れのチェックが入るため影響はありません(解析結果のステップ6)。
一方で、システムクロックを5秒戻したケースでは、システムクロックは5秒間以上起床せず、タイマーコールバックも同様に5秒間以上起床しない結果となりました。
システムクロックを調整した際にどんな影響があるか
システムクロックの時刻を進めた際の影響は、タイマーコールバックの実行に影響はありませんでした。
一方で、システムクロックの時刻を戻した際は、タイマーコールバックが周期的に実行されないことが分かりました。
これはハードウェアクロックがシステムクロックよりも早く進むような場合、ROSアプリケーションの動作中にずれた時間をNTPで同期させるようなケースで影響が出ます。
例えば、コンピュータの起動とともにROSアプリケーションを開始させるシステムで、動作の途中でシステムクロックが逆方向に調整された場合、タイマーコールバックが呼ばれない時間が生じます。
参考:「ハードウェアクロック」と「システムクロック」, LPI-Japan
弊社で測定用に使ってるRaspberryPiはハードウェアクロックの方が遅かったので、特に影響はないですが、1日で2.8秒のズレが発生するペースで時間差が広がっており、無視できるズレの小ささではありません。
もし同程度のペースでハードウェアクロックの方が早く進むようであれば、例として挙げたケースでシステムが致命的な挙動に繋がることもあるかもしれません。
システム時刻の調整に影響を受けない周期的な処理を行わせるためには、ROSを使わず、nanosleepでCLOCK_MONOTONICを使用したスリープで周期的な起床をさせる方法が考えられます。
CLOCK_MONOTONICを使えば、急激な時間の調整は発生しないため、今回のように起床しない時間は発生しません。
ただし、ゆるやかな時間の調整は発生するので、周期のゆらぎは発生するようです。
参考:clock_gettime(2): clock/time functions – Linux man page
2021/01/29 追加
本記事で報告した現象についてはissueとして報告済みです。
最新の情報については Sudden NTP sync can break timer callback periodicity #1080 をご参照ください。