-
# frozen_string_literal: true
-
-
1
module Monotime
-
# A measurement from the operating system's monotonic clock, with up to
-
# nanosecond precision.
-
1
class Instant
-
# A measurement, in nanoseconds. Should be considered opaque and
-
# non-portable outside the process that created it.
-
1
attr_reader :ns
-
1
protected :ns
-
-
1
include Comparable
-
-
1
class << self
-
# @overload clock_id
-
# The +Process.clock_gettime+ clock id used to create +Instant+ instances
-
# by the default monotonic function.
-
#
-
# @return [Numeric]
-
#
-
# @overload clock_id=(id)
-
#
-
# Override the default +Process.clock_gettime+ clock id. Some potential
-
# choices include but are not limited to:
-
#
-
# * +Process::CLOCK_MONOTONIC_RAW+
-
# * +Process::CLOCK_UPTIME_RAW+
-
# * +Process::CLOCK_UPTIME_PRECISE+
-
# * +Process::CLOCK_UPTIME_FAST+
-
# * +Process::CLOCK_UPTIME+
-
# * +Process::CLOCK_MONOTONIC_PRECISE+
-
# * +Process::CLOCK_MONOTONIC_FAST+
-
# * +Process::CLOCK_MONOTONIC+
-
# * +:MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC+
-
# * +:TIMES_BASED_CLOCK_MONOTONIC+
-
#
-
# These are platform-dependant and may vary in resolution, accuracy,
-
# performance, and behaviour in light of system suspend/resume and NTP
-
# frequency skew. They should be selected carefully based on your specific
-
# needs and environment.
-
#
-
# It is possible to set non-monotonic clock sources here. You probably
-
# shouldn't.
-
#
-
# Defaults to auto-selection from whatever is available from:
-
#
-
# * +CLOCK_UPTIME_RAW+ (if running under macOS)
-
# * +CLOCK_MONOTONIC+
-
# * +CLOCK_REALTIME+ (non-monotonic fallback, issues a run-time warning)
-
#
-
# @param id [Numeric, Symbol]
-
1
attr_accessor :clock_id
-
-
# The function used to create +Instant+ instances.
-
#
-
# This function must return a +Numeric+, monotonic count of nanoseconds
-
# since a fixed point in the past.
-
#
-
# Defaults to +-> { Process.clock_gettime(clock_id, :nanosecond) }+.
-
#
-
# @overload monotonic_function=(function)
-
# @param function [#call]
-
1
attr_accessor :monotonic_function
-
-
# Return the claimed resolution of the given clock id or the configured
-
# +clock_id+, as a +Duration+, or +nil+ if invalid.
-
#
-
# Note per Ruby issue #16740, the practical usability of this method is
-
# dubious and non-portable.
-
#
-
# @param clock [Numeric, Symbol] Optional clock id instead of default.
-
1
def clock_getres(clock = clock_id)
-
1
Duration.from_nanos(Integer(Process.clock_getres(clock, :nanosecond)))
-
rescue SystemCallError
-
# suppress errors
-
end
-
-
# The symbolic name of the currently-selected +clock_id+, if available.
-
#
-
# @return [Symbol, nil]
-
1
def clock_name
-
2
return clock_id if clock_id.is_a? Symbol
-
-
2
Process.constants.find do |c|
-
35
c.to_s.start_with?('CLOCK_') && Process.const_get(c) == clock_id
-
end
-
end
-
-
1
private
-
-
# Symbolised Process constant name and condition pairs in order of preference
-
CLOCKS = [
-
# Follow Rust's footsteps using Mach Absolute Time. This offers higher,
-
# nanosecond resolution over CLOCK_MONOTONIC, and appears to be slightly
-
# faster.
-
1
[:CLOCK_UPTIME_RAW, -> { RUBY_PLATFORM.include?('darwin') }],
-
1
[:CLOCK_MONOTONIC, -> { true }],
-
# Other fallbacks such as :TIMES_BASED_CLOCK_MONOTONIC appear unreliable
-
# and are probably even less worth using than this.
-
[:CLOCK_REALTIME, lambda {
-
warn 'No monotonic clock source detected, falling back to CLOCK_REALTIME'
-
true
-
}]
-
].freeze
-
-
1
private_constant :CLOCKS
-
-
1
def select_clock_id
-
4
clock = CLOCKS.select { |const, _| Process.const_defined?(const) }
-
1
.find { |_, condition| condition.call }
-
-
1
raise NotImplementedError, 'No clock source found' unless clock
-
-
1
Process.const_get(clock[0])
-
end
-
end
-
-
50
self.monotonic_function = -> { Process.clock_gettime(clock_id, :nanosecond) }
-
1
self.clock_id = select_clock_id
-
-
# Create a new +Instant+ from an optional nanosecond measurement.
-
#
-
# Users should generally *not* pass anything to this function.
-
#
-
# @param nanos [Integer]
-
# @see #now
-
1
def initialize(nanos = self.class.monotonic_function.call)
-
64
@ns = Integer(nanos)
-
64
freeze
-
end
-
-
# An alias to +new+, and generally preferred over it.
-
#
-
# @return [Instant]
-
1
def self.now
-
51
new
-
end
-
-
# Return a +Duration+ between this +Instant+ and another.
-
#
-
# @param earlier [Instant]
-
# @return [Duration]
-
1
def duration_since(earlier)
-
15
raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant)
-
-
# `earlier - self` is cleaner, but upsets type checks and duplicates our
-
# type checks.
-
-
# @type var earlier: Instant
-
14
Duration.new(earlier.ns - @ns)
-
end
-
-
# Return a +Duration+ since this +Instant+ and now.
-
#
-
# @return [Duration]
-
1
def elapsed
-
14
duration_since(self.class.now)
-
end
-
-
# Return whether this +Instant+ is in the past.
-
#
-
# @return [Boolean]
-
1
def in_past?
-
2
elapsed.positive?
-
end
-
-
1
alias past? in_past?
-
-
# Return whether this +Instant+ is in the future.
-
#
-
# @return [Boolean]
-
1
def in_future?
-
2
elapsed.negative?
-
end
-
-
1
alias future? in_future?
-
-
# Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
-
# that's either positive if any time was slept, or negative if sleeping would
-
# require time travel.
-
#
-
# @example Sleeps for a second
-
# start = Instant.now
-
# sleep 0.5 # do stuff for half a second
-
# start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
-
# start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
-
#
-
# @example Also sleeps for a second.
-
# one_second_in_the_future = Instant.now + Duration.from_secs(1)
-
# one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
-
# one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
-
#
-
# @param duration [nil, Duration, #to_nanos]
-
# @return [Duration] the slept duration, if +#positive?+, else the overshot time
-
1
def sleep(duration = nil)
-
5
remaining = if duration
-
4
Duration.from_nanos(duration.to_nanos - elapsed.to_nanos)
-
else
-
1
-elapsed
-
end
-
-
10
remaining.tap { |rem| rem.sleep if rem.positive? }
-
end
-
-
# Sleep for the given number of seconds past this +Instant+, if any.
-
#
-
# Equivalent to +#sleep(Duration.from_secs(secs))+
-
#
-
# @param secs [Numeric] number of seconds to sleep past this +Instant+
-
# @return [Duration] the slept duration, if +#positive?+, else the overshot time
-
# @see #sleep
-
1
def sleep_secs(secs)
-
1
sleep(Duration.from_secs(secs))
-
end
-
-
# Sleep for the given number of milliseconds past this +Instant+, if any.
-
#
-
# Equivalent to +#sleep(Duration.from_millis(millis))+
-
#
-
# @param millis [Numeric] number of milliseconds to sleep past this +Instant+
-
# @return [Duration] the slept duration, if +#positive?+, else the overshot time
-
# @see #sleep
-
1
def sleep_millis(millis)
-
1
sleep(Duration.from_millis(millis))
-
end
-
-
# Sugar for +#elapsed.to_s+.
-
#
-
# @see Duration#to_s
-
1
def to_s(...)
-
1
elapsed.to_s(...)
-
end
-
-
# Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
-
# a new +Instant+.
-
#
-
# @example
-
# (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
-
#
-
# @param other [Duration, #to_nanos]
-
# @return [Instant]
-
1
def +(other)
-
9
raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
-
-
8
Instant.new(@ns + other.to_nanos)
-
end
-
-
# Subtract another +Instant+ to generate a +Duration+ between the two,
-
# or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
-
# offset by it.
-
#
-
# @example
-
# (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
-
# (Instant.now - Instant.now).to_s # => "-3.87μs"
-
#
-
# @param other [Instant, Duration, #to_nanos]
-
# @return [Duration, Instant]
-
1
def -(other)
-
7
if other.is_a?(Instant)
-
# @type var other: Instant
-
1
Duration.new(@ns - other.ns)
-
6
elsif other.respond_to?(:to_nanos)
-
# @type var other: Duration | _ToNanos
-
5
Instant.new(@ns - other.to_nanos)
-
else
-
1
raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
-
end
-
end
-
-
# Determine if the given +Instant+ is before, equal to or after this one.
-
# +nil+ if not passed an +Instant+.
-
#
-
# @return [-1, 0, 1, nil]
-
1
def <=>(other)
-
17
@ns <=> other.ns if other.is_a?(Instant)
-
end
-
-
# Determine if +other+'s value equals that of this +Instant+.
-
# Use +eql?+ if type checks are desired for future compatibility.
-
#
-
# @return [Boolean]
-
# @see #eql?
-
1
def ==(other)
-
4
other.is_a?(Instant) && @ns == other.ns
-
end
-
-
1
alias eql? ==
-
-
# Generate a hash for this type and value.
-
#
-
# @return [Integer]
-
1
def hash
-
10
[self.class, @ns].hash
-
end
-
end
-
end