diff --git a/SilKit/IntegrationTests/CMakeLists.txt b/SilKit/IntegrationTests/CMakeLists.txt index 0eb1b3a59..062939788 100644 --- a/SilKit/IntegrationTests/CMakeLists.txt +++ b/SilKit/IntegrationTests/CMakeLists.txt @@ -118,6 +118,11 @@ add_silkit_test_to_executable(SilKitIntegrationTests SOURCES ITest_SimTask.cpp ) +add_silkit_test_to_executable(SilKitIntegrationTests + SOURCES ITest_DynStepSizes.cpp +) + + add_silkit_test_to_executable(SilKitFunctionalTests SOURCES FTest_WallClockCoupling.cpp ) diff --git a/SilKit/IntegrationTests/ITest_AsyncSimTask.cpp b/SilKit/IntegrationTests/ITest_AsyncSimTask.cpp index e97fb04e0..68edc222f 100644 --- a/SilKit/IntegrationTests/ITest_AsyncSimTask.cpp +++ b/SilKit/IntegrationTests/ITest_AsyncSimTask.cpp @@ -302,11 +302,12 @@ auto MakeCompletionThread(SimParticipant* p, ParticipantData* d) -> std::thread TEST(ITest_AsyncSimTask, test_async_simtask_other_simulation_steps_completed_handler) { - SimTestHarness testHarness({"A", "B", "C"}, "silkit://localhost:0"); + SimTestHarness testHarness({"A", "B", "C", "D"}, "silkit://localhost:0"); const auto a = testHarness.GetParticipant("A"); const auto b = testHarness.GetParticipant("B"); const auto c = testHarness.GetParticipant("C"); + const auto d = testHarness.GetParticipant("D"); ParticipantData ad, bd, cd; @@ -316,6 +317,8 @@ TEST(ITest_AsyncSimTask, test_async_simtask_other_simulation_steps_completed_han b->GetOrCreateLifecycleService()->SetStopHandler([&bd] { bd.running = false; }); c->GetOrCreateLifecycleService()->SetStopHandler([&cd] { cd.running = false; }); + d->GetOrCreateTimeSyncService()->SetSimulationStepHandler([](auto, auto) {}, 1ms); + const auto aLifecycleService = a->GetOrCreateLifecycleService(); a->GetOrCreateTimeSyncService()->SetSimulationStepHandlerAsync([aLifecycleService, &ad](auto now, auto) { diff --git a/SilKit/IntegrationTests/ITest_DynStepSizes.cpp b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp new file mode 100644 index 000000000..dc2d231d0 --- /dev/null +++ b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp @@ -0,0 +1,400 @@ +// SPDX-FileCopyrightText: 2023 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include +#include +#include +#include "ITestFixture.hpp" + +using namespace std::chrono_literals; + +namespace testing { +namespace internal { +template +class UniversalPrinter> +{ +public: + static void Print(const std::chrono::duration& value, ::std::ostream* os) + { + *os << std::chrono::duration_cast(value).count() << "ns"; + } +}; + +template +class UniversalPrinter, std::chrono::duration>> +{ +public: + static void Print(const std::pair, std::chrono::duration>& p, + ::std::ostream* os) + { + *os << "("; + UniversalPrinter::Print(p.first, os); + *os << ", "; + UniversalPrinter::Print(p.second, os); + *os << ")"; + } +}; + +} // namespace internal +} // namespace testing + +inline std::string ToString(const std::chrono::nanoseconds& ns) +{ + std::ostringstream os; + testing::internal::UniversalPrinter::Print(ns, &os); + return os.str(); +} +inline std::string ToString(const std::pair& p) +{ + std::ostringstream os; + testing::internal::UniversalPrinter>::Print(p, &os); + return os.str(); +} + +namespace { + +using namespace SilKit::Tests; +using namespace SilKit::Config; +using namespace SilKit::Services; +using namespace SilKit::Services::Orchestration; + +template +struct Dummy +{ + std::chrono::duration value; + + explicit Dummy(const std::chrono::duration& v) + : value{v} + { + } + + bool operator==(const Dummy& other) const + { + return value == other.value; + } + + friend void PrintTo(const Dummy& d, std::ostream* os) + { + *os << std::chrono::duration_cast(d.value).count() << "ns"; + } +}; + +#define SILKIT_ASSERT_CHRONO_EQ(expected, actual) ASSERT_EQ(Dummy{(expected)}, Dummy{(actual)}) +#define SILKIT_EXPECT_CHRONO_EQ(expected, actual) EXPECT_EQ(Dummy{(expected)}, Dummy{(actual)}) + + +struct ParticipantParams +{ + std::string name{}; + std::chrono::nanoseconds initialStepSize{1ms}; + // Enables/disables the dynamic simulation step behavior via participant configuration + // (Experimental.TimeSynchronization.DynamicSimulationStep). true == align to the minimal step + // among all participants, false == always advance by the participant's own step size. + bool dynamicSimulationStep{true}; + + // Change step size at these time points + std::map + changeStepSizeAtTimePoints{}; + + // Result: recorded time points and durations + std::vector> timePointsAndDurations{}; +}; + +struct ITest_DynStepSizes : ITest_SimTestHarness +{ + using ITest_SimTestHarness::ITest_SimTestHarness; + void RunTestSetup(std::vector& participantsParams); + void AssertAllStepsEqual(const std::vector& participantsParams); + void AssertAscendingStepsWithReferenceDuration(const std::vector& participantsParams, + std::chrono::nanoseconds refDuration); + void AssertStepsEqual(const std::vector& s1, + const std::vector& s2); +}; + +void ITest_DynStepSizes::RunTestSetup(std::vector& participantsParams) +{ + std::vector participantNames; + for (const auto& participantParams : participantsParams) + { + participantNames.push_back(participantParams.name); + } + SetupFromParticipantList(participantNames); + + std::mutex mx; + + using StepHandler = std::function; + // Keep the self-referential handlers alive for the whole run. + std::vector> stepHandlers; + + for (auto& participantParams : participantsParams) + { + std::string participantConfiguration; + participantConfiguration += R"({"Logging":{"Sinks":[{"Type":"File","Level":"Trace","LogName":")"; + participantConfiguration += "DynStepSizes_" + participantParams.name; + participantConfiguration += R"("}]},"Experimental":{"TimeSynchronization":{"DynamicSimulationStep":)"; + participantConfiguration += participantParams.dynamicSimulationStep ? "true" : "false"; + participantConfiguration += R"(}}})"; + + auto&& simParticipant = _simTestHarness->GetParticipant(participantParams.name, participantConfiguration); + auto&& lifecycleService = simParticipant->GetOrCreateLifecycleService(); + // The harness already created the time sync service for coordinated participants (reading the + // DynamicSimulationStep flag from the participant configuration above); reuse it and override the + // default no-op step handler below. + auto* timeSyncService = simParticipant->GetOrCreateTimeSyncService(); + + auto handler = std::make_shared(); + *handler = [timeSyncService, &participantParams, &mx, lifecycleService, handler](auto now, auto duration) { + if (now >= 100ms) + { + lifecycleService->Stop("stopping the test at 100ms"); + return; + } + + std::chrono::nanoseconds newStepSize{}; + bool changeStepSize = false; + { + std::lock_guard lock(mx); + participantParams.timePointsAndDurations.emplace_back(now, duration); + + // Check if we need to change the step size at this time point + auto it = participantParams.changeStepSizeAtTimePoints.find(now); + if (it != participantParams.changeStepSizeAtTimePoints.end()) + { + newStepSize = it->second; + changeStepSize = true; + } + } + + // Vary the step size by re-registering the same handler with a new step size. This is the + // supported mechanism for dynamic step sizes (the network simulator drives it the same way). + // Must be the last statement: it replaces the currently executing handler. + if (changeStepSize) + { + timeSyncService->SetSimulationStepHandler(*handler, newStepSize); + } + }; + stepHandlers.push_back(handler); + timeSyncService->SetSimulationStepHandler(*handler, participantParams.initialStepSize); + } + + auto ok = _simTestHarness->Run(5s); + ASSERT_TRUE(ok) << "SimTestHarness should terminate without timeout"; + + // Break the shared_ptr<->std::function self-reference cycles so the handlers are not leaked. + for (auto& handler : stepHandlers) + { + *handler = nullptr; + } +} + +void ITest_DynStepSizes::AssertAllStepsEqual(const std::vector& participantsParams) +{ + for (size_t i = 1; i < participantsParams.size(); ++i) + { + const auto& ref = participantsParams[0].timePointsAndDurations; + const auto& cmp = participantsParams[i].timePointsAndDurations; + + ASSERT_EQ(ref.size(), cmp.size()) + << "Different number of steps for " << participantsParams[0].name << " and " << participantsParams[i].name; + + for (size_t j = 0; j < ref.size(); ++j) + { + EXPECT_EQ(ref[j], cmp[j]) << "Differenz at index " << j << ": " << participantsParams[0].name + << "(now=" << ToString(ref[j].first) << ", duration=" << ToString(ref[j].second) + << ")" + << " vs " << participantsParams[i].name << "(now=" << ToString(cmp[j].first) + << ", duration=" << ToString(cmp[j].second) << ")"; + } + } +} + +void ITest_DynStepSizes::AssertAscendingStepsWithReferenceDuration( + const std::vector& participantsParams, std::chrono::nanoseconds refDuration) +{ + for (const auto& participant : participantsParams) + { + const auto& steps = participant.timePointsAndDurations; + ASSERT_FALSE(steps.empty()) << "No simulation steps for participant " << participant.name; + + for (size_t i = 0; i < steps.size(); ++i) + { + // Check if duration matches the reference duration + EXPECT_EQ(steps[i].second, refDuration) + << "Duration mismatch for " << participant.name << " at index " << i << ": expected " + << ToString(refDuration) << ", got " << ToString(steps[i].second); + + // Check if time points are strictly increasing by refDuration + if (i > 0) + { + auto diff = steps[i].first - steps[i - 1].first; + EXPECT_EQ(diff, refDuration) + << "Timestep difference for " << participant.name << " at index " << i << " is " << ToString(diff) + << ", expected " << ToString(refDuration) << " (" << ToString(steps[i - 1].first) << " -> " + << ToString(steps[i].first) << ")"; + } + } + } +} + +void ITest_DynStepSizes::AssertStepsEqual(const std::vector& s1, + const std::vector& s2) +{ + ASSERT_EQ(s1.size(), s2.size()) << "Different number of steps"; + + for (size_t j = 0; j < s1.size(); ++j) + { + SILKIT_EXPECT_CHRONO_EQ(s1[j], s2[j]) << "Differenz at index " << j << ": " + << "s1 now=" << ToString(s1[j]) << " vs s2 now=" << ToString(s2[j]); + } +} + +// Zero duration is invalid and should throw +TEST_F(ITest_DynStepSizes, invalid_duration) +{ + auto invalidDuration = 0ns; + std::vector participantsParams = {{"P1", invalidDuration, true}}; + EXPECT_THROW(RunTestSetup(participantsParams), SilKit::SilKitError); +} + +// Single participant with both time advance modes +TEST_F(ITest_DynStepSizes, one_participant_ByMinimalDuration) +{ + auto refDuration = 5ms; + std::vector participantsParams = {{"P1", refDuration, true}}; + RunTestSetup(participantsParams); + AssertAscendingStepsWithReferenceDuration(participantsParams, refDuration); +} +TEST_F(ITest_DynStepSizes, one_participant_ByOwnDuration) +{ + auto refDuration = 5ms; + std::vector participantsParams = {{"P1", refDuration, false}}; + RunTestSetup(participantsParams); + AssertAscendingStepsWithReferenceDuration(participantsParams, refDuration); +} + +// Two/Three participants with ByMinimalDuration mode; Expect steps aligned to the minimal duration +TEST_F(ITest_DynStepSizes, two_participants_ByMinimalDuration) +{ + std::vector participantsParams = {{"P1", 1ms, true}, + {"P2", 5ms, true}}; + RunTestSetup(participantsParams); + AssertAscendingStepsWithReferenceDuration(participantsParams, 1ms); +} +TEST_F(ITest_DynStepSizes, three_participants_ByMinimalDuration) +{ + std::vector participantsParams = {{"P1", 1ms, true}, + {"P2", 2ms, true}, + {"P3", 3ms, true}}; + RunTestSetup(participantsParams); + AssertAscendingStepsWithReferenceDuration(participantsParams, 1ms); +} + +// Two participants with mixed modes; Expect steps aligned to the minimal/own duration +TEST_F(ITest_DynStepSizes, two_participants_MixedTimeAdvanceModes) +{ + std::vector participantsParams = {{"P1", 5ms, true}, + {"P2", 1ms, false}}; + RunTestSetup(participantsParams); + AssertAscendingStepsWithReferenceDuration(participantsParams, 1ms); +} + +// Three participants with mixed modes; Expect steps of P3(ByMinimalDuration) are equal to the union of P1,P2(ByOwnDuration) +TEST_F(ITest_DynStepSizes, three_participants_MixedTimeAdvanceModes) +{ + std::vector participantsParams = {{"P1", 2ms, false}, + {"P2", 3ms, false}, + {"P3", 4ms, true}}; + RunTestSetup(participantsParams); + + AssertAscendingStepsWithReferenceDuration({participantsParams[0]}, 2ms); + AssertAscendingStepsWithReferenceDuration({participantsParams[1]}, 3ms); + + // Collect nows for P1 and P2 + std::set unionNows; + for (size_t i = 0; i < 2; ++i) + { + for (const auto& step : participantsParams[i].timePointsAndDurations) + { + unionNows.insert(step.first); + } + } + // Convert unionNows to sorted vector + std::vector unionNowsVec(unionNows.begin(), unionNows.end()); + // Collect nows for P3 + std::vector p3Nows; + for (const auto& step : participantsParams[2].timePointsAndDurations) + { + p3Nows.push_back(step.first); + } + // Compare P3 nows with union of P1 and P2 nows + AssertStepsEqual(p3Nows, unionNowsVec); +} + +// Change to a different step size during simulation +TEST_F(ITest_DynStepSizes, one_participant_change_step_size) +{ + std::vector participantsParams = { + {"P1", 1ms, false, {{9ms, 10ms}, {80ms, 2ms}}}}; + RunTestSetup(participantsParams); + + ParticipantParams refData; + refData.name = "Reference"; + refData.timePointsAndDurations = { + {0ms, 1ms}, {1ms, 1ms}, {2ms, 1ms}, {3ms, 1ms}, {4ms, 1ms}, {5ms, 1ms}, {6ms, 1ms}, {7ms, 1ms}, + {8ms, 1ms}, {9ms, 1ms}, {10ms, 10ms}, {20ms, 10ms}, {30ms, 10ms}, {40ms, 10ms}, {50ms, 10ms}, {60ms, 10ms}, + {70ms, 10ms}, {80ms, 10ms}, {90ms, 2ms}, {92ms, 2ms}, {94ms, 2ms}, {96ms, 2ms}, {98ms, 2ms}}; + + AssertAllStepsEqual({participantsParams[0], refData}); +} + +// Change to different step sizes during simulation; mixed time advance modes +TEST_F(ITest_DynStepSizes, two_participants_mixed_change_step_size) +{ + std::vector participantsParams = { + {"P1", 1ms, false, {{9ms, 10ms}, {80ms, 2ms}}}, + {"P2", 20ms, true}}; + + RunTestSetup(participantsParams); + + ParticipantParams refData; + refData.name = "Reference"; + refData.timePointsAndDurations = { + {0ms, 1ms}, {1ms, 1ms}, {2ms, 1ms}, {3ms, 1ms}, {4ms, 1ms}, {5ms, 1ms}, {6ms, 1ms}, {7ms, 1ms}, + {8ms, 1ms}, {9ms, 1ms}, {10ms, 10ms}, {20ms, 10ms}, {30ms, 10ms}, {40ms, 10ms}, {50ms, 10ms}, {60ms, 10ms}, + {70ms, 10ms}, {80ms, 10ms}, {90ms, 2ms}, {92ms, 2ms}, {94ms, 2ms}, {96ms, 2ms}, {98ms, 2ms}}; + + + AssertAllStepsEqual({participantsParams[0], refData}); + // P2 (ByMinimalDuration) follows P1 (ByOwnDuration) + AssertAllStepsEqual({participantsParams[0], participantsParams[1]}); +} + +// Change to different step sizes during simulation; both participants with ByMinimalDuration +TEST_F(ITest_DynStepSizes, two_participants_ByMinimalDuration_change_step_size) +{ + std::vector participantsParams = { + {"P1", 1ms, true, {{9ms, 10ms}, {80ms, 2ms}}}, + {"P2", 20ms, true}}; + + RunTestSetup(participantsParams); + + ParticipantParams refData; + refData.name = "Reference"; + refData.timePointsAndDurations = { + {0ms, 1ms}, {1ms, 1ms}, {2ms, 1ms}, {3ms, 1ms}, {4ms, 1ms}, {5ms, 1ms}, {6ms, 1ms}, {7ms, 1ms}, + {8ms, 1ms}, {9ms, 1ms}, {10ms, 10ms}, {20ms, 10ms}, {30ms, 10ms}, {40ms, 10ms}, {50ms, 10ms}, {60ms, 10ms}, + {70ms, 10ms}, {80ms, 10ms}, {90ms, 2ms}, {92ms, 2ms}, {94ms, 2ms}, {96ms, 2ms}, {98ms, 2ms}}; + + + AssertAllStepsEqual({participantsParams[0], refData}); + AssertAllStepsEqual({participantsParams[0], participantsParams[1]}); +} + + +} //end namespace diff --git a/SilKit/IntegrationTests/SimTestHarness/SimTestHarness.cpp b/SilKit/IntegrationTests/SimTestHarness/SimTestHarness.cpp index 134a73534..1a04fc5d8 100644 --- a/SilKit/IntegrationTests/SimTestHarness/SimTestHarness.cpp +++ b/SilKit/IntegrationTests/SimTestHarness/SimTestHarness.cpp @@ -304,11 +304,6 @@ void SimTestHarness::AddParticipant(const std::string& participantName, const st // mandatory sim task for time synced simulation // by default, we do no operation during simulation task, the user should override this auto* lifecycleService = participant->GetOrCreateLifecycleService(startConfiguration); - if (startConfiguration.operationMode == SilKit::Services::Orchestration::OperationMode::Coordinated) - { - auto* timeSyncService = participant->GetOrCreateTimeSyncService(); - timeSyncService->SetSimulationStepHandler([](auto, auto) {}, 1ms); - } lifecycleService->SetCommunicationReadyHandler([]() {}); diff --git a/SilKit/source/config/ParticipantConfiguration.cpp b/SilKit/source/config/ParticipantConfiguration.cpp index 583b9c050..5dec0a99b 100755 --- a/SilKit/source/config/ParticipantConfiguration.cpp +++ b/SilKit/source/config/ParticipantConfiguration.cpp @@ -169,7 +169,8 @@ bool operator==(const ParticipantConfiguration& lhs, const ParticipantConfigurat bool operator==(const TimeSynchronization& lhs, const TimeSynchronization& rhs) { - return lhs.animationFactor == rhs.animationFactor && lhs.enableMessageAggregation == rhs.enableMessageAggregation; + return lhs.animationFactor == rhs.animationFactor && lhs.enableMessageAggregation == rhs.enableMessageAggregation + && lhs.dynamicSimulationStep == rhs.dynamicSimulationStep; } bool operator==(const Experimental& lhs, const Experimental& rhs) diff --git a/SilKit/source/config/ParticipantConfiguration.hpp b/SilKit/source/config/ParticipantConfiguration.hpp index e1322cb46..8db87a021 100644 --- a/SilKit/source/config/ParticipantConfiguration.hpp +++ b/SilKit/source/config/ParticipantConfiguration.hpp @@ -309,6 +309,9 @@ struct TimeSynchronization { double animationFactor{0.0}; Aggregation enableMessageAggregation{Aggregation::Off}; + //! When enabled (the default), a participant aligns its simulation step duration to the minimal + //! step among all synchronized participants. Set to false to opt out. + bool dynamicSimulationStep{true}; }; // ================================================================================ diff --git a/SilKit/source/config/ParticipantConfiguration.schema.json b/SilKit/source/config/ParticipantConfiguration.schema.json index 0800c842f..e04200f57 100644 --- a/SilKit/source/config/ParticipantConfiguration.schema.json +++ b/SilKit/source/config/ParticipantConfiguration.schema.json @@ -838,6 +838,11 @@ "enum": [ "Off", "On", "Auto" ], "description": "Decide for simulations with time synchronization, if a message aggregation is performed. In case of the Auto mode, the message aggregation is enabled for simulations using the synchronous simulation step handler.", "default": "Off" + }, + "DynamicSimulationStep": { + "type": "boolean", + "description": "When enabled (the default), a participant aligns its simulation step duration to the minimal step among all synchronized participants, allowing dynamic step sizes. Set to false to opt out.", + "default": true } }, "additionalProperties": false diff --git a/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp b/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp index d2e3f7c20..aa93a60f0 100644 --- a/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp +++ b/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp @@ -62,6 +62,7 @@ struct TimeSynchronizationCache { std::optional animationFactor; std::optional enableMessageAggregation; + std::optional dynamicSimulationStep; }; struct MetricsCache @@ -346,6 +347,8 @@ void Cache(const TimeSynchronization& root, TimeSynchronizationCache& cache) cache.animationFactor); CacheNonDefault(defaultObject.enableMessageAggregation, root.enableMessageAggregation, "TimeSynchronization.EnableMessageAggregation", cache.enableMessageAggregation); + CacheNonDefault(defaultObject.dynamicSimulationStep, root.dynamicSimulationStep, + "TimeSynchronization.DynamicSimulationStep", cache.dynamicSimulationStep); } void Cache(const Metrics& root, MetricsCache& cache) @@ -518,6 +521,7 @@ void MergeTimeSynchronizationCache(const TimeSynchronizationCache& cache, TimeSy { MergeCacheField(cache.animationFactor, timeSynchronization.animationFactor); MergeCacheField(cache.enableMessageAggregation, timeSynchronization.enableMessageAggregation); + MergeCacheField(cache.dynamicSimulationStep, timeSynchronization.dynamicSimulationStep); } void MergeMetricsCache(const MetricsCache& cache, Metrics& metrics) diff --git a/SilKit/source/config/ParticipantConfiguration_Full.json b/SilKit/source/config/ParticipantConfiguration_Full.json index 6f04a8ed1..6fe22e703 100644 --- a/SilKit/source/config/ParticipantConfiguration_Full.json +++ b/SilKit/source/config/ParticipantConfiguration_Full.json @@ -264,7 +264,8 @@ "Experimental": { "TimeSynchronization": { "AnimationFactor": 1.5, - "EnableMessageAggregation": "Off" + "EnableMessageAggregation": "Off", + "DynamicSimulationStep": false }, "Metrics": { "CollectFromRemote": false, diff --git a/SilKit/source/config/ParticipantConfiguration_Full.yaml b/SilKit/source/config/ParticipantConfiguration_Full.yaml index 4c08d28a3..f88844e17 100644 --- a/SilKit/source/config/ParticipantConfiguration_Full.yaml +++ b/SilKit/source/config/ParticipantConfiguration_Full.yaml @@ -191,6 +191,7 @@ Experimental: TimeSynchronization: AnimationFactor: 1.5 EnableMessageAggregation: Off + DynamicSimulationStep: false Metrics: CollectFromRemote: false Sinks: diff --git a/SilKit/source/config/YamlReader.cpp b/SilKit/source/config/YamlReader.cpp index 6a796eb39..c52205d89 100644 --- a/SilKit/source/config/YamlReader.cpp +++ b/SilKit/source/config/YamlReader.cpp @@ -455,6 +455,7 @@ void YamlReader::Read(SilKit::Config::TimeSynchronization& obj) { OptionalRead(obj.animationFactor, "AnimationFactor"); OptionalRead(obj.enableMessageAggregation, "EnableMessageAggregation"); + OptionalRead(obj.dynamicSimulationStep, "DynamicSimulationStep"); } void YamlReader::Read(SilKit::Config::Experimental& obj) diff --git a/SilKit/source/config/YamlValidator.cpp b/SilKit/source/config/YamlValidator.cpp index d22b36136..1b742de8e 100644 --- a/SilKit/source/config/YamlValidator.cpp +++ b/SilKit/source/config/YamlValidator.cpp @@ -147,6 +147,7 @@ const std::set schemaPaths_v1 = { "/Experimental/Metrics/Sinks/Type", "/Experimental/TimeSynchronization", "/Experimental/TimeSynchronization/AnimationFactor", + "/Experimental/TimeSynchronization/DynamicSimulationStep", "/Experimental/TimeSynchronization/EnableMessageAggregation", "/Extensions", "/Extensions/SearchPathHints", diff --git a/SilKit/source/config/YamlWriter.cpp b/SilKit/source/config/YamlWriter.cpp index 1bcadc815..4ecee7869 100644 --- a/SilKit/source/config/YamlWriter.cpp +++ b/SilKit/source/config/YamlWriter.cpp @@ -567,6 +567,7 @@ void YamlWriter::Write(const SilKit::Config::TimeSynchronization& obj) MakeMap(); NonDefaultWrite(obj.animationFactor, "AnimationFactor", defaultObj.animationFactor); NonDefaultWrite(obj.enableMessageAggregation, "EnableMessageAggregation", defaultObj.enableMessageAggregation); + NonDefaultWrite(obj.dynamicSimulationStep, "DynamicSimulationStep", defaultObj.dynamicSimulationStep); } diff --git a/SilKit/source/core/internal/IParticipantInternal.hpp b/SilKit/source/core/internal/IParticipantInternal.hpp index 3da426d3d..45ef324bb 100644 --- a/SilKit/source/core/internal/IParticipantInternal.hpp +++ b/SilKit/source/core/internal/IParticipantInternal.hpp @@ -10,6 +10,7 @@ #include "silkit/experimental/services/orchestration/ISystemController.hpp" #include "silkit/experimental/netsim/NetworkSimulatorDatatypes.hpp" +#include "ParticipantConfiguration.hpp" #include "internal_fwd.hpp" #include "IServiceEndpoint.hpp" @@ -53,6 +54,8 @@ class IParticipantInternal : public IParticipant // Public methods virtual auto GetParticipantName() const -> const std::string& = 0; + virtual auto GetParticipantConfiguration() const -> const SilKit::Config::ParticipantConfiguration& = 0; + /*! \brief Returns the URI of the registry this participant is connecting to. * * The URI must be specified in the configuration (which has priority) or the CreateParticipant call. diff --git a/SilKit/source/core/mock/participant/MockParticipant.hpp b/SilKit/source/core/mock/participant/MockParticipant.hpp index 611a852e0..3f67ef4c4 100644 --- a/SilKit/source/core/mock/participant/MockParticipant.hpp +++ b/SilKit/source/core/mock/participant/MockParticipant.hpp @@ -629,6 +629,10 @@ class DummyParticipant : public IParticipantInternal { return _registryUri; } + auto GetParticipantConfiguration() const -> const SilKit::Config::ParticipantConfiguration& override + { + return _participantConfiguration; + } virtual auto GetTimeProvider() -> Services::Orchestration::ITimeProvider* { @@ -733,6 +737,7 @@ class DummyParticipant : public IParticipantInternal MockParticipantReplies mockParticipantReplies; DummyNetworkSimulator mockNetworkSimulator; DummyMetricsManager mockMetricsManager; + SilKit::Config::ParticipantConfiguration _participantConfiguration; }; // ================================================================================ diff --git a/SilKit/source/core/participant/Participant.hpp b/SilKit/source/core/participant/Participant.hpp index 8f864b597..44bf970fe 100644 --- a/SilKit/source/core/participant/Participant.hpp +++ b/SilKit/source/core/participant/Participant.hpp @@ -166,6 +166,11 @@ class Participant final : public IParticipantInternal return _participantConfig.middleware.registryUri; } + auto GetParticipantConfiguration() const -> const SilKit::Config::ParticipantConfiguration& override + { + return _participantConfig; + } + void SendMsg(const IServiceEndpoint* from, const Services::Can::WireCanFrameEvent& msg) override; void SendMsg(const IServiceEndpoint* from, const Services::Can::CanFrameTransmitEvent& msg) override; void SendMsg(const IServiceEndpoint* from, const Services::Can::CanControllerStatus& msg) override; diff --git a/SilKit/source/services/orchestration/LifecycleService.cpp b/SilKit/source/services/orchestration/LifecycleService.cpp index e946dc344..1d136603b 100644 --- a/SilKit/source/services/orchestration/LifecycleService.cpp +++ b/SilKit/source/services/orchestration/LifecycleService.cpp @@ -403,16 +403,21 @@ auto LifecycleService::GetTimeSyncService() -> ITimeSyncService* auto LifecycleService::CreateTimeSyncService() -> ITimeSyncService* { - if (!_timeSyncActive) - { - _participant->RegisterTimeSyncService(_timeSyncService); - _timeSyncActive = true; - return _timeSyncService; - } - else + if (_timeSyncActive) { throw ConfigurationError("You may not create the time synchronization service more than once."); } + + _participant->RegisterTimeSyncService(_timeSyncService); + _timeSyncActive = true; + + // Dynamic simulation step sizes are enabled by default and can be disabled per participant via + // Experimental.TimeSynchronization.DynamicSimulationStep. + const auto& participantConfiguration = _participant->GetParticipantConfiguration(); + _timeSyncService->SetDynamicStepSizeEnabled( + participantConfiguration.experimental.timeSynchronization.dynamicSimulationStep); + + return _timeSyncService; } void LifecycleService::ReceiveMsg(const IServiceEndpoint*, const SystemCommand& command) diff --git a/SilKit/source/services/orchestration/Test_LifecycleService.cpp b/SilKit/source/services/orchestration/Test_LifecycleService.cpp index 2093ca9a1..5bff246a7 100644 --- a/SilKit/source/services/orchestration/Test_LifecycleService.cpp +++ b/SilKit/source/services/orchestration/Test_LifecycleService.cpp @@ -44,7 +44,6 @@ class MockTimeSync : public TimeSyncService MOCK_METHOD(void, SetSimulationStepHandlerAsync, (SimulationStepHandler task, std::chrono::nanoseconds initialStepSize), (override)); MOCK_METHOD(void, CompleteSimulationStep, (), (override)); - MOCK_METHOD(void, SetPeriod, (std::chrono::nanoseconds)); MOCK_METHOD(std::chrono::nanoseconds, Now, (), (override, const)); }; @@ -1156,6 +1155,9 @@ TEST_F(Test_LifecycleService, error_on_create_time_sync_service_twice) LifecycleService lifecycleService(&participant); lifecycleService.SetLifecycleConfiguration(StartCoordinated()); + MockTimeSync mockTimeSync(&participant, &participant.mockTimeProvider, healthCheckConfig, &lifecycleService); + lifecycleService.SetTimeSyncService(&mockTimeSync); + EXPECT_NO_THROW({ try { diff --git a/SilKit/source/services/orchestration/TimeConfiguration.cpp b/SilKit/source/services/orchestration/TimeConfiguration.cpp index 98acf56c3..b931d0af8 100644 --- a/SilKit/source/services/orchestration/TimeConfiguration.cpp +++ b/SilKit/source/services/orchestration/TimeConfiguration.cpp @@ -20,6 +20,12 @@ TimeConfiguration::TimeConfiguration(Logging::ILoggerInternal* logger) _myNextTask.duration = 1ms; } +void TimeConfiguration::SetDynamicStepSizeEnabled(bool enabled) +{ + Lock lock{_mx}; + _dynamicStepSizeEnabled = enabled; +} + void TimeConfiguration::SetBlockingMode(bool blocking) { _blocking = blocking; @@ -82,35 +88,83 @@ void TimeConfiguration::OnReceiveNextSimStep(const std::string& participantName, participantName, nextStep.timePoint.count(), itOtherNextTask->second.timePoint.count()); } - _otherNextTasks.at(participantName) = std::move(nextStep); + _otherNextTasks.at(participantName) = nextStep; Logging::Debug(_logger, "Updated _otherNextTasks for participant {} with time {}", participantName, nextStep.timePoint.count()); } -void TimeConfiguration::SynchronizedParticipantRemoved(const std::string& otherParticipantName) + +void TimeConfiguration::SetStepDuration(std::chrono::nanoseconds duration) { Lock lock{_mx}; - if (_otherNextTasks.find(otherParticipantName) != _otherNextTasks.end()) - { - const std::string errorMessage{"Participant " + otherParticipantName + " unknown."}; - throw SilKitError{errorMessage}; - } - auto it = _otherNextTasks.find(otherParticipantName); - if (it != _otherNextTasks.end()) + + if (duration == 0ns) { - _otherNextTasks.erase(it); + throw SilKitError("Attempted to set step duration to zero."); } + + _myNextTask.duration = duration; } -void TimeConfiguration::SetStepDuration(std::chrono::nanoseconds duration) + +auto TimeConfiguration::GetMinimalAlignedDuration() const -> std::chrono::nanoseconds { - Lock lock{_mx}; - _myNextTask.duration = duration; + if (_otherNextTasks.empty()) + { + return std::chrono::nanoseconds::max(); + } + + auto earliestOtherTimepoint = std::chrono::nanoseconds::max(); + for (const auto& entry : _otherNextTasks) + { + // Both start and end of other participant's step could be the earliest next timepoint + auto nextStepStart = entry.second.timePoint; + auto nextStepEnd = entry.second.timePoint + entry.second.duration; + + if (nextStepStart > _currentTask.timePoint) + { + earliestOtherTimepoint = std::min(earliestOtherTimepoint, nextStepStart); + } + + if (nextStepEnd > _currentTask.timePoint) + { + earliestOtherTimepoint = std::min(earliestOtherTimepoint, nextStepEnd); + } + } + + Logging::Debug(_logger, "Earliest next timepoint among other participants is {}ns", earliestOtherTimepoint.count()); + + const auto minAlignedDuration = earliestOtherTimepoint - _currentTask.timePoint; + + if (minAlignedDuration < 0ns) + { + Logging::Error(_logger, + "Chonology error: Calculated minimal aligned duration is non-positive ({}ns). This indicates " + "that at least one participant has not advanced its time correctly.", + minAlignedDuration.count()); + return std::chrono::nanoseconds::max(); + } + + return minAlignedDuration; } void TimeConfiguration::AdvanceTimeStep() { Lock lock{_mx}; _currentTask = _myNextTask; + + if (_dynamicStepSizeEnabled) + { + const auto minAlignedDuration = GetMinimalAlignedDuration(); + + if (minAlignedDuration < _currentTask.duration) + { + Logging::Debug(_logger, "Adjusting my step duration from {}ms to {}ms", + std::chrono::duration_cast(_currentTask.duration).count(), + std::chrono::duration_cast(minAlignedDuration).count()); + _currentTask.duration = minAlignedDuration; + } + } + _myNextTask.timePoint = _currentTask.timePoint + _currentTask.duration; } diff --git a/SilKit/source/services/orchestration/TimeConfiguration.hpp b/SilKit/source/services/orchestration/TimeConfiguration.hpp index 59157c963..a4d2c6096 100755 --- a/SilKit/source/services/orchestration/TimeConfiguration.hpp +++ b/SilKit/source/services/orchestration/TimeConfiguration.hpp @@ -26,8 +26,6 @@ class TimeConfiguration bool RemoveSynchronizedParticipant(const std::string& otherParticipantName); auto GetSynchronizedParticipantNames() -> std::vector; void OnReceiveNextSimStep(const std::string& participantName, NextSimTask nextStep); - void SynchronizedParticipantRemoved(const std::string& otherParticipantName); - void SetStepDuration(std::chrono::nanoseconds duration); void AdvanceTimeStep(); auto CurrentSimStep() const -> NextSimTask; auto NextSimStep() const -> NextSimTask; @@ -41,6 +39,11 @@ class TimeConfiguration bool IsHopOn(); bool HoppedOn(); + void SetStepDuration(std::chrono::nanoseconds duration); + auto GetMinimalAlignedDuration() const -> std::chrono::nanoseconds; + + void SetDynamicStepSizeEnabled(bool enabled); + private: //Members mutable std::mutex _mx; using Lock = std::unique_lock; @@ -51,6 +54,11 @@ class TimeConfiguration bool _hoppedOn = false; Logging::ILoggerInternal* _logger; + + // When enabled, each simulation step is shortened ("aligned") to the minimal duration among all + // synchronized participants (see GetMinimalAlignedDuration / AdvanceTimeStep). Enabled by default + // via participant configuration; can be turned off with Experimental.TimeSynchronization.DynamicSimulationStep. + bool _dynamicStepSizeEnabled{false}; }; } // namespace Orchestration diff --git a/SilKit/source/services/orchestration/TimeSyncService.cpp b/SilKit/source/services/orchestration/TimeSyncService.cpp index d21b9888e..2511ab016 100644 --- a/SilKit/source/services/orchestration/TimeSyncService.cpp +++ b/SilKit/source/services/orchestration/TimeSyncService.cpp @@ -192,6 +192,7 @@ struct SynchronizedPolicy : public ITimeSyncPolicy return false; } + // No other participant has a lower time point if (_configuration->OtherParticipantHasLowerTimepoint()) { return false; @@ -453,11 +454,6 @@ void TimeSyncService::SetSimulationStepHandlerAsync(SimulationStepHandler task, _timeConfiguration.SetStepDuration(initialStepSize); } -void TimeSyncService::SetPeriod(std::chrono::nanoseconds period) -{ - _timeConfiguration.SetStepDuration(period); -} - bool TimeSyncService::SetupTimeSyncPolicy(bool isSynchronizingVirtualTime) { std::lock_guard lock{_timeSyncPolicyMx}; @@ -830,6 +826,12 @@ bool TimeSyncService::IsBlocking() const return _timeConfiguration.IsBlocking(); } +void TimeSyncService::SetDynamicStepSizeEnabled(bool enabled) +{ + _timeConfiguration.SetDynamicStepSizeEnabled(enabled); +} + + } // namespace Orchestration } // namespace Services } // namespace SilKit diff --git a/SilKit/source/services/orchestration/TimeSyncService.hpp b/SilKit/source/services/orchestration/TimeSyncService.hpp index 9cf516c08..f2d23e70d 100644 --- a/SilKit/source/services/orchestration/TimeSyncService.hpp +++ b/SilKit/source/services/orchestration/TimeSyncService.hpp @@ -56,7 +56,7 @@ class TimeSyncService void SetSimulationStepHandler(SimulationStepHandler task, std::chrono::nanoseconds initialStepSize) override; void SetSimulationStepHandlerAsync(SimulationStepHandler task, std::chrono::nanoseconds initialStepSize) override; void CompleteSimulationStep() override; - void SetPeriod(std::chrono::nanoseconds period); + void ReceiveMsg(const IServiceEndpoint* from, const NextSimTask& task) override; auto Now() const -> std::chrono::nanoseconds override; @@ -99,6 +99,8 @@ class TimeSyncService void RemoveOtherSimulationStepsCompletedHandler(HandlerId handlerId); void InvokeOtherSimulationStepsCompletedHandlers(); + void SetDynamicStepSizeEnabled(bool enabled); + private: // ---------------------------------------- // private methods @@ -154,6 +156,7 @@ class TimeSyncService std::atomic _wallClockReachedBeforeCompletion{false}; Util::SynchronizedHandlers> _otherSimulationStepsCompletedHandlers; + }; // ================================================================================