Skip to content

Optimize command and telemetry decom performance in Ruby and Python#2726

Merged
jmthomas merged 7 commits intomainfrom
performance
Jan 16, 2026
Merged

Optimize command and telemetry decom performance in Ruby and Python#2726
jmthomas merged 7 commits intomainfrom
performance

Conversation

@jmthomas
Copy link
Copy Markdown
Member

Summary

  • Optimize Ruby command building performance (~7% improvement in critical path)
  • Optimize Ruby telemetry decom path (~26% improvement by eliminating redundant JSON serialization)
  • Optimize Python telemetry decom path (~6% improvement)
  • Add performance benchmark tests for both Ruby and Python

Ruby Optimizations

  • Inline packet lookup in build_cmd to avoid repeated method calls
  • Use given_raw parameter in CommandDecomTopic.write_packet to avoid re-reading from buffer
  • Eliminate redundant JSON serialization in TelemetryDecomTopic by reusing pre-serialized JSON
  • Add CvtModel.set_json method to accept pre-serialized JSON
  • Remove unnecessary clone.freeze on received_time

Python Optimizations

  • Eliminate redundant JSON serialization in TelemetryDecomTopic (main improvement)
  • Add CvtModel.set_json() method to accept pre-serialized JSON
  • Convert RESERVED_ITEM_NAMES and VALUE_TYPES from list to set for O(1) lookup
  • Inline packet lookup in Commands.build_cmd to match Ruby implementation
  • Use given_raw parameter in CommandDecomTopic to match Ruby implementation

Throughput Summary (Ruby vs Python)

Path Ruby 3.4.5 Python 3.11.13
Full command path ~7,500 cmd/sec ~4,800 cmd/sec
Full decom path ~5,200 pkt/sec ~3,300 pkt/sec

Test plan

  • Ruby unit tests pass
  • Python unit tests pass
  • Performance benchmarks show expected improvements

🤖 Generated with Claude Code

jmthomas and others added 4 commits January 10, 2026 13:31
- Remove clone.freeze from received_time= in C extension and Ruby
- Replace JSON deep copy with deep_dup for packet.extra in clone
- Skip buffer.dup in internal_buffer_equals when buffer is frozen
- Add performance benchmarking spec for InterfaceMicroservice

Performance improvements:
- identify!: +16% faster
- update!: +15% faster
- Memory allocation: -70% reduction

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Inline packet lookup in build_cmd to avoid redundant upcase calls
- Use write_item directly in set_parameters to skip redundant get_item
- Change RESERVED_ITEM_NAMES from Array to Set for O(1) lookup
- Use batch read_items and given_raw in CommandDecomTopic to avoid
  re-reading values from buffer

Benchmarks show ~7.3% improvement in full command path (134.57 -> 124.79 μs)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…tion

- Add CvtModel.set_json method that accepts pre-serialized JSON
- Call as_json once in TelemetryDecomTopic.write_packet and reuse result
- Eliminates double as_json calls that were serializing same data twice

Benchmarks show ~24% improvement in TelemetryDecomTopic.write_packet
(228 -> 173 μs) and ~26% improvement in full decom path (242 -> 179 μs)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Eliminate redundant JSON serialization in TelemetryDecomTopic by reusing
  pre-serialized JSON for both topic write and CVT set
- Add CvtModel.set_json() method to accept pre-serialized JSON
- Convert RESERVED_ITEM_NAMES and VALUE_TYPES from list to set for O(1) lookup
- Inline packet lookup in Commands.build_cmd to match Ruby implementation
- Use given_raw parameter in CommandDecomTopic to match Ruby implementation

Add Python performance benchmark tests for command and telemetry paths.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@jmthomas jmthomas requested a review from ryanmelt January 10, 2026 21:56
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 10, 2026

Codecov Report

❌ Patch coverage is 89.28571% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.23%. Comparing base (0df95af) to head (dc8bd2a).
⚠️ Report is 29 commits behind head on main.

Files with missing lines Patch % Lines
openc3/lib/openc3/models/cvt_model.rb 85.71% 1 Missing ⚠️
openc3/lib/openc3/packets/structure.rb 66.66% 1 Missing ⚠️
openc3/lib/openc3/topics/command_decom_topic.rb 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2726      +/-   ##
==========================================
+ Coverage   79.22%   79.23%   +0.01%     
==========================================
  Files         670      670              
  Lines       54178    54206      +28     
  Branches      734      734              
==========================================
+ Hits        42923    42952      +29     
+ Misses      11175    11174       -1     
  Partials       80       80              
Flag Coverage Δ
python 81.01% <ø> (+0.02%) ⬆️
ruby-api 84.19% <ø> (+0.04%) ⬆️
ruby-backend 82.22% <89.28%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

json_hash[item.name + "__C"] = packet.read_item(item, :CONVERTED, packet.buffer, given_raw) if item.write_conversion or item.states
json_hash[item.name + "__F"] = packet.read_item(item, :FORMATTED, packet.buffer, given_raw) if item.format_string
json_hash[item.name + "__U"] = packet.read_item(item, :WITH_UNITS, packet.buffer, given_raw) if item.units
end
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This same logic is present in the packet decom method so I think it makes sense here

raise(ArgumentError, "received_time must be a Time but is a #{received_time.class}")
end
@received_time = received_time.clone.freeze
@received_time = received_time
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure why we were cloning and freezing here. Claude's analysis of received_time indicate there are no mutations found after assignment.

# Also update the current value table with the latest decommutated data
CvtModel.set(json_hash, target_name: packet.target_name, packet_name: packet.packet_name, scope: scope)
# Pass pre-serialized JSON to avoid calling as_json twice
CvtModel.set_json(json_data, json_safe_hash, target_name: packet.target_name, packet_name: packet.packet_name, scope: scope)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was a big optimization since we're not calling as_json twice

jmthomas and others added 2 commits January 12, 2026 21:12
- Make performance tests opt-in via PERFORMANCE=1 env var instead of
  excluding them via CI env var
- Reduce excessive sleep times in protocol and microservice specs
- Optimize limits_api_spec.rb: wrap DecomMicroservice setup in helper,
  only 3 of 36 tests actually need it (~33s saved)
- Optimize commands_spec.rb: setup InterfaceCmdHandlerThread only for
  connected tests that need it (~46s saved)
- Reduce sleep times in packet_log_writer, stream_log, template_protocol,
  cmd_response_protocol, and router_microservice specs

Total improvement: ~98 seconds saved (4:38 -> 3:00)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- test_handles_exceptions_in_user_processors: Add retry loop to wait for
  async decom processing instead of fixed 10ms sleep
- test_is_single_threaded: Fix variable shadowing bug where 'for threads
  in threads' caused incorrect thread joining, and relax timing threshold
  from 1.0s to 0.9s for CI variability

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@jmthomas jmthomas requested a review from clayandgen January 13, 2026 18:47
@jmthomas jmthomas merged commit 6ba6927 into main Jan 16, 2026
27 of 32 checks passed
@jmthomas jmthomas deleted the performance branch January 16, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant