Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -278,18 +278,24 @@ class NativeTopicSampler {
* @param topic_name Full topic path
* @return true if this is a system topic that should be filtered
*/
static bool is_system_topic(const std::string & topic_name);
static bool is_system_topic(const std::string & topic_name);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation broken: is_system_topic uses 4-space indent, project convention is 2-space (Google clang-format). Please run clang-format.


private:
/**
* @brief Get the message type for a topic from the graph
*/
std::string get_topic_type(const std::string & topic_name);
private:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private: label and get_topic_type have wrong indentation (4-space). Also, the Doxygen @brief comment for get_topic_type() was removed, please restore it.

std::string get_topic_type(const std::string & topic_name);
Comment on lines +281 to +284
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation is inconsistent. Lines 281 and 283-284 use different indentation from the surrounding lines. According to the project style (2-space indent per .clang-format), these lines should be indented consistently with the rest of the class definition.

Copilot uses AI. Check for mistakes.

rclcpp::Node * node_;

/// Native JSON serializer for topic deserialization
std::shared_ptr<ros2_medkit_serialization::JsonSerializer> serializer_;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /// Native JSON serializer for topic deserialization doc comment on serializer_ was removed. Please keep existing documentation.


// Getter for cached component-topic map
// Returns a copy to avoid exposing internal data while the mutex is released.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New get_component_topic_map() comment says "Returns a copy" which is good, but the method should have a proper Doxygen @brief block per project conventions (see other methods in this header)

std::map<std::string, ComponentTopics>
get_component_topic_map();

// --- Component topic map cache ---
std::map<std::string, ComponentTopics> topic_map_cache_;
size_t cached_graph_change_count_{0};
mutable std::mutex topic_map_mutex_;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header file declares std::mutex topic_map_mutex_ on line 298 but does not include the header. While this may compile due to transitive includes (possibly through rclcpp), it violates best practices to explicitly include headers for all types used directly in the interface. Add #include to the includes section.

Copilot uses AI. Check for mistakes.
};

} // namespace ros2_medkit_gateway
36 changes: 34 additions & 2 deletions src/ros2_medkit_gateway/src/native_topic_sampler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -486,11 +486,43 @@ std::map<std::string, ComponentTopics> NativeTopicSampler::build_component_topic
RCLCPP_DEBUG(node_->get_logger(), "Built topic map for %zu components", component_map.size());
return component_map;
}
std::map<std::string, ComponentTopics>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line between build_component_topic_map() closing brace (line 487) and get_component_topic_map(). Separate function definitions with a blank line per project style.

NativeTopicSampler::get_component_topic_map()
{
std::lock_guard<std::mutex> lock(topic_map_mutex_);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lock_guard holds topic_map_mutex_ for the entire duration including build_component_topic_map() (L514), which makes multiple expensive ROS 2 graph calls (get_publishers_info_by_topic + get_subscriptions_info_by_topic per topic). This blocks all concurrent callers. Better pattern: check fingerprint under lock → release lock → build outside lock → re-acquire lock → double-check → swap.


// Compute a lightweight fingerprint of the ROS graph (topics+types)
// Note: ROS 2 does not expose a simple graph change counter; using
// get_topic_names_and_types() and hashing the result is a reliable
// way to detect graph changes that affect topic/component mapping.
auto all_topics = node_->get_topic_names_and_types();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_component_topic_map() calls it here (L498) for fingerprinting, then build_component_topic_map() calls it AGAIN internally (L450). This doubles the graph query cost on every miss. Refactor build_component_topic_map() to accept the already-fetched topic list, or share the query result.


size_t current_graph_fingerprint = 1469598103934665603ULL; // FNV-1a seed-ish
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 1469598103934665603ULL and 0x9e3779b97f4a7c15ULL (L503, 507, 509) - these should be named constants (kFnvOffsetBasis, kHashCombineMagic) or just use std::hash

std::hash<std::string> hasher;
// Incorporate topic count
current_graph_fingerprint ^= all_topics.size() + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fingerprint only hashes topic names+types, but build_component_topic_map() also queries per-topic publisher/subscriber info. Cache won't invalidate when: a new node publishes to existing topic, a subscriber leaves, or node names change. Must also hash count_publishers() + count_subscribers() per topic into the fingerprint.

for (const auto & kv : all_topics) {
const auto & topic = kv.first;
const auto & types = kv.second;
current_graph_fingerprint ^= hasher(topic) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
if (!types.empty()) {
current_graph_fingerprint ^= hasher(types[0]) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
}
}
Comment on lines +494 to +511
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The graph fingerprinting approach only considers topics and types from get_topic_names_and_types(), but build_component_topic_map() also depends on publisher and subscriber node information via get_publishers_info_by_topic() and get_subscriptions_info_by_topic(). This means the cache may not be invalidated when:

  1. A new publisher/subscriber joins an existing topic (topic list unchanged, but component mapping changes)
  2. A publisher/subscriber leaves a topic (topic might still exist but component no longer publishes/subscribes)

Consider also incorporating the count of publishers/subscribers per topic into the fingerprint, or using a more comprehensive graph change detection mechanism.

Copilot uses AI. Check for mistakes.
Comment on lines +500 to +511
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic numbers 1469598103934665603ULL and 0x9e3779b97f4a7c15ULL appear to be FNV-1a hash constants, but the comment "FNV-1a seed-ish" is vague. For maintainability, these should either:

  1. Be defined as named constants with explanatory comments (e.g., FNV1A_OFFSET_BASIS and FNV1A_PRIME), or
  2. Use std::hash directly without attempting to replicate FNV-1a algorithm

The current implementation appears to be a custom hash combination that mixes std::hash with FNV-1a-like operations, which is non-standard and difficult to verify for correctness.

Suggested change
size_t current_graph_fingerprint = 1469598103934665603ULL; // FNV-1a seed-ish
std::hash<std::string> hasher;
// Incorporate topic count
current_graph_fingerprint ^= all_topics.size() + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
for (const auto & kv : all_topics) {
const auto & topic = kv.first;
const auto & types = kv.second;
current_graph_fingerprint ^= hasher(topic) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
if (!types.empty()) {
current_graph_fingerprint ^= hasher(types[0]) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
}
}
// Build a canonical string representation of the topic graph and hash it.
// This avoids custom hash-combine logic and magic constants while still
// providing a stable fingerprint for cache invalidation.
std::ostringstream graph_repr;
graph_repr << all_topics.size() << ';';
for (const auto & kv : all_topics) {
const auto & topic = kv.first;
const auto & types = kv.second;
graph_repr << topic << '|';
if (!types.empty()) {
graph_repr << types[0];
}
graph_repr << ';';
}
size_t current_graph_fingerprint = std::hash<std::string>{}(graph_repr.str());

Copilot uses AI. Check for mistakes.

if (topic_map_cache_.empty() || cached_graph_change_count_ != current_graph_fingerprint) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condition: topic_map_cache_.empty() - after the very first call, the cache will never be empty even if the graph truly has zero topics (empty map is still cached). Consider using std::optional or a bool flag cache_valid_ instead.

topic_map_cache_ = build_component_topic_map();
cached_graph_change_count_ = current_graph_fingerprint;
Comment on lines +494 to +515
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache invalidation check calls node_->get_topic_names_and_types() on every request to compute the fingerprint. This is a relatively expensive ROS 2 graph query that partially defeats the purpose of caching. While cheaper than building the full map, this still queries the ROS graph on every call.

Consider alternative approaches:

  1. Use a periodic refresh timer instead of on-demand checking
  2. Subscribe to graph events (though ROS 2 doesn't expose a simple callback mechanism)
  3. Accept that the cache may be slightly stale and refresh it periodically or on explicit invalidation calls
  4. At minimum, add performance benchmarking to verify this is actually faster than the uncached approach in realistic scenarios
Suggested change
// Compute a lightweight fingerprint of the ROS graph (topics+types)
// Note: ROS 2 does not expose a simple graph change counter; using
// get_topic_names_and_types() and hashing the result is a reliable
// way to detect graph changes that affect topic/component mapping.
auto all_topics = node_->get_topic_names_and_types();
size_t current_graph_fingerprint = 1469598103934665603ULL; // FNV-1a seed-ish
std::hash<std::string> hasher;
// Incorporate topic count
current_graph_fingerprint ^= all_topics.size() + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
for (const auto & kv : all_topics) {
const auto & topic = kv.first;
const auto & types = kv.second;
current_graph_fingerprint ^= hasher(topic) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
if (!types.empty()) {
current_graph_fingerprint ^= hasher(types[0]) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
}
}
if (topic_map_cache_.empty() || cached_graph_change_count_ != current_graph_fingerprint) {
topic_map_cache_ = build_component_topic_map();
cached_graph_change_count_ = current_graph_fingerprint;
// Throttle expensive ROS graph queries: only recompute the fingerprint
// if a minimum amount of time has elapsed since the last query.
using Clock = std::chrono::steady_clock;
static Clock::time_point last_graph_query_time{};
static size_t last_graph_fingerprint = 0U;
constexpr auto kMin_graph_refresh_interval = std::chrono::milliseconds(500);
const auto now = Clock::now();
// If we have a recent fingerprint and the cached graph change count matches,
// return the cached map without re-querying the ROS graph. This accepts a
// slightly stale cache in exchange for lower query overhead.
if (!topic_map_cache_.empty() &&
cached_graph_change_count_ == last_graph_fingerprint &&
last_graph_query_time != Clock::time_point{} &&
(now - last_graph_query_time) < kMin_graph_refresh_interval) {
// Return a copy to avoid callers holding a reference to internal cache
return topic_map_cache_;
}
// Compute a lightweight fingerprint of the ROS graph (topics+types)
// Note: ROS 2 does not expose a simple graph change counter; using
// get_topic_names_and_types() and hashing the result is a reliable
// way to detect graph changes that affect topic/component mapping.
auto all_topics = node_->get_topic_names_and_types();
last_graph_query_time = now;
size_t current_graph_fingerprint = 1469598103934665603ULL; // FNV-1a seed-ish
std::hash<std::string> hasher;
// Incorporate topic count
current_graph_fingerprint ^=
all_topics.size() + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
for (const auto & kv : all_topics) {
const auto & topic = kv.first;
const auto & types = kv.second;
current_graph_fingerprint ^=
hasher(topic) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
if (!types.empty()) {
current_graph_fingerprint ^=
hasher(types[0]) + 0x9e3779b97f4a7c15ULL + (current_graph_fingerprint << 6) + (current_graph_fingerprint >> 2);
}
}
if (topic_map_cache_.empty() || cached_graph_change_count_ != current_graph_fingerprint) {
topic_map_cache_ = build_component_topic_map();
cached_graph_change_count_ = current_graph_fingerprint;
last_graph_fingerprint = current_graph_fingerprint;

Copilot uses AI. Check for mistakes.
}

// Return a copy to avoid callers holding a reference to internal cache
return topic_map_cache_;
}


ComponentTopics NativeTopicSampler::get_component_topics(const std::string & component_fqn) {
// Build full map and extract for this component
// TODO(optimization): Cache the map and invalidate on graph changes
auto full_map = build_component_topic_map();
const auto full_map = get_component_topic_map();

auto it = full_map.find(component_fqn);
if (it != full_map.end()) {
Expand Down
Loading