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
Improve duration counting using monotonic clock #424
Conversation
When measuring durations, it's preferable to use the system monotonic clock rather than `Time.now` in order to avoid problems with the wall clock potentially moving forwards or backwards for corrections that would throw off calculation of elapsed time. https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
This extracts the stateful duration attributes from the Span class into a Duration class that is able to calculate duration for itself using the wall clock if times are explicitly set, or a monotonic clock if no times are explicitly set. This was extracted partially because the logic in Span was getting messy, and partly because Rubocop was complaining that the Span class was too long.
5dbb4b7
to
1d60503
Compare
@soulcutter You mentioned that this one might need to be shelved for a bit. If that's the case, would it make sense to close it now, and perhaps re-open it when you'd like to resubmit for consideration? |
Before working further on this, is this a change that you would consider merging when complete? |
If it improves the accuracy of our traces, I would certainly consider it. But before I make a promise about merging this, could you maybe share a summary list of benefits/drawbacks to sell me on this? |
I think the link in the description illustrates the problems with using "wall time" when measuring time - wall time doesn't always move forwards - there are cases where the wall time is inaccurate for measuring duration |
Related to: #625 (comment) Hey @delner / @mikhailov - I might be able to get this restarted (at this point I'd probably start fresh, though). I can easily-enough remove the ruby-concurrent dependency (although it's a solid lib used by Rails and many other professional apps & gems). Are there any other changes beyond that you'd look for, though - I saw |
thinkin about it, I'd be even MORE happy if y'all would pick it up. Seems like it would make it more likely to actually get merged if you were driving it. Also... like.. you're getting paid to do it, and it has been raised independently by 3 different people, one of which (me) put some time in to offer an approach. Like... can you prioritize it? It's not a big thing, but it's an important thing for timing accuracy which I think it important to your business. |
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.
Thank you for all the work so far, @soulcutter.
We are scheduling ourselves to finish this work, we'll pick it up soon!
I left a few comments as a reminder to ourselves on a few changes we need to do.
@@ -11,6 +13,8 @@ module Datadog | |||
# within a larger operation. Spans can be nested within each other, and in those instances | |||
# will have a parent-child relationship. | |||
class Span | |||
extend Forwardable |
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.
Forwardable has some performance that we'd like to avoid. It allocates a new object on every method call, which is not desirable. It is also slower, but I'm more concerned with the memory allocation for each call to the delegated methods:
require 'benchmark/ips'
require 'benchmark/memory'
require 'forwardable'
class Foo
extend Forwardable
def initialize
@delegate = Bar.new
end
def_delegators :@delegate, :perform
def def_perform
@delegate.perform
end
end
class Bar
def perform; end
end
instance = Foo.new
Benchmark.memory do |x|
x.report('forwardable') { instance.perform }
x.report('def perform') { instance.def_perform }
x.compare!
end
Benchmark.ips do |x|
x.report('forwardable') { instance.perform }
x.report('def perform') { instance.def_perform }
x.compare!
end
Calculating -------------------------------------
forwardable 40.000 memsize ( 0.000 retained)
1.000 objects ( 0.000 retained)
0.000 strings ( 0.000 retained)
def perform 0.000 memsize ( 0.000 retained)
0.000 objects ( 0.000 retained)
0.000 strings ( 0.000 retained)
Comparison:
def perform: 0 allocated
forwardable: 40 allocated - Infx more
Warming up --------------------------------------
forwardable 831.919k i/100ms
def perform 1.807M i/100ms
Calculating -------------------------------------
forwardable 7.722M (± 6.1%) i/s - 39.100M in 5.082914s
def perform 18.442M (± 2.6%) i/s - 92.146M in 5.000256s
Comparison:
def perform: 18441718.1 i/s
forwardable: 7722268.6 i/s - 2.39x (± 0.00) slower
end | ||
|
||
def duration_marker | ||
Concurrent.monotonic_time |
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 can use the existing Datadog::Utils::Time.get_time
monotonic time method.
With this, we can remove the dependency on concurrent-ruby
.
When measuring durations, it's preferable to use the system monotonic
clock rather than
Time.now
in order to avoid problems with the wallclock potentially moving forwards or backwards for corrections that
would throw off calculation of elapsed time.
https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
This was written in order to preserve the existing behavior in cases where the start time or end time were manually set.
NEW BEHAVIOR
start
andfinish
called in-order, with no explicitstart_time
orfinish_time
given it will use the monotonic clock to measure duration.Existing (preserved) behavior
finish
called without first having calledstart
the duration will be0
start
called with an explicitstart_time
, andfinish
called with no explicitfinish_time
then duration uses the wall clock for the end timestart
with nostart_time
, andfinish
called with an explicitfinish_time
, duration uses the wall clock for the start timeTODO
appraisal rake test:grape