File | % covered | Lines | Relevant Lines | Lines covered | Lines missed | Avg. Hits / Line |
---|---|---|---|---|---|---|
lib/monotime/instant.rb | 97.14 % | 296 | 70 | 68 | 2 | 6.40 |
lib/monotime.rb | 100.00 % | 9 | 4 | 4 | 0 | 1.00 |
lib/monotime/duration.rb | 100.00 % | 360 | 93 | 93 | 0 | 11.90 |
# frozen_string_literal: true
- 1
require_relative 'monotime/version'
- 1
require_relative 'monotime/duration'
- 1
require_relative 'monotime/instant'
# The +monotime+ namespace
- 1
module Monotime
end
# frozen_string_literal: true
- 1
module Monotime
# A type representing a span of time in nanoseconds.
- 1
class Duration
- 1
include Comparable
# Create a new +Duration+ of a specified number of nanoseconds, zero by
# default.
#
# Users are strongly advised to use +Duration.from_nanos+ instead.
#
# @param nanos [Integer]
# @see from_nanos
- 1
def initialize(nanos = 0)
- 108
@ns = Integer(nanos)
- 108
freeze
end
# A static instance for zero durations
- 2
ZERO = allocate.tap { |d| d.__send__(:initialize, 0) }
- 1
class << self
# The sleep function used by all +Monotime+ sleep functions.
#
# This function must accept a positive +Float+ number of seconds and return
# the +Float+ time slept.
#
# Defaults to +Kernel.method(:sleep)+
#
# @overload sleep_function=(function)
# @param function [#call]
- 1
attr_accessor :sleep_function
# Precision for +Duration#to_s+ if not otherwise specified
#
# Defaults to 9.
#
# @overload default_to_s_precision=(precision)
# @param precision [Numeric]
- 1
attr_accessor :default_to_s_precision
end
- 1
self.sleep_function = Kernel.method(:sleep)
- 1
self.default_to_s_precision = 9
- 1
class << self
# @!visibility private
- 1
def new(nanos = 0)
- 113
return ZERO if 0 == nanos # rubocop:disable Style/*
- 107
super
end
# Return a zero +Duration+.
#
# @return [Duration]
- 1
def zero
- 2
ZERO
end
# Generate a new +Duration+ measuring the given number of seconds.
#
# @param secs [Numeric]
# @return [Duration]
- 1
def from_secs(secs)
- 38
new(Integer(secs * 1_000_000_000))
end
- 1
alias secs from_secs
# Generate a new +Duration+ measuring the given number of milliseconds.
#
# @param millis [Numeric]
# @return [Duration]
- 1
def from_millis(millis)
- 13
new(Integer(millis * 1_000_000))
end
- 1
alias millis from_millis
# Generate a new +Duration+ measuring the given number of microseconds.
#
# @param micros [Numeric]
# @return [Duration]
- 1
def from_micros(micros)
- 14
new(Integer(micros * 1_000))
end
- 1
alias micros from_micros
# Generate a new +Duration+ measuring the given number of nanoseconds.
#
# @param nanos [Numeric]
# @return [Duration]
- 1
def from_nanos(nanos)
- 19
new(Integer(nanos))
end
- 1
alias nanos from_nanos
# Return a +Duration+ measuring the elapsed time of the yielded block.
#
# @example
# Duration.measure { sleep(0.5) }.to_s # => "512.226109ms"
#
# @return [Duration]
- 1
def measure
- 1
start = Instant.now
- 1
yield
- 1
start.elapsed
end
# Return the result of the yielded block alongside a +Duration+.
#
# @example
# Duration.with_measure { "bloop" } # => ["bloop", #<Monotime::Duration: ...>]
#
# @return [Object, Duration]
- 1
def with_measure
- 1
start = Instant.now
- 1
ret = yield
- 1
[ret, start.elapsed]
end
end
# Add another +Duration+ or +#to_nanos+-coercible object to this one,
# returning a new +Duration+.
#
# @example
# (Duration.from_secs(10) + Duration.from_secs(5)).to_s # => "15s"
#
# @param other [Duration, #to_nanos]
# @return [Duration]
- 1
def +(other)
- 5
raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
- 4
Duration.new(to_nanos + other.to_nanos)
end
# Subtract another +Duration+ or +#to_nanos+-coercible object from this one,
# returning a new +Duration+.
#
# @example
# (Duration.from_secs(10) - Duration.from_secs(5)).to_s # => "5s"
#
# @param other [Duration, #to_nanos]
# @return [Duration]
- 1
def -(other)
- 2
raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
- 1
Duration.new(to_nanos - other.to_nanos)
end
# Divide this duration by a +Numeric+.
#
# @example
# (Duration.from_secs(10) / 2).to_s # => "5s"
#
# @param other [Numeric]
# @return [Duration]
- 1
def /(other)
- 2
Duration.new(to_nanos / other)
end
# Multiply this duration by a +Numeric+.
#
# @example
# (Duration.from_secs(10) * 2).to_s # => "20s"
#
# @param other [Numeric]
# @return [Duration]
- 1
def *(other)
- 2
Duration.new(to_nanos * other)
end
# Unary minus: make a positive +Duration+ negative, and vice versa.
#
# @example
# -Duration.from_secs(-1).to_s # => "1s"
# -Duration.from_secs(1).to_s # => "-1s"
#
# @return [Duration]
- 1
def -@
- 3
Duration.new(-to_nanos)
end
# Return a +Duration+ that's absolute (positive).
#
# @example
# Duration.from_secs(-1).abs.to_s # => "1s"
# Duration.from_secs(1).abs.to_s # => "1s"
#
# @return [Duration]
- 1
def abs
- 3
return self if positive? || zero?
- 2
Duration.new(to_nanos.abs)
end
# Compare the *value* of this +Duration+ with another, or any +#to_nanos+-coercible
# object, or nil if not comparable.
#
# @param other [Duration, #to_nanos, Object]
# @return [-1, 0, 1, nil]
- 1
def <=>(other)
- 7
to_nanos <=> other.to_nanos if other.respond_to?(:to_nanos)
end
# Compare the equality of the *value* of this +Duration+ with another, or
# any +#to_nanos+-coercible object, or nil if not comparable.
#
# @param other [Duration, #to_nanos, Object]
# @return [Boolean]
- 1
def ==(other)
- 21
other.respond_to?(:to_nanos) && to_nanos == other.to_nanos
end
# Check equality of the value and type of this +Duration+ with another.
#
# @param other [Duration, Object]
# @return [Boolean]
- 1
def eql?(other)
- 3
other.is_a?(Duration) && to_nanos == other.to_nanos
end
# Generate a hash for this type and value.
#
# @return [Integer]
- 1
def hash
- 10
[self.class, to_nanos].hash
end
# Return this +Duration+ in seconds.
#
# @return [Float]
- 1
def to_secs
- 7
to_nanos / 1_000_000_000.0
end
- 1
alias secs to_secs
# Return this +Duration+ in milliseconds.
#
# @return [Float]
- 1
def to_millis
- 5
to_nanos / 1_000_000.0
end
- 1
alias millis to_millis
# Return this +Duration+ in microseconds.
#
# @return [Float]
- 1
def to_micros
- 2
to_nanos / 1_000.0
end
- 1
alias micros to_micros
# Return this +Duration+ in nanoseconds.
#
# @return [Integer]
- 1
def to_nanos
- 183
@ns
end
- 1
alias nanos to_nanos
# Return true if this +Duration+ is positive.
#
# @return [Boolean]
- 1
def positive?
- 13
to_nanos.positive?
end
# Return true if this +Duration+ is negative.
#
# @return [Boolean]
- 1
def negative?
- 9
to_nanos.negative?
end
# Return true if this +Duration+ is zero.
#
# @return [Boolean]
- 1
def zero?
- 5
to_nanos.zero?
end
# Return true if this +Duration+ is non-zero.
#
# @return [Boolean]
- 1
def nonzero?
- 3
to_nanos.nonzero?
end
# Sleep for the duration of this +Duration+. Equivalent to
# +Kernel.sleep(duration.to_secs)+.
#
# The sleep function may be overridden globally using +Duration.sleep_function=+
#
# @example
# Duration.from_secs(1).sleep # => 1
# Duration.from_secs(-1).sleep # => raises NotImplementedError
#
# @raise [NotImplementedError] negative +Duration+ sleeps are not yet supported.
# @return [Integer]
# @see Instant#sleep
# @see sleep_function=
- 1
def sleep
- 5
raise NotImplementedError, 'time travel module missing' if negative?
- 5
self.class.sleep_function.call(to_secs)
end
DIVISORS = [
- 1
[1_000_000_000.0, 's'],
[1_000_000.0, 'ms'],
[1_000.0, 'μs'],
[0.0, 'ns']
].map(&:freeze).freeze
- 1
private_constant :DIVISORS
# Format this +Duration+ into a human-readable string, with a given number
# of decimal places.
#
# The default precision may be set globally using +Duration.default_to_s_precision=+
#
# The exact format is subject to change, users with specific requirements
# are encouraged to use their own formatting methods.
#
# @example
# Duration.from_nanos(100).to_s # => "100ns"
# Duration.from_micros(100).to_s # => "100μs"
# Duration.from_millis(100).to_s # => "100ms"
# Duration.from_secs(100).to_s # => "100s"
# Duration.from_nanos(1234567).to_s # => "1.234567ms"
# Duration.from_nanos(1234567).to_s(2) # => "1.23ms"
#
# @param precision [Integer] the maximum number of decimal places
# @return [String]
# @see default_to_s_precision=
- 1
def to_s(precision = self.class.default_to_s_precision)
- 31
precision = Integer(precision).abs
- 31
nanos = to_nanos
# This is infallible provided DIVISORS has an entry for 0
- 103
div, unit = DIVISORS.find { |d, _| nanos.abs >= d }
- 31
if div&.zero?
- 3
format('%d%s', nanos, unit)
else
# `#' for `f' forces to show the decimal point.
- 28
format("%#.#{precision}f", nanos / div).sub(/\.?0*\z/, '') << unit.to_s
end
end
end
end
# 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