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_DifferentPeriods.cpp b/SilKit/IntegrationTests/ITest_DifferentPeriods.cpp index 8b45c818d..23e882b48 100644 --- a/SilKit/IntegrationTests/ITest_DifferentPeriods.cpp +++ b/SilKit/IntegrationTests/ITest_DifferentPeriods.cpp @@ -31,6 +31,12 @@ const std::string testMessage{"TestMessage"}; const std::chrono::nanoseconds subscriberPeriod = 7ns; const std::vector publisherPeriods = {3ns, 7ns, 17ns}; +// Upper bound for how long we wait on any lifecycle future. The simulation itself finishes in well +// under a second; this only guards against a deadlock (e.g. a failed assertion in a step/receive +// handler that prevents the stop condition from ever being reached) so the test fails as a gtest +// failure instead of hanging until the outer CTest timeout kills it. Generous for slow sanitizer CI. +const std::chrono::seconds testTimeout{60}; + std::ostream& operator<<(std::ostream& out, const nanoseconds& timestamp) { out << timestamp.count(); @@ -78,9 +84,16 @@ class Publisher _simulationFuture = _lifecycleService->StartLifecycle(); } - auto WaitForShutdown() -> ParticipantState + // Waits up to `timeout` for the lifecycle to finish. Returns false on timeout (leaving the + // future untouched so we never block on .get()); on success stores the final state. + auto TryWaitForShutdown(std::chrono::nanoseconds timeout, ParticipantState& finalState) -> bool { - return _simulationFuture.get(); + if (_simulationFuture.wait_for(timeout) != std::future_status::ready) + { + return false; + } + finalState = _simulationFuture.get(); + return true; } uint32_t NumMessagesSent() const @@ -154,6 +167,13 @@ class Subscriber return _lifecycleService->StartLifecycle(); } + // Force the whole coordinated simulation to unwind. Used to break a deadlock so all participant + // futures become ready instead of the test hanging forever. + void AbortSimulation() + { + _systemController->AbortSimulation(); + } + uint32_t NumMessagesReceived(const uint32_t publisherIndex) { return _messageIndexes[publisherIndex]; @@ -257,16 +277,42 @@ TEST_F(ITest_DifferentPeriods, different_simtask_periods) } - auto finalState = subscriberFuture.get(); - EXPECT_EQ(ParticipantState::Shutdown, finalState); + // The subscriber calls Stop() once every message has been received; wait for its lifecycle to + // finish, but never block indefinitely. If it does not complete in time (e.g. a handler + // assertion prevented the stop condition), abort the simulation so all participant threads + // unwind, then fail the test. + const bool subscriberFinished = subscriberFuture.wait_for(testTimeout) == std::future_status::ready; + if (!subscriberFinished) + { + subscriber.AbortSimulation(); + ADD_FAILURE() << "Timed out after " << testTimeout.count() + << "s waiting for the subscriber to shut down; simulation aborted"; + } + + // Ready now either from a normal shutdown or from the abort above. + ASSERT_EQ(std::future_status::ready, subscriberFuture.wait_for(testTimeout)) + << "Subscriber lifecycle future did not become ready even after abort"; + const auto finalState = subscriberFuture.get(); + if (subscriberFinished) + { + EXPECT_EQ(ParticipantState::Shutdown, finalState); + } for (auto publisherIndex = 0u; publisherIndex < publisherCount; publisherIndex++) { auto& publisher = publishers[publisherIndex]; - EXPECT_EQ(ParticipantState::Shutdown, publisher.WaitForShutdown()); - EXPECT_EQ(numMessages, publisher.NumMessagesSent()); - EXPECT_EQ(numMessages, subscriber.NumMessagesReceived(publisherIndex)); + ParticipantState publisherState{}; + ASSERT_TRUE(publisher.TryWaitForShutdown(testTimeout, publisherState)) + << "Timed out waiting for Publisher" << publisherIndex << " to shut down"; + + // Message-count expectations only hold on the happy path; after an abort they are meaningless. + if (subscriberFinished) + { + EXPECT_EQ(ParticipantState::Shutdown, publisherState); + EXPECT_EQ(numMessages, publisher.NumMessagesSent()); + EXPECT_EQ(numMessages, subscriber.NumMessagesReceived(publisherIndex)); + } } } diff --git a/SilKit/IntegrationTests/ITest_DynStepSizes.cpp b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp new file mode 100644 index 000000000..e9a5026e5 --- /dev/null +++ b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: 2023 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include +#include +#include "ITestFixture.hpp" + +using namespace std::chrono_literals; + +// Defined in std::chrono so both googletest's value printer (used for EXPECT_EQ operands, including +// std::pair) and its message stream can find it via ADL. +namespace std { +namespace chrono { +inline std::ostream& operator<<(std::ostream& out, const nanoseconds& timestamp) +{ + return out << timestamp.count() << "ns"; +} +} // namespace chrono +} // namespace std + +namespace { + +using namespace SilKit::Tests; +using namespace SilKit::Config; +using namespace SilKit::Services; +using namespace SilKit::Services::Orchestration; + +struct ParticipantParams +{ + std::string name{}; + std::chrono::nanoseconds initialStepSize{1ms}; + // Tri-state dynamic simulation step behavior via participant configuration + // (Experimental.TimeSynchronization.DynamicSimulationStep). true == request it for all + align to + // the minimal step; false == hard opt-out, always advance by the participant's own step size; + // std::nullopt == omit the config key entirely (follow the network / enable if a peer requests it). + std::optional 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; + // A handler re-registers itself (see below) to change its step size, so each handler captures a + // reference to its own slot in this vector. Reserve up front so the slots never move. + std::vector stepHandlers; + stepHandlers.reserve(participantsParams.size()); + + for (auto& participantParams : participantsParams) + { + std::string participantConfiguration; + participantConfiguration += R"({"Logging":{"Sinks":[{"Type":"File","Level":"Trace","LogName":")"; + participantConfiguration += "DynStepSizes_" + participantParams.name; + participantConfiguration += R"("}]},"Experimental":{"TimeSynchronization":{)"; + if (participantParams.dynamicSimulationStep.has_value()) + { + participantConfiguration += R"("DynamicSimulationStep":)"; + participantConfiguration += participantParams.dynamicSimulationStep.value() ? "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 = stepHandlers.emplace_back(); + 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); + } + }; + timeSyncService->SetSimulationStepHandler(handler, participantParams.initialStepSize); + } + + auto ok = _simTestHarness->Run(5s); + ASSERT_TRUE(ok) << "SimTestHarness should terminate without timeout"; +} + +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=" << ref[j].first << ", duration=" << ref[j].second << ")" + << " vs " << participantsParams[i].name << "(now=" << cmp[j].first + << ", duration=" << 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 " + << refDuration << ", got " << 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 " << diff + << ", expected " << refDuration << " (" << steps[i - 1].first << " -> " << 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) + { + EXPECT_EQ(s1[j], s2[j]) << "Differenz at index " << j << ": " + << "s1 now=" << s1[j] << " vs s2 now=" << 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); +} + +// Remote enabling: a participant that requests dynamic step sizes (true) makes a participant with no +// DynamicSimulationStep config (nullopt -> follow) use dynamic stepping too, while a hard opt-out +// participant (false) keeps advancing at its own duration. +TEST_F(ITest_DynStepSizes, follower_enabled_remotely_optout_unaffected) +{ + std::vector participantsParams = { + {"Driver", 1ms, true}, // requests dynamic step sizes for all + {"Follower", 5ms, std::nullopt}, // no config -> follows the network + {"OptOut", 5ms, false}}; // hard opt-out + RunTestSetup(participantsParams); + + // Driver aligns to the minimal step (1ms). + AssertAscendingStepsWithReferenceDuration({participantsParams[0]}, 1ms); + // Follower was remotely enabled by the Driver's advertisement -> also steps at the minimal 1ms, + // NOT at its own 5ms. + AssertAscendingStepsWithReferenceDuration({participantsParams[1]}, 1ms); + // Opt-out ignored the advertisement and keeps its own 5ms step. + AssertAscendingStepsWithReferenceDuration({participantsParams[2]}, 5ms); +} + +// 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/cmake/SilKitTest.cmake b/SilKit/cmake/SilKitTest.cmake index d550f5f5b..94948e535 100644 --- a/SilKit/cmake/SilKitTest.cmake +++ b/SilKit/cmake/SilKitTest.cmake @@ -63,15 +63,24 @@ function(add_silkit_test_to_executable SILKIT_TEST_EXECUTABLE_NAME) return() endif() + set(sva TIMEOUT) set(mva SOURCES LIBS CONFIGS TESTSUITE_NAME) cmake_parse_arguments(arg "" - "" + "${sva}" "${mva}" ${ARGN} ) + # Bound every CTest entry so a hung test (e.g. a time-sync regression that never reaches its + # shutdown condition) fails fast instead of waiting for CTest's implicit 1500s default. Each + # entry runs a whole gtest suite and sanitizer builds are much slower, hence the generous + # default. Callers may override per suite with TIMEOUT. + if(NOT DEFINED arg_TIMEOUT) + set(arg_TIMEOUT 600) + endif() + target_sources("${SILKIT_TEST_EXECUTABLE_NAME}" PRIVATE ${arg_SOURCES}) target_link_libraries("${SILKIT_TEST_EXECUTABLE_NAME}" PRIVATE ${arg_LIBS}) @@ -116,5 +125,7 @@ function(add_silkit_test_to_executable SILKIT_TEST_EXECUTABLE_NAME) "--gtest_filter=${testSuite}.*" WORKING_DIRECTORY $ ) + + set_tests_properties("${testSuite}" PROPERTIES TIMEOUT ${arg_TIMEOUT}) endforeach () endfunction() diff --git a/SilKit/source/config/ParticipantConfiguration.cpp b/SilKit/source/config/ParticipantConfiguration.cpp index 811fe4d2e..8dd7f5149 100755 --- a/SilKit/source/config/ParticipantConfiguration.cpp +++ b/SilKit/source/config/ParticipantConfiguration.cpp @@ -170,7 +170,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 80b2568ed..195a1f270 100644 --- a/SilKit/source/config/ParticipantConfiguration.hpp +++ b/SilKit/source/config/ParticipantConfiguration.hpp @@ -309,6 +309,12 @@ struct TimeSynchronization { double animationFactor{0.0}; Aggregation enableMessageAggregation{Aggregation::Off}; + //! Tri-state, absent by default. When true, this participant advertises to all peers that + //! dynamic simulation step sizes should be used (aligning each step to the minimal step among + //! all synchronized participants) and enables it locally. When absent, the participant follows + //! the network: it enables dynamic stepping if any peer advertises it. When false, it is a hard + //! opt-out that never enables dynamic stepping regardless of peers. + std::optional dynamicSimulationStep; }; // ================================================================================ diff --git a/SilKit/source/config/ParticipantConfiguration.schema.json b/SilKit/source/config/ParticipantConfiguration.schema.json index 8b0fcd4c1..aebd8212d 100644 --- a/SilKit/source/config/ParticipantConfiguration.schema.json +++ b/SilKit/source/config/ParticipantConfiguration.schema.json @@ -850,6 +850,10 @@ "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": "Controls dynamic simulation step sizes (aligning each simulation step to the minimal step among all synchronized participants). When true, the participant enables it locally and advertises to all peers that it should be used. When absent (the default), the participant follows the network and enables it if any peer requests it. When false, it is a hard opt-out that never enables it regardless of peers." } }, "additionalProperties": false diff --git a/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp b/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp index 47f9e9f5d..1e37345bb 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,14 @@ void Cache(const TimeSynchronization& root, TimeSynchronizationCache& cache) cache.animationFactor); CacheNonDefault(defaultObject.enableMessageAggregation, root.enableMessageAggregation, "TimeSynchronization.EnableMessageAggregation", cache.enableMessageAggregation); + if (root.dynamicSimulationStep.has_value()) + { + // Tri-state: cache both explicit true and explicit false (passing the negation as the + // "default" so CacheNonDefault always stores the value while still detecting conflicting + // includes). Absent stays uncached so it can distinguish "follow network" from explicit false. + CacheNonDefault(!root.dynamicSimulationStep.value(), root.dynamicSimulationStep.value(), + "TimeSynchronization.DynamicSimulationStep", cache.dynamicSimulationStep); + } } void Cache(const Metrics& root, MetricsCache& cache) @@ -518,6 +527,10 @@ void MergeTimeSynchronizationCache(const TimeSynchronizationCache& cache, TimeSy { MergeCacheField(cache.animationFactor, timeSynchronization.animationFactor); MergeCacheField(cache.enableMessageAggregation, timeSynchronization.enableMessageAggregation); + if (cache.dynamicSimulationStep.has_value()) + { + timeSynchronization.dynamicSimulationStep = cache.dynamicSimulationStep.value(); + } } 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..801741333 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": true }, "Metrics": { "CollectFromRemote": false, diff --git a/SilKit/source/config/ParticipantConfiguration_Full.yaml b/SilKit/source/config/ParticipantConfiguration_Full.yaml index 4c08d28a3..4effd1acf 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: true Metrics: CollectFromRemote: false Sinks: diff --git a/SilKit/source/config/YamlReader.cpp b/SilKit/source/config/YamlReader.cpp index d85639a11..df7e6f7f0 100644 --- a/SilKit/source/config/YamlReader.cpp +++ b/SilKit/source/config/YamlReader.cpp @@ -483,6 +483,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 86417d62e..846fd2a44 100644 --- a/SilKit/source/config/YamlValidator.cpp +++ b/SilKit/source/config/YamlValidator.cpp @@ -145,6 +145,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 e7db490bc..3292eaecb 100644 --- a/SilKit/source/config/YamlWriter.cpp +++ b/SilKit/source/config/YamlWriter.cpp @@ -567,6 +567,10 @@ void YamlWriter::Write(const SilKit::Config::TimeSynchronization& obj) MakeMap(); NonDefaultWrite(obj.animationFactor, "AnimationFactor", defaultObj.animationFactor); NonDefaultWrite(obj.enableMessageAggregation, "EnableMessageAggregation", defaultObj.enableMessageAggregation); + if (obj.dynamicSimulationStep.has_value()) + { + WriteKeyValue("DynamicSimulationStep", obj.dynamicSimulationStep.value()); + } } diff --git a/SilKit/source/core/internal/IParticipantInternal.hpp b/SilKit/source/core/internal/IParticipantInternal.hpp index 4a085c49d..27e2d759d 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 "config/ParticipantConfiguration.hpp" #include "core/internal/internal_fwd.hpp" #include "core/internal/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/internal/ServiceConfigKeys.hpp b/SilKit/source/core/internal/ServiceConfigKeys.hpp index dc0667135..1a93f22f0 100644 --- a/SilKit/source/core/internal/ServiceConfigKeys.hpp +++ b/SilKit/source/core/internal/ServiceConfigKeys.hpp @@ -73,6 +73,9 @@ const std::string controllerTypeOther = "Other"; // Lifecycle & TimeSync const std::string lifecycleIsCoordinated = "LifecycleIsCoordinated"; const std::string timeSyncActive = "TimeSyncActive"; +// Set to "1" by a participant that requests dynamic simulation step sizes for the whole simulation +// (e.g. a network simulator). Peers that are not a hard opt-out enable dynamic stepping when they see it. +const std::string timeSyncDynamicStepSize = "TimeSyncDynamicStepSize"; } // namespace Discovery } // namespace Core diff --git a/SilKit/source/core/mock/participant/MockParticipant.hpp b/SilKit/source/core/mock/participant/MockParticipant.hpp index d43bca5e6..96bfdeb8b 100644 --- a/SilKit/source/core/mock/participant/MockParticipant.hpp +++ b/SilKit/source/core/mock/participant/MockParticipant.hpp @@ -628,6 +628,10 @@ class DummyParticipant : public IParticipantInternal { return _registryUri; } + auto GetParticipantConfiguration() const -> const SilKit::Config::ParticipantConfiguration& override + { + return _participantConfiguration; + } virtual auto GetTimeProvider() -> Services::Orchestration::ITimeProvider* { @@ -732,6 +736,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 7cdd5181d..232295296 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 22f2321d3..02c3f3f99 100644 --- a/SilKit/source/services/orchestration/LifecycleService.cpp +++ b/SilKit/source/services/orchestration/LifecycleService.cpp @@ -424,16 +424,23 @@ 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 configured per participant via the tri-state + // Experimental.TimeSynchronization.DynamicSimulationStep (true = request for all + enable locally, + // absent = follow the network, false = hard opt-out). The final decision also depends on peers' + // advertisements, so the tri-state is passed through and evaluated inside the TimeSyncService. + const auto& participantConfiguration = _participant->GetParticipantConfiguration(); + _timeSyncService->ConfigureDynamicStepSize( + 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 dc915aac3..e214191fa 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/Test_TimeSyncService.cpp b/SilKit/source/services/orchestration/Test_TimeSyncService.cpp index 1fa4f92f8..e4832b406 100644 --- a/SilKit/source/services/orchestration/Test_TimeSyncService.cpp +++ b/SilKit/source/services/orchestration/Test_TimeSyncService.cpp @@ -265,4 +265,70 @@ TEST_F(Test_TimeSyncService, hop_on_outside_simulation_step) ASSERT_EQ(sentNextSimTasks[3].timePoint, 2ms); } +// A remote TimeSyncService descriptor advertising (or not) that it requests dynamic simulation step +// sizes. timeSyncActive is intentionally left unset because the dynamic-step reaction is independent +// of the time-sync membership handling. +auto MakeDynamicStepDescriptor(const std::string& participantName, bool requestsDynamicStep) + -> SilKit::Core::ServiceDescriptor +{ + SilKit::Core::ServiceDescriptor sd; + sd.SetServiceType(Core::ServiceType::InternalController); + sd.SetParticipantNameAndComputeId(participantName); + sd.SetNetworkName("default"); + sd.SetNetworkType(Config::NetworkType::Undefined); + sd.SetServiceName("TimeSyncService"); + sd.SetServiceId(1234); + sd.SetSupplementalDataItem(SilKit::Core::Discovery::controllerType, "TimeSyncService"); + sd.SetSupplementalDataItem(SilKit::Core::Discovery::timeSyncDynamicStepSize, requestsDynamicStep ? "1" : "0"); + return sd; +} + +TEST_F(Test_TimeSyncService, dynamic_step_follower_enables_when_peer_requests) +{ + using SilKit::Core::Discovery::ServiceDiscoveryEvent; + ASSERT_EQ(serviceDiscoveryHandlers.size(), 1u); + + // Absent config == follow the network: off until a peer requests it. + timeSyncService->ConfigureDynamicStepSize(std::nullopt); + EXPECT_FALSE(timeSyncService->GetTimeConfiguration()->IsDynamicStepSizeEnabled()); + + // A peer (e.g. a network simulator) advertises the request -> we enable dynamic stepping. + serviceDiscoveryHandlers[0](ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDynamicStepDescriptor("Netsim", true)); + EXPECT_TRUE(timeSyncService->GetTimeConfiguration()->IsDynamicStepSizeEnabled()); + + // The requesting peer leaves -> revert to disabled. + serviceDiscoveryHandlers[0](ServiceDiscoveryEvent::Type::ServiceRemoved, + MakeDynamicStepDescriptor("Netsim", true)); + EXPECT_FALSE(timeSyncService->GetTimeConfiguration()->IsDynamicStepSizeEnabled()); +} + +TEST_F(Test_TimeSyncService, dynamic_step_peer_without_request_does_not_enable) +{ + using SilKit::Core::Discovery::ServiceDiscoveryEvent; + + timeSyncService->ConfigureDynamicStepSize(std::nullopt); + serviceDiscoveryHandlers[0](ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDynamicStepDescriptor("Peer", false)); + EXPECT_FALSE(timeSyncService->GetTimeConfiguration()->IsDynamicStepSizeEnabled()); +} + +TEST_F(Test_TimeSyncService, dynamic_step_hard_opt_out_ignores_peer_request) +{ + using SilKit::Core::Discovery::ServiceDiscoveryEvent; + + // Explicit false is a hard opt-out: a peer's request must not enable it. + timeSyncService->ConfigureDynamicStepSize(false); + serviceDiscoveryHandlers[0](ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDynamicStepDescriptor("Netsim", true)); + EXPECT_FALSE(timeSyncService->GetTimeConfiguration()->IsDynamicStepSizeEnabled()); +} + +TEST_F(Test_TimeSyncService, dynamic_step_driver_enables_without_peers) +{ + // Explicit true enables locally regardless of peers (and also advertises to them). + timeSyncService->ConfigureDynamicStepSize(true); + EXPECT_TRUE(timeSyncService->GetTimeConfiguration()->IsDynamicStepSizeEnabled()); +} + } // namespace diff --git a/SilKit/source/services/orchestration/TimeConfiguration.cpp b/SilKit/source/services/orchestration/TimeConfiguration.cpp index 1ba44ea34..9e8f7960b 100644 --- a/SilKit/source/services/orchestration/TimeConfiguration.cpp +++ b/SilKit/source/services/orchestration/TimeConfiguration.cpp @@ -20,6 +20,18 @@ TimeConfiguration::TimeConfiguration(Logging::ILoggerInternal* logger) _myNextTask.duration = 1ms; } +void TimeConfiguration::SetDynamicStepSizeEnabled(bool enabled) +{ + Lock lock{_mx}; + _dynamicStepSizeEnabled = enabled; +} + +bool TimeConfiguration::IsDynamicStepSizeEnabled() const +{ + Lock lock{_mx}; + return _dynamicStepSizeEnabled; +} + void TimeConfiguration::SetBlockingMode(bool blocking) { _blocking = blocking; @@ -92,30 +104,78 @@ void TimeConfiguration::OnReceiveNextSimStep(const std::string& participantName, .Dispatch(); } -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 c66c2c96f..12b82badb 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,12 @@ class TimeConfiguration bool IsHopOn(); bool HoppedOn(); + void SetStepDuration(std::chrono::nanoseconds duration); + auto GetMinimalAlignedDuration() const -> std::chrono::nanoseconds; + + void SetDynamicStepSizeEnabled(bool enabled); + bool IsDynamicStepSizeEnabled() const; + private: //Members mutable std::mutex _mx; using Lock = std::unique_lock; @@ -51,6 +55,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). Disabled by default; + // can be turned on via 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 1a9d711a4..fa2cd5d30 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; @@ -345,6 +346,25 @@ TimeSyncService::TimeSyncService(Core::IParticipantInternal* participant, ITimeP std::string timeSyncActive; descriptor.GetSupplementalDataItem(Core::Discovery::timeSyncActive, timeSyncActive); + + // Track whether this peer requests dynamic simulation step sizes. Unless this + // participant is a hard opt-out, its dynamic stepping follows any advertising peer. + std::string peerDynamicStep; + descriptor.GetSupplementalDataItem(Core::Discovery::timeSyncDynamicStepSize, peerDynamicStep); + { + std::lock_guard lock{_dynamicStepMx}; + if (discoveryEventType == Core::Discovery::ServiceDiscoveryEvent::Type::ServiceCreated + && peerDynamicStep == "1") + { + _dynamicStepPeers.insert(descriptorParticipantName); + } + else if (discoveryEventType == Core::Discovery::ServiceDiscoveryEvent::Type::ServiceRemoved) + { + _dynamicStepPeers.erase(descriptorParticipantName); + } + } + RecomputeDynamicStepEnabled(); + if (timeSyncActive == "1") { if (discoveryEventType == Core::Discovery::ServiceDiscoveryEvent::Type::ServiceCreated) @@ -463,11 +483,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}; @@ -628,6 +643,16 @@ void TimeSyncService::InitializeTimeSyncPolicy(bool isSynchronizingVirtualTime) _serviceDescriptor.SetSupplementalDataItem(SilKit::Core::Discovery::timeSyncActive, (isSynchronizingVirtualTime) ? "1" : "0"); + + // Advertise to peers whether this participant requests dynamic simulation step sizes for the + // whole simulation (config == true). Peers that are not a hard opt-out enable it when they see this. + bool advertiseDynamicStep; + { + std::lock_guard lock{_dynamicStepMx}; + advertiseDynamicStep = (_dynamicStepConfig == std::optional{true}); + } + _serviceDescriptor.SetSupplementalDataItem(SilKit::Core::Discovery::timeSyncDynamicStepSize, + advertiseDynamicStep ? "1" : "0"); ResetTime(); } catch (const std::exception& e) @@ -844,6 +869,43 @@ bool TimeSyncService::IsBlocking() const return _timeConfiguration.IsBlocking(); } +void TimeSyncService::ConfigureDynamicStepSize(std::optional configured) +{ + { + std::lock_guard lock{_dynamicStepMx}; + _dynamicStepConfig = configured; + } + RecomputeDynamicStepEnabled(); +} + +void TimeSyncService::RecomputeDynamicStepEnabled() +{ + bool enabled; + bool enabledByRemote; + { + std::lock_guard lock{_dynamicStepMx}; + // true -> always on (this participant drives dynamic stepping) + // false -> hard opt-out, never on + // absent-> follow the network: on iff at least one peer advertises the request + const bool isDriver = (_dynamicStepConfig == std::optional{true}); + const bool isOptOut = (_dynamicStepConfig == std::optional{false}); + enabledByRemote = (!isDriver && !isOptOut && !_dynamicStepPeers.empty()); + enabled = isDriver || enabledByRemote; + } + _timeConfiguration.SetDynamicStepSizeEnabled(enabled); + + // Notify (once) that dynamic stepping was turned on because a remote participant requested it, + // rather than by this participant's own configuration. + if (enabledByRemote && !_dynamicStepRemoteLogOnce.WasCalled()) + { + _logger->MakeMessage(Logging::Level::Info, TopicOf(*this)) + .SetMessage("Dynamic simulation step sizes were enabled because a remote participant requested them. " + "Set Experimental.TimeSynchronization.DynamicSimulationStep to false to opt out.") + .Dispatch(); + } +} + + } // namespace Orchestration } // namespace Services } // namespace SilKit diff --git a/SilKit/source/services/orchestration/TimeSyncService.hpp b/SilKit/source/services/orchestration/TimeSyncService.hpp index edfcf69d9..5ee43a13b 100644 --- a/SilKit/source/services/orchestration/TimeSyncService.hpp +++ b/SilKit/source/services/orchestration/TimeSyncService.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "silkit/services/orchestration/ITimeSyncService.hpp" @@ -20,6 +22,7 @@ #include "services/orchestration/TimeConfiguration.hpp" #include "services/orchestration/WatchDog.hpp" #include "services/metrics/Metrics.hpp" +#include "services/logging/LogFunctions.hpp" namespace SilKit { namespace Services { @@ -56,7 +59,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,10 +102,19 @@ class TimeSyncService void RemoveOtherSimulationStepsCompletedHandler(HandlerId handlerId); void InvokeOtherSimulationStepsCompletedHandlers(); + //! Configure the tri-state dynamic-step-size preference from participant configuration: + //! true = enable locally and advertise the request to peers; false = hard opt-out; + //! std::nullopt (default) = follow the network (enable if any peer advertises the request). + void ConfigureDynamicStepSize(std::optional configured); + private: // ---------------------------------------- // private methods + //! Recomputes whether dynamic step sizes are active from the local preference and the set of + //! peers currently advertising the request, and pushes the result into the TimeConfiguration. + void RecomputeDynamicStepEnabled(); + //! Creates the _timeSyncPolicy. Returns true if the call assigned the _timeSyncPolicy, and false if it was already //! assigned before. bool SetupTimeSyncPolicy(bool isSynchronizingVirtualTime); @@ -154,6 +166,16 @@ class TimeSyncService std::atomic _wallClockReachedBeforeCompletion{false}; Util::SynchronizedHandlers> _otherSimulationStepsCompletedHandlers; + + // Dynamic simulation step sizes: local tri-state preference from config and the set of peers + // currently advertising the request. Guarded by _dynamicStepMx because the service-discovery + // handler (network thread) and ConfigureDynamicStepSize (setup thread) both touch them. + mutable std::mutex _dynamicStepMx; + std::optional _dynamicStepConfig; + std::set _dynamicStepPeers; + // Ensures the "enabled by a remote participant" notice is logged only once. + Services::Logging::LogOnceFlag _dynamicStepRemoteLogOnce; + }; // ================================================================================