-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add Clock.monotonic for measuring elapsed time #3827
Conversation
src/clock.cr
Outdated
{% end %} | ||
|
||
def self.monotonic : self | ||
{% if flag?(:darwin) %} |
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.
Note that Sierra now supports clock_gettime
, but since the precision isn't as good, and more importantly it's not portable (a binary won't run on older versions) I chose to stick to mach_absolute_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.
@ysbaddaden perhaps you can make that comment into the code so the knowledge of this decision is not lost in the Pull Request?
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.
Done.
Specs are failing because 1 nanosecond didn't elapsed? Seriously? Or do I have a bug? |
@ysbaddaden In all math operations (like I think I wouldn't relate |
(though I guess the spec should pass even more, because it would give a Time::Span with value 0?) |
2058f34
to
1b8fb07
Compare
I made some corrections. Like |
1b8fb07
to
9c78085
Compare
Green! Now, some questions:
|
@ysbaddaden It's a 10th of a microsecond precision , not a millisecond. There are 10,000 ticks per millisecond. That code comment was noted as wrong a long time ago, and I thought it got changed. |
I would be in favour of this implementation storing time in ticks since an undefined epoch (instead of seconds since an undefined epoch) to keep it in sync with the other time implementations. |
@RX14 how would you achieve that? |
@ysbaddaden you would get nanosecond values from the OS in an Int64, then just do an integer division by 100 to get your value in ticks, which you store in the class just like |
Ah, I wrongly read you and thought you wanted ticks since the UNIX epoch... I understand now. I tried to store an UInt64, but got some weird errors, so I scrapped it; as long as the public API is accepted, we can still refactor the internals later. |
ff0b213
to
5534f12
Compare
Technical debt accumulates over time. If we know that a better implementation exists, why merge a subpar one? I don't see any particularly urgent reason this class needs to be in the stdlib right now. |
5534f12
to
4085ac0
Compare
Granted. I just did it. |
src/clock.cr
Outdated
include Comparable(Float64 | Float32) | ||
|
||
# :nodoc: | ||
TicksPerSeconds = 1_000_000_000 |
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 should be equal to Time::Span::TicksPerSecond
IMHO.
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.
No, Clock
and Time::Span
don't have the same precision: 1ns vs 100ns.
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 realised, just wasn't sure if there was any need for the difference. We throw away precision in Time.compute_second_and_tenth_microsecond
on linux. I think it's probably for the best to keep the higher precision here however.
38e5d9c
to
119c9a5
Compare
I added a commit that changes This reduces the number of days that An issue is that ticks for |
@ysbaddaden Awesome! I wouldn't mind changing the internal representation of Time (and of course Time::Span) to allow nanosecond precision. If it doesn't fit in a Int64 then lets expand it. For example we could store nano seconds in a separate variable or something like that. LLVM also supports 128-bits integers. I know Rust added support for them some weeks ago. I tried to do the same in Crystal and it was pretty easy and worked well, I just didn't committed it or told anyone because I wanted to discuss it first... for example I don't know if 128-bits integers are supported in all platforms (if LLVM does the transforms in all of them). In any case, if we eventually end up using 128 bits it's the same as now using two Int64s, so I'd go for it :-) Another thing that would be nice is if Time knew its location/offset (other than just utc/local/unknown)... that's another topic to discuss, but this will also make Time bigger, and I don't think it's bad. |
Expanding the Time struct to 128bits seems acceptable. We'd have enough space to add nanosecond precision and the TZ+DST offset. Let's try an Int128 :-) |
Reducing the length of a Time::Span from 30,000 years to just 200 seems like quite a drastic change. I'm fairly sure I use time spans longer than that when calculating Julian dates for predict.cr. I would be in favour of keeping Time::Span as is, but that doesn't solve the issue with Clock precision. |
Note that adding Int128 would take some time, so I'd probably start with an implementation that just uses a couple of Int64. |
@RX14 seriously? 😨 I guess we should have 2 ticks everywhere (seconds / nanoseconds). |
We can have the following struct Time
@seconds : Int64
@nanoseconds : Int32
@offset : Int16
@kind : Kind
enum Kind : Int8
Unspecified = 0
UTC = 1
Local = 2
end
end |
@ysbaddaden Nice! Though probably |
(though I wouldn't worry about |
Yeah, TZ and DST is hard. They can change at arbitrary dates for example. It's better and recommended to store the offset itself. I propose to keep the Kind, because there is some space, and we can know whether it's UTC or Local (e.g. London). Or maybe we don't care, it's both and that's it? |
Go's design document on this is a very interesting read: https://github.com/golang/proposal/blob/master/design/12914-monotonic.md It might be a good idea to do something similar so that users don't have to think whether they want monotonic time or not: they just use |
Now While it is a really amazing solution, I really don't think Crystal should follow Go's approach to merge the concepts of monotonic and wall time into one struct (see #4556 (issue-comment)). Based on #4556 (issue-comment) I'd suggest to rethink proper naming. Some ideas for the public API: # measure execution time of a block
Time.measure { do_something }
Clock.measure { do_something }
Stopwatch.measure { do_something }
# start a clock to take (multiple) measurements
x = Time.monotonic
x = Time::Measure.new
x = Time.clock
x = Time.stopwatch
x = Clock.start
x = Clock.new
x = Stopwatch.start
x = Stopwatch.new It would also be nice if a clock could be created with an initial time span. This would allow to serialize it and create a new one with time elapsed starting from X. This would require an instance variable for the time difference. This would also allow to stop and restart a clock. But this can be added later. |
Please make |
119c9a5
to
7b30bc8
Compare
Programmers should have to think about what they're doing 😜 This approach is not just doing everything right, it also has its drawbacks. And I'm not convinced it is worth going that way without any legacy concerns. The motivation for this solution was to casually fix existing code and avoid that future code using a different feature for monotonic time wouldn't compile under Go < 1.9. Some instances of Combining wall and monotonic time in the And keep in mind that, for simple measurements of elapsed time, using the block method should be the preferred style anyway; for that it doesn't matter if Crystal shouldn't be forced to |
Rebased but closed anyway. We can do better :) |
Time
uses a realtime clock which is subject to jumps and fluctuations over time (e.g. leap seconds, manually changing computer's time) which is a source of bugs when measuring elapsed time.A monotonic clock, in comparison, doesn't mean anything by itself but is guaranteed to be linearly increasing, thus useful to measure time duration more correctly. It's not perfect, since it's still affected by NTP adjustments (at least on Linux) but still better than realtime.
I propose here a new
Clock
type to mark the difference fromTime
, with sugarelapsed?
andduration(&block)
methods.Achieve work for a given duration:
Measuring how long a block takes to run:
The clock precision is platform specific, yet all calculations are achieved through
Time::Span
which has a 10th of millisecond precision.