New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce memory usage of the HTTP transport #1165
Conversation
1300ffa
to
468b9d1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we separate our the changes of direct msgpack encoding from the transport changes?
Can we add benchmark suite for the msgpack changes on it's own? (e.g. encoding a trace vs using the transport)
lib/ddtrace/span.rb
Outdated
packer.write_map_header(11) # Set header with how many elements in the map | ||
end | ||
|
||
packer.write(:span_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these symbols end up being re-encoded every time? If so can it be memoized? (Saying this knowing nothing about the Ruby msgpack library, or Ruby itself 😆)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are converted to string by MessagePack, but Ruby has an internalized string for each symbol.
Although I think it's worth trying it with strings directly, as there's a chance Ruby could create a copy of that internalized string each time we ask for it.
I'll report on results here, thanks for the heads up!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It did improve the results, thank you @Kyle-Verhoog! 🎉
Memory usage stayed the same, as symbols already have an internal string representation, but performance was improved.
@@ -269,36 +271,36 @@ def to_msgpack(packer = nil) | |||
if !@start_time.nil? && !@end_time.nil? | |||
packer.write_map_header(13) # Set header with how many elements in the map | |||
|
|||
packer.write(:start) | |||
packer.write('start') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
worth adding a comment on why we use strings instead of symbols?
any benefits to making these constants/freezing them outside the scope of this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any benefits to making these constants/freezing them outside the scope of this method?
Instead of doing that, given there are so many strings, I added # frozen_string_literal: true
to the top of the file, which freezes all strings in this. I did check all strings declared in this file and they are all safe to freeze.
I also benchmark only the # frozen_string_literal: true
change, to see if the other strings frozen in the file would make a change to our numbers, but they didn't, so the performance improvement does come from the change from symbol to string only.
worth adding a comment on why we use strings instead of symbols?
I'll add a comment for that, good call!
I thought I had a comment somewhere, but don't see it. Do we have benchmarks specifically for the encoding piece? e.g. |
We could add these, and ultimately also test our JSON serialization. I think we have it pretty well covered at this moment, but we can add this more granular benchmark as well. |
More granular might make sense as we can use it to optimize that specific piece. e.g. if we want to improve |
@brettlangdon cool, I scheduled a separate follow up task to benchmark specifically the serialization. |
@marcotc that sounds good to me, thanks for tracking it! |
Should we separate the span encoding changes from the transport changes? e.g. add |
@brettlangdon I don't think we need to separate it. At the end of day, encoding is an integral part of the "please send these spans to Datadog" part of our tracer, which is what we are benchmarking here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What versions of Ruby have we benchmarked these changes with?
I see there is a case to use filter_map
on >= 2.7, what kind of performance difference is there from this in 2.6 vs 2.7 (same for the other pieces)?
lib/ddtrace/span.rb
Outdated
packer.write_map_header(13) # Set header with how many elements in the map | ||
|
||
packer.write('start') | ||
packer.write((@start_time.to_f * 1e9).to_i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is written in two places now, read only property?
#start_time_ns
+ #duration_ns
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, updated it.
I also changed the operation to perform fewer operations to get the value as nanoseconds.
The move to a separate method, combined with the arithmetic improvements increased performance bit a very tiny bit.
@brettlangdon Results are for the fastest version, 2.7. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm, the only real speed i could think of would be tinkering with the http client itself, but probably not worth the risks involved of swapping in a 3rd party library, and i guess would also increase memory usage of the tracer on startup
# DEV Initializing +Net::HTTP+ directly help us avoid expensive | ||
# options processing done in +Net::HTTP.start+: | ||
# https://github.com/ruby/ruby/blob/b2d96abb42abbe2e01f010ffc9ac51f0f9a50002/lib/net/http.rb#L614-L618 | ||
req = ::Net::HTTP.new(hostname, port, nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is kinda out there but have we considered using a 3rd party http library? Might not be worth the pain but I believe some other vendors use http.rb
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a follow up task to investigate this 👍
|
||
# DEV: We use strings as keys here, instead of symbols, as | ||
# DEV: MessagePack will ultimately convert them to strings. | ||
# DEV: By providing strings directly, we skip this indirection operation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be faster than defining a bunch of constants, ie
SPAN_ID = 'span_id'.freeze
...
...
...
packer.write(SPAN_ID)
etc etc ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvmd, i see discussion here: #1165 (comment)
encoded_traces = traces.map { |t| encode_one(t) }.reject(&:nil?) | ||
encoded_traces = if traces.respond_to?(:filter_map) | ||
# DEV Supported since Ruby 2.7, saves an intermediate object creation | ||
traces.filter_map { |t| encode_one(t) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
After rebasing changes to use monotonic clock, no visible performance impact can be measured. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
@ericmustin I forgot I based #1178 on top of this branch (because of some shared fixtures that it easier to write future benchmarks). Would you mind ✅ this again when you have some time? |
tl;dr: 21-50% memory reduction, 19-30% faster HTTP transport.
This PR reduces memory of our default HTTP transport. As result of these changes, performance has been increased as well.
The largest gains were due:
span.rb
: before we were creating an intermediateHash
object in order to serialize spans; now we directly interface with MessagePack objects.Net::HTTP.start
is inefficient when processing its named arguments: we now use a different interface to configure these options, which avoids the expensive processing step.One area of with large possible improvements is to use a different HTTP adapter. The native
Net::HTTP
is great because it's always available, but it has showed up many times in the memory profiler, due to many strings being created during the processing of HTTP requests and responses.Results
spec/ddtrace/benchmark/transport_benchmark_spec.rb
includes the benchmarks used to profile the tracer and produce the numbers reported here.Memory
CPU