diff --git a/spec/std/time/measure_spec.cr b/spec/std/time/measure_spec.cr new file mode 100644 index 000000000000..cdc2ea4548c0 --- /dev/null +++ b/spec/std/time/measure_spec.cr @@ -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 + 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 diff --git a/src/crystal/system/unix/time.cr b/src/crystal/system/unix/time.cr index f93bb0818b51..69eeb9e3e6c2 100644 --- a/src/crystal/system/unix/time.cr +++ b/src/crystal/system/unix/time.cr @@ -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 @@ -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 diff --git a/src/lib_c/x86_64-macosx-darwin/c/mach/mach_time.cr b/src/lib_c/x86_64-macosx-darwin/c/mach/mach_time.cr new file mode 100644 index 000000000000..306c8c325f59 --- /dev/null +++ b/src/lib_c/x86_64-macosx-darwin/c/mach/mach_time.cr @@ -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 diff --git a/src/time/measure.cr b/src/time/measure.cr new file mode 100644 index 000000000000..049536201540 --- /dev/null +++ b/src/time/measure.cr @@ -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 + # 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. + # + # ``` + # 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