From bbbeee17fb0c04a623f7459590e5809086922332 Mon Sep 17 00:00:00 2001 From: whd <7058128+superwhd@users.noreply.github.com> Date: Tue, 30 Mar 2021 09:54:14 -0700 Subject: [PATCH] [ping-sender] show ping reply statistics (#6341) --- include/openthread/instance.h | 2 +- include/openthread/ping_sender.h | 48 ++++++++++++++++++----- src/cli/cli.cpp | 28 ++++++++++++- src/cli/cli.hpp | 2 + src/core/config/ping_sender.h | 11 ++++++ src/core/net/icmp6.hpp | 8 ++++ src/core/utils/ping_sender.cpp | 65 ++++++++++++++++++++++++++----- src/core/utils/ping_sender.hpp | 26 ++++++++++++- tests/scripts/thread-cert/node.py | 11 ++++-- tools/otci/tests/test_otci.py | 1 + 10 files changed, 176 insertions(+), 26 deletions(-) diff --git a/include/openthread/instance.h b/include/openthread/instance.h index c3e8abbb9..900d018c0 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (88) +#define OPENTHREAD_API_VERSION (89) /** * @addtogroup api-instance diff --git a/include/openthread/ping_sender.h b/include/openthread/ping_sender.h index 61f4b0586..52200987b 100644 --- a/include/openthread/ping_sender.h +++ b/include/openthread/ping_sender.h @@ -62,12 +62,26 @@ extern "C" { typedef struct otPingSenderReply { otIp6Address mSenderAddress; ///< Sender IPv6 address (address from which ping reply was received). - uint32_t mRoundTripTime; ///< Round trip time in msec. + uint16_t mRoundTripTime; ///< Round trip time in msec. uint16_t mSize; ///< Data size (number of bytes) in reply (excluding IPv6 and ICMP6 headers). uint16_t mSequenceNumber; ///< Sequence number. uint8_t mHopLimit; ///< Hop limit. } otPingSenderReply; +/** + * This structure represents statistics of a ping request. + * + */ +typedef struct otPingSenderStatistics +{ + uint16_t mSentCount; ///< The number of ping requests already sent. + uint16_t mReceivedCount; ///< The number of ping replies received. + uint32_t mTotalRoundTripTime; ///< The total round trip time of ping requests. + uint16_t mMinRoundTripTime; ///< The min round trip time among ping requests. + uint16_t mMaxRoundTripTime; ///< The max round trip time among ping requests. + bool mIsMulticast; ///< Whether this is a multicast ping request. +} otPingSenderStatistics; + /** * This function pointer type specifies the callback to notify receipt of a ping reply. * @@ -75,7 +89,17 @@ typedef struct otPingSenderReply * @param[in] aContext A pointer to application-specific context. * */ -typedef void (*otPingSenderCallback)(const otPingSenderReply *aReply, void *aContext); +typedef void (*otPingSenderReplyCallback)(const otPingSenderReply *aReply, void *aContext); + +/** + * This function pointer type specifies the callback to report the ping statistics. + * + * @param[in] aStatistics A pointer to a `otPingSenderStatistics` containing info about the received ping + * statistics. + * @param[in] aContext A pointer to application-specific context. + * + */ +typedef void (*otPingSenderStatisticsCallback)(const otPingSenderStatistics *aStatistics, void *aContext); /** * This structure represents a ping request configuration. @@ -83,14 +107,18 @@ typedef void (*otPingSenderCallback)(const otPingSenderReply *aReply, void *aCon */ typedef struct otPingSenderConfig { - otIp6Address mDestination; ///< Destination address to ping. - otPingSenderCallback mCallback; ///< Callback function to report replies (can be NULL if not needed). - void * mCallbackContext; ///< A pointer to the callback application-specific context. - uint16_t mSize; ///< Data size (# of bytes) excludes IPv6/ICMPv6 header. Zero for default. - uint16_t mCount; ///< Number of ping messages to send. Zero to use default. - uint32_t mInterval; ///< Ping tx interval in milliseconds. Zero to use default. - uint8_t mHopLimit; ///< Hop limit (used if `mAllowZeroHopLimit` is false). Zero for default. - bool mAllowZeroHopLimit; ///< Indicates whether hop limit is zero. + otIp6Address mDestination; ///< Destination address to ping. + otPingSenderReplyCallback mReplyCallback; ///< Callback function to report replies (can be NULL if not needed). + otPingSenderStatisticsCallback + mStatisticsCallback; ///< Callback function to report statistics (can be NULL if not needed). + void * mCallbackContext; ///< A pointer to the callback application-specific context. + uint16_t mSize; ///< Data size (# of bytes) excludes IPv6/ICMPv6 header. Zero for default. + uint16_t mCount; ///< Number of ping messages to send. Zero to use default. + uint32_t mInterval; ///< Ping tx interval in milliseconds. Zero to use default. + uint16_t mTimeout; ///< Time in milliseconds to wait for a reply after sending out the request. + ///< Zero to use default. + uint8_t mHopLimit; ///< Hop limit (used if `mAllowZeroHopLimit` is false). Zero for default. + bool mAllowZeroHopLimit; ///< Indicates whether hop limit is zero. } otPingSenderConfig; /** diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index cfbc556ec..4fe634d79 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -3155,6 +3155,29 @@ void Interpreter::HandlePingReply(const otPingSenderReply *aReply) OutputLine(": icmp_seq=%d hlim=%d time=%dms", aReply->mSequenceNumber, aReply->mHopLimit, aReply->mRoundTripTime); } +void Interpreter::HandlePingStatistics(const otPingSenderStatistics *aStatistics, void *aContext) +{ + static_cast(aContext)->HandlePingStatistics(aStatistics); +} + +void Interpreter::HandlePingStatistics(const otPingSenderStatistics *aStatistics) +{ + OutputFormat("%u packets transmitted, %u packets received.", aStatistics->mSentCount, aStatistics->mReceivedCount); + if (!aStatistics->mIsMulticast) + { + uint32_t packetLossRate = + 1000 * (aStatistics->mSentCount - aStatistics->mReceivedCount) / aStatistics->mSentCount; + OutputFormat(" Packet loss = %u.%u%%.", packetLossRate / 10, packetLossRate % 10); + } + if (aStatistics->mReceivedCount != 0) + { + uint32_t avgRoundTripTime = 1000 * aStatistics->mTotalRoundTripTime / aStatistics->mReceivedCount; + OutputFormat(" Round-trip min/avg/max = %u/%u.%u/%u ms.", aStatistics->mMinRoundTripTime, + avgRoundTripTime / 1000, avgRoundTripTime % 1000, aStatistics->mMaxRoundTripTime); + } + OutputLine(""); +} + otError Interpreter::ProcessPing(uint8_t aArgsLength, char *aArgs[]) { otError error = OT_ERROR_NONE; @@ -3195,8 +3218,9 @@ otError Interpreter::ProcessPing(uint8_t aArgsLength, char *aArgs[]) VerifyOrExit(aArgsLength <= 5, error = OT_ERROR_INVALID_ARGS); - config.mCallback = Interpreter::HandlePingReply; - config.mCallbackContext = this; + config.mReplyCallback = Interpreter::HandlePingReply; + config.mStatisticsCallback = Interpreter::HandlePingStatistics; + config.mCallbackContext = this; error = otPingSenderPing(mInstance, &config); diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index a632be88f..8f54fd294 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -540,6 +540,7 @@ private: #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE static void HandlePingReply(const otPingSenderReply *aReply, void *aContext); + static void HandlePingStatistics(const otPingSenderStatistics *aStatistics, void *aContext); #endif static void HandleActiveScanResult(otActiveScanResult *aResult, void *aContext); static void HandleEnergyScanResult(otEnergyScanResult *aResult, void *aContext); @@ -582,6 +583,7 @@ private: #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE void HandlePingReply(const otPingSenderReply *aReply); + void HandlePingStatistics(const otPingSenderStatistics *aStatistics); #endif void HandleActiveScanResult(otActiveScanResult *aResult); void HandleEnergyScanResult(otEnergyScanResult *aResult); diff --git a/src/core/config/ping_sender.h b/src/core/config/ping_sender.h index 18aba577c..2143389c8 100644 --- a/src/core/config/ping_sender.h +++ b/src/core/config/ping_sender.h @@ -57,6 +57,17 @@ #define OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL 1000 #endif +/** + * @def OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_DEADLINE + * + * Specifies the default ping timeout in milliseconds. The timeout is the max acceptable time gap between each ping + * request and its reply. + * + */ +#ifndef OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_TIMEOUT +#define OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_TIMEOUT 3000 +#endif + /** * @def OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_SIZE * diff --git a/src/core/net/icmp6.hpp b/src/core/net/icmp6.hpp index 88eda4624..d8bf0659a 100644 --- a/src/core/net/icmp6.hpp +++ b/src/core/net/icmp6.hpp @@ -325,6 +325,14 @@ public: */ bool ShouldHandleEchoRequest(const MessageInfo &aMessageInfo); + /** + * This method returns the ICMPv6 Echo sequence number. + * + * @returns The sequence number of the next ICMPv6 Echo request. + * + */ + uint16_t GetEchoSequence(void) const { return mEchoSequence; } + private: Error HandleEchoRequest(Message &aRequestMessage, const MessageInfo &aMessageInfo); diff --git a/src/core/utils/ping_sender.cpp b/src/core/utils/ping_sender.cpp index 22d4045b4..80bfb9ba6 100644 --- a/src/core/utils/ping_sender.cpp +++ b/src/core/utils/ping_sender.cpp @@ -60,12 +60,26 @@ void PingSender::Config::SetUnspecifiedToDefault(void) { mInterval = kDefaultInterval; } + + if (mTimeout == 0) + { + mTimeout = kDefaultTimeout; + } } -void PingSender::Config::InvokeCallback(const Reply &aReply) const +void PingSender::Config::InvokeReplyCallback(const Reply &aReply) const { - VerifyOrExit(mCallback != nullptr); - mCallback(&aReply, mCallbackContext); + VerifyOrExit(mReplyCallback != nullptr); + mReplyCallback(&aReply, mCallbackContext); + +exit: + return; +} + +void PingSender::Config::InvokeStatisticsCallback(const Statistics &aStatistics) const +{ + VerifyOrExit(mStatisticsCallback != nullptr); + mStatisticsCallback(&aStatistics, mCallbackContext); exit: return; @@ -74,6 +88,7 @@ exit: PingSender::PingSender(Instance &aInstance) : InstanceLocator(aInstance) , mIdentifier(0) + , mTargetEchoSequence(0) , mTimer(aInstance, PingSender::HandleTimer) , mIcmpHandler(PingSender::HandleIcmpReceive, this) { @@ -88,8 +103,12 @@ Error PingSender::Ping(const Config &aConfig) mConfig = aConfig; mConfig.SetUnspecifiedToDefault(); + VerifyOrExit(mConfig.mInterval <= Timer::kMaxDelay, error = kErrorInvalidArgs); + mStatistics.Clear(); + mStatistics.mIsMulticast = static_cast(&mConfig.mDestination)->IsMulticast(); + mIdentifier++; SendPing(); @@ -123,7 +142,9 @@ void PingSender::SendPing(void) SuccessOrExit(message->SetLength(mConfig.mSize)); } + mTargetEchoSequence = Get().GetEchoSequence(); SuccessOrExit(Get().SendEchoRequest(*message, messageInfo, mIdentifier)); + mStatistics.mSentCount++; #if OPENTHREAD_CONFIG_OTNS_ENABLE Get().EmitPingRequest(mConfig.GetDestination(), mConfig.mSize, now.GetValue(), mConfig.mHopLimit); @@ -135,10 +156,14 @@ exit: FreeMessage(message); mConfig.mCount--; - if (mConfig.mCount != 0) + if (mConfig.mCount > 0) { mTimer.Start(mConfig.mInterval); } + else if (!mStatistics.mIsMulticast) + { + mTimer.Start(mConfig.mTimeout); + } } void PingSender::HandleTimer(Timer &aTimer) @@ -148,7 +173,14 @@ void PingSender::HandleTimer(Timer &aTimer) void PingSender::HandleTimer(void) { - SendPing(); + if (mConfig.mCount > 0) + { + SendPing(); + } + else // The last reply times out, triggering the callback to print statistics in CLI. + { + mConfig.InvokeStatisticsCallback(mStatistics); + } } void PingSender::HandleIcmpReceive(void * aContext, @@ -174,17 +206,32 @@ void PingSender::HandleIcmpReceive(const Message & aMessage, SuccessOrExit(aMessage.Read(aMessage.GetOffset(), timestamp)); timestamp = HostSwap32(timestamp); - reply.mSenderAddress = aMessageInfo.GetPeerAddr(); - reply.mRoundTripTime = TimerMilli::GetNow() - TimeMilli(timestamp); + reply.mSenderAddress = aMessageInfo.GetPeerAddr(); + reply.mRoundTripTime = + static_cast(OT_MIN(TimerMilli::GetNow() - TimeMilli(timestamp), NumericLimits::Max())); reply.mSize = aMessage.GetLength() - aMessage.GetOffset(); reply.mSequenceNumber = aIcmpHeader.GetSequence(); reply.mHopLimit = aMessageInfo.GetHopLimit(); + mStatistics.mReceivedCount++; + mStatistics.mTotalRoundTripTime += reply.mRoundTripTime; + mStatistics.mMaxRoundTripTime = OT_MAX(mStatistics.mMaxRoundTripTime, reply.mRoundTripTime); + mStatistics.mMinRoundTripTime = OT_MIN(mStatistics.mMinRoundTripTime, reply.mRoundTripTime); + #if OPENTHREAD_CONFIG_OTNS_ENABLE Get().EmitPingReply(aMessageInfo.GetPeerAddr(), reply.mSize, timestamp, reply.mHopLimit); #endif - - mConfig.InvokeCallback(reply); + // Received all ping replies, no need to wait longer. + if (!mStatistics.mIsMulticast && mConfig.mCount == 0 && aIcmpHeader.GetSequence() == mTargetEchoSequence) + { + mTimer.Stop(); + } + mConfig.InvokeReplyCallback(reply); + // Received all ping replies, no need to wait longer. + if (!mStatistics.mIsMulticast && mConfig.mCount == 0 && aIcmpHeader.GetSequence() == mTargetEchoSequence) + { + mConfig.InvokeStatisticsCallback(mStatistics); + } exit: return; diff --git a/src/core/utils/ping_sender.hpp b/src/core/utils/ping_sender.hpp index 9e283ed49..a9c5a318e 100644 --- a/src/core/utils/ping_sender.hpp +++ b/src/core/utils/ping_sender.hpp @@ -44,6 +44,7 @@ #include "common/locator.hpp" #include "common/message.hpp" #include "common/non_copyable.hpp" +#include "common/numeric_limits.hpp" #include "common/time.hpp" #include "common/timer.hpp" #include "net/icmp6.hpp" @@ -65,6 +66,25 @@ public: */ typedef otPingSenderReply Reply; + /** + * This class represents the statistics of several ping requests. + * + */ + struct Statistics : public otPingSenderStatistics + { + Statistics(void) { Clear(); } + + void Clear(void) + { + mSentCount = 0; + mReceivedCount = 0; + mTotalRoundTripTime = 0; + mMinRoundTripTime = NumericLimits::Max(); + mMaxRoundTripTime = NumericLimits::Min(); + mIsMulticast = false; + } + }; + /** * This class represents a ping request configuration. * @@ -100,10 +120,12 @@ public: enum : uint32_t { kDefaultInterval = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL, + kDefaultTimeout = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_TIMEOUT, }; void SetUnspecifiedToDefault(void); - void InvokeCallback(const Reply &aReply) const; + void InvokeReplyCallback(const Reply &aReply) const; + void InvokeStatisticsCallback(const Statistics &aStatistics) const; }; /** @@ -145,7 +167,9 @@ private: const Ip6::Icmp::Header &aIcmpHeader); Config mConfig; + Statistics mStatistics; uint16_t mIdentifier; + uint16_t mTargetEchoSequence; TimerMilli mTimer; Ip6::Icmp::Handler mIcmpHandler; }; diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index 58ab2c4ba..48c8cced0 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -1765,10 +1765,14 @@ class NodeImpl: result = True # ncp-sim doesn't print Done done = (self.node_type == 'ncp-sim') - while len(responders) < num_responses or not done: + + # ncp-sim doesn't print statistics + received_statistics = (self.node_type == 'ncp-sim') + is_multicast = ipaddress.IPv6Address(ipaddr).is_multicast + while len(responders) < num_responses or not done or (not is_multicast and not received_statistics): self.simulator.go(1) try: - i = self._expect([r'from (\S+):', r'Done'], timeout=0.1) + i = self._expect([r'from (\S+):', r'Done', r'packets transmitted'], timeout=0.1) except (pexpect.TIMEOUT, socket.timeout): if self.simulator.now() < end: continue @@ -1781,7 +1785,8 @@ class NodeImpl: responders[self.pexpect.match.groups()[0]] = 1 elif i == 1: done = True - + elif i == 2: + received_statistics = True return result def reset(self): diff --git a/tools/otci/tests/test_otci.py b/tools/otci/tests/test_otci.py index 0a1583545..dfdfb2673 100644 --- a/tools/otci/tests/test_otci.py +++ b/tools/otci/tests/test_otci.py @@ -403,6 +403,7 @@ class TestOTCI(unittest.TestCase): for dst_ip in leader.get_ipaddrs(): commissioner.ping(dst_ip, size=10, count=1, interval=2, hoplimit=3) + commissioner.wait(1) self.assertEqual('disabled', commissioner.get_commissioiner_state()) commissioner.commissioner_start()