loading
Generated 2023-10-19T19:17:52+00:00

All Files ( 98.8% covered at 9.34 hits/line )

3 files in total.
167 relevant lines, 165 lines covered and 2 lines missed. ( 98.8% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/monotime.rb 100.00 % 9 4 4 0 1.00
lib/monotime/duration.rb 100.00 % 360 93 93 0 11.90
lib/monotime/instant.rb 97.14 % 296 70 68 2 6.40

lib/monotime.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative 'monotime/version'
  3. 1 require_relative 'monotime/duration'
  4. 1 require_relative 'monotime/instant'
  5. # The +monotime+ namespace
  6. 1 module Monotime
  7. end

lib/monotime/duration.rb

100.0% lines covered

93 relevant lines. 93 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Monotime
  3. # A type representing a span of time in nanoseconds.
  4. 1 class Duration
  5. 1 include Comparable
  6. # Create a new +Duration+ of a specified number of nanoseconds, zero by
  7. # default.
  8. #
  9. # Users are strongly advised to use +Duration.from_nanos+ instead.
  10. #
  11. # @param nanos [Integer]
  12. # @see from_nanos
  13. 1 def initialize(nanos = 0)
  14. 108 @ns = Integer(nanos)
  15. 108 freeze
  16. end
  17. # A static instance for zero durations
  18. 2 ZERO = allocate.tap { |d| d.__send__(:initialize, 0) }
  19. 1 class << self
  20. # The sleep function used by all +Monotime+ sleep functions.
  21. #
  22. # This function must accept a positive +Float+ number of seconds and return
  23. # the +Float+ time slept.
  24. #
  25. # Defaults to +Kernel.method(:sleep)+
  26. #
  27. # @overload sleep_function=(function)
  28. # @param function [#call]
  29. 1 attr_accessor :sleep_function
  30. # Precision for +Duration#to_s+ if not otherwise specified
  31. #
  32. # Defaults to 9.
  33. #
  34. # @overload default_to_s_precision=(precision)
  35. # @param precision [Numeric]
  36. 1 attr_accessor :default_to_s_precision
  37. end
  38. 1 self.sleep_function = Kernel.method(:sleep)
  39. 1 self.default_to_s_precision = 9
  40. 1 class << self
  41. # @!visibility private
  42. 1 def new(nanos = 0)
  43. 113 return ZERO if 0 == nanos # rubocop:disable Style/*
  44. 107 super
  45. end
  46. # Return a zero +Duration+.
  47. #
  48. # @return [Duration]
  49. 1 def zero
  50. 2 ZERO
  51. end
  52. # Generate a new +Duration+ measuring the given number of seconds.
  53. #
  54. # @param secs [Numeric]
  55. # @return [Duration]
  56. 1 def from_secs(secs)
  57. 38 new(Integer(secs * 1_000_000_000))
  58. end
  59. 1 alias secs from_secs
  60. # Generate a new +Duration+ measuring the given number of milliseconds.
  61. #
  62. # @param millis [Numeric]
  63. # @return [Duration]
  64. 1 def from_millis(millis)
  65. 13 new(Integer(millis * 1_000_000))
  66. end
  67. 1 alias millis from_millis
  68. # Generate a new +Duration+ measuring the given number of microseconds.
  69. #
  70. # @param micros [Numeric]
  71. # @return [Duration]
  72. 1 def from_micros(micros)
  73. 14 new(Integer(micros * 1_000))
  74. end
  75. 1 alias micros from_micros
  76. # Generate a new +Duration+ measuring the given number of nanoseconds.
  77. #
  78. # @param nanos [Numeric]
  79. # @return [Duration]
  80. 1 def from_nanos(nanos)
  81. 19 new(Integer(nanos))
  82. end
  83. 1 alias nanos from_nanos
  84. # Return a +Duration+ measuring the elapsed time of the yielded block.
  85. #
  86. # @example
  87. # Duration.measure { sleep(0.5) }.to_s # => "512.226109ms"
  88. #
  89. # @return [Duration]
  90. 1 def measure
  91. 1 start = Instant.now
  92. 1 yield
  93. 1 start.elapsed
  94. end
  95. # Return the result of the yielded block alongside a +Duration+.
  96. #
  97. # @example
  98. # Duration.with_measure { "bloop" } # => ["bloop", #<Monotime::Duration: ...>]
  99. #
  100. # @return [Object, Duration]
  101. 1 def with_measure
  102. 1 start = Instant.now
  103. 1 ret = yield
  104. 1 [ret, start.elapsed]
  105. end
  106. end
  107. # Add another +Duration+ or +#to_nanos+-coercible object to this one,
  108. # returning a new +Duration+.
  109. #
  110. # @example
  111. # (Duration.from_secs(10) + Duration.from_secs(5)).to_s # => "15s"
  112. #
  113. # @param other [Duration, #to_nanos]
  114. # @return [Duration]
  115. 1 def +(other)
  116. 5 raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
  117. 4 Duration.new(to_nanos + other.to_nanos)
  118. end
  119. # Subtract another +Duration+ or +#to_nanos+-coercible object from this one,
  120. # returning a new +Duration+.
  121. #
  122. # @example
  123. # (Duration.from_secs(10) - Duration.from_secs(5)).to_s # => "5s"
  124. #
  125. # @param other [Duration, #to_nanos]
  126. # @return [Duration]
  127. 1 def -(other)
  128. 2 raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
  129. 1 Duration.new(to_nanos - other.to_nanos)
  130. end
  131. # Divide this duration by a +Numeric+.
  132. #
  133. # @example
  134. # (Duration.from_secs(10) / 2).to_s # => "5s"
  135. #
  136. # @param other [Numeric]
  137. # @return [Duration]
  138. 1 def /(other)
  139. 2 Duration.new(to_nanos / other)
  140. end
  141. # Multiply this duration by a +Numeric+.
  142. #
  143. # @example
  144. # (Duration.from_secs(10) * 2).to_s # => "20s"
  145. #
  146. # @param other [Numeric]
  147. # @return [Duration]
  148. 1 def *(other)
  149. 2 Duration.new(to_nanos * other)
  150. end
  151. # Unary minus: make a positive +Duration+ negative, and vice versa.
  152. #
  153. # @example
  154. # -Duration.from_secs(-1).to_s # => "1s"
  155. # -Duration.from_secs(1).to_s # => "-1s"
  156. #
  157. # @return [Duration]
  158. 1 def -@
  159. 3 Duration.new(-to_nanos)
  160. end
  161. # Return a +Duration+ that's absolute (positive).
  162. #
  163. # @example
  164. # Duration.from_secs(-1).abs.to_s # => "1s"
  165. # Duration.from_secs(1).abs.to_s # => "1s"
  166. #
  167. # @return [Duration]
  168. 1 def abs
  169. 3 return self if positive? || zero?
  170. 2 Duration.new(to_nanos.abs)
  171. end
  172. # Compare the *value* of this +Duration+ with another, or any +#to_nanos+-coercible
  173. # object, or nil if not comparable.
  174. #
  175. # @param other [Duration, #to_nanos, Object]
  176. # @return [-1, 0, 1, nil]
  177. 1 def <=>(other)
  178. 7 to_nanos <=> other.to_nanos if other.respond_to?(:to_nanos)
  179. end
  180. # Compare the equality of the *value* of this +Duration+ with another, or
  181. # any +#to_nanos+-coercible object, or nil if not comparable.
  182. #
  183. # @param other [Duration, #to_nanos, Object]
  184. # @return [Boolean]
  185. 1 def ==(other)
  186. 21 other.respond_to?(:to_nanos) && to_nanos == other.to_nanos
  187. end
  188. # Check equality of the value and type of this +Duration+ with another.
  189. #
  190. # @param other [Duration, Object]
  191. # @return [Boolean]
  192. 1 def eql?(other)
  193. 3 other.is_a?(Duration) && to_nanos == other.to_nanos
  194. end
  195. # Generate a hash for this type and value.
  196. #
  197. # @return [Integer]
  198. 1 def hash
  199. 10 [self.class, to_nanos].hash
  200. end
  201. # Return this +Duration+ in seconds.
  202. #
  203. # @return [Float]
  204. 1 def to_secs
  205. 7 to_nanos / 1_000_000_000.0
  206. end
  207. 1 alias secs to_secs
  208. # Return this +Duration+ in milliseconds.
  209. #
  210. # @return [Float]
  211. 1 def to_millis
  212. 5 to_nanos / 1_000_000.0
  213. end
  214. 1 alias millis to_millis
  215. # Return this +Duration+ in microseconds.
  216. #
  217. # @return [Float]
  218. 1 def to_micros
  219. 2 to_nanos / 1_000.0
  220. end
  221. 1 alias micros to_micros
  222. # Return this +Duration+ in nanoseconds.
  223. #
  224. # @return [Integer]
  225. 1 def to_nanos
  226. 183 @ns
  227. end
  228. 1 alias nanos to_nanos
  229. # Return true if this +Duration+ is positive.
  230. #
  231. # @return [Boolean]
  232. 1 def positive?
  233. 13 to_nanos.positive?
  234. end
  235. # Return true if this +Duration+ is negative.
  236. #
  237. # @return [Boolean]
  238. 1 def negative?
  239. 9 to_nanos.negative?
  240. end
  241. # Return true if this +Duration+ is zero.
  242. #
  243. # @return [Boolean]
  244. 1 def zero?
  245. 5 to_nanos.zero?
  246. end
  247. # Return true if this +Duration+ is non-zero.
  248. #
  249. # @return [Boolean]
  250. 1 def nonzero?
  251. 3 to_nanos.nonzero?
  252. end
  253. # Sleep for the duration of this +Duration+. Equivalent to
  254. # +Kernel.sleep(duration.to_secs)+.
  255. #
  256. # The sleep function may be overridden globally using +Duration.sleep_function=+
  257. #
  258. # @example
  259. # Duration.from_secs(1).sleep # => 1
  260. # Duration.from_secs(-1).sleep # => raises NotImplementedError
  261. #
  262. # @raise [NotImplementedError] negative +Duration+ sleeps are not yet supported.
  263. # @return [Integer]
  264. # @see Instant#sleep
  265. # @see sleep_function=
  266. 1 def sleep
  267. 5 raise NotImplementedError, 'time travel module missing' if negative?
  268. 5 self.class.sleep_function.call(to_secs)
  269. end
  270. DIVISORS = [
  271. 1 [1_000_000_000.0, 's'],
  272. [1_000_000.0, 'ms'],
  273. [1_000.0, 'μs'],
  274. [0.0, 'ns']
  275. ].map(&:freeze).freeze
  276. 1 private_constant :DIVISORS
  277. # Format this +Duration+ into a human-readable string, with a given number
  278. # of decimal places.
  279. #
  280. # The default precision may be set globally using +Duration.default_to_s_precision=+
  281. #
  282. # The exact format is subject to change, users with specific requirements
  283. # are encouraged to use their own formatting methods.
  284. #
  285. # @example
  286. # Duration.from_nanos(100).to_s # => "100ns"
  287. # Duration.from_micros(100).to_s # => "100μs"
  288. # Duration.from_millis(100).to_s # => "100ms"
  289. # Duration.from_secs(100).to_s # => "100s"
  290. # Duration.from_nanos(1234567).to_s # => "1.234567ms"
  291. # Duration.from_nanos(1234567).to_s(2) # => "1.23ms"
  292. #
  293. # @param precision [Integer] the maximum number of decimal places
  294. # @return [String]
  295. # @see default_to_s_precision=
  296. 1 def to_s(precision = self.class.default_to_s_precision)
  297. 31 precision = Integer(precision).abs
  298. 31 nanos = to_nanos
  299. # This is infallible provided DIVISORS has an entry for 0
  300. 103 div, unit = DIVISORS.find { |d, _| nanos.abs >= d }
  301. 31 if div&.zero?
  302. 3 format('%d%s', nanos, unit)
  303. else
  304. # `#' for `f' forces to show the decimal point.
  305. 28 format("%#.#{precision}f", nanos / div).sub(/\.?0*\z/, '') << unit.to_s
  306. end
  307. end
  308. end
  309. end

lib/monotime/instant.rb

97.14% lines covered

70 relevant lines. 68 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Monotime
  3. # A measurement from the operating system's monotonic clock, with up to
  4. # nanosecond precision.
  5. 1 class Instant
  6. # A measurement, in nanoseconds. Should be considered opaque and
  7. # non-portable outside the process that created it.
  8. 1 attr_reader :ns
  9. 1 protected :ns
  10. 1 include Comparable
  11. 1 class << self
  12. # @overload clock_id
  13. # The +Process.clock_gettime+ clock id used to create +Instant+ instances
  14. # by the default monotonic function.
  15. #
  16. # @return [Numeric]
  17. #
  18. # @overload clock_id=(id)
  19. #
  20. # Override the default +Process.clock_gettime+ clock id. Some potential
  21. # choices include but are not limited to:
  22. #
  23. # * +Process::CLOCK_MONOTONIC_RAW+
  24. # * +Process::CLOCK_UPTIME_RAW+
  25. # * +Process::CLOCK_UPTIME_PRECISE+
  26. # * +Process::CLOCK_UPTIME_FAST+
  27. # * +Process::CLOCK_UPTIME+
  28. # * +Process::CLOCK_MONOTONIC_PRECISE+
  29. # * +Process::CLOCK_MONOTONIC_FAST+
  30. # * +Process::CLOCK_MONOTONIC+
  31. # * +:MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC+
  32. # * +:TIMES_BASED_CLOCK_MONOTONIC+
  33. #
  34. # These are platform-dependant and may vary in resolution, accuracy,
  35. # performance, and behaviour in light of system suspend/resume and NTP
  36. # frequency skew. They should be selected carefully based on your specific
  37. # needs and environment.
  38. #
  39. # It is possible to set non-monotonic clock sources here. You probably
  40. # shouldn't.
  41. #
  42. # Defaults to auto-selection from whatever is available from:
  43. #
  44. # * +CLOCK_UPTIME_RAW+ (if running under macOS)
  45. # * +CLOCK_MONOTONIC+
  46. # * +CLOCK_REALTIME+ (non-monotonic fallback, issues a run-time warning)
  47. #
  48. # @param id [Numeric, Symbol]
  49. 1 attr_accessor :clock_id
  50. # The function used to create +Instant+ instances.
  51. #
  52. # This function must return a +Numeric+, monotonic count of nanoseconds
  53. # since a fixed point in the past.
  54. #
  55. # Defaults to +-> { Process.clock_gettime(clock_id, :nanosecond) }+.
  56. #
  57. # @overload monotonic_function=(function)
  58. # @param function [#call]
  59. 1 attr_accessor :monotonic_function
  60. # Return the claimed resolution of the given clock id or the configured
  61. # +clock_id+, as a +Duration+, or +nil+ if invalid.
  62. #
  63. # Note per Ruby issue #16740, the practical usability of this method is
  64. # dubious and non-portable.
  65. #
  66. # @param clock [Numeric, Symbol] Optional clock id instead of default.
  67. 1 def clock_getres(clock = clock_id)
  68. 1 Duration.from_nanos(Integer(Process.clock_getres(clock, :nanosecond)))
  69. rescue SystemCallError
  70. # suppress errors
  71. end
  72. # The symbolic name of the currently-selected +clock_id+, if available.
  73. #
  74. # @return [Symbol, nil]
  75. 1 def clock_name
  76. 2 return clock_id if clock_id.is_a? Symbol
  77. 2 Process.constants.find do |c|
  78. 35 c.to_s.start_with?('CLOCK_') && Process.const_get(c) == clock_id
  79. end
  80. end
  81. 1 private
  82. # Symbolised Process constant name and condition pairs in order of preference
  83. CLOCKS = [
  84. # Follow Rust's footsteps using Mach Absolute Time. This offers higher,
  85. # nanosecond resolution over CLOCK_MONOTONIC, and appears to be slightly
  86. # faster.
  87. 1 [:CLOCK_UPTIME_RAW, -> { RUBY_PLATFORM.include?('darwin') }],
  88. 1 [:CLOCK_MONOTONIC, -> { true }],
  89. # Other fallbacks such as :TIMES_BASED_CLOCK_MONOTONIC appear unreliable
  90. # and are probably even less worth using than this.
  91. [:CLOCK_REALTIME, lambda {
  92. warn 'No monotonic clock source detected, falling back to CLOCK_REALTIME'
  93. true
  94. }]
  95. ].freeze
  96. 1 private_constant :CLOCKS
  97. 1 def select_clock_id
  98. 4 clock = CLOCKS.select { |const, _| Process.const_defined?(const) }
  99. 1 .find { |_, condition| condition.call }
  100. 1 raise NotImplementedError, 'No clock source found' unless clock
  101. 1 Process.const_get(clock[0])
  102. end
  103. end
  104. 50 self.monotonic_function = -> { Process.clock_gettime(clock_id, :nanosecond) }
  105. 1 self.clock_id = select_clock_id
  106. # Create a new +Instant+ from an optional nanosecond measurement.
  107. #
  108. # Users should generally *not* pass anything to this function.
  109. #
  110. # @param nanos [Integer]
  111. # @see #now
  112. 1 def initialize(nanos = self.class.monotonic_function.call)
  113. 64 @ns = Integer(nanos)
  114. 64 freeze
  115. end
  116. # An alias to +new+, and generally preferred over it.
  117. #
  118. # @return [Instant]
  119. 1 def self.now
  120. 51 new
  121. end
  122. # Return a +Duration+ between this +Instant+ and another.
  123. #
  124. # @param earlier [Instant]
  125. # @return [Duration]
  126. 1 def duration_since(earlier)
  127. 15 raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant)
  128. # `earlier - self` is cleaner, but upsets type checks and duplicates our
  129. # type checks.
  130. # @type var earlier: Instant
  131. 14 Duration.new(earlier.ns - @ns)
  132. end
  133. # Return a +Duration+ since this +Instant+ and now.
  134. #
  135. # @return [Duration]
  136. 1 def elapsed
  137. 14 duration_since(self.class.now)
  138. end
  139. # Return whether this +Instant+ is in the past.
  140. #
  141. # @return [Boolean]
  142. 1 def in_past?
  143. 2 elapsed.positive?
  144. end
  145. 1 alias past? in_past?
  146. # Return whether this +Instant+ is in the future.
  147. #
  148. # @return [Boolean]
  149. 1 def in_future?
  150. 2 elapsed.negative?
  151. end
  152. 1 alias future? in_future?
  153. # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
  154. # that's either positive if any time was slept, or negative if sleeping would
  155. # require time travel.
  156. #
  157. # @example Sleeps for a second
  158. # start = Instant.now
  159. # sleep 0.5 # do stuff for half a second
  160. # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
  161. # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
  162. #
  163. # @example Also sleeps for a second.
  164. # one_second_in_the_future = Instant.now + Duration.from_secs(1)
  165. # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
  166. # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
  167. #
  168. # @param duration [nil, Duration, #to_nanos]
  169. # @return [Duration] the slept duration, if +#positive?+, else the overshot time
  170. 1 def sleep(duration = nil)
  171. 5 remaining = if duration
  172. 4 Duration.from_nanos(duration.to_nanos - elapsed.to_nanos)
  173. else
  174. 1 -elapsed
  175. end
  176. 10 remaining.tap { |rem| rem.sleep if rem.positive? }
  177. end
  178. # Sleep for the given number of seconds past this +Instant+, if any.
  179. #
  180. # Equivalent to +#sleep(Duration.from_secs(secs))+
  181. #
  182. # @param secs [Numeric] number of seconds to sleep past this +Instant+
  183. # @return [Duration] the slept duration, if +#positive?+, else the overshot time
  184. # @see #sleep
  185. 1 def sleep_secs(secs)
  186. 1 sleep(Duration.from_secs(secs))
  187. end
  188. # Sleep for the given number of milliseconds past this +Instant+, if any.
  189. #
  190. # Equivalent to +#sleep(Duration.from_millis(millis))+
  191. #
  192. # @param millis [Numeric] number of milliseconds to sleep past this +Instant+
  193. # @return [Duration] the slept duration, if +#positive?+, else the overshot time
  194. # @see #sleep
  195. 1 def sleep_millis(millis)
  196. 1 sleep(Duration.from_millis(millis))
  197. end
  198. # Sugar for +#elapsed.to_s+.
  199. #
  200. # @see Duration#to_s
  201. 1 def to_s(...)
  202. 1 elapsed.to_s(...)
  203. end
  204. # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
  205. # a new +Instant+.
  206. #
  207. # @example
  208. # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
  209. #
  210. # @param other [Duration, #to_nanos]
  211. # @return [Instant]
  212. 1 def +(other)
  213. 9 raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
  214. 8 Instant.new(@ns + other.to_nanos)
  215. end
  216. # Subtract another +Instant+ to generate a +Duration+ between the two,
  217. # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
  218. # offset by it.
  219. #
  220. # @example
  221. # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
  222. # (Instant.now - Instant.now).to_s # => "-3.87μs"
  223. #
  224. # @param other [Instant, Duration, #to_nanos]
  225. # @return [Duration, Instant]
  226. 1 def -(other)
  227. 7 if other.is_a?(Instant)
  228. # @type var other: Instant
  229. 1 Duration.new(@ns - other.ns)
  230. 6 elsif other.respond_to?(:to_nanos)
  231. # @type var other: Duration | _ToNanos
  232. 5 Instant.new(@ns - other.to_nanos)
  233. else
  234. 1 raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
  235. end
  236. end
  237. # Determine if the given +Instant+ is before, equal to or after this one.
  238. # +nil+ if not passed an +Instant+.
  239. #
  240. # @return [-1, 0, 1, nil]
  241. 1 def <=>(other)
  242. 17 @ns <=> other.ns if other.is_a?(Instant)
  243. end
  244. # Determine if +other+'s value equals that of this +Instant+.
  245. # Use +eql?+ if type checks are desired for future compatibility.
  246. #
  247. # @return [Boolean]
  248. # @see #eql?
  249. 1 def ==(other)
  250. 4 other.is_a?(Instant) && @ns == other.ns
  251. end
  252. 1 alias eql? ==
  253. # Generate a hash for this type and value.
  254. #
  255. # @return [Integer]
  256. 1 def hash
  257. 10 [self.class, @ns].hash
  258. end
  259. end
  260. end