loading
Generated 2023-09-22T18:59:31+00:00

All Files ( 98.15% covered at 9.8 hits/line )

4 files in total.
162 relevant lines, 159 lines covered and 3 lines missed. ( 98.15% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/monotime.rb 100.00 % 5 3 3 0 1.00
lib/monotime/duration.rb 100.00 % 356 92 92 0 12.28
lib/monotime/instant.rb 95.38 % 279 65 62 3 6.97
lib/monotime/version.rb 100.00 % 6 2 2 0 1.00

lib/monotime.rb

100.0% lines covered

3 relevant lines. 3 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'

lib/monotime/duration.rb

100.0% lines covered

92 relevant lines. 92 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. 105 @ns = Integer(nanos)
  15. 105 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. 109 return ZERO if nanos.zero?
  44. 104 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. 36 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. 12 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. 15 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. 6 raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
  129. 5 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. 1 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. 1 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. 252 @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. 4 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, '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. 103 div, unit = DIVISORS.find { |d, _| to_nanos.abs >= d }
  299. 31 if div.zero?
  300. 3 format('%d%s', to_nanos, unit)
  301. else
  302. 28 format("%#.#{precision}f", to_nanos / div).sub(/\.?0*\z/, '') << unit
  303. end
  304. end
  305. end
  306. end

lib/monotime/instant.rb

95.38% lines covered

65 relevant lines. 62 lines covered and 3 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(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. 1 def select_clock_id
  83. 1 if RUBY_PLATFORM.include?('darwin') && Process.const_defined?(:CLOCK_UPTIME_RAW)
  84. # Offers nanosecond resolution and appears to be slightly faster on two
  85. # different Macs (M1 and x64)
  86. #
  87. # There is also :MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC which calls
  88. # mach_absolute_time() directly, but documentation for that recommends
  89. # CLOCK_UPTIME_RAW, and the performance difference is minimal.
  90. Process::CLOCK_UPTIME_RAW
  91. 1 elsif Process.const_defined?(:CLOCK_MONOTONIC)
  92. 1 Process::CLOCK_MONOTONIC
  93. else
  94. # There is also :TIMES_BASED_CLOCK_MONOTONIC, but having seen it just return
  95. # 0 instead of an error on a MSVC build this may be the safer option.
  96. warn 'No monotonic clock source detected, falling back to CLOCK_REALTIME'
  97. Process::CLOCK_REALTIME
  98. end
  99. end
  100. end
  101. 48 self.monotonic_function = -> { Process.clock_gettime(clock_id, :nanosecond) }
  102. 1 self.clock_id = select_clock_id
  103. # Create a new +Instant+ from an optional nanosecond measurement.
  104. #
  105. # Users should generally *not* pass anything to this function.
  106. #
  107. # @param nanos [Integer]
  108. # @see #now
  109. 1 def initialize(nanos = self.class.monotonic_function.call)
  110. 62 @ns = Integer(nanos)
  111. 62 freeze
  112. end
  113. # An alias to +new+, and generally preferred over it.
  114. #
  115. # @return [Instant]
  116. 1 def self.now
  117. 49 new
  118. end
  119. # Return a +Duration+ between this +Instant+ and another.
  120. #
  121. # @param earlier [Instant]
  122. # @return [Duration]
  123. 1 def duration_since(earlier)
  124. 15 raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant)
  125. 14 earlier - self
  126. end
  127. # Return a +Duration+ since this +Instant+ and now.
  128. #
  129. # @return [Duration]
  130. 1 def elapsed
  131. 14 duration_since(self.class.now)
  132. end
  133. # Return whether this +Instant+ is in the past.
  134. #
  135. # @return [Boolean]
  136. 1 def in_past?
  137. 2 elapsed.positive?
  138. end
  139. 1 alias past? in_past?
  140. # Return whether this +Instant+ is in the future.
  141. #
  142. # @return [Boolean]
  143. 1 def in_future?
  144. 2 elapsed.negative?
  145. end
  146. 1 alias future? in_future?
  147. # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+
  148. # that's either positive if any time was slept, or negative if sleeping would
  149. # require time travel.
  150. #
  151. # @example Sleeps for a second
  152. # start = Instant.now
  153. # sleep 0.5 # do stuff for half a second
  154. # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept)
  155. # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep)
  156. #
  157. # @example Also sleeps for a second.
  158. # one_second_in_the_future = Instant.now + Duration.from_secs(1)
  159. # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept)
  160. # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep)
  161. #
  162. # @param duration [nil, Duration, #to_nanos]
  163. # @return [Duration] the slept duration, if +#positive?+, else the overshot time
  164. 1 def sleep(duration = nil)
  165. 5 remaining = duration ? duration - elapsed : -elapsed
  166. 10 remaining.tap { |rem| rem.sleep if rem.positive? }
  167. end
  168. # Sleep for the given number of seconds past this +Instant+, if any.
  169. #
  170. # Equivalent to +#sleep(Duration.from_secs(secs))+
  171. #
  172. # @param secs [Numeric] number of seconds to sleep past this +Instant+
  173. # @return [Duration] the slept duration, if +#positive?+, else the overshot time
  174. # @see #sleep
  175. 1 def sleep_secs(secs)
  176. 1 sleep(Duration.from_secs(secs))
  177. end
  178. # Sleep for the given number of milliseconds past this +Instant+, if any.
  179. #
  180. # Equivalent to +#sleep(Duration.from_millis(millis))+
  181. #
  182. # @param millis [Numeric] number of milliseconds to sleep past this +Instant+
  183. # @return [Duration] the slept duration, if +#positive?+, else the overshot time
  184. # @see #sleep
  185. 1 def sleep_millis(millis)
  186. 1 sleep(Duration.from_millis(millis))
  187. end
  188. # Sugar for +#elapsed.to_s+.
  189. #
  190. # @see Duration#to_s
  191. 1 def to_s(...)
  192. 1 elapsed.to_s(...)
  193. end
  194. # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning
  195. # a new +Instant+.
  196. #
  197. # @example
  198. # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms"
  199. #
  200. # @param other [Duration, #to_nanos]
  201. # @return [Instant]
  202. 1 def +(other)
  203. 8 return TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos)
  204. 8 Instant.new(@ns + other.to_nanos)
  205. end
  206. # Subtract another +Instant+ to generate a +Duration+ between the two,
  207. # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+
  208. # offset by it.
  209. #
  210. # @example
  211. # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s"
  212. # (Instant.now - Instant.now).to_s # => "-3.87μs"
  213. #
  214. # @param other [Instant, Duration, #to_nanos]
  215. # @return [Duration, Instant]
  216. 1 def -(other)
  217. 20 if other.is_a?(Instant)
  218. 14 Duration.new(@ns - other.ns)
  219. 6 elsif other.respond_to?(:to_nanos)
  220. 5 Instant.new(@ns - other.to_nanos)
  221. else
  222. 1 raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]'
  223. end
  224. end
  225. # Determine if the given +Instant+ is before, equal to or after this one.
  226. # +nil+ if not passed an +Instant+.
  227. #
  228. # @return [-1, 0, 1, nil]
  229. 1 def <=>(other)
  230. 17 @ns <=> other.ns if other.is_a?(Instant)
  231. end
  232. # Determine if +other+'s value equals that of this +Instant+.
  233. # Use +eql?+ if type checks are desired for future compatibility.
  234. #
  235. # @return [Boolean]
  236. # @see #eql?
  237. 1 def ==(other)
  238. 4 other.is_a?(Instant) && @ns == other.ns
  239. end
  240. 1 alias eql? ==
  241. # Generate a hash for this type and value.
  242. #
  243. # @return [Integer]
  244. 1 def hash
  245. 10 [self.class, @ns].hash
  246. end
  247. end
  248. end

lib/monotime/version.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Monotime
  3. # Version of the `monotime` gem
  4. 1 MONOTIME_VERSION = '0.8.2'
  5. end