Skip to content
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

Measure time with monotonic clock #5107

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
30 changes: 30 additions & 0 deletions spec/std/time/measure_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "spec"

describe Time::Measure do
it "Time.measure" do
elapsed = Time.measure { sleep 0.001 }
elapsed.should be >= 1.millisecond
end

it "returns elapsed time" do
timer = Time::Measure.new
previous = timer.elapsed

5.times do
elapsed = timer.elapsed
elapsed.should be >= previous
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add previous = elapsed after this line to ensure it is monotonic? Currently, the 3rd measurement might be smaller than the second.

end
end

it "elapsed?" do
timer = Time::Measure.new

# disabled: randomly fails
# timer.elapsed?(0.seconds).should be_true

timer.elapsed?(5.seconds).should be_false
timer.elapsed?(5.0).should be_false
sleep 0.001
timer.elapsed?(0.001).should be_true
end
end
33 changes: 33 additions & 0 deletions src/crystal/system/unix/time.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
require "c/sys/time"
require "c/time"

{% if flag?(:darwin) %}
# Darwin supports clock_gettime starting from macOS Sierra, but we can't
# use it because it would prevent running binaries built on macOS Sierra
# to run on older macOS releases.
#
# Furthermore, mach_absolute_time is reported to have a higher precision.
require "c/mach/mach_time"
{% end %}

module Crystal::System::Time
UnixEpochInSeconds = 62135596800_i64

Expand Down Expand Up @@ -37,4 +46,28 @@ module Crystal::System::Time
{timeval.tv_sec.to_i64 + UnixEpochInSeconds, timeval.tv_usec.to_i * 1_000}
{% end %}
end

def self.monotonic
{% if flag?(:darwin) %}
info = mach_timebase_info
nanoseconds = LibC.mach_absolute_time.to_i64 * info.numer / info.denom
{nanoseconds / 1_000_000_000, nanoseconds.remainder(1_000_000_000).to_i32}
{% else %}
if LibC.clock_gettime(LibC::CLOCK_MONOTONIC, out tp) == 1
raise Errno.new("clock_gettime(CLOCK_MONOTONIC)")
end
{tp.tv_sec.to_i64, tp.tv_nsec.to_i32}
{% end %}
end

{% if flag?(:darwin) %}
@@mach_timebase_info : LibC::MachTimebaseInfo?

private def self.mach_timebase_info
@@mach_timebase_info ||= begin
LibC.mach_timebase_info(out info)
info
end
end
{% end %}
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-macosx-darwin/c/mach/mach_time.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
lib LibC
struct MachTimebaseInfo
numer : UInt32
denom : UInt32
end

fun mach_timebase_info(info : MachTimebaseInfo*) : LibC::Int
fun mach_absolute_time : UInt64
end
72 changes: 72 additions & 0 deletions src/time/measure.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require "crystal/system/time"

struct Time
# Measure elapsed time.
#
# Time measurement relies on a monotonic clock, that should be independent to
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion:

Time measurement relies on a monotonic clock that is strictly linearly increasing.
This clock should be independent from time fluctuations, such as leap seconds, time zone adjustments or manual changes to the computer's wall time.

# time fluctuations, such as leap seconds or manually changing the computer
# time.
struct Measure
private getter seconds : Int64
private getter nanoseconds : Int32

# Starts a clock to measure elapsed time, or repeatedly report elapsed time
# since an initial start time.
def initialize
@seconds, @nanoseconds = Crystal::System::Time.monotonic
end

# Returns the time span since the clock was started.
#
# ```
# timer = Time::Measure.new
#
# loop do
# do_something
# p timer.elapsed # => 00:00:01.000000023
# end
# ```
def elapsed
seconds, nanoseconds = Crystal::System::Time.monotonic
Time::Span.new(seconds: seconds - @seconds, nanoseconds: nanoseconds - @nanoseconds)
end

# Returns true once *span* time has passed since the clock was created.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe it's better to use if instead of once. It feels to me like it might suggest this could be blocking until that amount of time has passed.

#
# ```
# timeout = 5.seconds
# timer = Time::Measure.new
#
# until timer.elapsed?(timeout)
# do_domething
# end
# ```
def elapsed?(span : Time::Span)
elapsed >= span
end

# Returns true once *span* seconds have passed since the clock was created.
#
# ```
# timer = Time::Measure.new
#
# until timer.elapsed?(5.0)
# do_domething
# end
# ```
def elapsed?(span : Int | Float)
elapsed?(span.seconds)
end
end

# Measures how long the block took to run.
#
# ```
# elapsed = Time.measure { do_something } # => 00:01:53.009871361
# ```
def self.measure(&block) : Time::Span
clock = Measure.new
yield
clock.elapsed
end
end