Repository: OpenDDS/OpenDDS
Pull request #4990: Use an InternalTopic to report statistics about the internals of RtpsDiscovery in the RtpsRelay
Author: login: mitza-oci
Created: 2025-04-30T20:07:21Z | Merged: 2025-05-06T20:05:38Z



Diff:
diff --git a/dds/CMakeLists.txt b/dds/CMakeLists.txt
index 80579c78a98..8fd40cfb6df 100644
--- a/dds/CMakeLists.txt
+++ b/dds/CMakeLists.txt
@@ -363,6 +363,7 @@ target_sources(OpenDDS_Dcps
     DCPS/SporadicTask.h
     DCPS/StaticDiscovery.h
     DCPS/StaticIncludes.h
+    DCPS/Statistics.h
     DCPS/Stats_T.h
     DCPS/StatusConditionImpl.h
     DCPS/SubscriberImpl.h
diff --git a/dds/DCPS/Cached_Allocator_With_Overflow_T.h b/dds/DCPS/Cached_Allocator_With_Overflow_T.h
index 0287a710402..b72c2f6b8c5 100644
--- a/dds/DCPS/Cached_Allocator_With_Overflow_T.h
+++ b/dds/DCPS/Cached_Allocator_With_Overflow_T.h
@@ -96,8 +96,9 @@ class Cached_Allocator_With_Overflow : public ACE_New_Allocator, public PoolAllo
 
     // addr() call is really not absolutely necessary because of the way
     // ACE_Cached_Mem_Pool_Node's internal structure arranged.
-    void* rtn =  this->free_list_.remove()->addr();
+    void* const rtn = this->free_list_.remove()->addr();
     if (0 == rtn) {
+      heap_allocated_ += sizeof(T);
       return ACE_Allocator::instance()->malloc(sizeof(T));
     }
 
@@ -135,6 +136,7 @@ class Cached_Allocator_With_Overflow : public ACE_New_Allocator, public PoolAllo
     unsigned char* tmp = static_cast<unsigned char*>(ptr);
 
     if (tmp < begin_ || tmp >= end_) {
+      heap_allocated_ -= sizeof(T);
       ACE_Allocator::instance()->free(tmp);
     } else if (ptr != 0) {
       this->free_list_.add((ACE_Cached_Mem_Pool_Node<T> *) ptr) ;
@@ -154,6 +156,8 @@ class Cached_Allocator_With_Overflow : public ACE_New_Allocator, public PoolAllo
 
   size_t n_chunks() const { return n_chunks_; }
 
+  size_t bytes_heap_allocated() const { return heap_allocated_.value(); }
+
 private:
   /// Remember how we allocate the memory in the first place so
   /// we can clear things up later.
@@ -165,6 +169,7 @@ class Cached_Allocator_With_Overflow : public ACE_New_Allocator, public PoolAllo
   ACE_Locked_Free_List<ACE_Cached_Mem_Pool_Node<T>, ACE_LOCK> free_list_;
 
   const size_t n_chunks_;
+  ACE_Atomic_Op<ACE_LOCK, size_t> heap_allocated_;
 };
 
 typedef Cached_Allocator_With_Overflow<ACE_Message_Block, ACE_Thread_Mutex> MessageBlockAllocator;
diff --git a/dds/DCPS/InternalDataReader.h b/dds/DCPS/InternalDataReader.h
index 22db3515d15..90a51afd4c6 100644
--- a/dds/DCPS/InternalDataReader.h
+++ b/dds/DCPS/InternalDataReader.h
@@ -28,6 +28,11 @@ OPENDDS_BEGIN_VERSIONED_NAMESPACE_DECL
 namespace OpenDDS {
 namespace DCPS {
 
+template <typename T>
+struct InternalTraits {
+  typedef std::less<T> KeyCompare;
+};
+
 class InternalEntity : public virtual RcObject {};
 typedef WeakRcHandle<InternalEntity> InternalEntity_wrch;
 
@@ -581,7 +586,7 @@ class InternalDataReader : public InternalEntity {
     }
   };
 
-  typedef OPENDDS_MAP_T(T, Instance) InstanceMap;
+  typedef OPENDDS_MAP_CMP_T(T, Instance, typename InternalTraits<T>::KeyCompare) InstanceMap;
   InstanceMap instance_map_;
 
   mutable ACE_Thread_Mutex mutex_;
diff --git a/dds/DCPS/InternalDataWriter.h b/dds/DCPS/InternalDataWriter.h
index 9574fb1d065..55d343abb75 100644
--- a/dds/DCPS/InternalDataWriter.h
+++ b/dds/DCPS/InternalDataWriter.h
@@ -183,7 +183,7 @@ class InternalDataWriter : public InternalEntity {
   typedef OPENDDS_SET(InternalDataReader_wrch) ReaderSet;
   ReaderSet readers_;
   ReaderSet all_instance_readers_;
-  typedef OPENDDS_MAP_T(T, ReaderSet) InstanceReaderSet;
+  typedef OPENDDS_MAP_CMP_T(T, ReaderSet, typename InternalTraits<T>::KeyCompare) InstanceReaderSet;
   InstanceReaderSet instance_readers_;
 
   void write_set(const T& sample,
@@ -254,7 +254,7 @@ class InternalDataWriter : public InternalEntity {
     SampleList samples_;
   };
 
-  typedef OPENDDS_MAP_T(T, SampleHolder) InstanceMap;
+  typedef OPENDDS_MAP_CMP_T(T, SampleHolder, typename InternalTraits<T>::KeyCompare) InstanceMap;
   InstanceMap instance_map_;
 
   ACE_Thread_Mutex mutex_;
diff --git a/dds/DCPS/JobQueue.h b/dds/DCPS/JobQueue.h
index be0d4bdb949..648f2aed04b 100644
--- a/dds/DCPS/JobQueue.h
+++ b/dds/DCPS/JobQueue.h
@@ -69,12 +69,18 @@ class OpenDDS_Dcps_Export JobQueue : public virtual RcEventHandler {
     }
   }
 
+  size_t size() const
+  {
+    ACE_GUARD_RETURN(ACE_Thread_Mutex, guard, mutex_, 0);
+    return job_queue_.size();
+  }
+
 private:
-  ACE_Thread_Mutex mutex_;
+  mutable ACE_Thread_Mutex mutex_;
   typedef OPENDDS_VECTOR(JobPtr) Queue;
   Queue job_queue_;
 
-  int handle_exception(ACE_HANDLE /*fd*/);
+  int handle_exception(ACE_HANDLE fd);
 };
 
 typedef RcHandle<JobQueue> JobQueue_rch;
diff --git a/dds/DCPS/PeriodicTask.h b/dds/DCPS/PeriodicTask.h
index cd19ccfbaec..ede66f0eb2a 100644
--- a/dds/DCPS/PeriodicTask.h
+++ b/dds/DCPS/PeriodicTask.h
@@ -120,6 +120,30 @@ class PmfPeriodicTask : public PeriodicTask {
   }
 };
 
+template <typename Delegate>
+class PmfPeriodicTask<const Delegate> : public PeriodicTask {
+public:
+  typedef void (Delegate::*CPMF)(const MonotonicTimePoint&) const;
+
+  PmfPeriodicTask(ReactorTask_rch reactor_task, const Delegate& delegate, CPMF function)
+    : PeriodicTask(reactor_task)
+    , delegate_(delegate)
+    , function_(function)
+    {}
+
+private:
+  const WeakRcHandle<Delegate> delegate_;
+  const CPMF function_;
+
+  void execute(const MonotonicTimePoint& now)
+  {
+    RcHandle<Delegate> handle = delegate_.lock();
+    if (handle) {
+      ((*handle).*function_)(now);
+    }
+  }
+};
+
 } // namespace DCPS
 } // namespace OpenDDS
 
diff --git a/dds/DCPS/RTPS/CMakeLists.txt b/dds/DCPS/RTPS/CMakeLists.txt
index c41e1ae9679..07ca9dc66b1 100644
--- a/dds/DCPS/RTPS/CMakeLists.txt
+++ b/dds/DCPS/RTPS/CMakeLists.txt
@@ -74,4 +74,7 @@ opendds_target_sources(OpenDDS_Rtps
   SKIP_OPENDDS_IDL
   ${common_idl_options}
 )
+set_target_properties(OpenDDS_Rtps PROPERTIES
+  UNITY_BUILD_UNIQUE_ID "OPENDDS_RTPS_UNITY_BUILD_ID"
+)
 opendds_install_interface_files(OpenDDS_Rtps INCLUDE_BASE "${OPENDDS_SOURCE_DIR}")
diff --git a/dds/DCPS/RTPS/RtpsDiscovery.cpp b/dds/DCPS/RTPS/RtpsDiscovery.cpp
index b0acf2c6cd7..4380f756809 100644
--- a/dds/DCPS/RTPS/RtpsDiscovery.cpp
+++ b/dds/DCPS/RTPS/RtpsDiscovery.cpp
@@ -11,8 +11,10 @@
 #include <dds/DCPS/DomainParticipantImpl.h>
 #include <dds/DCPS/LogAddr.h>
 #include <dds/DCPS/Marked_Default_Qos.h>
+#include <dds/DCPS/Qos_Helper.h>
 #include <dds/DCPS/Registered_Data_Types.h>
 #include <dds/DCPS/Service_Participant.h>
+#include <dds/DCPS/Statistics.h>
 #include <dds/DCPS/SubscriberImpl.h>
 
 #include <dds/DCPS/transport/framework/TransportConfig.h>
@@ -34,11 +36,15 @@ using DCPS::TimeDuration;
 RtpsDiscovery::RtpsDiscovery(const RepoKey& key)
   : key_(key)
   , config_(DCPS::make_rch<RtpsDiscoveryConfig>(key))
+  , stats_writer_(DCPS::make_rch<DCPS::StatisticsDataWriter>(DCPS::DataWriterQosBuilder().durability_transient_local()))
+  , stats_task_(DCPS::make_rch<PeriodicTask>(TheServiceParticipant->reactor_task(), *this, &RtpsDiscovery::write_stats))
 {
+  TheServiceParticipant->statistics_topic()->connect(stats_writer_);
 }
 
 RtpsDiscovery::~RtpsDiscovery()
 {
+  TheServiceParticipant->statistics_topic()->disconnect(stats_writer_);
 }
 
 int
@@ -188,6 +194,10 @@ RtpsDiscovery::add_domain_participant(DDS::DomainId_t domain,
     // ads.id may change during Spdp constructor
     ACE_GUARD_RETURN(ACE_Thread_Mutex, g, participants_lock_, ads);
     participants_[domain][ads.id] = spdp;
+    const DCPS::TimeDuration period = TheServiceParticipant->statistics_period();
+    if (!period.is_zero()) {
+      stats_task_->enable(false, period);
+    }
   } catch (const std::exception& e) {
     ads.id = GUID_UNKNOWN;
     ACE_ERROR((LM_ERROR, "(%P|%t) RtpsDiscovery::add_domain_participant() - "
@@ -215,6 +225,10 @@ RtpsDiscovery::add_domain_participant_secure(
       domain, ads.id, qos, this, tls, id, perm, part_crypto));
     ACE_GUARD_RETURN(ACE_Thread_Mutex, g, participants_lock_, ads);
     participants_[domain][ads.id] = spdp;
+    const DCPS::TimeDuration period = TheServiceParticipant->statistics_period();
+    if (!period.is_zero()) {
+      stats_task_->enable(false, period);
+    }
   } catch (const std::exception& e) {
     ads.id = GUID_UNKNOWN;
     ACE_ERROR((LM_WARNING, "(%P|%t) RtpsDiscovery::add_domain_participant_secure() - "
@@ -485,6 +499,9 @@ bool RtpsDiscovery::remove_domain_participant(
   if (domain->second.empty()) {
     participants_.erase(domain);
   }
+  if (participants_.empty()) {
+    stats_task_->disable();
+  }
   g.release();
 
   participant->shutdown();
@@ -783,6 +800,19 @@ void RtpsDiscovery::request_remote_complete_type_objects(
   spdp->request_remote_complete_type_objects(remote_entity, remote_type_info, cond);
 }
 
+void RtpsDiscovery::write_stats(const MonotonicTimePoint&) const
+{
+  ACE_Guard<ACE_Thread_Mutex> guard(participants_lock_);
+  DCPS::Statistics statistics;
+  for (DomainParticipantMap::const_iterator domain = participants_.begin(); domain != participants_.end(); ++domain) {
+    for (ParticipantMap::const_iterator part = domain->second.begin(); part != domain->second.end(); ++part) {
+      statistics.id = ("RtpsDiscovery " + DCPS::to_dds_string(domain->first) + ' ' + DCPS::to_string(part->first)).c_str();
+      part->second->fill_stats(statistics.stats);
+      stats_writer_->write(statistics);
+    }
+  }
+}
+
 } // namespace DCPS
 } // namespace OpenDDS
 
diff --git a/dds/DCPS/RTPS/RtpsDiscovery.h b/dds/DCPS/RTPS/RtpsDiscovery.h
index 1be046de25d..dcd39134287 100644
--- a/dds/DCPS/RTPS/RtpsDiscovery.h
+++ b/dds/DCPS/RTPS/RtpsDiscovery.h
@@ -13,6 +13,7 @@
 
 #include <dds/DCPS/AtomicBool.h>
 #include <dds/DCPS/PoolAllocator.h>
+#include <dds/DCPS/Statistics.h>
 #include <dds/DCPS/debug.h>
 
 #include <dds/OpenDDSConfigWrapper.h>
@@ -356,6 +357,12 @@ class OpenDDS_Rtps_Export RtpsDiscovery : public DCPS::Discovery {
                      DCPS::SubscriberImpl* sub,
                      const DDS::DataReaderQos& qos);
 
+  DCPS::StatisticsDataWriter_rch stats_writer_;
+  typedef DCPS::PmfPeriodicTask<const RtpsDiscovery> PeriodicTask;
+  DCPS::RcHandle<PeriodicTask> stats_task_;
+
+  void write_stats(const MonotonicTimePoint&) const;
+
 public:
   class Config : public Discovery::Config {
   public:
diff --git a/dds/DCPS/RTPS/Sedp.cpp b/dds/DCPS/RTPS/Sedp.cpp
index b1efb7f17ce..5d4ddb3a1bd 100644
--- a/dds/DCPS/RTPS/Sedp.cpp
+++ b/dds/DCPS/RTPS/Sedp.cpp
@@ -397,7 +397,8 @@ Sedp::Sedp(const GUID_t& participant_id, Spdp& owner, ACE_Thread_Mutex& lock)
           OPENDDS_STRING("_SPDPTransportInst_") +
           DCPS::GuidConverter(owner.guid_).uniqueParticipantId() +
           DCPS::to_dds_string(owner.domain_))
-{}
+  , total_deferred_samples_(0)
+  {}
 
 DDS::ReturnCode_t
 Sedp::init(const GUID_t& guid,
@@ -1093,6 +1094,7 @@ Sedp::associate(DiscoveredParticipant& participant
   static const int rel_dur = AC_REMOTE_RELIABLE | AC_REMOTE_DURABLE;
   const int part_mesg = AC_REMOTE_DURABLE |
     ((beq & BEST_EFFORT_PARTICIPANT_MESSAGE_DATA_READER) ? AC_EMPTY : AC_REMOTE_RELIABLE);
+  const size_t builtin_pending_start = participant.builtin_pending_records_.size();
 
   // See RTPS v2.1 section 8.5.5.1
   if ((local_available & DISC_BUILTIN_ENDPOINT_PUBLICATION_DETECTOR) &&
@@ -1302,6 +1304,8 @@ Sedp::associate(DiscoveredParticipant& participant
   }
 #endif
 
+  spdp_.total_builtin_pending_ += participant.builtin_pending_records_.size() - builtin_pending_start;
+
   if (spdp_.shutting_down()) { return; }
 
   associated_participants_.insert(participant.make_part_guid());
@@ -1330,6 +1334,8 @@ void Sedp::process_association_records_i(DiscoveredParticipant& participant)
 
       participant.builtin_associated_records_.push_back(record);
       participant.builtin_pending_records_.erase(pos++);
+      ++spdp_.total_builtin_associated_;
+      --spdp_.total_builtin_pending_;
 
     } else {
       if (DCPS::DCPS_debug_level > 6) {
@@ -1350,6 +1356,8 @@ void Sedp::process_association_records_i(DiscoveredParticipant& participant)
 
       participant.writer_associated_records_.push_back(*pos);
       participant.writer_pending_records_.erase(pos++);
+      --spdp_.total_writer_pending_;
+      ++spdp_.total_writer_associated_;
     } else {
       if (DCPS::DCPS_debug_level > 6) {
         ACE_DEBUG((LM_INFO, ACE_TEXT("(%P|%t) Sedp::process_association_records_i writer not ready local %C remote %C pending %B done %B\n"),
@@ -1368,6 +1376,8 @@ void Sedp::process_association_records_i(DiscoveredParticipant& participant)
 
       participant.reader_associated_records_.push_back(*pos);
       participant.reader_pending_records_.erase(pos++);
+      --spdp_.total_reader_pending_;
+      ++spdp_.total_reader_associated_;
     } else {
       if (DCPS::DCPS_debug_level > 6) {
         ACE_DEBUG((LM_INFO, ACE_TEXT("(%P|%t) Sedp::process_association_records_i reader not ready local %C remote %C pending %B done %B\n"),
@@ -1402,8 +1412,10 @@ void Sedp::disassociate_volatile(DiscoveredParticipant& participant)
     const BuiltinAssociationRecord& record = *pos;
     if (record.local_id() == local_writer && record.remote_id() == remote_reader) {
       participant.builtin_pending_records_.erase(pos++);
+      --spdp_.total_builtin_pending_;
     } else if (record.local_id() == local_reader && record.remote_id() == remote_writer) {
       participant.builtin_pending_records_.erase(pos++);
+      --spdp_.total_builtin_pending_;
     } else {
       ++pos;
     }
@@ -1415,9 +1427,11 @@ void Sedp::disassociate_volatile(DiscoveredParticipant& participant)
     if (record.local_id() == local_writer && record.remote_id() == remote_reader) {
       record.transport_client_->disassociate(record.remote_id());
       participant.builtin_associated_records_.erase(pos++);
+      --spdp_.total_builtin_associated_;
     } else if (record.local_id() == local_reader && record.remote_id() == remote_writer) {
       record.transport_client_->disassociate(record.remote_id());
       participant.builtin_associated_records_.erase(pos++);
+      --spdp_.total_builtin_associated_;
     } else {
       ++pos;
     }
@@ -1443,6 +1457,7 @@ void Sedp::associate_volatile(DiscoveredParticipant& participant)
                                     participant.make_guid(ENTITYID_P2P_BUILTIN_PARTICIPANT_VOLATILE_SECURE_WRITER),
                                     AC_REMOTE_RELIABLE | AC_GENERATE_REMOTE_MATCHED_CRYPTO_HANDLE);
     participant.builtin_pending_records_.push_back(record);
+    ++spdp_.total_builtin_pending_;
   }
   if ((local_available & BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER) &&
       (remote_available & BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER)) {
@@ -1450,6 +1465,7 @@ void Sedp::associate_volatile(DiscoveredParticipant& participant)
                                     participant.make_guid(ENTITYID_P2P_BUILTIN_PARTICIPANT_VOLATILE_SECURE_READER),
                                     AC_REMOTE_RELIABLE | AC_GENERATE_REMOTE_MATCHED_CRYPTO_HANDLE);
     participant.builtin_pending_records_.push_back(record);
+    ++spdp_.total_builtin_pending_;
   }
 }
 
@@ -1596,6 +1612,7 @@ Sedp::disassociate(DiscoveredParticipant& participant)
     remove_from_bit_i(*it);
   }
 
+  spdp_.total_builtin_pending_ -= participant.builtin_pending_records_.size();
   participant.builtin_pending_records_.clear();
 
   for (DiscoveredParticipant::BuiltinAssociationRecords::const_iterator pos = participant.builtin_associated_records_.begin(), limit = participant.builtin_associated_records_.end(); pos != limit; ++pos) {
@@ -1603,6 +1620,7 @@ Sedp::disassociate(DiscoveredParticipant& participant)
     record.transport_client_->disassociate(record.remote_id());
   }
 
+  spdp_.total_builtin_associated_ -= participant.builtin_associated_records_.size();
   participant.builtin_associated_records_.clear();
 
   //FUTURE: if/when topic propagation is supported, add it here
@@ -3355,6 +3373,7 @@ void Sedp::Writer::send_sample(DCPS::Message_Block_Ptr payload,
       DeferredSamples::iterator samples_for_reader = deferred_samples_.insert(
         std::make_pair(reader, PerReaderDeferredSamples())).first;
       samples_for_reader->second.insert(std::make_pair(sequence, el));
+      ++sedp_.total_deferred_samples_;
       return;
     }
   }
@@ -3374,6 +3393,7 @@ void Sedp::Writer::send_deferred_samples(const GUID_t& reader)
         i != samples_for_reader->second.end(); ++i) {
       send_sample_i(i->second);
     }
+    sedp_.total_deferred_samples_ -= samples_for_reader->second.size();
     deferred_samples_.erase(samples_for_reader);
   }
 }
@@ -7053,6 +7073,7 @@ void Sedp::cleanup_writer_association(DCPS::DataWriterCallbacks_wrch callbacks,
     for (DiscoveredParticipant::WriterAssociationRecords::iterator pos = part_iter->second.writer_pending_records_.begin(), limit = part_iter->second.writer_pending_records_.end(); pos != limit; ++pos) {
       if ((*pos)->writer_id() == writer && (*pos)->reader_id() == reader) {
         part_iter->second.writer_pending_records_.erase(pos);
+        --spdp_.total_writer_pending_;
         break;
       }
     }
@@ -7061,6 +7082,7 @@ void Sedp::cleanup_writer_association(DCPS::DataWriterCallbacks_wrch callbacks,
       if ((*pos)->writer_id() == writer && (*pos)->reader_id() == reader) {
         event_dispatcher_->dispatch(DCPS::make_rch<WriterRemoveAssociations>(*pos));
         part_iter->second.writer_associated_records_.erase(pos);
+        --spdp_.total_writer_associated_;
         break;
       }
     }
@@ -7084,6 +7106,7 @@ void Sedp::cleanup_reader_association(DCPS::DataReaderCallbacks_wrch callbacks,
     for (DiscoveredParticipant::ReaderAssociationRecords::iterator pos = part_iter->second.reader_pending_records_.begin(), limit = part_iter->second.reader_pending_records_.end(); pos != limit; ++pos) {
       if ((*pos)->reader_id() == reader && (*pos)->writer_id() == writer) {
         part_iter->second.reader_pending_records_.erase(pos);
+        --spdp_.total_reader_pending_;
         break;
       }
     }
@@ -7092,6 +7115,7 @@ void Sedp::cleanup_reader_association(DCPS::DataReaderCallbacks_wrch callbacks,
       if ((*pos)->reader_id() == reader && (*pos)->writer_id() == writer) {
         event_dispatcher_->dispatch(DCPS::make_rch<ReaderRemoveAssociations>(*pos));
         part_iter->second.reader_associated_records_.erase(pos);
+        --spdp_.total_reader_associated_;
         break;
       }
     }
@@ -7784,6 +7808,7 @@ void Sedp::match_continue(UsedEndpoints& ue,
       Spdp::DiscoveredParticipantIter iter = spdp_.participants_.find(make_id(writer, ENTITYID_PARTICIPANT));
       if (iter != spdp_.participants_.end()) {
         iter->second.reader_pending_records_.push_back(rar);
+        ++spdp_.total_reader_pending_;
         process_association_records_i(iter->second);
       }
     } else if (call_writer) {
@@ -7793,6 +7818,7 @@ void Sedp::match_continue(UsedEndpoints& ue,
       Spdp::DiscoveredParticipantIter iter = spdp_.participants_.find(make_id(reader, ENTITYID_PARTICIPANT));
       if (iter != spdp_.participants_.end()) {
         iter->second.writer_pending_records_.push_back(war);
+        ++spdp_.total_writer_pending_;
         process_association_records_i(iter->second);
       }
     }
@@ -8062,6 +8088,111 @@ void Sedp::ReaderRemoveAssociations::handle_event()
   }
 }
 
+#ifndef OPENDDS_RTPS_UNITY_BUILD_ID
+#  define OPENDDS_RTPS_UNITY_BUILD_ID SedpAnonymous
+#endif
+
+namespace { namespace OPENDDS_RTPS_UNITY_BUILD_ID {
+  const DDS::UInt32 Stats_Index_Topics = 0,
+    Stats_Index_TopicNames = 1,
+    Stats_Index_TopicsIgnored = 2,
+    Stats_Index_TotalDeferredSamples = 3,
+    Stats_Index_TotalReaderBytesAllocated = 4,
+    Stats_Index_JobQueueSize = 5,
+    Stats_Index_AssociatedParticipants = 6,
+    Stats_Index_IgnoredGuids = 7,
+    Stats_Index_LocalPublications = 8,
+    Stats_Index_LocalSubscriptions = 9,
+    Stats_Index_DiscoveredPublications = 10,
+    Stats_Index_DiscoveredSubscriptions = 11,
+    Stats_Index_TypeLookupReaderDependencies = 12,
+    Stats_Index_TypeLookupSequenceNumbers = 13,
+    Stats_Index_TypeLookupMatchingData = 14,
+    Stats_Index_PendingRemoteReaderCryptoTokens = 15,
+    Stats_Index_PendingRemoteWriterCryptoTokens = 16,
+    Stats_Len = 17;
+} }
+
+DCPS::StatisticSeq Sedp::stats_template()
+{
+  using namespace OPENDDS_RTPS_UNITY_BUILD_ID;
+  DCPS::StatisticSeq stats(Stats_Len);
+  stats.length(Stats_Len);
+  stats[Stats_Index_Topics].name = "Topics";
+  stats[Stats_Index_TopicNames].name = "TopicNames";
+  stats[Stats_Index_TopicsIgnored].name = "TopicsIgnored";
+  stats[Stats_Index_TotalDeferredSamples].name = "TotalDeferredSamples";
+  stats[Stats_Index_TotalReaderBytesAllocated].name = "TotalReaderBytesAllocated";
+  stats[Stats_Index_JobQueueSize].name = "JobQueueSize";
+  stats[Stats_Index_LocalPublications].name = "LocalPublications";
+  stats[Stats_Index_LocalSubscriptions].name = "LocalSubscriptions";
+  stats[Stats_Index_DiscoveredPublications].name = "DiscoveredPublications";
+  stats[Stats_Index_DiscoveredSubscriptions].name = "DiscoveredSubscriptions";
+  stats[Stats_Index_TypeLookupReaderDependencies].name = "TypeLookupReaderDependencies";
+  stats[Stats_Index_TypeLookupSequenceNumbers].name = "TypeLookupSequenceNumbers";
+  stats[Stats_Index_TypeLookupMatchingData].name = "TypeLookupMatchingData";
+  stats[Stats_Index_PendingRemoteReaderCryptoTokens].name = "PendingRemoteReaderCryptoTokens";
+  stats[Stats_Index_PendingRemoteWriterCryptoTokens].name = "PendingRemoteWriterCryptoTokens";
+  return stats;
+}
+
+void Sedp::fill_stats(DCPS::StatisticSeq& stats, DDS::UInt32 begin) const
+{
+  using namespace OPENDDS_RTPS_UNITY_BUILD_ID;
+  // lock held in Sedp
+  stats[begin + Stats_Index_Topics].value = topics_.size(); // need nested sizes?
+  stats[begin + Stats_Index_TopicNames].value = topic_names_.size();
+  stats[begin + Stats_Index_TopicsIgnored].value = ignored_topics_.size();
+  stats[begin + Stats_Index_TotalDeferredSamples].value = total_deferred_samples_;
+  stats[begin + Stats_Index_TotalReaderBytesAllocated].value = total_reader_bytes_allocated();
+  stats[begin + Stats_Index_JobQueueSize].value = job_queue_ ? job_queue_->size() : 0;
+  stats[begin + Stats_Index_LocalPublications].value = local_publications_.size();
+  stats[begin + Stats_Index_LocalSubscriptions].value = local_subscriptions_.size();
+  stats[begin + Stats_Index_DiscoveredPublications].value = discovered_publications_.size();
+  stats[begin + Stats_Index_DiscoveredSubscriptions].value = discovered_subscriptions_.size();
+  stats[begin + Stats_Index_TypeLookupReaderDependencies].value = tlreader_dependencies();
+  stats[begin + Stats_Index_TypeLookupSequenceNumbers].value = orig_seq_numbers_.size();
+  stats[begin + Stats_Index_TypeLookupMatchingData].value = matching_data_buffer_.size();
+#if OPENDDS_CONFIG_SECURITY
+  stats[begin + Stats_Index_PendingRemoteReaderCryptoTokens].value = pending_remote_reader_crypto_tokens_.size();
+  stats[begin + Stats_Index_PendingRemoteWriterCryptoTokens].value = pending_remote_writer_crypto_tokens_.size();
+#endif
+}
+
+size_t Sedp::total_reader_bytes_allocated() const
+{
+  return reader_bytes_allocated(publications_reader_)
+    + reader_bytes_allocated(subscriptions_reader_)
+    + reader_bytes_allocated(participant_message_reader_)
+    + reader_bytes_allocated(type_lookup_request_reader_)
+    + reader_bytes_allocated(type_lookup_reply_reader_)
+#if OPENDDS_CONFIG_SECURITY
+    + reader_bytes_allocated(publications_secure_reader_)
+    + reader_bytes_allocated(subscriptions_secure_reader_)
+    + reader_bytes_allocated(participant_message_secure_reader_)
+    + reader_bytes_allocated(participant_stateless_message_reader_)
+    + reader_bytes_allocated(participant_volatile_message_secure_reader_)
+    + reader_bytes_allocated(dcps_participant_secure_reader_)
+    + reader_bytes_allocated(type_lookup_request_secure_reader_)
+    + reader_bytes_allocated(type_lookup_reply_secure_reader_)
+#endif
+    ;
+}
+
+size_t Sedp::reader_bytes_allocated(const RcHandle<Reader>& reader)
+{
+  return reader ? reader->bytes_heap_allocated() : 0;
+}
+
+size_t Sedp::tlreader_dependencies() const
+{
+  return (type_lookup_reply_reader_ ? type_lookup_reply_reader_->dependencies_participants() : 0)
+#if OPENDDS_CONFIG_SECURITY
+    + (type_lookup_reply_secure_reader_ ? type_lookup_reply_secure_reader_->dependencies_participants() : 0)
+#endif
+    ;
+}
+
 } // namespace RTPS
 } // namespace OpenDDS
 
diff --git a/dds/DCPS/RTPS/Sedp.h b/dds/DCPS/RTPS/Sedp.h
index 578258cc55e..db9c8c1b757 100644
--- a/dds/DCPS/RTPS/Sedp.h
+++ b/dds/DCPS/RTPS/Sedp.h
@@ -871,6 +871,8 @@ class Sedp : public virtual DCPS::RcEventHandler {
 
     bool assoc(const DCPS::AssociationData& publication);
 
+    size_t bytes_heap_allocated() const { return mb_alloc_.bytes_heap_allocated(); }
+
     // Implementing TransportReceiveListener
 
     void data_received(const DCPS::ReceivedDataSample& sample);
@@ -989,6 +991,8 @@ class Sedp : public virtual DCPS::RcEventHandler {
 
     void cleanup(const DCPS::GUID_t& guid, const XTypes::TypeIdentifier& ti);
 
+    size_t dependencies_participants() const { return dependencies_.size(); }
+
   private:
     virtual void data_received_i(const DCPS::ReceivedDataSample& sample,
       const DCPS::EntityId_t& entity_id,
@@ -1565,9 +1569,9 @@ class Sedp : public virtual DCPS::RcEventHandler {
   LivelinessWriter_rch participant_message_secure_writer_;
   SecurityWriter_rch participant_stateless_message_writer_;
   DiscoveryWriter_rch dcps_participant_secure_writer_;
-  friend class Spdp;
   SecurityWriter_rch participant_volatile_message_secure_writer_;
 #endif
+  friend class Spdp;
   TypeLookupRequestWriter_rch type_lookup_request_writer_;
   TypeLookupReplyWriter_rch type_lookup_reply_writer_;
 #if OPENDDS_CONFIG_SECURITY
@@ -1660,6 +1664,13 @@ class Sedp : public virtual DCPS::RcEventHandler {
   };
 
   RtpsDiscoveryCore core_;
+
+  static DCPS::StatisticSeq stats_template();
+  void fill_stats(DCPS::StatisticSeq& stats, DDS::UInt32 begin) const;
+  size_t total_deferred_samples_;
+  size_t total_reader_bytes_allocated() const;
+  static size_t reader_bytes_allocated(const RcHandle<Reader>& reader);
+  size_t tlreader_dependencies() const;
 };
 
 bool locators_changed(const ParticipantProxy_t& x,
diff --git a/dds/DCPS/RTPS/Spdp.cpp b/dds/DCPS/RTPS/Spdp.cpp
index f311d451383..9609d2ee83b 100644
--- a/dds/DCPS/RTPS/Spdp.cpp
+++ b/dds/DCPS/RTPS/Spdp.cpp
@@ -274,6 +274,14 @@ Spdp::Spdp(DDS::DomainId_t domain,
   , ice_agent_(ICE::Agent::instance())
   , n_participants_in_authentication_(0)
 #endif
+  , stats_template_(stats_template())
+  , total_location_updates_(0)
+  , total_builtin_pending_(0)
+  , total_builtin_associated_(0)
+  , total_writer_pending_(0)
+  , total_writer_associated_(0)
+  , total_reader_pending_(0)
+  , total_reader_associated_(0)
 {
   ACE_GUARD(ACE_Thread_Mutex, g, lock_);
 
@@ -338,6 +346,14 @@ Spdp::Spdp(DDS::DomainId_t domain,
   , crypto_handle_(crypto_handle)
   , ice_agent_(ICE::Agent::instance())
   , n_participants_in_authentication_(0)
+  , stats_template_(stats_template())
+  , total_location_updates_(0)
+  , total_builtin_pending_(0)
+  , total_builtin_associated_(0)
+  , total_writer_pending_(0)
+  , total_writer_associated_(0)
+  , total_reader_pending_(0)
+  , total_reader_associated_(0)
 {
   ACE_GUARD(ACE_Thread_Mutex, g, lock_);
 
@@ -507,6 +523,7 @@ Spdp::enqueue_location_update_i(DiscoveredParticipantIter iter,
 {
   // We have the global lock.
   iter->second.location_updates_.push_back(DiscoveredParticipant::LocationUpdate(mask, from, DCPS::SystemTimePoint::now()));
+  ++total_location_updates_;
 
   if (DCPS::log_bits) {
     ACE_DEBUG((LM_DEBUG, "(%P|%t) DEBUG: Spdp::enqueue_location_update_i: %@ for %C size=%B reason=%C\n", this, LogGuid(iter->first).c_str(), iter->second.location_updates_.size(), reason));
@@ -527,6 +544,7 @@ void Spdp::process_location_updates_i(const DiscoveredParticipantIter& iter, con
 
   DiscoveredParticipant::LocationUpdateList location_updates;
   std::swap(iter->second.location_updates_, location_updates);
+  total_location_updates_ -= location_updates.size();
 
   if (DCPS::log_bits) {
     ACE_DEBUG((LM_DEBUG, "(%P|%t) DEBUG: Spdp::process_location_updates_i: %@ %C has %B location update(s) force_publish=%d reason=%C\n", this, LogGuid(iter->first).c_str(), location_updates.size(), force_publish, reason));
@@ -686,7 +704,8 @@ bool is_ip_equal(const ACE_INET_Addr& a, const DCPS::Locator_t& locator)
   return cmp_ip4(a, locator);
 }
 
-void print_locator(const CORBA::ULong i, const DCPS::Locator_t& o){
+void print_locator(const CORBA::ULong i, const DCPS::Locator_t& o)
+{
   const unsigned char* a = reinterpret_cast<const unsigned char*>(o.address);
   ACE_INET_Addr addr;
   bool b = locator_to_address(addr, o, false) == 0;
@@ -4762,6 +4781,71 @@ bool Spdp::has_domain_participant(const GUID_t& remote) const
   return has_discovered_participant(remote);
 }
 
+#ifndef OPENDDS_RTPS_UNITY_BUILD_ID
+#  define OPENDDS_RTPS_UNITY_BUILD_ID SpdpAnonymous
+#endif
+
+namespace { namespace OPENDDS_RTPS_UNITY_BUILD_ID {
+  const DDS::UInt32 Stats_Index_LeaseExpirations = 0,
+    Stats_Index_HandshakeDeadlines = 1,
+    Stats_Index_HandshakeResends = 2,
+    Stats_Index_DiscoveredParticipants = 3,
+    Stats_Index_TotalLocationUpdates = 4,
+    Stats_Index_TotalBuiltinPending = 5,
+    Stats_Index_TotalBuiltinAssociated = 6,
+    Stats_Index_TotalWriterPending = 7,
+    Stats_Index_TotalWriterAssociated = 8,
+    Stats_Index_TotalReaderPending = 9,
+    Stats_Index_TotalReaderAssociated = 10,
+    Stats_Index_DirectedGuids = 11,
+    Stats_Len = 12;
+} }
+
+DCPS::StatisticSeq Spdp::stats_template()
+{
+  using namespace OPENDDS_RTPS_UNITY_BUILD_ID;
+  const DCPS::StatisticSeq sedp_template = Sedp::stats_template();
+  DCPS::StatisticSeq stats(Stats_Len + sedp_template.length());
+  stats.length(Stats_Len + sedp_template.length());
+  stats[Stats_Index_LeaseExpirations].name = "LeaseExpirations";
+  stats[Stats_Index_HandshakeDeadlines].name = "HandshakeDeadlines";
+  stats[Stats_Index_HandshakeResends].name = "HandshakeResends";
+  stats[Stats_Index_DiscoveredParticipants].name = "DiscoveredParticipants";
+  stats[Stats_Index_TotalLocationUpdates].name = "TotalLocationUpdates";
+  stats[Stats_Index_TotalBuiltinPending].name = "TotalBuiltinPending";
+  stats[Stats_Index_TotalBuiltinAssociated].name = "TotalBuiltinAssociated";
+  stats[Stats_Index_TotalWriterPending].name = "TotalWriterPending";
+  stats[Stats_Index_TotalWriterAssociated].name = "TotalWriterAssociated";
+  stats[Stats_Index_TotalReaderPending].name = "TotalReaderPending";
+  stats[Stats_Index_TotalReaderAssociated].name = "TotalReaderAssociated";
+  stats[Stats_Index_DirectedGuids].name = "DirectedGuids";
+  for (DDS::UInt32 i = 0; i < sedp_template.length(); ++i) {
+    stats[Stats_Len + i].name = sedp_template[i].name;
+  }
+  return stats;
+}
+
+void Spdp::fill_stats(DCPS::StatisticSeq& stats) const
+{
+  using namespace OPENDDS_RTPS_UNITY_BUILD_ID;
+  stats = stats_template_;
+  ACE_GUARD(ACE_Thread_Mutex, g, lock_);
+  stats[Stats_Index_LeaseExpirations].value = lease_expirations_.size();
+#if OPENDDS_CONFIG_SECURITY
+  stats[Stats_Index_HandshakeDeadlines].value = handshake_deadlines_.size();
+  stats[Stats_Index_HandshakeResends].value = handshake_resends_.size();
+#endif
+  stats[Stats_Index_DiscoveredParticipants].value = participants_.size();
+  stats[Stats_Index_TotalLocationUpdates].value = total_location_updates_;
+  stats[Stats_Index_TotalBuiltinPending].value = total_builtin_pending_;
+  stats[Stats_Index_TotalBuiltinAssociated].value = total_builtin_associated_;
+  stats[Stats_Index_TotalWriterPending].value = total_writer_pending_;
+  stats[Stats_Index_TotalWriterAssociated].value = total_writer_associated_;
+  stats[Stats_Index_TotalReaderPending].value = total_reader_pending_;
+  stats[Stats_Index_TotalReaderAssociated].value = total_reader_associated_;
+  stats[Stats_Index_DirectedGuids].value = tport_ ? tport_->directed_guids_.size() : 0;
+}
+
 DCPS::TopicStatus Spdp::assert_topic(GUID_t& topicId, const char* topicName,
   const char* dataTypeName, const DDS::TopicQos& qos,
   bool hasDcpsKey, DCPS::TopicCallbacks* topic_callbacks)
@@ -4865,6 +4949,13 @@ void Spdp::purge_discovered_participant(const DiscoveredParticipantIter& iter)
     --n_participants_in_authentication_;
   }
 #endif
+  total_location_updates_ -= iter->second.location_updates_.size();
+  total_builtin_pending_ -= iter->second.builtin_pending_records_.size();
+  total_builtin_associated_ -= iter->second.builtin_associated_records_.size();
+  total_writer_pending_ -= iter->second.writer_pending_records_.size();
+  total_writer_associated_ -= iter->second.writer_associated_records_.size();
+  total_reader_pending_ -= iter->second.reader_pending_records_.size();
+  total_reader_associated_ -= iter->second.reader_associated_records_.size();
 }
 
 #if OPENDDS_CONFIG_SECURITY
diff --git a/dds/DCPS/RTPS/Spdp.h b/dds/DCPS/RTPS/Spdp.h
index c1c1d23ec69..cf3e3e73a31 100644
--- a/dds/DCPS/RTPS/Spdp.h
+++ b/dds/DCPS/RTPS/Spdp.h
@@ -355,6 +355,8 @@ class OpenDDS_Rtps_Export Spdp
     sedp_->request_remote_complete_type_objects(remote_entity, remote_type_info, cond);
   }
 
+  void fill_stats(DCPS::StatisticSeq& stats) const;
+
 protected:
   Sedp& endpoint_manager() { return *sedp_; }
 
@@ -695,6 +697,11 @@ class OpenDDS_Rtps_Export Spdp
   void set_auth_state(DiscoveredParticipant& dp, AuthState state);
 #endif
 
+  static DCPS::StatisticSeq stats_template();
+  const DCPS::StatisticSeq stats_template_;
+  size_t total_location_updates_, total_builtin_pending_, total_builtin_associated_,
+    total_writer_pending_, total_writer_associated_, total_reader_pending_, total_reader_associated_;
+
   friend class ::DDS_TEST;
 };
 
diff --git a/dds/DCPS/Service_Participant.cpp b/dds/DCPS/Service_Participant.cpp
index 874058d56af..dec863d8b6a 100644
--- a/dds/DCPS/Service_Participant.cpp
+++ b/dds/DCPS/Service_Participant.cpp
@@ -157,6 +157,7 @@ Service_Participant::Service_Participant()
   , priority_max_(0)
   , shut_down_(false)
   , network_interface_address_topic_(make_rch<InternalTopic<NetworkInterfaceAddress> >())
+  , statistics_topic_(make_rch<StatisticsTopic>())
   , config_topic_(make_rch<InternalTopic<ConfigPair> >())
   , config_store_(make_rch<ConfigStoreImpl>(config_topic_))
   , config_reader_(make_rch<InternalDataReader<ConfigPair> >(DataReaderQosBuilder().reliability_reliable().durability_transient_local()))
@@ -2333,6 +2334,22 @@ Service_Participant::printer_value_writer_indent(unsigned int value)
   config_store_->set_uint32(COMMON_PRINTER_VALUE_WRITER_INDENT, value);
 }
 
+TimeDuration
+Service_Participant::statistics_period() const
+{
+  return config_store_->get(COMMON_STATISTICS_PERIOD,
+                            COMMON_STATISTICS_PERIOD_default,
+                            ConfigStoreImpl::Format_FractionalSeconds);
+}
+
+void
+Service_Participant::statistics_period(const TimeDuration& value)
+{
+  config_store_->set(COMMON_STATISTICS_PERIOD,
+                     value,
+                     ConfigStoreImpl::Format_FractionalSeconds);
+}
+
 void
 Service_Participant::ConfigReaderListener::on_data_available(InternalDataReader_rch reader)
 {
diff --git a/dds/DCPS/Service_Participant.h b/dds/DCPS/Service_Participant.h
index 61e6c0ce3ee..77eade0e4ef 100644
--- a/dds/DCPS/Service_Participant.h
+++ b/dds/DCPS/Service_Participant.h
@@ -20,6 +20,7 @@
 #include "ReactorTask_rch.h"
 #include "Recorder.h"
 #include "Replayer.h"
+#include "Statistics.h"
 #include "TimeSource.h"
 #include "unique_ptr.h"
 
@@ -149,6 +150,9 @@ const char COMMON_ORB_VERBOSE_LOGGING[] = "COMMON_ORB_VERBOSE_LOGGING";
 const char COMMON_PRINTER_VALUE_WRITER_INDENT[] = "COMMON_PRINTER_VALUE_WRITER_INDENT";
 const unsigned int COMMON_PRINTER_VALUE_WRITER_INDENT_default = 4;
 
+const char COMMON_STATISTICS_PERIOD[] = "COMMON_STATISTICS_PERIOD";
+const TimeDuration COMMON_STATISTICS_PERIOD_default;
+
 const char COMMON_SCHEDULER[] = "COMMON_SCHEDULER";
 const String COMMON_SCHEDULER_default = "";
 
@@ -365,8 +369,6 @@ class OpenDDS_Dcps_Export Service_Participant {
   void set_default_discovery(const Discovery::RepoKey& defaultDiscovery);
   Discovery::RepoKey get_default_discovery();
 
-
-
   /// Convert domainId to repository key.
   Discovery::RepoKey domain_to_repo(const DDS::DomainId_t domain) const;
 
@@ -579,13 +581,21 @@ class OpenDDS_Dcps_Export Service_Participant {
   void type_object_encoding(TypeObjectEncoding encoding);
   void type_object_encoding(const char* encoding);
 
+  unsigned int printer_value_writer_indent() const;
+  void printer_value_writer_indent(unsigned int value);
+
+  TimeDuration statistics_period() const;
+  void statistics_period(const TimeDuration& value);
+
   RcHandle<InternalTopic<NetworkInterfaceAddress> > network_interface_address_topic() const
   {
     return network_interface_address_topic_;
   }
 
-  unsigned int printer_value_writer_indent() const;
-  void printer_value_writer_indent(unsigned int value);
+  StatisticsTopic_rch statistics_topic() const
+  {
+    return statistics_topic_;
+  }
 
   ConfigTopic_rch config_topic() const
   {
@@ -861,6 +871,8 @@ class OpenDDS_Dcps_Export Service_Participant {
 
   RcHandle<InternalTopic<NetworkInterfaceAddress> > network_interface_address_topic_;
 
+  StatisticsTopic_rch statistics_topic_;
+
   ConfigTopic_rch config_topic_;
   RcHandle<ConfigStoreImpl> config_store_;
   ConfigReader_rch config_reader_;
diff --git a/dds/DCPS/Statistics.h b/dds/DCPS/Statistics.h
new file mode 100644
index 00000000000..fd10d27c48b
--- /dev/null
+++ b/dds/DCPS/Statistics.h
@@ -0,0 +1,51 @@
+/*
+ * Distributed under the OpenDDS License.
+ * See: http://www.opendds.org/license.html
+ */
+
+#ifndef OPENDDS_DCPS_STATISTICS_H
+#define OPENDDS_DCPS_STATISTICS_H
+
+#include "dcps_export.h"
+
+#ifndef ACE_LACKS_PRAGMA_ONCE
+#  pragma once
+#endif /* ACE_LACKS_PRAGMA_ONCE */
+
+#include "InternalDataReader.h"
+#include "InternalDataWriter.h"
+#include "InternalTopic.h"
+
+#include <dds/OpenddsDcpsExtC.h>
+
+OPENDDS_BEGIN_VERSIONED_NAMESPACE_DECL
+
+namespace OpenDDS {
+namespace DCPS {
+
+// The Statistics struct is defined in dds/OpenddsDcpsExt.idl
+
+template <>
+struct InternalTraits<Statistics> {
+  struct KeyCompare {
+    bool operator()(const Statistics& lhs, const Statistics& rhs) const
+    {
+      using ::operator<; // TAO::String_Manager's operator< is in the global namespace
+      return lhs.id < rhs.id;
+    }
+  };
+};
+
+typedef InternalDataWriter<Statistics> StatisticsDataWriter;
+typedef RcHandle<StatisticsDataWriter> StatisticsDataWriter_rch;
+
+typedef InternalDataReader<Statistics> StatisticsDataReader;
+typedef RcHandle<StatisticsDataReader> StatisticsDataReader_rch;
+
+typedef InternalTopic<Statistics> StatisticsTopic;
+typedef RcHandle<StatisticsTopic> StatisticsTopic_rch;
+
+}
+}
+OPENDDS_END_VERSIONED_NAMESPACE_DECL
+#endif
diff --git a/dds/OpenddsDcpsExt.idl b/dds/OpenddsDcpsExt.idl
index e6492eae24b..a7662b84a13 100644
--- a/dds/OpenddsDcpsExt.idl
+++ b/dds/OpenddsDcpsExt.idl
@@ -128,6 +128,20 @@ module OpenDDS
     };
 
     typedef sequence<TransportStatistics> TransportStatisticsSequence;
+
+    struct Statistic {
+      string name;
+      unsigned long long value;
+    };
+
+    typedef sequence<Statistic> StatisticSeq;
+
+    // top-level type for an InternalTopic -- see Service_Participant.h
+    struct Statistics {
+      @key string id;
+      StatisticSeq stats;
+    };
+
   }; // module DDS
 }; // module OpenDDS
 
diff --git a/docs/news.d/rtpsrelay-new-statistics.rst b/docs/news.d/rtpsrelay-new-statistics.rst
new file mode 100644
index 00000000000..842b697a1f8
--- /dev/null
+++ b/docs/news.d/rtpsrelay-new-statistics.rst
@@ -0,0 +1,6 @@
+.. news-prs: 4990
+
+.. news-start-section: Additions
+- Output from :option:`RtpsRelay -LogRelayStatistics` / :option:`RtpsRelay -PublishRelayStatistics` includes internal OpenDDS statistics
+
+.. news-end-section
diff --git a/java/dds/OpenDDS/DCPS/.gitignore b/java/dds/OpenDDS/DCPS/.gitignore
index 8b523302bc0..6be37fcf1eb 100644
--- a/java/dds/OpenDDS/DCPS/.gitignore
+++ b/java/dds/OpenDDS/DCPS/.gitignore
@@ -165,6 +165,14 @@
 /IncompatibleQosStatus.java
 /IncompatibleQosStatusHelper.java
 /IncompatibleQosStatusHolder.java
+/Statistic.java
+/StatisticHelper.java
+/StatisticHolder.java
+/StatisticSeqHelper.java
+/StatisticSeqHolder.java
+/Statistics.java
+/StatisticsHelper.java
+/StatisticsHolder.java
 /InternalThreadBuiltinTopicData.java
 /InternalThreadBuiltinTopicDataDataReader.java
 /InternalThreadBuiltinTopicDataDataReaderHelper.java
diff --git a/tests/unit-tests/dds/DCPS/Statistics.cpp b/tests/unit-tests/dds/DCPS/Statistics.cpp
new file mode 100644
index 00000000000..3853819166a
--- /dev/null
+++ b/tests/unit-tests/dds/DCPS/Statistics.cpp
@@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
+#include <dds/DCPS/Qos_Helper.h>
+
+#include <gtest/gtest.h>
+
+using namespace OpenDDS::DCPS;
+
+TEST(dds_DCPS_Statistics, key_compare)
+{
+  typedef InternalTraits<Statistics>::KeyCompare Comparator;
+  const Comparator cmp = Comparator();
+
+  const StatisticSeq empty, nonempty(1, 1, StatisticSeq::allocbuf(1), true);
+  const Statistics is1 = {"a", empty}, is2 = {"a", nonempty}, is3 = {"b", empty};
+
+  EXPECT_TRUE(cmp(is1, is3));
+  EXPECT_TRUE(cmp(is2, is3));
+  EXPECT_FALSE(cmp(is1, is2));
+  EXPECT_FALSE(cmp(is2, is1));
+  EXPECT_FALSE(cmp(is3, is1));
+  EXPECT_FALSE(cmp(is3, is2));
+}
diff --git a/tools/dds/rtpsrelaylib/Relay.idl b/tools/dds/rtpsrelaylib/Relay.idl
index 7dedd85d135..f3d722e6004 100644
--- a/tools/dds/rtpsrelaylib/Relay.idl
+++ b/tools/dds/rtpsrelaylib/Relay.idl
@@ -159,6 +159,20 @@ module RtpsRelay {
     unsigned long slots;
   };
 
+  @mutable
+  @autoid(HASH)
+  struct Statistic {
+    string name;
+    unsigned long long value;
+  };
+
+  @mutable
+  @autoid(HASH)
+  struct OpenDDSModuleStatistics {
+    string id;
+    sequence<Statistic> stats;
+  };
+
   const string RELAY_STATISTICS_TOPIC_NAME = "Relay Statistics";
   @topic
   @mutable
@@ -200,6 +214,7 @@ module RtpsRelay {
     unsigned long admission_queue_size;
     ClientPartitionStatistics client_partitions;
     RelayPartitionStatistics relay_partitions;
+    sequence<OpenDDSModuleStatistics> opendds_modules;
   };
 };
 
diff --git a/tools/rtpsrelay/CMakeLists.txt b/tools/rtpsrelay/CMakeLists.txt
index 316a20bd343..b4cca3fe4db 100644
--- a/tools/rtpsrelay/CMakeLists.txt
+++ b/tools/rtpsrelay/CMakeLists.txt
@@ -21,6 +21,7 @@ add_executable(RtpsRelay
   RelayHttpMetaDiscovery.cpp
   RelayParticipantStatusReporter.cpp
   RelayPartitionsListener.cpp
+  RelayStatisticsReporter.cpp
   RelayStatusReporter.cpp
   RelayThreadMonitor.cpp
   RtpsRelay.cpp
diff --git a/tools/rtpsrelay/RelayStatisticsReporter.cpp b/tools/rtpsrelay/RelayStatisticsReporter.cpp
new file mode 100644
index 00000000000..b45c5383771
--- /dev/null
+++ b/tools/rtpsrelay/RelayStatisticsReporter.cpp
@@ -0,0 +1,92 @@
+#include "RelayStatisticsReporter.h"
+
+namespace RtpsRelay {
+
+using OpenDDS::DCPS::DataReaderQosBuilder;
+using OpenDDS::DCPS::Statistics;
+using OpenDDS::DCPS::StatisticsDataReader;
+using OpenDDS::DCPS::make_rch;
+
+RelayStatisticsReporter::RelayStatisticsReporter(const Config& config, RelayStatisticsDataWriter_var writer)
+  : config_(config)
+  , writer_(writer)
+  , topic_name_(DDS::Topic_var(writer_->get_topic())->get_name())
+  , internal_reader_(make_rch<StatisticsDataReader>(DataReaderQosBuilder().reliability_reliable()))
+{
+  log_relay_statistics_.relay_id(config.relay_id());
+  publish_relay_statistics_.relay_id(config.relay_id());
+
+  if (config.log_relay_statistics() || config.publish_relay_statistics()) {
+    TheServiceParticipant->statistics_topic()->connect(internal_reader_);
+  }
+}
+
+RelayStatisticsReporter::~RelayStatisticsReporter()
+{
+  TheServiceParticipant->statistics_topic()->disconnect(internal_reader_);
+}
+
+void RelayStatisticsReporter::get_opendds_stats(RelayStatistics& out)
+{
+  StatisticsDataReader::SampleSequence samples;
+  OpenDDS::DCPS::InternalSampleInfoSequence infos;
+  internal_reader_->read(samples, infos, DDS::LENGTH_UNLIMITED, DDS::ANY_SAMPLE_STATE, DDS::ANY_VIEW_STATE, DDS::ALIVE_INSTANCE_STATE);
+
+  out.opendds_modules().resize(samples.size());
+  for (size_t idx = 0; idx != samples.size(); ++idx) {
+    Statistics& is = samples[idx];
+    OpenDDSModuleStatistics& mod = out.opendds_modules()[idx];
+    mod.id() = is.id;
+    mod.stats().resize(is.stats.length());
+    for (DDS::UInt32 idxStats = 0; idxStats < is.stats.length(); ++idxStats) {
+      mod.stats()[idxStats] = Statistic{is.stats[idxStats].name.in(), is.stats[idxStats].value};
+    }
+  }
+}
+
+void RelayStatisticsReporter::log_report(const OpenDDS::DCPS::MonotonicTimePoint& now, bool force)
+{
+  if (!log_helper_.prepare_report(log_relay_statistics_, now, force, config_.log_relay_statistics())) {
+    return;
+  }
+
+  get_opendds_stats(log_relay_statistics_);
+
+  ACE_DEBUG((LM_INFO, "(%P|%t) STAT: %C %C\n", topic_name_.in(), OpenDDS::DCPS::to_json(log_relay_statistics_).c_str()));
+
+  log_helper_.reset(log_relay_statistics_, now);
+  log_relay_statistics_.new_address_count(0);
+  log_relay_statistics_.expired_address_count(0);
+  log_relay_statistics_.admission_deferral_count(0);
+  log_relay_statistics_.max_ips_per_client(0);
+}
+
+void RelayStatisticsReporter::publish_report(ACE_Guard<ACE_Thread_Mutex>& guard,
+                                             const OpenDDS::DCPS::MonotonicTimePoint& now,
+                                             bool force)
+{
+  if (!publish_helper_.prepare_report(publish_relay_statistics_, now, force, config_.publish_relay_statistics())) {
+    return;
+  }
+
+  const auto writer_copy = writer_;
+  auto stats_copy = publish_relay_statistics_;
+
+  get_opendds_stats(stats_copy);
+
+  publish_helper_.reset(publish_relay_statistics_, now);
+  publish_relay_statistics_.new_address_count(0);
+  publish_relay_statistics_.expired_address_count(0);
+  publish_relay_statistics_.admission_deferral_count(0);
+  publish_relay_statistics_.max_ips_per_client(0);
+
+  guard.release();
+
+  const auto ret = writer_copy->write(stats_copy, DDS::HANDLE_NIL);
+  if (ret != DDS::RETCODE_OK) {
+    ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: "
+               "RelayStatisticsReporter::publish_report failed to write relay statistics\n"));
+  }
+}
+
+}
diff --git a/tools/rtpsrelay/RelayStatisticsReporter.h b/tools/rtpsrelay/RelayStatisticsReporter.h
index 59bf35620c5..6b6950b67b0 100644
--- a/tools/rtpsrelay/RelayStatisticsReporter.h
+++ b/tools/rtpsrelay/RelayStatisticsReporter.h
@@ -10,21 +10,17 @@
 #include "CommonIoStatsReportHelper.h"
 
 #include <dds/DCPS/JsonValueWriter.h>
+#include <dds/DCPS/Qos_Helper.h>
+#include <dds/DCPS/Statistics.h>
 
 namespace RtpsRelay {
 
 class RelayStatisticsReporter {
 public:
   RelayStatisticsReporter(const Config& config,
-                          RelayStatisticsDataWriter_var writer)
-    : config_(config)
-    , writer_(writer)
-  {
-    DDS::Topic_var topic = writer_->get_topic();
-    topic_name_ = topic->get_name();
-    log_relay_statistics_.relay_id(config.relay_id());
-    publish_relay_statistics_.relay_id(config.relay_id());
-  }
+                          RelayStatisticsDataWriter_var writer);
+
+  ~RelayStatisticsReporter();
 
   void input_message(size_t byte_count,
                      const OpenDDS::DCPS::TimeDuration& time,
@@ -402,7 +398,6 @@ class RelayStatisticsReporter {
   }
 
 private:
-
   void report(ACE_Guard<ACE_Thread_Mutex>& guard,
               const OpenDDS::DCPS::MonotonicTimePoint& now,
               bool force = false)
@@ -411,47 +406,12 @@ class RelayStatisticsReporter {
     publish_report(guard, now, force);
   }
 
-  void log_report(const OpenDDS::DCPS::MonotonicTimePoint& now,
-                  bool force)
-  {
-    if (!log_helper_.prepare_report(log_relay_statistics_, now, force, config_.log_relay_statistics())) {
-      return;
-    }
-
-    ACE_DEBUG((LM_INFO, "(%P|%t) STAT: %C %C\n", topic_name_.in(), OpenDDS::DCPS::to_json(log_relay_statistics_).c_str()));
-
-    log_helper_.reset(log_relay_statistics_, now);
-    log_relay_statistics_.new_address_count(0);
-    log_relay_statistics_.expired_address_count(0);
-    log_relay_statistics_.admission_deferral_count(0);
-    log_relay_statistics_.max_ips_per_client(0);
-  }
-
+  void log_report(const OpenDDS::DCPS::MonotonicTimePoint& now, bool force);
   void publish_report(ACE_Guard<ACE_Thread_Mutex>& guard,
                       const OpenDDS::DCPS::MonotonicTimePoint& now,
-                      bool force)
-  {
-    if (!publish_helper_.prepare_report(publish_relay_statistics_, now, force, config_.publish_relay_statistics())) {
-      return;
-    }
-
-    const auto writer_copy = writer_;
-    const auto stats_copy = publish_relay_statistics_;
-
-    publish_helper_.reset(publish_relay_statistics_, now);
-    publish_relay_statistics_.new_address_count(0);
-    publish_relay_statistics_.expired_address_count(0);
-    publish_relay_statistics_.admission_deferral_count(0);
-    publish_relay_statistics_.max_ips_per_client(0);
+                      bool force);
 
-    guard.release();
-
-    const auto ret = writer_copy->write(stats_copy, DDS::HANDLE_NIL);
-    if (ret != DDS::RETCODE_OK) {
-      ACE_ERROR((LM_ERROR, "(%P|%t) ERROR: "
-        "RelayStatisticsReporter::publish_report failed to write relay statistics\n"));
-    }
-  }
+  void get_opendds_stats(RelayStatistics& out);
 
   mutable ACE_Thread_Mutex mutex_;
   const Config& config_;
@@ -466,6 +426,8 @@ class RelayStatisticsReporter {
 
   RelayStatisticsDataWriter_var writer_;
   CORBA::String_var topic_name_;
+
+  OpenDDS::DCPS::StatisticsDataReader_rch internal_reader_;
 };
 
 }
diff --git a/tools/rtpsrelay/RtpsRelay.cpp b/tools/rtpsrelay/RtpsRelay.cpp
index 1376c4ad9f1..4c9dde4a887 100644
--- a/tools/rtpsrelay/RtpsRelay.cpp
+++ b/tools/rtpsrelay/RtpsRelay.cpp
@@ -347,6 +347,14 @@ int run(int argc, ACE_TCHAR* argv[])
   TheServiceParticipant->bit_autopurge_nowriter_samples_delay(one_minute);
   TheServiceParticipant->bit_autopurge_disposed_samples_delay(one_minute);
 
+  const bool either = config.log_relay_statistics() || config.publish_relay_statistics(),
+    both = config.log_relay_statistics() && config.publish_relay_statistics();
+  const auto interval = both ? std::min(config.log_relay_statistics(), config.publish_relay_statistics())
+    : (config.log_relay_statistics() ? config.log_relay_statistics() : config.publish_relay_statistics());
+  if (either) {
+    TheServiceParticipant->statistics_period(interval);
+  }
+
   // Set up the relay participant.
   DDS::DomainParticipantQos participant_qos;
   factory->get_default_participant_qos(participant_qos);


Reviews:
user: login: jrw972
body: In terms of naming, I think we should think about dropping "internal" from the name.  Yes, they are statistics internal to OpenDDS but they are intended for user consumption.

Should the periodic tasks that collect the statistics also log the statistics when configured to do so? 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T15:54:48Z
user: login: mitza-oci
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T16:02:38Z
user: login: jrw972
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T16:03:55Z
user: login: mitza-oci
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T16:09:50Z
user: login: jrw972
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T17:34:23Z
user: login: iguessthislldo
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T21:26:17Z
user: login: sonndinh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:51:56Z
user: login: mitza-oci
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:55:00Z
user: login: mitza-oci
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:57:29Z
user: login: sonndinh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T16:36:05Z
user: login: sonndinh
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-06T16:36:41Z
user: login: mitza-oci
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T19:03:38Z

Review comments:
diff_hunk: @@ -783,6 +800,19 @@ void RtpsDiscovery::request_remote_complete_type_objects(
   spdp->request_remote_complete_type_objects(remote_entity, remote_type_info, cond);
 }
 
+void RtpsDiscovery::write_stats(const MonotonicTimePoint&) const
+{
+  ACE_Guard<ACE_Thread_Mutex> guard(participants_lock_);
+  DCPS::InternalStatistics statistics;
+  for (DomainParticipantMap::const_iterator domain = participants_.begin(); domain != participants_.end(); ++domain) {
+    for (ParticipantMap::const_iterator part = domain->second.begin(); part != domain->second.end(); ++part) {
+      statistics.id = ("RtpsDiscovery " + DCPS::to_dds_string(domain->first) + ' ' + DCPS::to_string(part->first)).c_str();
path: dds/DCPS/RTPS/RtpsDiscovery.cpp
user: login: jrw972
body: Do we want to allow the user to give a friendly name to a participant and use it (if set) here?
created_at: 2025-05-01T15:38:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4762,6 +4781,68 @@ bool Spdp::has_domain_participant(const GUID_t& remote) const
   return has_discovered_participant(remote);
 }
 
+#ifndef OPENDDS_RTPS_UNITY_BUILD_ID
+#  define OPENDDS_RTPS_UNITY_BUILD_ID SpdpAnonymous
+#endif
+
+namespace { namespace OPENDDS_RTPS_UNITY_BUILD_ID {
+  const DDS::UInt32 Stats_Index_LeaseExpirations = 0,
+    Stats_Index_HandshakeDeadlines = 1,
+    Stats_Index_HandshakeResends = 2,
+    Stats_Index_DiscoveredParticipants = 3,
+    Stats_Index_TotalLocationUpdates = 4,
+    Stats_Index_TotalBuiltinPending = 5,
+    Stats_Index_TotalBuiltinAssociated = 6,
+    Stats_Index_TotalWriterPending = 7,
+    Stats_Index_TotalWriterAssociated = 8,
+    Stats_Index_TotalReaderPending = 9,
+    Stats_Index_TotalReaderAssociated = 10,
+    Stats_Len = 11;
+} }
+
+DCPS::InternalStatisticSeq Spdp::stats_template()
+{
+  using namespace OPENDDS_RTPS_UNITY_BUILD_ID;
+  const DCPS::InternalStatisticSeq sedp_template = Sedp::stats_template();
+  DCPS::InternalStatisticSeq stats(Stats_Len + sedp_template.length());
+  stats.length(Stats_Len + sedp_template.length());
+  stats[Stats_Index_LeaseExpirations].name = "LeaseExpirations";
+  stats[Stats_Index_HandshakeDeadlines].name = "HandshakeDeadlines";
+  stats[Stats_Index_HandshakeResends].name = "HandshakeResends";
+  stats[Stats_Index_DiscoveredParticipants].name = "DiscoveredParticipants";
+  stats[Stats_Index_TotalLocationUpdates].name = "TotalLocationUpdates";
+  stats[Stats_Index_TotalBuiltinPending].name = "TotalBuiltinPending";
+  stats[Stats_Index_TotalBuiltinAssociated].name = "TotalBuiltinAssociated";
+  stats[Stats_Index_TotalWriterPending].name = "TotalWriterPending";
+  stats[Stats_Index_TotalWriterAssociated].name = "TotalWriterAssociated";
+  stats[Stats_Index_TotalReaderPending].name = "TotalReaderPending";
+  stats[Stats_Index_TotalReaderAssociated].name = "TotalReaderAssociated";
+  for (DDS::UInt32 i = 0; i < sedp_template.length(); ++i) {
+    stats[Stats_Len + i].name = sedp_template[i].name;
+  }
+  return stats;
+}
+
+void Spdp::fill_stats(DCPS::InternalStatisticSeq& stats) const
path: dds/DCPS/RTPS/Spdp.cpp
user: login: jrw972
body: What about `directed_guids_`?
created_at: 2025-05-01T15:51:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -783,6 +800,19 @@ void RtpsDiscovery::request_remote_complete_type_objects(
   spdp->request_remote_complete_type_objects(remote_entity, remote_type_info, cond);
 }
 
+void RtpsDiscovery::write_stats(const MonotonicTimePoint&) const
+{
+  ACE_Guard<ACE_Thread_Mutex> guard(participants_lock_);
+  DCPS::InternalStatistics statistics;
+  for (DomainParticipantMap::const_iterator domain = participants_.begin(); domain != participants_.end(); ++domain) {
+    for (ParticipantMap::const_iterator part = domain->second.begin(); part != domain->second.end(); ++part) {
+      statistics.id = ("RtpsDiscovery " + DCPS::to_dds_string(domain->first) + ' ' + DCPS::to_string(part->first)).c_str();
path: dds/DCPS/RTPS/RtpsDiscovery.cpp
user: login: mitza-oci
body: For many applications, including RtpsRelay, the domain ID is sufficient.

I think it could be an option for future development.
created_at: 2025-05-01T16:02:38Z
author_association: MEMBER
diff_hunk: @@ -783,6 +800,19 @@ void RtpsDiscovery::request_remote_complete_type_objects(
   spdp->request_remote_complete_type_objects(remote_entity, remote_type_info, cond);
 }
 
+void RtpsDiscovery::write_stats(const MonotonicTimePoint&) const
+{
+  ACE_Guard<ACE_Thread_Mutex> guard(participants_lock_);
+  DCPS::InternalStatistics statistics;
+  for (DomainParticipantMap::const_iterator domain = participants_.begin(); domain != participants_.end(); ++domain) {
+    for (ParticipantMap::const_iterator part = domain->second.begin(); part != domain->second.end(); ++part) {
+      statistics.id = ("RtpsDiscovery " + DCPS::to_dds_string(domain->first) + ' ' + DCPS::to_string(part->first)).c_str();
path: dds/DCPS/RTPS/RtpsDiscovery.cpp
user: login: jrw972
body: For posterity, when using security, it could be the subject name.
created_at: 2025-05-01T16:03:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -783,6 +800,19 @@ void RtpsDiscovery::request_remote_complete_type_objects(
   spdp->request_remote_complete_type_objects(remote_entity, remote_type_info, cond);
 }
 
+void RtpsDiscovery::write_stats(const MonotonicTimePoint&) const
+{
+  ACE_Guard<ACE_Thread_Mutex> guard(participants_lock_);
+  DCPS::InternalStatistics statistics;
+  for (DomainParticipantMap::const_iterator domain = participants_.begin(); domain != participants_.end(); ++domain) {
+    for (ParticipantMap::const_iterator part = domain->second.begin(); part != domain->second.end(); ++part) {
+      statistics.id = ("RtpsDiscovery " + DCPS::to_dds_string(domain->first) + ' ' + DCPS::to_string(part->first)).c_str();
path: dds/DCPS/RTPS/RtpsDiscovery.cpp
user: login: mitza-oci
body: Two participants could use the same security credentials
created_at: 2025-05-01T16:09:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
path: tests/unit-tests/dds/DCPS/Statistics.cpp
user: login: sonndinh
body: ```suggestion
```
created_at: 2025-05-06T15:33:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
+#include <dds/DCPS/Qos_Helper.h>
+
+#include <gtest/gtest.h>
+
+using namespace OpenDDS::DCPS;
+
+TEST(dds_DCPS_Statistics, key_compare)
+{
+  typedef InternalTraits<Statistics>::KeyCompare Comparator;
+  const Comparator cmp = Comparator();
+
+  const StatisticSeq empty, nonempty(1, 1, StatisticSeq::allocbuf(1), true);
+  const Statistics is1 = {"a", empty}, is2 = {"a", nonempty}, is3 = {"b", empty};
path: tests/unit-tests/dds/DCPS/Statistics.cpp
user: login: sonndinh
body: My guess is the variable names such as `is1` were derived from the previous class name `InternalStatistics`. Should the variable names be changed now that the class name has changed?
created_at: 2025-05-06T15:42:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
+#include <dds/DCPS/Qos_Helper.h>
+
+#include <gtest/gtest.h>
+
+using namespace OpenDDS::DCPS;
+
+TEST(dds_DCPS_Statistics, key_compare)
+{
+  typedef InternalTraits<Statistics>::KeyCompare Comparator;
+  const Comparator cmp = Comparator();
+
+  const StatisticSeq empty, nonempty(1, 1, StatisticSeq::allocbuf(1), true);
+  const Statistics is1 = {"a", empty}, is2 = {"a", nonempty}, is3 = {"b", empty};
path: tests/unit-tests/dds/DCPS/Statistics.cpp
user: login: mitza-oci
body: They could be, but it's just in the unit test
created_at: 2025-05-06T15:55:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
path: tests/unit-tests/dds/DCPS/Statistics.cpp
user: login: mitza-oci
body: This is a common pattern in unit tests - the thing we're testing is its own "group"
created_at: 2025-05-06T15:57:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
+#include <dds/DCPS/Qos_Helper.h>
+
+#include <gtest/gtest.h>
+
+using namespace OpenDDS::DCPS;
+
+TEST(dds_DCPS_Statistics, key_compare)
+{
+  typedef InternalTraits<Statistics>::KeyCompare Comparator;
+  const Comparator cmp = Comparator();
+
+  const StatisticSeq empty, nonempty(1, 1, StatisticSeq::allocbuf(1), true);
+  const Statistics is1 = {"a", empty}, is2 = {"a", nonempty}, is3 = {"b", empty};
path: tests/unit-tests/dds/DCPS/Statistics.cpp
user: login: sonndinh
body: There is a similar name in RelayStatisticsReporter.cpp, but since it's only used in a short loop, I think its use is clear.
created_at: 2025-05-06T16:36:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+#include <dds/DCPS/Statistics.h>
+
+#include <dds/DCPS/Qos_Helper.h>
+
+#include <gtest/gtest.h>
+
+using namespace OpenDDS::DCPS;
+
+TEST(dds_DCPS_Statistics, key_compare)
+{
+  typedef InternalTraits<Statistics>::KeyCompare Comparator;
+  const Comparator cmp = Comparator();
+
+  const StatisticSeq empty, nonempty(1, 1, StatisticSeq::allocbuf(1), true);
+  const Statistics is1 = {"a", empty}, is2 = {"a", nonempty}, is3 = {"b", empty};
path: tests/unit-tests/dds/DCPS/Statistics.cpp
user: login: mitza-oci
body: I can change that in the next PR
created_at: 2025-05-06T19:03:38Z
author_association: MEMBER

PR comments:
user: login: mitza-oci
created_at: 2025-05-01T16:00:52Z
body: > In terms of naming, I think we should think about dropping "internal" from the name. Yes, they are statistics internal to OpenDDS but they are intended for user consumption.

I'm fine with that but it may be confusing with other things named similarly, like `Stats_T.h` and `WriterStats` in `DataReaderImpl.h`

> Should the periodic tasks that collect the statistics also log the statistics when configured to do so?

How about making that its own reader/listener?  That way the logging happens in one place instead of scattered.
author_association: MEMBER
user: login: jrw972
created_at: 2025-05-01T16:08:35Z
body: Logging in one place is a good idea.

Those stats are removed in the v4 branch.  That's doesn't mean anything but there will be no ambiguity when this feature is pulled into v4.
author_association: CONTRIBUTOR
user: login: mitza-oci
created_at: 2025-05-01T20:59:00Z
body: > > In terms of naming, I think we should think about dropping "internal" from the name. Yes, they are statistics internal to OpenDDS but they are intended for user consumption.
> 
> I'm fine with that but it may be confusing with other things named similarly, like `Stats_T.h` and `WriterStats` in `DataReaderImpl.h`

There's a naming conflict here https://github.com/OpenDDS/OpenDDS/actions/runs/14782684162/job/41504880416?pr=4990#step:6:49739  --  other ideas @jrw972 ?


author_association: MEMBER
user: login: jrw972
created_at: 2025-05-02T15:52:54Z
body: > > > In terms of naming, I think we should think about dropping "internal" from the name. Yes, they are statistics internal to OpenDDS but they are intended for user consumption.
> > 
> > 
> > I'm fine with that but it may be confusing with other things named similarly, like `Stats_T.h` and `WriterStats` in `DataReaderImpl.h`
> 
> There's a naming conflict here https://github.com/OpenDDS/OpenDDS/actions/runs/14782684162/job/41504880416?pr=4990#step:6:49739 -- other ideas @jrw972 ?

It's either rename `Statistics` in `monitor.idl` or revert back to internal statistics.  I don't have a strong preference.  We can always rename in v4.
author_association: CONTRIBUTOR
user: login: mitza-oci
created_at: 2025-05-02T15:54:58Z
body: > > > > In terms of naming, I think we should think about dropping "internal" from the name. Yes, they are statistics internal to OpenDDS but they are intended for user consumption.
> > > 
> > > 
> > > I'm fine with that but it may be confusing with other things named similarly, like `Stats_T.h` and `WriterStats` in `DataReaderImpl.h`
> > 
> > 
> > There's a naming conflict here https://github.com/OpenDDS/OpenDDS/actions/runs/14782684162/job/41504880416?pr=4990#step:6:49739 -- other ideas @jrw972 ?
> 
> It's either rename `Statistics` in `monitor.idl` or revert back to internal statistics. I don't have a strong preference. We can always rename in v4.

Since monitor is in its own shared library, we can put it in its own namespace.  Other code isn't depending on the namespace.
author_association: MEMBER
user: login: iguessthislldo
created_at: 2025-05-05T21:29:41Z
body: Need to add the new header to the header set so it's installed:

https://github.com/OpenDDS/OpenDDS/blob/9f2a9e8dd4e750de4dd01f849e800cda46d400ef/dds/CMakeLists.txt#L196-L197
author_association: MEMBER