From 97f5bec4f063470859abb6411d83d69aa8b0be33 Mon Sep 17 00:00:00 2001 From: Konrad Breitsprecher Date: Fri, 19 Dec 2025 15:47:47 +0100 Subject: [PATCH 1/5] Signed-off-by: Konrad Breitsprecher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change TimeAdvanceMode::ByMinimalDuration to trigger SimTasks with timestamps of all other sync. participants; Add Integration Tests; Remove automatic creation of TimeSynService in SimTestHarness Signed-off-by: Konrad Breitsprecher rework: reorganize and activate debug statements config: make default TimeAdvanceMode configurable (hacky boolean) remove API for dynsteps, enable dyn steps by default, allo opt out by config Signed-off-by: Marius Börschig --- SilKit/IntegrationTests/CMakeLists.txt | 5 + .../IntegrationTests/ITest_AsyncSimTask.cpp | 5 +- .../IntegrationTests/ITest_DynStepSizes.cpp | 400 ++++++++++++++++++ .../SimTestHarness/SimTestHarness.cpp | 5 - .../config/ParticipantConfiguration.cpp | 3 +- .../config/ParticipantConfiguration.hpp | 3 + .../ParticipantConfiguration.schema.json | 5 + .../ParticipantConfigurationFromXImpl.cpp | 4 + .../config/ParticipantConfiguration_Full.json | 3 +- .../config/ParticipantConfiguration_Full.yaml | 1 + SilKit/source/config/YamlReader.cpp | 1 + SilKit/source/config/YamlValidator.cpp | 1 + SilKit/source/config/YamlWriter.cpp | 1 + .../core/internal/IParticipantInternal.hpp | 3 + .../core/mock/participant/MockParticipant.hpp | 5 + .../source/core/participant/Participant.hpp | 5 + .../orchestration/LifecycleService.cpp | 19 +- .../orchestration/Test_LifecycleService.cpp | 4 +- .../orchestration/TimeConfiguration.cpp | 78 +++- .../orchestration/TimeConfiguration.hpp | 12 +- .../orchestration/TimeSyncService.cpp | 12 +- .../orchestration/TimeSyncService.hpp | 5 +- 22 files changed, 544 insertions(+), 36 deletions(-) create mode 100644 SilKit/IntegrationTests/ITest_DynStepSizes.cpp 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 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..8ffc2b3d4 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 8b0fcd4c1..c4be6cd5f 100644 --- a/SilKit/source/config/ParticipantConfiguration.schema.json +++ b/SilKit/source/config/ParticipantConfiguration.schema.json @@ -850,6 +850,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 47f9e9f5d..6650081d4 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 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..c47451098 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 4a085c49d..aed18c1e1 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 "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/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..3916d999b 100644 --- a/SilKit/source/services/orchestration/LifecycleService.cpp +++ b/SilKit/source/services/orchestration/LifecycleService.cpp @@ -424,16 +424,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 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/TimeConfiguration.cpp b/SilKit/source/services/orchestration/TimeConfiguration.cpp index 1ba44ea34..0d3213baa 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; @@ -92,30 +98,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..837ea2644 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 1a9d711a4..ad00fadc9 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; @@ -463,11 +464,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}; @@ -844,6 +840,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 edfcf69d9..14f7cb6de 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; + }; // ================================================================================ From 7bb0e8504da72311f99d3a234498424a35f1ab67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Mon, 6 Jul 2026 09:14:31 +0200 Subject: [PATCH 2/5] fixup! Signed-off-by: Konrad Breitsprecher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marius Börschig --- SilKit/source/core/internal/IParticipantInternal.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SilKit/source/core/internal/IParticipantInternal.hpp b/SilKit/source/core/internal/IParticipantInternal.hpp index aed18c1e1..27e2d759d 100644 --- a/SilKit/source/core/internal/IParticipantInternal.hpp +++ b/SilKit/source/core/internal/IParticipantInternal.hpp @@ -10,7 +10,7 @@ #include "silkit/experimental/services/orchestration/ISystemController.hpp" #include "silkit/experimental/netsim/NetworkSimulatorDatatypes.hpp" -#include "ParticipantConfiguration.hpp" +#include "config/ParticipantConfiguration.hpp" #include "core/internal/internal_fwd.hpp" #include "core/internal/IServiceEndpoint.hpp" From 22cf536ad101bac6ab5ec39723a309bda569c61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Mon, 6 Jul 2026 11:17:39 +0200 Subject: [PATCH 3/5] fixup! fixup! Signed-off-by: Konrad Breitsprecher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit disable by default, ensure ITest_DifferentPeriods does not hang indefinitely Signed-off-by: Marius Börschig --- .../ITest_DifferentPeriods.cpp | 60 ++++++++++++++++--- SilKit/cmake/SilKitTest.cmake | 13 +++- .../config/ParticipantConfiguration.hpp | 6 +- .../ParticipantConfiguration.schema.json | 4 +- .../config/ParticipantConfiguration_Full.json | 2 +- .../config/ParticipantConfiguration_Full.yaml | 2 +- .../orchestration/LifecycleService.cpp | 2 +- .../orchestration/TimeConfiguration.hpp | 4 +- 8 files changed, 75 insertions(+), 18 deletions(-) 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/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.hpp b/SilKit/source/config/ParticipantConfiguration.hpp index 8ffc2b3d4..6f977dff6 100644 --- a/SilKit/source/config/ParticipantConfiguration.hpp +++ b/SilKit/source/config/ParticipantConfiguration.hpp @@ -309,9 +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}; + //! Disabled by default. When set to true, a participant aligns its simulation step duration to + //! the minimal step among all synchronized participants. + bool dynamicSimulationStep{false}; }; // ================================================================================ diff --git a/SilKit/source/config/ParticipantConfiguration.schema.json b/SilKit/source/config/ParticipantConfiguration.schema.json index c4be6cd5f..a7e4779a6 100644 --- a/SilKit/source/config/ParticipantConfiguration.schema.json +++ b/SilKit/source/config/ParticipantConfiguration.schema.json @@ -853,8 +853,8 @@ }, "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 + "description": "Disabled by default. When set to true, a participant aligns its simulation step duration to the minimal step among all synchronized participants, allowing dynamic step sizes.", + "default": false } }, "additionalProperties": false diff --git a/SilKit/source/config/ParticipantConfiguration_Full.json b/SilKit/source/config/ParticipantConfiguration_Full.json index 6fe22e703..801741333 100644 --- a/SilKit/source/config/ParticipantConfiguration_Full.json +++ b/SilKit/source/config/ParticipantConfiguration_Full.json @@ -265,7 +265,7 @@ "TimeSynchronization": { "AnimationFactor": 1.5, "EnableMessageAggregation": "Off", - "DynamicSimulationStep": false + "DynamicSimulationStep": true }, "Metrics": { "CollectFromRemote": false, diff --git a/SilKit/source/config/ParticipantConfiguration_Full.yaml b/SilKit/source/config/ParticipantConfiguration_Full.yaml index f88844e17..4effd1acf 100644 --- a/SilKit/source/config/ParticipantConfiguration_Full.yaml +++ b/SilKit/source/config/ParticipantConfiguration_Full.yaml @@ -191,7 +191,7 @@ Experimental: TimeSynchronization: AnimationFactor: 1.5 EnableMessageAggregation: Off - DynamicSimulationStep: false + DynamicSimulationStep: true Metrics: CollectFromRemote: false Sinks: diff --git a/SilKit/source/services/orchestration/LifecycleService.cpp b/SilKit/source/services/orchestration/LifecycleService.cpp index 3916d999b..82686cbdf 100644 --- a/SilKit/source/services/orchestration/LifecycleService.cpp +++ b/SilKit/source/services/orchestration/LifecycleService.cpp @@ -432,7 +432,7 @@ auto LifecycleService::CreateTimeSyncService() -> ITimeSyncService* _participant->RegisterTimeSyncService(_timeSyncService); _timeSyncActive = true; - // Dynamic simulation step sizes are enabled by default and can be disabled per participant via + // Dynamic simulation step sizes are disabled by default and can be enabled per participant via // Experimental.TimeSynchronization.DynamicSimulationStep. const auto& participantConfiguration = _participant->GetParticipantConfiguration(); _timeSyncService->SetDynamicStepSizeEnabled( diff --git a/SilKit/source/services/orchestration/TimeConfiguration.hpp b/SilKit/source/services/orchestration/TimeConfiguration.hpp index 837ea2644..812a5f745 100755 --- a/SilKit/source/services/orchestration/TimeConfiguration.hpp +++ b/SilKit/source/services/orchestration/TimeConfiguration.hpp @@ -56,8 +56,8 @@ class TimeConfiguration 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. + // synchronized participants (see GetMinimalAlignedDuration / AdvanceTimeStep). Disabled by default; + // can be turned on via Experimental.TimeSynchronization.DynamicSimulationStep. bool _dynamicStepSizeEnabled{false}; }; From fc49a3bc07347de3454bb98f27f80a3bc43e18b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Mon, 6 Jul 2026 12:19:47 +0200 Subject: [PATCH 4/5] fixup! fixup! fixup! Signed-off-by: Konrad Breitsprecher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add capability check for remote participants using dynstep, allow opt-out on client side Signed-off-by: Marius Börschig --- .../IntegrationTests/ITest_DynStepSizes.cpp | 38 +++++++++-- .../config/ParticipantConfiguration.hpp | 9 ++- .../ParticipantConfiguration.schema.json | 3 +- .../ParticipantConfigurationFromXImpl.cpp | 15 ++++- SilKit/source/config/YamlWriter.cpp | 5 +- .../core/internal/ServiceConfigKeys.hpp | 3 + .../orchestration/LifecycleService.cpp | 8 ++- .../orchestration/Test_TimeSyncService.cpp | 66 +++++++++++++++++++ .../orchestration/TimeConfiguration.cpp | 6 ++ .../orchestration/TimeConfiguration.hpp | 1 + .../orchestration/TimeSyncService.cpp | 62 ++++++++++++++++- .../orchestration/TimeSyncService.hpp | 21 +++++- 12 files changed, 217 insertions(+), 20 deletions(-) diff --git a/SilKit/IntegrationTests/ITest_DynStepSizes.cpp b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp index dc2d231d0..0e292b805 100644 --- a/SilKit/IntegrationTests/ITest_DynStepSizes.cpp +++ b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "ITestFixture.hpp" using namespace std::chrono_literals; @@ -93,10 +94,11 @@ 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}; + // 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 @@ -137,8 +139,12 @@ void ITest_DynStepSizes::RunTestSetup(std::vector& participan 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"("}]},"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); @@ -336,6 +342,26 @@ TEST_F(ITest_DynStepSizes, three_participants_MixedTimeAdvanceModes) 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) { diff --git a/SilKit/source/config/ParticipantConfiguration.hpp b/SilKit/source/config/ParticipantConfiguration.hpp index 6f977dff6..195a1f270 100644 --- a/SilKit/source/config/ParticipantConfiguration.hpp +++ b/SilKit/source/config/ParticipantConfiguration.hpp @@ -309,9 +309,12 @@ struct TimeSynchronization { double animationFactor{0.0}; Aggregation enableMessageAggregation{Aggregation::Off}; - //! Disabled by default. When set to true, a participant aligns its simulation step duration to - //! the minimal step among all synchronized participants. - bool dynamicSimulationStep{false}; + //! 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 a7e4779a6..aebd8212d 100644 --- a/SilKit/source/config/ParticipantConfiguration.schema.json +++ b/SilKit/source/config/ParticipantConfiguration.schema.json @@ -853,8 +853,7 @@ }, "DynamicSimulationStep": { "type": "boolean", - "description": "Disabled by default. When set to true, a participant aligns its simulation step duration to the minimal step among all synchronized participants, allowing dynamic step sizes.", - "default": false + "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 6650081d4..1e37345bb 100644 --- a/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp +++ b/SilKit/source/config/ParticipantConfigurationFromXImpl.cpp @@ -347,8 +347,14 @@ 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); + 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) @@ -521,7 +527,10 @@ void MergeTimeSynchronizationCache(const TimeSynchronizationCache& cache, TimeSy { MergeCacheField(cache.animationFactor, timeSynchronization.animationFactor); MergeCacheField(cache.enableMessageAggregation, timeSynchronization.enableMessageAggregation); - MergeCacheField(cache.dynamicSimulationStep, timeSynchronization.dynamicSimulationStep); + if (cache.dynamicSimulationStep.has_value()) + { + timeSynchronization.dynamicSimulationStep = cache.dynamicSimulationStep.value(); + } } void MergeMetricsCache(const MetricsCache& cache, Metrics& metrics) diff --git a/SilKit/source/config/YamlWriter.cpp b/SilKit/source/config/YamlWriter.cpp index c47451098..3292eaecb 100644 --- a/SilKit/source/config/YamlWriter.cpp +++ b/SilKit/source/config/YamlWriter.cpp @@ -567,7 +567,10 @@ 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); + if (obj.dynamicSimulationStep.has_value()) + { + WriteKeyValue("DynamicSimulationStep", obj.dynamicSimulationStep.value()); + } } 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/services/orchestration/LifecycleService.cpp b/SilKit/source/services/orchestration/LifecycleService.cpp index 82686cbdf..02c3f3f99 100644 --- a/SilKit/source/services/orchestration/LifecycleService.cpp +++ b/SilKit/source/services/orchestration/LifecycleService.cpp @@ -432,10 +432,12 @@ auto LifecycleService::CreateTimeSyncService() -> ITimeSyncService* _participant->RegisterTimeSyncService(_timeSyncService); _timeSyncActive = true; - // Dynamic simulation step sizes are disabled by default and can be enabled per participant via - // Experimental.TimeSynchronization.DynamicSimulationStep. + // 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->SetDynamicStepSizeEnabled( + _timeSyncService->ConfigureDynamicStepSize( participantConfiguration.experimental.timeSynchronization.dynamicSimulationStep); return _timeSyncService; 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 0d3213baa..9e8f7960b 100644 --- a/SilKit/source/services/orchestration/TimeConfiguration.cpp +++ b/SilKit/source/services/orchestration/TimeConfiguration.cpp @@ -26,6 +26,12 @@ void TimeConfiguration::SetDynamicStepSizeEnabled(bool enabled) _dynamicStepSizeEnabled = enabled; } +bool TimeConfiguration::IsDynamicStepSizeEnabled() const +{ + Lock lock{_mx}; + return _dynamicStepSizeEnabled; +} + void TimeConfiguration::SetBlockingMode(bool blocking) { _blocking = blocking; diff --git a/SilKit/source/services/orchestration/TimeConfiguration.hpp b/SilKit/source/services/orchestration/TimeConfiguration.hpp index 812a5f745..12b82badb 100755 --- a/SilKit/source/services/orchestration/TimeConfiguration.hpp +++ b/SilKit/source/services/orchestration/TimeConfiguration.hpp @@ -43,6 +43,7 @@ class TimeConfiguration auto GetMinimalAlignedDuration() const -> std::chrono::nanoseconds; void SetDynamicStepSizeEnabled(bool enabled); + bool IsDynamicStepSizeEnabled() const; private: //Members mutable std::mutex _mx; diff --git a/SilKit/source/services/orchestration/TimeSyncService.cpp b/SilKit/source/services/orchestration/TimeSyncService.cpp index ad00fadc9..fa2cd5d30 100644 --- a/SilKit/source/services/orchestration/TimeSyncService.cpp +++ b/SilKit/source/services/orchestration/TimeSyncService.cpp @@ -346,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) @@ -624,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) @@ -840,9 +869,40 @@ bool TimeSyncService::IsBlocking() const return _timeConfiguration.IsBlocking(); } -void TimeSyncService::SetDynamicStepSizeEnabled(bool enabled) +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(); + } } diff --git a/SilKit/source/services/orchestration/TimeSyncService.hpp b/SilKit/source/services/orchestration/TimeSyncService.hpp index 14f7cb6de..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 { @@ -99,12 +102,19 @@ class TimeSyncService void RemoveOtherSimulationStepsCompletedHandler(HandlerId handlerId); void InvokeOtherSimulationStepsCompletedHandlers(); - void SetDynamicStepSizeEnabled(bool enabled); + //! 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); @@ -157,6 +167,15 @@ class TimeSyncService 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; + }; // ================================================================================ From be928f319a2ff1395fc50b812581c35bafc64cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C3=B6rschig?= Date: Mon, 6 Jul 2026 14:48:34 +0200 Subject: [PATCH 5/5] fixup! fixup! fixup! fixup! Signed-off-by: Konrad Breitsprecher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix memory leaks in test Signed-off-by: Marius Börschig --- .../IntegrationTests/ITest_DynStepSizes.cpp | 114 ++++-------------- 1 file changed, 24 insertions(+), 90 deletions(-) diff --git a/SilKit/IntegrationTests/ITest_DynStepSizes.cpp b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp index 0e292b805..e9a5026e5 100644 --- a/SilKit/IntegrationTests/ITest_DynStepSizes.cpp +++ b/SilKit/IntegrationTests/ITest_DynStepSizes.cpp @@ -2,12 +2,10 @@ // // SPDX-License-Identifier: MIT -#include #include #include #include #include -#include #include #include #include @@ -15,48 +13,16 @@ using namespace std::chrono_literals; -namespace testing { -namespace internal { -template -class UniversalPrinter> +// 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) { -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(); + return out << timestamp.count() << "ns"; } +} // namespace chrono +} // namespace std namespace { @@ -65,31 +31,6 @@ 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{}; @@ -131,8 +72,10 @@ void ITest_DynStepSizes::RunTestSetup(std::vector& participan std::mutex mx; using StepHandler = std::function; - // Keep the self-referential handlers alive for the whole run. - std::vector> stepHandlers; + // 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) { @@ -154,8 +97,8 @@ void ITest_DynStepSizes::RunTestSetup(std::vector& participan // 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) { + 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"); @@ -182,21 +125,14 @@ void ITest_DynStepSizes::RunTestSetup(std::vector& participan // Must be the last statement: it replaces the currently executing handler. if (changeStepSize) { - timeSyncService->SetSimulationStepHandler(*handler, newStepSize); + timeSyncService->SetSimulationStepHandler(handler, newStepSize); } }; - stepHandlers.push_back(handler); - timeSyncService->SetSimulationStepHandler(*handler, participantParams.initialStepSize); + 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) @@ -212,10 +148,9 @@ void ITest_DynStepSizes::AssertAllStepsEqual(const std::vector 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) << ")"; + << "Timestep difference for " << participant.name << " at index " << i << " is " << diff + << ", expected " << refDuration << " (" << steps[i - 1].first << " -> " << steps[i].first << ")"; } } } @@ -255,8 +189,8 @@ void ITest_DynStepSizes::AssertStepsEqual(const std::vector