Skip to content

Commit 08b6c42

Browse files
authored
Fixes flaky RecordAndPlayback test in INTEGRATION_log_system (#463)
The flakiness comes from two sources: 1. Poses recorded by the LogRecorder are published by the SceneBroadcaster system throttled at 60 Hz. The throttle mechanism uses real-time instead of sim-time which causes a variance in the number of recorded poses from run to run. However, the expected number of recorded poses was calculated with the assumption that the simulation would run with a 1.0 RTF. If the CPU load is high, there could be a mismatch between the expected and the actual number of recorded poses, which causes the test to fail. This can be checked by running the test with `cpulimit -l 20 -f bin/INTEGRATION_log_system -- --gtest_filter="*RecordAndPlayback"` 2. An attempt is made to match up played back poses with the closest timestamp in the recorded file. These poses are again published by the SceneBroadcaster, so they are subject to the same kind of timing issues as the recorded poses. The solution in this patch for the first issue is to determine the expected number of recorded poses by counting them in a separate system that mimics the throttling mechanism of ign-transport. For the second issue, a testing system is added to the playback server bypassing the SceSceneBroadcaster altogether. Signed-off-by: Addisu Z. Taddese <[email protected]>
1 parent f02c5e4 commit 08b6c42

File tree

1 file changed

+91
-85
lines changed

1 file changed

+91
-85
lines changed

test/integration/log_system.cc

+91-85
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,9 @@ TEST_F(LogSystemTest, RecordAndPlayback)
707707
// Create temp directory to store log
708708
this->CreateLogsDir();
709709

710+
// Used to count the expected number of poses recorded. Counting is necessary
711+
// as the number varied depending on the CPU load.
712+
int expectedPoseCount = 0;
710713
// Record
711714
{
712715
// World with moving entities
@@ -725,6 +728,23 @@ TEST_F(LogSystemTest, RecordAndPlayback)
725728
recordServerConfig.SetSdfString(recordSdfRoot.Element()->ToString(""));
726729
Server recordServer(recordServerConfig);
727730

731+
std::chrono::steady_clock::time_point lastPoseTime;
732+
const int poseHz = 60;
733+
const std::chrono::duration<double> msgPeriod{1.0 / poseHz};
734+
// This system counts the expected number of poses recorded by reproducing
735+
// the throttle mechanism used by ign-transport.
736+
test::Relay recordedPoseCounter;
737+
recordedPoseCounter.OnPostUpdate(
738+
[&](const UpdateInfo &, const EntityComponentManager &)
739+
{
740+
auto tNow = std::chrono::steady_clock::now();
741+
if ((tNow - lastPoseTime) > msgPeriod)
742+
{
743+
lastPoseTime = tNow;
744+
++expectedPoseCount;
745+
}
746+
});
747+
recordServer.AddSystem(recordedPoseCounter.systemPtr);
728748
// Run for a few seconds to record different poses
729749
recordServer.Run(true, 1000, false);
730750
}
@@ -814,98 +834,84 @@ TEST_F(LogSystemTest, RecordAndPlayback)
814834
// Callback function for entities played back
815835
// Compare current pose being played back with the pose with the closest
816836
// timestamp in the recorded file.
817-
std::function<void(const msgs::Pose_V &)> msgCb =
818-
[&](const msgs::Pose_V &_playedMsg) -> void
819-
{
820-
// Playback continues even after the log file is over
821-
if (batch.end() == recordedIter)
822-
return;
823-
824-
ASSERT_TRUE(_playedMsg.has_header());
825-
ASSERT_TRUE(_playedMsg.header().has_stamp());
826-
EXPECT_EQ(0, _playedMsg.header().stamp().sec());
827-
828-
// Get next recorded message
829-
EXPECT_EQ("ignition.msgs.Pose_V", recordedIter->Type());
830-
recordedMsg.ParseFromString(recordedIter->Data());
831-
832-
ASSERT_TRUE(recordedMsg.has_header());
833-
ASSERT_TRUE(recordedMsg.header().has_stamp());
834-
EXPECT_EQ(0, recordedMsg.header().stamp().sec());
835-
836-
// Log clock timestamp matches message timestamp
837-
EXPECT_EQ(recordedMsg.header().stamp().nsec(),
838-
recordedIter->TimeReceived().count());
839-
840-
// Dynamic poses are throttled according to real time during playback,
841-
// so we can't guarantee the exact timestamp as recorded.
842-
EXPECT_NEAR(_playedMsg.header().stamp().nsec(),
843-
recordedMsg.header().stamp().nsec(), 100000000);
844-
845-
// Loop through all recorded poses, update map
846-
std::map<std::string, msgs::Pose> entityRecordedPose;
847-
for (int i = 0; i < recordedMsg.pose_size(); ++i)
848-
{
849-
entityRecordedPose[recordedMsg.pose(i).name()] = recordedMsg.pose(i);
850-
}
837+
test::Relay playbackPoseTester;
838+
playbackPoseTester.OnPostUpdate(
839+
[&](const UpdateInfo &_info, const EntityComponentManager &_ecm)
840+
{
841+
// Playback continues even after the log file is over
842+
if (batch.end() == recordedIter)
843+
return;
844+
845+
// Get next recorded message
846+
EXPECT_EQ("ignition.msgs.Pose_V", recordedIter->Type());
847+
recordedMsg.ParseFromString(recordedIter->Data());
848+
849+
ASSERT_TRUE(recordedMsg.has_header());
850+
ASSERT_TRUE(recordedMsg.header().has_stamp());
851+
EXPECT_EQ(0, recordedMsg.header().stamp().sec());
852+
853+
// Log clock timestamp matches message timestamp
854+
EXPECT_EQ(recordedMsg.header().stamp().nsec(),
855+
recordedIter->TimeReceived().count());
856+
857+
// A recorded messages is matched when its timestamp is within 100us
858+
// of the current iteration's sim time.
859+
if (std::abs((_info.simTime - recordedIter->TimeReceived()).count()) >
860+
100000)
861+
{
862+
return;
863+
}
851864

852-
// Has 6 dynamic entities: 4 in dbl pendulum and 2 in nested model
853-
EXPECT_EQ(6, _playedMsg.pose().size());
854-
EXPECT_EQ(6u, entityRecordedPose.size());
865+
// Has 6 dynamic entities: 4 in dbl pendulum and 2 in nested model
866+
EXPECT_EQ(6, recordedMsg.pose_size());
855867

856-
// Loop through all entities and compare played poses to recorded ones
857-
for (int i = 0; i < _playedMsg.pose_size(); ++i)
858-
{
859-
auto posePlayed = msgs::Convert(_playedMsg.pose(i));
860-
auto poseRecorded = msgs::Convert(
861-
entityRecordedPose[_playedMsg.pose(i).name()]);
862-
863-
EXPECT_NEAR(posePlayed.Pos().X(), poseRecorded.Pos().X(), 0.1)
864-
<< _playedMsg.pose(i).name();
865-
EXPECT_NEAR(posePlayed.Pos().Y(), poseRecorded.Pos().Y(), 0.1)
866-
<< _playedMsg.pose(i).name();
867-
EXPECT_NEAR(posePlayed.Pos().Z(), poseRecorded.Pos().Z(), 0.1)
868-
<< _playedMsg.pose(i).name();
869-
870-
EXPECT_NEAR(posePlayed.Rot().Roll(), poseRecorded.Rot().Roll(), 0.1)
871-
<< _playedMsg.pose(i).name();
872-
EXPECT_NEAR(posePlayed.Rot().Pitch(), poseRecorded.Rot().Pitch(), 0.1)
873-
<< _playedMsg.pose(i).name();
874-
EXPECT_NEAR(posePlayed.Rot().Yaw(), poseRecorded.Rot().Yaw(), 0.1)
875-
<< _playedMsg.pose(i).name();
876-
}
868+
// Loop through all recorded poses, and check them against the
869+
// playedback poses.
870+
for (int i = 0; i < recordedMsg.pose_size(); ++i)
871+
{
872+
const math::Pose3d &poseRecorded = msgs::Convert(recordedMsg.pose(i));
873+
const std::string &name = recordedMsg.pose(i).name();
874+
875+
auto entity = _ecm.EntityByComponents(
876+
components::Name(recordedMsg.pose(i).name()));
877+
ASSERT_NE(kNullEntity, entity);
878+
auto poseComp = _ecm.Component<components::Pose>(entity);
879+
ASSERT_NE(nullptr, poseComp);
880+
const auto &posePlayed = poseComp->Data();
881+
882+
EXPECT_NEAR(posePlayed.Pos().X(), poseRecorded.Pos().X(), 0.1)
883+
<< name;
884+
EXPECT_NEAR(posePlayed.Pos().Y(), poseRecorded.Pos().Y(), 0.1)
885+
<< name;
886+
EXPECT_NEAR(posePlayed.Pos().Z(), poseRecorded.Pos().Z(), 0.1)
887+
<< name;
888+
889+
EXPECT_NEAR(posePlayed.Rot().Roll(), poseRecorded.Rot().Roll(), 0.1)
890+
<< name;
891+
EXPECT_NEAR(posePlayed.Rot().Pitch(), poseRecorded.Rot().Pitch(), 0.1)
892+
<< name;
893+
EXPECT_NEAR(posePlayed.Rot().Yaw(), poseRecorded.Rot().Yaw(), 0.1)
894+
<< name;
895+
}
877896

878-
++recordedIter;
879-
nTotal++;
880-
};
897+
++recordedIter;
898+
});
881899

882-
// Subscribe to ignition topic and compare to logged poses
883-
transport::Node node;
884-
// TODO(louise) The world name should match the recorded world
885-
node.Subscribe("/world/default/dynamic_pose/info", msgCb);
886-
887-
int playbackSteps = 500;
888-
int poseHz = 60;
889-
int expectedPoseCount = playbackSteps * 1e-3 / (1.0/poseHz);
890-
// Run for a few seconds to play back different poses
891-
playServer.Run(true, playbackSteps, false);
892-
893-
int sleep = 0;
894-
int maxSleep = 30;
895-
for (; nTotal < expectedPoseCount && sleep < maxSleep; ++sleep)
896-
{
897-
std::this_thread::sleep_for(std::chrono::milliseconds(100));
898-
}
899-
EXPECT_NE(maxSleep, sleep);
900+
playServer.AddSystem(playbackPoseTester.systemPtr);
901+
902+
// Playback a subset of the log file and check for poses. Note: the poses are
903+
// checked in the playbackPoseTester
904+
playServer.Run(true, 500, false);
905+
906+
// Count the total number of pose messages in the log file
907+
for (auto it = batch.begin(); it != batch.end(); ++it, ++nTotal) { }
900908

901-
#if !defined (__APPLE__)
902-
/// \todo(anyone) there seems to be a race condition that sometimes cause an
903-
/// additional messages to be published by the scene broadcaster
904-
// 60Hz
905-
EXPECT_TRUE(nTotal == expectedPoseCount || nTotal == expectedPoseCount + 1)
909+
// The expectedPoseCount might be off by ±2 because the ign transport's
910+
// throttle mechanism (which is used by the SceneBroadcaster when publishing
911+
// poses) uses real-time
912+
EXPECT_LE(std::abs(nTotal - expectedPoseCount), 2)
906913
<< "nTotal [" << nTotal << "] expectedPoseCount [" << expectedPoseCount
907914
<< "]";
908-
#endif
909915

910916
this->RemoveLogsDir();
911917
}

0 commit comments

Comments
 (0)