diff --git a/src/core/config/netdata_publisher.h b/src/core/config/netdata_publisher.h index c3a088e4b..e9719c7f9 100644 --- a/src/core/config/netdata_publisher.h +++ b/src/core/config/netdata_publisher.h @@ -61,6 +61,18 @@ (OPENTHREAD_CONFIG_SRP_SERVER_ENABLE || OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE) #endif +/** + * @def OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + * + * Define to 1 to enable coalescing scheduled entry updates in Network Data Publisher. + * + * When enabled, Publisher attempts to align the random delay times of multiple pending entry additions or removals + * so they can be performed together, reducing the number of Server Data Notification messages sent to the Leader. + */ +#ifndef OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE +#define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE 1 +#endif + /** * @def OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_DELAY_TO_ADD * diff --git a/src/core/thread/network_data_publisher.cpp b/src/core/thread/network_data_publisher.cpp index 35b755442..24b78c959 100644 --- a/src/core/thread/network_data_publisher.cpp +++ b/src/core/thread/network_data_publisher.cpp @@ -253,6 +253,26 @@ void Publisher::HandleTimer(void) #endif } +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + +void Publisher::CoalesceUpdateTime(UpdateTimeCoalescer &aCoalescer) const +{ +#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE + aCoalescer.Evaluate(mDnsSrpServiceEntry); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + aCoalescer.Evaluate(mBorderAdmitterEntry); +#endif +#endif + +#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE + for (const PrefixEntry &entry : mPrefixEntries) + { + aCoalescer.Evaluate(entry); + } +#endif +} +#endif // OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + //--------------------------------------------------------------------------------------------------------------------- // Publisher::Entry @@ -305,7 +325,7 @@ void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEnt if (aNumEntries < aDesiredNumEntries) { - mUpdateTime = now + Random::NonCrypto::GenerateInClosedRange(1, kMaxDelayToAdd); + SelectUpdateTime(now, 1, kMaxDelayToAdd); SetState(kAdding); Get().GetTimer().FireAtIfEarlier(mUpdateTime); LogUpdateTime(now); @@ -341,16 +361,15 @@ void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEnt if (aNumEntries > aDesiredNumEntries) { - mUpdateTime = now; - - if (aDesiredNumEntries > 0) + if (aDesiredNumEntries == 0) { - mUpdateTime += Random::NonCrypto::GenerateInClosedRange(1, kMaxDelayToRemove); + mUpdateTime = now; + } + else + { + uint32_t extraDelay = (aNumPreferredEntries < aDesiredNumEntries) ? kExtraDelayToRemovePreferred : 0; - if (aNumPreferredEntries < aDesiredNumEntries) - { - mUpdateTime += kExtraDelayToRemovePreferred; - } + SelectUpdateTime(now, 1 + extraDelay, kMaxDelayToRemove + extraDelay); } SetState(kRemoving); @@ -380,6 +399,20 @@ void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEnt } } +void Publisher::Entry::SelectUpdateTime(TimeMilli aNow, uint32_t aMinDelay, uint32_t aMaxDelay) +{ + mUpdateTime = aNow + Random::NonCrypto::GenerateInClosedRange(aMinDelay, aMaxDelay); + +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + { + UpdateTimeCoalescer coalescer(aNow + aMinDelay, aNow + aMaxDelay, mUpdateTime); + + Get().CoalesceUpdateTime(coalescer); + mUpdateTime = coalescer.GetCoalescedTime(); + } +#endif +} + void Publisher::Entry::HandleTimer(void) { // Timer is used to delay adding/removing the entry. If we have @@ -1198,6 +1231,66 @@ exit: #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + +//--------------------------------------------------------------------------------------------------------------------- +// Publisher::UpdateTimeCoalescer + +Publisher::UpdateTimeCoalescer::UpdateTimeCoalescer(TimeMilli aMinTime, TimeMilli aMaxTime, TimeMilli aUpdateTime) + : mMinTime(aMinTime) + , mMaxTime(aMaxTime) + , mUpdateTime(aUpdateTime) + , mCoalescedTime(aUpdateTime) + , mMinDiff(kMaxThresholdToCoalesce) +{ +} + +void Publisher::UpdateTimeCoalescer::Evaluate(const Entry &aEntry) +{ + // Checks whether `aEntry` is already scheduled to be updated + // within `[mMinTime, mMaxTime]` and is within + // `kMaxThresholdToCoalesce` of the randomly selected `mUpdateTime`. + // If so, `mCoalescedTime` is updated to align with the closest + // such entry so that multiple updates can be sent together to the + // Leader. + // + // For `kAdding`, the `[mMinTime, mMaxTime]` range width is + // smaller than `kMaxThresholdToCoalesce` (`kMaxDelayToAdd`), so + // any scheduled entry within that range is always matched. For + // `kRemoving`, restricting to `[mMinTime, mMaxTime]` ensures that + // preferred and non-preferred removal time windows never cross + // into each other. + + uint32_t diff; + + VerifyOrExit((aEntry.GetState() == Entry::kAdding) || (aEntry.GetState() == Entry::kRemoving)); + + VerifyOrExit(IsValueInRange(aEntry.GetUpdateTime(), mMinTime, mMaxTime)); + + if (aEntry.GetUpdateTime() > mUpdateTime) + { + diff = aEntry.GetUpdateTime() - mUpdateTime; + VerifyOrExit(diff < mMinDiff); + } + else + { + // Use `<=` here (versus `<` above) so that if two entries are + // equidistant from `mUpdateTime`, we prefer the earlier update + // time over delaying further. + + diff = mUpdateTime - aEntry.GetUpdateTime(); + VerifyOrExit(diff <= mMinDiff); + } + + mMinDiff = diff; + mCoalescedTime = aEntry.GetUpdateTime(); + +exit: + return; +} + +#endif // OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + } // namespace NetworkData } // namespace ot diff --git a/src/core/thread/network_data_publisher.hpp b/src/core/thread/network_data_publisher.hpp index d6eed6c49..9f09601d2 100644 --- a/src/core/thread/network_data_publisher.hpp +++ b/src/core/thread/network_data_publisher.hpp @@ -337,8 +337,12 @@ public: #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE private: + class UpdateTimeCoalescer; + class Entry : public InstanceLocatorInit { + friend class UpdateTimeCoalescer; + protected: enum State : uint8_t { @@ -379,6 +383,7 @@ private: private: void Add(void); void Remove(State aNextState); + void SelectUpdateTime(TimeMilli aNow, uint32_t aMinDelay, uint32_t aMaxDelay); void LogUpdateTime(TimeMilli aNow) const; static const char *StateToString(State aState); @@ -541,6 +546,25 @@ private: }; #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + class UpdateTimeCoalescer + { + public: + UpdateTimeCoalescer(TimeMilli aMinTime, TimeMilli aMaxTime, TimeMilli aUpdateTime); + void Evaluate(const Entry &aEntry); + TimeMilli GetCoalescedTime(void) const { return mCoalescedTime; } + + private: + static constexpr uint32_t kMaxThresholdToCoalesce = Entry::kMaxDelayToAdd; + + TimeMilli mMinTime; + TimeMilli mMaxTime; + TimeMilli mUpdateTime; + TimeMilli mCoalescedTime; + uint32_t mMinDiff; + }; +#endif + #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE bool IsADnsSrpServiceEntry(const Entry &aEntry) const { return (&aEntry == &mDnsSrpServiceEntry); } #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE @@ -559,6 +583,9 @@ private: TimerMilli &GetTimer(void) { return mTimer; } void HandleNotifierEvents(Events aEvents); void HandleTimer(void); +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + void CoalesceUpdateTime(UpdateTimeCoalescer &aCoalescer) const; +#endif using PublisherTimer = TimerMilliIn; diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 0de1947e4..fc993add7 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -131,6 +131,7 @@ #define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION "OT-simul-nexus" #define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_OUI 0x020100 #define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE 1 +#define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE 1 #define OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS 256 #define OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL (10 * 60) #define OPENTHREAD_CONFIG_PLATFORM_DNS_ENABLE 1 diff --git a/tests/nexus/test_netdata_publisher.cpp b/tests/nexus/test_netdata_publisher.cpp index 536dcc25e..a3dfd8659 100644 --- a/tests/nexus/test_netdata_publisher.cpp +++ b/tests/nexus/test_netdata_publisher.cpp @@ -915,12 +915,96 @@ void Test_NetDataPublisher(void) Log("All steps completed."); } +void Test_NetDataPublisherCoalesceUpdates(void) +{ + static constexpr uint8_t kNumPrefixes = 3; + + static const char *const kPrefixStrings[kNumPrefixes] = { + "fd00:1111::/64", + "fd00:2222::/64", + "fd00:3333::/64", + }; + + Core nexus; + Node &leader = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + Ip6::Prefix prefixes[kNumPrefixes]; + NetworkData::OnMeshPrefixConfig prefixConfig; + uint8_t initVersion; + + Log("======================================================================================="); + Log("Test_NetDataPublisherCoalesceUpdates"); + + leader.SetName("LEADER"); + router.SetName("ROUTER1"); + + for (uint8_t i = 0; i < kNumPrefixes; i++) + { + SuccessOrQuit(prefixes[i].FromString(kPrefixStrings[i])); + } + + nexus.AdvanceTime(0); + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + router.Join(leader); + + // Wait for all nodes and network data to stabilize. + nexus.AdvanceTime(kNetDataUpdateTime); + + Log("Simultaneous added entries on `router` coalesce into a single update"); + + initVersion = leader.Get().GetVersion(NetworkData::kFullSet); + + router.Get().PublishDnsSrpServiceUnicast(kDnsSrpPort, 0); + + prefixConfig.Clear(); + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + prefixConfig.mSlaac = true; + prefixConfig.mOnMesh = true; + prefixConfig.mStable = true; + prefixConfig.mPreferred = true; + + for (const Ip6::Prefix &prefix : prefixes) + { + prefixConfig.GetPrefix() = prefix; + SuccessOrQuit( + router.Get().PublishOnMeshPrefix(prefixConfig, NetworkData::Publisher::kFromUser)); + } + + for (uint32_t t = 0; t < 4000; t++) + { + nexus.AdvanceTime(1); + + if (router.Get().IsDnsSrpServiceAdded()) + { + break; + } + } + + // All entries must be added at the exact same millisecond. + VerifyOrQuit(router.Get().IsDnsSrpServiceAdded()); + + for (const Ip6::Prefix &prefix : prefixes) + { + VerifyOrQuit(router.Get().IsPrefixAdded(prefix)); + } + + nexus.AdvanceTime(5000); + + // Leader's Network Data version must have incremented by only 1. + VerifyOrQuit(leader.Get().GetVersion(NetworkData::kFullSet) == + static_cast(initVersion + 1)); +} + } // namespace Nexus } // namespace ot int main(void) { ot::Nexus::Test_NetDataPublisher(); + ot::Nexus::Test_NetDataPublisherCoalesceUpdates(); printf("All tests passed\n"); return 0; }