├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── Steepfile ├── bin ├── console └── setup ├── lib ├── monotime.rb └── monotime │ ├── duration.rb │ ├── include.rb │ ├── instant.rb │ └── version.rb ├── monotime.gemspec ├── sig └── monotime.rbs ├── steep_expectations.yml └── test ├── monotime_test.rb └── test_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, macos-latest] 9 | ruby: ['2.7', '3.0', '3.1', '3.2', head, jruby, jruby-head, truffleruby, truffleruby-head] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: ${{ matrix.ruby }} 16 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 17 | - run: bundle exec rake 18 | type_check: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: '3.2' 25 | bundler-cache: true 26 | - run: bundle exec steep check --with-expectations 27 | - run: bundle exec rbs -I sig test --target 'Monotime::*' rake test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | Exclude: 4 | - "bin/*" 5 | - "test/*" 6 | - "*.gemspec" 7 | - "Rakefile" 8 | - "Gemfile" 9 | 10 | Layout/LineLength: 11 | Max: 96 12 | 13 | Metrics/ClassLength: 14 | Max: 140 15 | 16 | Metrics/MethodLength: 17 | Max: 20 18 | 19 | Style/AsciiComments: 20 | Enabled: false 21 | 22 | Style/AccessModifierDeclarations: 23 | Enabled: false 24 | 25 | Style/FormatStringToken: 26 | Enabled: false 27 | 28 | Style/HashEachMethods: 29 | Enabled: true 30 | 31 | Style/HashTransformKeys: 32 | Enabled: true 33 | 34 | Style/HashTransformValues: 35 | Enabled: true 36 | 37 | Style/ExplicitBlockArgument: 38 | Enabled: false 39 | 40 | Style/MixinUsage: 41 | Enabled: false 42 | 43 | Style/TrailingCommaInArrayLiteral: 44 | Enabled: false -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.8.2] - 2023-09-22 4 | 5 | ### Added 6 | 7 | - `Instant.clock_name` is back and now tracks `clock_id` using reflection. 8 | - Explicit minimum Ruby version in gemspec (2.7.0). 9 | 10 | ### Changed 11 | 12 | - Clock auto-selection redux. We choose the first available from: 13 | 14 | 1. `CLOCK_UPTIME_RAW` on macOS, faster and higher resolution, also used by Rust 15 | 2. `CLOCK_MONOTONIC` 16 | 3. `CLOCK_REALTIME`, a non-monotonic fallback that issues a warning on startup 17 | 18 | - Slight performance bump for `Duration.measure` on Ruby <= 3.1 19 | 20 | ## [0.8.1] - 2023-09-18 21 | 22 | ### Changed 23 | 24 | - After further consideration, return to defaulting to `CLOCK_MONOTONIC` instead 25 | of the overly-elaborate auto-selection introduced in 0.8.0. 26 | 27 | ### Removed 28 | 29 | - `Instant.clock_name`. No I'm not incrementing to 0.9. It's been a few hours, 30 | you're not using it, shut up. 31 | 32 | ## [0.8.0] - 2023-09-17 33 | 34 | ### Added 35 | 36 | - Default precision for `Duration#to_s` can be set using 37 | `Duration.default_to_s_precision=`. 38 | - Default sleep function can be set using `Duration.sleep_function=` 39 | - `Duration::ZERO` and `Duration.zero` for an easy, memory-efficient 40 | zero-duration singleton. 41 | - `Instant.clock_id` and `Instant.clock_id=` to control the default clock 42 | source. 43 | - `Instant.clock_getres` to get the minimum supported `Duration` from the 44 | selected clock source. 45 | - `Instant.monotonic_function=` to completely replace the default monotonic 46 | function. 47 | 48 | ### Changed 49 | 50 | - The default clock source is now chosen from a selection of options instead of 51 | defaulting to `CLOCK_MONOTONIC``. Where possible options are used which are 52 | unaffected by NTP frequency skew and which do not count time in system suspend. 53 | - CI matrix drops Ruby 2.5 and 2.6 and adds 3.1, 3.2, head branches of Ruby, 54 | JRuby, and TruffleRuby, and also tests under macOS. 55 | 56 | ### Fixed 57 | 58 | - CI on TruffleRuby has been fixed by disabling SimpleCov. 59 | - Several fragile tests depending on relatively narrow sleep times have been fixed. 60 | 61 | ### Thanks 62 | 63 | - [@petergoldstein] for fixing CI on TruffleRuby and adding 3.1 and 3.2. 64 | - [@fig] for fixing a README error. 65 | 66 | ## [0.7.1] - 2021-10-22 67 | 68 | ### Added 69 | 70 | - `simplecov` introduced to test suite. 71 | - `monotime/include.rb` to auto-include types globally. 72 | 73 | ### Changed 74 | 75 | - All `Instant` and `Duration` instances are now frozen. 76 | - Migrate from Travis CI to Github Actions 77 | - Update development dependency on `rake`. 78 | 79 | ## [0.7.0] - 2019-04-24 80 | 81 | ### Added 82 | 83 | - `Duration.with_measure`, which yields and returns an array containing its 84 | evaluated return value and its `Duration`. 85 | 86 | ### Changed 87 | 88 | - Break `Duration` and `Instant` into their own files. 89 | - Rename `Monotime::VERSION` to `Monotime::MONOTIME_VERSION` to reduce 90 | potential for collision if the module is included. 91 | - Update to bundler 2.0. 92 | - Rework README.md. Includes fix for [issue #1] (added a "See Also" section). 93 | 94 | ## [0.6.1] - 2018-10-26 95 | 96 | ### Fixed 97 | 98 | - Build gem from a clean git checkout, not my local development directory. 99 | No functional changes. 100 | 101 | ## [0.6.0] - 2018-10-26 102 | 103 | ### Added 104 | 105 | - This `CHANGELOG.md` by request of [@celsworth]. 106 | - Aliases for `Duration.from_*` and `Duration#to_*` without the prefix. e.g. 107 | `Duration.from_secs(42).to_secs == 42` can now be written as 108 | `Duration.secs(42).secs == 42`. 109 | - `Duration#nonzero?`. 110 | - `Instant#in_past?` and `Instant#in_future?`. 111 | 112 | ## [0.5.0] - 2018-10-13 113 | 114 | ### Added 115 | 116 | - `Duration#abs` to make a `Duration` positive. 117 | - `Duration#-@` to invert the sign of a `Duration`. 118 | - `Duration#positive?` 119 | - `Duration#negative?` 120 | - `Duration#zero?` 121 | 122 | ### Changed 123 | 124 | - `Instant#sleep` with no argument now sleeps until the `Instant`. 125 | - `Duration.from_*` no longer coerce their argument to `Float`. 126 | - `Duration#==` checks value via `#to_nanos`, not type. 127 | - `Duration#eql?` checks value and type. 128 | - `Duration#<=>` compares value via `#to_nanos`. 129 | 130 | ## [0.4.0] - 2018-10-09 131 | 132 | ### Added 133 | 134 | - `Instant#sleep` - sleep to a given `Duration` past an `Instant`. 135 | - `Instant#sleep_secs` and `Instant#sleep_millis` convenience methods. 136 | - `Duration#sleep` - sleep for the `Duration`. 137 | - `Duration#*` - multiply a `Duration` by a number. 138 | - `Duration#/` - divide a `Duration` by a number. 139 | 140 | ### Changed 141 | 142 | More `#to_nanos` `Duration` duck-typing. 143 | 144 | ## [0.3.0] - 2018-10-04 145 | 146 | ### Added 147 | 148 | - `#to_nanos` is now used to duck-type `Duration` everywhere. 149 | 150 | ### Changed 151 | 152 | - Make `<=>` return nil on invalid types, rather than raising a `TypeError`. 153 | 154 | ### Removed 155 | 156 | - Dependency on `dry-equalizer`. 157 | 158 | ## [0.2.0] - 2018-10-03 159 | 160 | ### Added 161 | 162 | - `Instant#to_s` as an alias for `#elapsed.to_s` 163 | - `Duration#to_nanos`, with some limited duck-typing. 164 | 165 | ### Changed 166 | 167 | - Switch to microseconds internally. 168 | - `Duration#to_{secs,millis,micros}` now return a `Float`. 169 | - `Instant#ns` is now `protected`. 170 | 171 | ### Fixed 172 | 173 | - `Duration#to_s` zero-stripping with precision=0. 174 | - `Instant#-` argument ordering with other `Instant`. 175 | - `Duration#to_micros` returns microseconds, not picoseconds. 176 | 177 | ### Removed 178 | 179 | - `Instant` and `Duration` maths methods no longer support passing an `Integer` 180 | number of nanoseconds. 181 | 182 | ## [0.1.0] - 2018-10-02 183 | 184 | ### Added 185 | 186 | - Initial release 187 | 188 | [0.1.0]: https://github.com/Freaky/monotime/commits/v0.1.0 189 | [0.2.0]: https://github.com/Freaky/monotime/commits/v0.2.0 190 | [0.3.0]: https://github.com/Freaky/monotime/commits/v0.3.0 191 | [0.4.0]: https://github.com/Freaky/monotime/commits/v0.4.0 192 | [0.5.0]: https://github.com/Freaky/monotime/commits/v0.5.0 193 | [0.6.0]: https://github.com/Freaky/monotime/commits/v0.6.0 194 | [0.6.1]: https://github.com/Freaky/monotime/commits/v0.6.1 195 | [0.7.0]: https://github.com/Freaky/monotime/commits/v0.7.0 196 | [0.7.1]: https://github.com/Freaky/monotime/commits/v0.7.0 197 | [0.8.0]: https://github.com/Freaky/monotime/commits/v0.8.0 198 | [0.8.1]: https://github.com/Freaky/monotime/commits/v0.8.1 199 | [0.8.2]: https://github.com/Freaky/monotime/commits/v0.8.2 200 | [issue #1]: https://github.com/Freaky/monotime/issues/1 201 | [Ruby #16740]: https://bugs.ruby-lang.org/issues/16740 202 | [@celsworth]: https://github.com/celsworth 203 | [@petergoldstein]: https://github.com/petergoldstein 204 | [@fig]: https://github.com/fig 205 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in monotime.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Thomas Hurst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Monotime 3 | 4 | A sensible interface to Ruby's monotonic clock, inspired by Rust. 5 | 6 | [![Gem Version](https://badge.fury.io/rb/monotime.svg)](https://badge.fury.io/rb/monotime) 7 | [![Build Status](https://github.com/Freaky/monotime/actions/workflows/ci.yml/badge.svg)](https://github.com/Freaky/monotime/actions) 8 | [![Inline docs](http://inch-ci.org/github/Freaky/monotime.svg?branch=master)](http://inch-ci.org/github/Freaky/monotime) 9 | [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/monotime) 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'monotime' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install monotime 26 | 27 | `Monotime` is tested on Ruby 2.7+, TruffleRuby, and JRuby. 28 | 29 | ## Usage 30 | 31 | ```ruby 32 | require 'monotime' 33 | # or, to automatically include Monotime::* in the global scope, 34 | # as used by these examples: 35 | require 'monotime/include' 36 | ``` 37 | 38 | `Monotime` offers a `Duration` type for describing spans of time, and an 39 | `Instant` type for describing points in time. Both operate at nanosecond 40 | resolution to the limits of whatever your Ruby implementation supports. 41 | 42 | For example, to measure an elapsed time, either create an `Instant` to mark the 43 | start point, perform the action and then ask for the `Duration` that has elapsed 44 | since: 45 | 46 | ```ruby 47 | start = Instant.now 48 | do_something 49 | elapsed = start.elapsed 50 | ``` 51 | 52 | Or use a convenience method: 53 | 54 | ```ruby 55 | elapsed = Duration.measure { do_something } 56 | # or 57 | return_value, elapsed = Duration.with_measure { compute_something } 58 | ``` 59 | 60 | `Duration` offers formatting: 61 | 62 | ```ruby 63 | Duration.millis(42).to_s # => "42ms" 64 | Duration.nanos(12345).to_s # => "12.345μs" 65 | Duration.secs(1.12345).to_s(2) # => "1.12s" 66 | ``` 67 | 68 | Conversions: 69 | 70 | ```ruby 71 | Duration.secs(10).millis # => 10000.0 72 | Duration.micros(12345).secs # => 0.012345 73 | ``` 74 | 75 | And basic mathematical operations: 76 | 77 | ```ruby 78 | (Duration.millis(42) + Duration.secs(1)).to_s # => "1.042s" 79 | (Duration.millis(42) - Duration.secs(1)).to_s # => "-958ms" 80 | (Duration.secs(42) * 2).to_s # => "84s" 81 | (Duration.secs(42) / 2).to_s # => "21s" 82 | ``` 83 | 84 | `Instant` does some simple maths too: 85 | 86 | ```ruby 87 | # Instant - Duration => Instant 88 | (Instant.now - Duration.secs(1)).elapsed.to_s # => "1.000014627s" 89 | 90 | # Instant - Instant => Duration 91 | (Instant.now - Instant.now).to_s # => "-5.585μs" 92 | ``` 93 | 94 | `Duration` and `Instant` are also `Comparable` with other instances of their 95 | type, and can be used in hashes, sets, and similar structures. 96 | 97 | ## Sleeping 98 | 99 | `Duration` can be used to sleep a thread, assuming it's positive (time travel 100 | is not yet implemented): 101 | 102 | ```ruby 103 | # Equivalent 104 | sleep(Duration.secs(1).secs) # => 1 105 | Duration.secs(1).sleep # => 1 106 | ``` 107 | 108 | So can `Instant`, taking a `Duration` and sleeping until the given `Duration` 109 | past the time the `Instant` was created, if any. This can be useful for 110 | maintaining a precise cadence between tasks: 111 | 112 | ```ruby 113 | interval = Duration.secs(60) 114 | start = Instant.now 115 | loop do 116 | do_stuff 117 | start.sleep(interval) 118 | start += interval 119 | end 120 | ``` 121 | 122 | Or you can declare an `Instant` in the future and sleep to that point: 123 | 124 | ```ruby 125 | interval = Duration.secs(60) 126 | deadline = Instant.now + interval 127 | loop do 128 | do_stuff 129 | deadline.sleep 130 | deadline += interval 131 | end 132 | ``` 133 | 134 | `Instant#sleep` returns a `Duration` which was slept, or a negative `Duration` 135 | indicating that the desired sleep point was in the past. 136 | 137 | ## Duration duck typing 138 | 139 | Operations taking a `Duration` can also accept any type which implements 140 | `#to_nanos`, returning an (Integer) number of nanoseconds the value represents. 141 | 142 | For example, to treat built-in numeric types as second durations, you could do: 143 | 144 | ```ruby 145 | class Numeric 146 | def to_nanos 147 | Integer(self * 1_000_000_000) 148 | end 149 | end 150 | 151 | (Duration.secs(1) + 41).to_s # => "42s" 152 | (Instant.now - 42).to_s # => "42.000010545s" 153 | ``` 154 | 155 | ## Development 156 | 157 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 158 | 159 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 160 | 161 | ## Contributing 162 | 163 | Bug reports and pull requests are welcome on GitHub at . 164 | 165 | ## License 166 | 167 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 168 | 169 | ## See Also 170 | 171 | ### Core Ruby 172 | 173 | For a zero-dependency alternative upon which `monotime` is based, see 174 | [`Process.clock_gettime`](https://www.rubydoc.info/stdlib/core/Process:clock_gettime). 175 | 176 | `Process::CLOCK_MONOTONIC` is a safe default, but other options may offer higher 177 | resolution or alternative behaviour in light of system suspend/resume or NTP 178 | frequency skew. 179 | 180 | ### Other Gems 181 | 182 | [hitimes](https://rubygems.org/gems/hitimes) is a popular and mature alternative 183 | which also includes a variety of features for gathering statistics about 184 | measurements. 185 | 186 | [concurrent-ruby](https://rubygems.org/gems/concurrent-ruby) includes 187 | `Concurrent.monotonic_time`, which is at the time of writing a trivial proxy to 188 | the aforementioned `Process::clock_gettime` with `Process::CLOCK_MONOTONIC`. 189 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /Steepfile: -------------------------------------------------------------------------------- 1 | 2 | target :monotime do 3 | check 'lib' 4 | signature 'sig' 5 | 6 | configure_code_diagnostics(Steep::Diagnostic::Ruby.strict) 7 | end 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'monotime' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require 'pry' 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/monotime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'monotime/version' 4 | require_relative 'monotime/duration' 5 | require_relative 'monotime/instant' 6 | -------------------------------------------------------------------------------- /lib/monotime/duration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Monotime 4 | # A type representing a span of time in nanoseconds. 5 | class Duration 6 | include Comparable 7 | 8 | # Create a new +Duration+ of a specified number of nanoseconds, zero by 9 | # default. 10 | # 11 | # Users are strongly advised to use +Duration.from_nanos+ instead. 12 | # 13 | # @param nanos [Integer] 14 | # @see from_nanos 15 | def initialize(nanos = 0) 16 | @ns = Integer(nanos) 17 | freeze 18 | end 19 | 20 | # A static instance for zero durations 21 | ZERO = allocate.tap { |d| d.__send__(:initialize, 0) } 22 | 23 | class << self 24 | # The sleep function used by all +Monotime+ sleep functions. 25 | # 26 | # This function must accept a positive +Float+ number of seconds and return 27 | # the +Float+ time slept. 28 | # 29 | # Defaults to +Kernel.method(:sleep)+ 30 | # 31 | # @overload sleep_function=(function) 32 | # @param function [#call] 33 | attr_accessor :sleep_function 34 | 35 | # Precision for +Duration#to_s+ if not otherwise specified 36 | # 37 | # Defaults to 9. 38 | # 39 | # @overload default_to_s_precision=(precision) 40 | # @param precision [Numeric] 41 | attr_accessor :default_to_s_precision 42 | end 43 | 44 | self.sleep_function = Kernel.method(:sleep) 45 | self.default_to_s_precision = 9 46 | 47 | class << self 48 | # @!visibility private 49 | def new(nanos = 0) 50 | return ZERO if 0 == nanos # rubocop:disable Style/* 51 | 52 | super 53 | end 54 | 55 | # Return a zero +Duration+. 56 | # 57 | # @return [Duration] 58 | def zero 59 | ZERO 60 | end 61 | 62 | # Generate a new +Duration+ measuring the given number of seconds. 63 | # 64 | # @param secs [Numeric] 65 | # @return [Duration] 66 | def from_secs(secs) 67 | new(Integer(secs * 1_000_000_000)) 68 | end 69 | 70 | alias secs from_secs 71 | 72 | # Generate a new +Duration+ measuring the given number of milliseconds. 73 | # 74 | # @param millis [Numeric] 75 | # @return [Duration] 76 | def from_millis(millis) 77 | new(Integer(millis * 1_000_000)) 78 | end 79 | 80 | alias millis from_millis 81 | 82 | # Generate a new +Duration+ measuring the given number of microseconds. 83 | # 84 | # @param micros [Numeric] 85 | # @return [Duration] 86 | def from_micros(micros) 87 | new(Integer(micros * 1_000)) 88 | end 89 | 90 | alias micros from_micros 91 | 92 | # Generate a new +Duration+ measuring the given number of nanoseconds. 93 | # 94 | # @param nanos [Numeric] 95 | # @return [Duration] 96 | def from_nanos(nanos) 97 | new(Integer(nanos)) 98 | end 99 | 100 | alias nanos from_nanos 101 | 102 | # Return a +Duration+ measuring the elapsed time of the yielded block. 103 | # 104 | # @example 105 | # Duration.measure { sleep(0.5) }.to_s # => "512.226109ms" 106 | # 107 | # @return [Duration] 108 | def measure 109 | start = Instant.now 110 | yield 111 | start.elapsed 112 | end 113 | 114 | # Return the result of the yielded block alongside a +Duration+. 115 | # 116 | # @example 117 | # Duration.with_measure { "bloop" } # => ["bloop", #] 118 | # 119 | # @return [Object, Duration] 120 | def with_measure 121 | start = Instant.now 122 | ret = yield 123 | [ret, start.elapsed] 124 | end 125 | end 126 | 127 | # Add another +Duration+ or +#to_nanos+-coercible object to this one, 128 | # returning a new +Duration+. 129 | # 130 | # @example 131 | # (Duration.from_secs(10) + Duration.from_secs(5)).to_s # => "15s" 132 | # 133 | # @param other [Duration, #to_nanos] 134 | # @return [Duration] 135 | def +(other) 136 | raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos) 137 | 138 | Duration.new(to_nanos + other.to_nanos) 139 | end 140 | 141 | # Subtract another +Duration+ or +#to_nanos+-coercible object from this one, 142 | # returning a new +Duration+. 143 | # 144 | # @example 145 | # (Duration.from_secs(10) - Duration.from_secs(5)).to_s # => "5s" 146 | # 147 | # @param other [Duration, #to_nanos] 148 | # @return [Duration] 149 | def -(other) 150 | raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos) 151 | 152 | Duration.new(to_nanos - other.to_nanos) 153 | end 154 | 155 | # Divide this duration by a +Numeric+. 156 | # 157 | # @example 158 | # (Duration.from_secs(10) / 2).to_s # => "5s" 159 | # 160 | # @param other [Numeric] 161 | # @return [Duration] 162 | def /(other) 163 | Duration.new(to_nanos / other) 164 | end 165 | 166 | # Multiply this duration by a +Numeric+. 167 | # 168 | # @example 169 | # (Duration.from_secs(10) * 2).to_s # => "20s" 170 | # 171 | # @param other [Numeric] 172 | # @return [Duration] 173 | def *(other) 174 | Duration.new(to_nanos * other) 175 | end 176 | 177 | # Unary minus: make a positive +Duration+ negative, and vice versa. 178 | # 179 | # @example 180 | # -Duration.from_secs(-1).to_s # => "1s" 181 | # -Duration.from_secs(1).to_s # => "-1s" 182 | # 183 | # @return [Duration] 184 | def -@ 185 | Duration.new(-to_nanos) 186 | end 187 | 188 | # Return a +Duration+ that's absolute (positive). 189 | # 190 | # @example 191 | # Duration.from_secs(-1).abs.to_s # => "1s" 192 | # Duration.from_secs(1).abs.to_s # => "1s" 193 | # 194 | # @return [Duration] 195 | def abs 196 | return self if positive? || zero? 197 | 198 | Duration.new(to_nanos.abs) 199 | end 200 | 201 | # Compare the *value* of this +Duration+ with another, or any +#to_nanos+-coercible 202 | # object, or nil if not comparable. 203 | # 204 | # @param other [Duration, #to_nanos, Object] 205 | # @return [-1, 0, 1, nil] 206 | def <=>(other) 207 | to_nanos <=> other.to_nanos if other.respond_to?(:to_nanos) 208 | end 209 | 210 | # Compare the equality of the *value* of this +Duration+ with another, or 211 | # any +#to_nanos+-coercible object, or nil if not comparable. 212 | # 213 | # @param other [Duration, #to_nanos, Object] 214 | # @return [Boolean] 215 | def ==(other) 216 | other.respond_to?(:to_nanos) && to_nanos == other.to_nanos 217 | end 218 | 219 | # Check equality of the value and type of this +Duration+ with another. 220 | # 221 | # @param other [Duration, Object] 222 | # @return [Boolean] 223 | def eql?(other) 224 | other.is_a?(Duration) && to_nanos == other.to_nanos 225 | end 226 | 227 | # Generate a hash for this type and value. 228 | # 229 | # @return [Integer] 230 | def hash 231 | [self.class, to_nanos].hash 232 | end 233 | 234 | # Return this +Duration+ in seconds. 235 | # 236 | # @return [Float] 237 | def to_secs 238 | to_nanos / 1_000_000_000.0 239 | end 240 | 241 | alias secs to_secs 242 | 243 | # Return this +Duration+ in milliseconds. 244 | # 245 | # @return [Float] 246 | def to_millis 247 | to_nanos / 1_000_000.0 248 | end 249 | 250 | alias millis to_millis 251 | 252 | # Return this +Duration+ in microseconds. 253 | # 254 | # @return [Float] 255 | def to_micros 256 | to_nanos / 1_000.0 257 | end 258 | 259 | alias micros to_micros 260 | 261 | # Return this +Duration+ in nanoseconds. 262 | # 263 | # @return [Integer] 264 | def to_nanos 265 | @ns 266 | end 267 | 268 | alias nanos to_nanos 269 | 270 | # Return true if this +Duration+ is positive. 271 | # 272 | # @return [Boolean] 273 | def positive? 274 | to_nanos.positive? 275 | end 276 | 277 | # Return true if this +Duration+ is negative. 278 | # 279 | # @return [Boolean] 280 | def negative? 281 | to_nanos.negative? 282 | end 283 | 284 | # Return true if this +Duration+ is zero. 285 | # 286 | # @return [Boolean] 287 | def zero? 288 | to_nanos.zero? 289 | end 290 | 291 | # Return true if this +Duration+ is non-zero. 292 | # 293 | # @return [Boolean] 294 | def nonzero? 295 | to_nanos.nonzero? 296 | end 297 | 298 | # Sleep for the duration of this +Duration+. Equivalent to 299 | # +Kernel.sleep(duration.to_secs)+. 300 | # 301 | # The sleep function may be overridden globally using +Duration.sleep_function=+ 302 | # 303 | # @example 304 | # Duration.from_secs(1).sleep # => 1 305 | # Duration.from_secs(-1).sleep # => raises NotImplementedError 306 | # 307 | # @raise [NotImplementedError] negative +Duration+ sleeps are not yet supported. 308 | # @return [Integer] 309 | # @see Instant#sleep 310 | # @see sleep_function= 311 | def sleep 312 | raise NotImplementedError, 'time travel module missing' if negative? 313 | 314 | self.class.sleep_function.call(to_secs) 315 | end 316 | 317 | DIVISORS = [ 318 | [1_000_000_000.0, 's'], 319 | [1_000_000.0, 'ms'], 320 | [1_000.0, 'μs'], 321 | [0.0, 'ns'] 322 | ].map(&:freeze).freeze 323 | 324 | private_constant :DIVISORS 325 | 326 | # Format this +Duration+ into a human-readable string, with a given number 327 | # of decimal places. 328 | # 329 | # The default precision may be set globally using +Duration.default_to_s_precision=+ 330 | # 331 | # The exact format is subject to change, users with specific requirements 332 | # are encouraged to use their own formatting methods. 333 | # 334 | # @example 335 | # Duration.from_nanos(100).to_s # => "100ns" 336 | # Duration.from_micros(100).to_s # => "100μs" 337 | # Duration.from_millis(100).to_s # => "100ms" 338 | # Duration.from_secs(100).to_s # => "100s" 339 | # Duration.from_nanos(1234567).to_s # => "1.234567ms" 340 | # Duration.from_nanos(1234567).to_s(2) # => "1.23ms" 341 | # 342 | # @param precision [Integer] the maximum number of decimal places 343 | # @return [String] 344 | # @see default_to_s_precision= 345 | def to_s(precision = self.class.default_to_s_precision) 346 | precision = Integer(precision).abs 347 | nanos = to_nanos 348 | 349 | # This is infallible provided DIVISORS has an entry for 0 350 | div, unit = DIVISORS.find { |d, _| nanos.abs >= d } 351 | 352 | if div&.zero? 353 | format('%d%s', nanos, unit) 354 | else 355 | # `#' for `f' forces to show the decimal point. 356 | format("%#.#{precision}f", nanos / div).sub(/\.?0*\z/, '') << unit.to_s 357 | end 358 | end 359 | end 360 | end 361 | -------------------------------------------------------------------------------- /lib/monotime/include.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'monotime' 4 | 5 | include Monotime 6 | -------------------------------------------------------------------------------- /lib/monotime/instant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Monotime 4 | # A measurement from the operating system's monotonic clock, with up to 5 | # nanosecond precision. 6 | class Instant 7 | # A measurement, in nanoseconds. Should be considered opaque and 8 | # non-portable outside the process that created it. 9 | attr_reader :ns 10 | protected :ns 11 | 12 | include Comparable 13 | 14 | class << self 15 | # @overload clock_id 16 | # The +Process.clock_gettime+ clock id used to create +Instant+ instances 17 | # by the default monotonic function. 18 | # 19 | # @return [Numeric] 20 | # 21 | # @overload clock_id=(id) 22 | # 23 | # Override the default +Process.clock_gettime+ clock id. Some potential 24 | # choices include but are not limited to: 25 | # 26 | # * +Process::CLOCK_MONOTONIC_RAW+ 27 | # * +Process::CLOCK_UPTIME_RAW+ 28 | # * +Process::CLOCK_UPTIME_PRECISE+ 29 | # * +Process::CLOCK_UPTIME_FAST+ 30 | # * +Process::CLOCK_UPTIME+ 31 | # * +Process::CLOCK_MONOTONIC_PRECISE+ 32 | # * +Process::CLOCK_MONOTONIC_FAST+ 33 | # * +Process::CLOCK_MONOTONIC+ 34 | # * +:MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC+ 35 | # * +:TIMES_BASED_CLOCK_MONOTONIC+ 36 | # 37 | # These are platform-dependant and may vary in resolution, accuracy, 38 | # performance, and behaviour in light of system suspend/resume and NTP 39 | # frequency skew. They should be selected carefully based on your specific 40 | # needs and environment. 41 | # 42 | # It is possible to set non-monotonic clock sources here. You probably 43 | # shouldn't. 44 | # 45 | # Defaults to auto-selection from whatever is available from: 46 | # 47 | # * +CLOCK_UPTIME_RAW+ (if running under macOS) 48 | # * +CLOCK_MONOTONIC+ 49 | # * +CLOCK_REALTIME+ (non-monotonic fallback, issues a run-time warning) 50 | # 51 | # @param id [Numeric, Symbol] 52 | attr_accessor :clock_id 53 | 54 | # The function used to create +Instant+ instances. 55 | # 56 | # This function must return a +Numeric+, monotonic count of nanoseconds 57 | # since a fixed point in the past. 58 | # 59 | # Defaults to +-> { Process.clock_gettime(clock_id, :nanosecond) }+. 60 | # 61 | # @overload monotonic_function=(function) 62 | # @param function [#call] 63 | attr_accessor :monotonic_function 64 | 65 | # Return the claimed resolution of the given clock id or the configured 66 | # +clock_id+, as a +Duration+, or +nil+ if invalid. 67 | # 68 | # Note per Ruby issue #16740, the practical usability of this method is 69 | # dubious and non-portable. 70 | # 71 | # @param clock [Numeric, Symbol] Optional clock id instead of default. 72 | def clock_getres(clock = clock_id) 73 | Duration.from_nanos(Integer(Process.clock_getres(clock, :nanosecond))) 74 | rescue SystemCallError 75 | # suppress errors 76 | end 77 | 78 | # The symbolic name of the currently-selected +clock_id+, if available. 79 | # 80 | # @return [Symbol, nil] 81 | def clock_name 82 | return clock_id if clock_id.is_a? Symbol 83 | 84 | Process.constants.find do |c| 85 | c.to_s.start_with?('CLOCK_') && Process.const_get(c) == clock_id 86 | end 87 | end 88 | 89 | private 90 | 91 | def select_clock_id 92 | if RUBY_PLATFORM.include?('darwin') && Process.const_defined?(:CLOCK_UPTIME_RAW) 93 | # Offers nanosecond resolution and appears to be slightly faster on two 94 | # different Macs (M1 and x64) 95 | # 96 | # There is also :MACH_ABSOLUTE_TIME_BASED_CLOCK_MONOTONIC which calls 97 | # mach_absolute_time() directly, but documentation for that recommends 98 | # CLOCK_UPTIME_RAW, and the performance difference is minimal. 99 | Process.const_get(:CLOCK_UPTIME_RAW) 100 | elsif Process.const_defined?(:CLOCK_MONOTONIC) 101 | Process.const_get(:CLOCK_MONOTONIC) 102 | else 103 | # There is also :TIMES_BASED_CLOCK_MONOTONIC, but having seen it just return 104 | # 0 instead of an error on a MSVC build this may be the safer option. 105 | warn 'No monotonic clock source detected, falling back to CLOCK_REALTIME' 106 | Process::CLOCK_REALTIME 107 | end 108 | end 109 | end 110 | 111 | self.monotonic_function = -> { Process.clock_gettime(clock_id, :nanosecond) } 112 | self.clock_id = select_clock_id 113 | 114 | # Create a new +Instant+ from an optional nanosecond measurement. 115 | # 116 | # Users should generally *not* pass anything to this function. 117 | # 118 | # @param nanos [Integer] 119 | # @see #now 120 | def initialize(nanos = self.class.monotonic_function.call) 121 | @ns = Integer(nanos) 122 | freeze 123 | end 124 | 125 | # An alias to +new+, and generally preferred over it. 126 | # 127 | # @return [Instant] 128 | def self.now 129 | new 130 | end 131 | 132 | # Return a +Duration+ between this +Instant+ and another. 133 | # 134 | # @param earlier [Instant] 135 | # @return [Duration] 136 | def duration_since(earlier) 137 | raise TypeError, 'Not an Instant' unless earlier.is_a?(Instant) 138 | 139 | # `earlier - self` is cleaner, but upsets type checks and duplicates our 140 | # type checks. 141 | 142 | # @type var earlier: Instant 143 | Duration.new(earlier.ns - @ns) 144 | end 145 | 146 | # Return a +Duration+ since this +Instant+ and now. 147 | # 148 | # @return [Duration] 149 | def elapsed 150 | duration_since(self.class.now) 151 | end 152 | 153 | # Return whether this +Instant+ is in the past. 154 | # 155 | # @return [Boolean] 156 | def in_past? 157 | elapsed.positive? 158 | end 159 | 160 | alias past? in_past? 161 | 162 | # Return whether this +Instant+ is in the future. 163 | # 164 | # @return [Boolean] 165 | def in_future? 166 | elapsed.negative? 167 | end 168 | 169 | alias future? in_future? 170 | 171 | # Sleep until this +Instant+, plus an optional +Duration+, returning a +Duration+ 172 | # that's either positive if any time was slept, or negative if sleeping would 173 | # require time travel. 174 | # 175 | # @example Sleeps for a second 176 | # start = Instant.now 177 | # sleep 0.5 # do stuff for half a second 178 | # start.sleep(Duration.from_secs(1)).to_s # => "490.088706ms" (slept) 179 | # start.sleep(Duration.from_secs(1)).to_s # => "-12.963502ms" (did not sleep) 180 | # 181 | # @example Also sleeps for a second. 182 | # one_second_in_the_future = Instant.now + Duration.from_secs(1) 183 | # one_second_in_the_future.sleep.to_s # => "985.592712ms" (slept) 184 | # one_second_in_the_future.sleep.to_s # => "-4.71217ms" (did not sleep) 185 | # 186 | # @param duration [nil, Duration, #to_nanos] 187 | # @return [Duration] the slept duration, if +#positive?+, else the overshot time 188 | def sleep(duration = nil) 189 | remaining = if duration 190 | Duration.from_nanos(duration.to_nanos - elapsed.to_nanos) 191 | else 192 | -elapsed 193 | end 194 | 195 | remaining.tap { |rem| rem.sleep if rem.positive? } 196 | end 197 | 198 | # Sleep for the given number of seconds past this +Instant+, if any. 199 | # 200 | # Equivalent to +#sleep(Duration.from_secs(secs))+ 201 | # 202 | # @param secs [Numeric] number of seconds to sleep past this +Instant+ 203 | # @return [Duration] the slept duration, if +#positive?+, else the overshot time 204 | # @see #sleep 205 | def sleep_secs(secs) 206 | sleep(Duration.from_secs(secs)) 207 | end 208 | 209 | # Sleep for the given number of milliseconds past this +Instant+, if any. 210 | # 211 | # Equivalent to +#sleep(Duration.from_millis(millis))+ 212 | # 213 | # @param millis [Numeric] number of milliseconds to sleep past this +Instant+ 214 | # @return [Duration] the slept duration, if +#positive?+, else the overshot time 215 | # @see #sleep 216 | def sleep_millis(millis) 217 | sleep(Duration.from_millis(millis)) 218 | end 219 | 220 | # Sugar for +#elapsed.to_s+. 221 | # 222 | # @see Duration#to_s 223 | def to_s(...) 224 | elapsed.to_s(...) 225 | end 226 | 227 | # Add a +Duration+ or +#to_nanos+-coercible object to this +Instant+, returning 228 | # a new +Instant+. 229 | # 230 | # @example 231 | # (Instant.now + Duration.from_secs(1)).to_s # => "-999.983976ms" 232 | # 233 | # @param other [Duration, #to_nanos] 234 | # @return [Instant] 235 | def +(other) 236 | raise TypeError, 'Not one of: [Duration, #to_nanos]' unless other.respond_to?(:to_nanos) 237 | 238 | Instant.new(@ns + other.to_nanos) 239 | end 240 | 241 | # Subtract another +Instant+ to generate a +Duration+ between the two, 242 | # or a +Duration+ or +#to_nanos+-coercible object, to generate an +Instant+ 243 | # offset by it. 244 | # 245 | # @example 246 | # (Instant.now - Duration.from_secs(1)).to_s # => "1.000016597s" 247 | # (Instant.now - Instant.now).to_s # => "-3.87μs" 248 | # 249 | # @param other [Instant, Duration, #to_nanos] 250 | # @return [Duration, Instant] 251 | def -(other) 252 | if other.is_a?(Instant) 253 | # @type var other: Instant 254 | Duration.new(@ns - other.ns) 255 | elsif other.respond_to?(:to_nanos) 256 | # @type var other: Duration | _ToNanos 257 | Instant.new(@ns - other.to_nanos) 258 | else 259 | raise TypeError, 'Not one of: [Instant, Duration, #to_nanos]' 260 | end 261 | end 262 | 263 | # Determine if the given +Instant+ is before, equal to or after this one. 264 | # +nil+ if not passed an +Instant+. 265 | # 266 | # @return [-1, 0, 1, nil] 267 | def <=>(other) 268 | @ns <=> other.ns if other.is_a?(Instant) 269 | end 270 | 271 | # Determine if +other+'s value equals that of this +Instant+. 272 | # Use +eql?+ if type checks are desired for future compatibility. 273 | # 274 | # @return [Boolean] 275 | # @see #eql? 276 | def ==(other) 277 | other.is_a?(Instant) && @ns == other.ns 278 | end 279 | 280 | alias eql? == 281 | 282 | # Generate a hash for this type and value. 283 | # 284 | # @return [Integer] 285 | def hash 286 | [self.class, @ns].hash 287 | end 288 | end 289 | end 290 | -------------------------------------------------------------------------------- /lib/monotime/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Monotime 4 | # Version of the `monotime` gem 5 | MONOTIME_VERSION = '0.8.2' 6 | end 7 | -------------------------------------------------------------------------------- /monotime.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "monotime/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "monotime" 8 | spec.version = Monotime::MONOTIME_VERSION 9 | spec.authors = ["Thomas Hurst"] 10 | spec.email = ["tom@hur.st"] 11 | 12 | spec.summary = %q{A sensible interface to the monotonic clock} 13 | spec.homepage = "https://github.com/Freaky/monotime" 14 | spec.license = "MIT" 15 | 16 | spec.required_ruby_version = '>= 2.7.0' 17 | 18 | # Specify which files should be added to the gem when it is released. 19 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 20 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 21 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 22 | end 23 | spec.bindir = "exe" 24 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 25 | spec.require_paths = ["lib"] 26 | 27 | spec.add_development_dependency "bundler", "~> 2" 28 | spec.add_development_dependency "rake", "~> 13.0" 29 | spec.add_development_dependency "minitest", "~> 5.0" 30 | spec.add_development_dependency "simplecov", "~> 0.21" 31 | 32 | if RUBY_PLATFORM != 'java' && RUBY_VERSION.split(".").first.to_i >= 3 33 | spec.add_development_dependency "steep", "~> 1.5" 34 | spec.add_development_dependency "rbs", "~> 3.2" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /sig/monotime.rbs: -------------------------------------------------------------------------------- 1 | # Classes 2 | module Monotime 3 | MONOTIME_VERSION: String 4 | 5 | interface _ToNanos 6 | def is_a?: (Module) -> bool 7 | def respond_to?: (Symbol name, ?boolish include_all) -> bool 8 | def to_nanos: -> Integer 9 | end 10 | 11 | interface _MonotonicFunction 12 | def call: -> Integer 13 | end 14 | 15 | interface _SleepFunction 16 | def call: (Float) -> Integer 17 | end 18 | 19 | class Duration 20 | include Comparable 21 | 22 | ZERO: Duration 23 | DIVISORS: ::Array[[Float, String]] 24 | @ns: Integer 25 | 26 | def ns: -> Integer 27 | def initialize: (?int nanos) -> void 28 | def self.new: (?int nanos) -> Duration 29 | def self.zero: -> Duration 30 | def self.from_secs: (real secs) -> Duration 31 | alias self.secs self.from_secs 32 | def self.from_millis: (real millis) -> Duration 33 | alias self.millis self.from_millis 34 | def self.from_micros: (real micros) -> Duration 35 | alias self.micros self.from_micros 36 | def self.from_nanos: (int nanos) -> Duration 37 | alias self.nanos self.from_nanos 38 | def self.measure: () { () -> untyped } -> Duration 39 | def self.with_measure: () { () -> untyped } -> [untyped, Duration] 40 | def self.sleep_function: -> _SleepFunction 41 | def self.sleep_function=: (_SleepFunction) -> void 42 | def self.default_to_s_precision: -> Integer 43 | def self.default_to_s_precision=: (int) -> void 44 | def +: (_ToNanos other) -> Duration 45 | def -: (_ToNanos other) -> Duration 46 | def /: (real other) -> Duration 47 | def *: (real other) -> Duration 48 | def -@: -> Duration 49 | def abs: -> Duration 50 | def <=>: (_ToNanos | untyped other) -> Integer? 51 | def ==: (_ToNanos | untyped other) -> bool 52 | def eql?: (untyped other) -> bool 53 | def hash: -> Integer 54 | def to_secs: -> Float 55 | alias secs to_secs 56 | def to_millis: -> Float 57 | alias millis to_millis 58 | def to_micros: -> Float 59 | alias micros to_micros 60 | def to_nanos: -> Integer 61 | alias nanos to_nanos 62 | def positive?: -> bool 63 | def negative?: -> bool 64 | def zero?: -> boolish 65 | def nonzero?: -> boolish 66 | def sleep: -> Integer 67 | def to_s: (?int precision) -> String 68 | end 69 | 70 | class Instant 71 | include Comparable 72 | 73 | attr_reader ns: Integer 74 | 75 | def self.clock_getres: (?untyped clock) -> Duration? 76 | def self.clock_name: -> Symbol? 77 | def self.clock_id: -> untyped 78 | def self.clock_id=: (untyped clock) -> void 79 | def self.monotonic_function: -> _MonotonicFunction 80 | def self.monotonic_function=: (_MonotonicFunction function) -> void 81 | def self.select_clock_id: -> Integer 82 | def initialize: (?int nanos) -> void 83 | def self.now: -> Instant 84 | def duration_since: (Instant earlier) -> Duration 85 | def elapsed: -> Duration 86 | def in_past?: -> bool 87 | alias past? in_past? 88 | def in_future?: -> bool 89 | alias future? in_future? 90 | def sleep: (?(_ToNanos | nil) duration) -> Duration 91 | def sleep_secs: (real secs) -> Duration 92 | def sleep_millis: (real millis) -> Duration 93 | def to_s: (?int precision) -> String 94 | def +: (_ToNanos other) -> Instant 95 | def -: (Duration | Instant | _ToNanos other) -> (Duration | Instant) 96 | def <=>: (untyped other) -> Integer? 97 | def ==: (untyped other) -> bool 98 | alias eql? == 99 | def hash: -> Integer 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /steep_expectations.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - file: lib/monotime/duration.rb 3 | diagnostics: 4 | - range: 5 | start: 6 | line: 163 7 | character: 19 8 | end: 9 | line: 163 10 | character: 35 11 | severity: ERROR 12 | message: |- 13 | Cannot find compatible overloading of method `/` of type `::Integer` 14 | Method types: 15 | def /: (::Integer) -> ::Integer 16 | | (::Float) -> ::Float 17 | | (::Rational) -> ::Rational 18 | | (::Complex) -> ::Complex 19 | code: Ruby::UnresolvedOverloading 20 | - range: 21 | start: 22 | line: 174 23 | character: 19 24 | end: 25 | line: 174 26 | character: 35 27 | severity: ERROR 28 | message: |- 29 | Cannot find compatible overloading of method `*` of type `::Integer` 30 | Method types: 31 | def *: (::Float) -> ::Float 32 | | (::Rational) -> ::Rational 33 | | (::Complex) -> ::Complex 34 | | (::Integer) -> ::Integer 35 | code: Ruby::UnresolvedOverloading 36 | - range: 37 | start: 38 | line: 317 39 | character: 4 40 | end: 41 | line: 322 42 | character: 26 43 | severity: ERROR 44 | message: |- 45 | Cannot assign a value of type `::Array[::Array[(::Float | ::String)]]` to a constant of type `::Array[[::Float, ::String]]` 46 | ::Array[::Array[(::Float | ::String)]] <: ::Array[[::Float, ::String]] 47 | ::Array[(::Float | ::String)] <: [::Float, ::String] 48 | code: Ruby::IncompatibleAssignment 49 | - range: 50 | start: 51 | line: 356 52 | character: 35 53 | end: 54 | line: 356 55 | character: 46 56 | severity: ERROR 57 | message: |- 58 | Cannot find compatible overloading of method `/` of type `::Integer` 59 | Method types: 60 | def /: (::Integer) -> ::Integer 61 | | (::Float) -> ::Float 62 | | (::Rational) -> ::Rational 63 | | (::Complex) -> ::Complex 64 | code: Ruby::UnresolvedOverloading 65 | - file: lib/monotime/include.rb 66 | diagnostics: 67 | - range: 68 | start: 69 | line: 5 70 | character: 0 71 | end: 72 | line: 5 73 | character: 7 74 | severity: ERROR 75 | message: Type `::Object` does not have method `include` 76 | code: Ruby::NoMethod 77 | -------------------------------------------------------------------------------- /test/monotime_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MonotimeTest < Minitest::Test 4 | include Monotime 5 | 6 | puts "Clock source: #{Instant.clock_name}" 7 | 8 | def test_that_it_has_a_version_number 9 | refute_nil ::Monotime::MONOTIME_VERSION 10 | end 11 | 12 | def test_instant_monotonic 13 | 10.times do 14 | assert Instant.now <= Instant.now 15 | end 16 | end 17 | 18 | def test_instant_equality 19 | a = Instant.now 20 | dur = Duration::from_nanos(1) 21 | assert_equal a, a 22 | assert_equal a.hash, a.dup.hash 23 | assert((a <=> a).zero?) 24 | assert(a < a + dur) 25 | assert(a > a - dur) 26 | refute_equal a, a + dur 27 | assert a.eql?(a + Duration.from_nanos(0)) 28 | refute a.eql?(a + Duration.from_nanos(1)) 29 | 30 | assert_nil a <=> 'meep' 31 | end 32 | 33 | def test_instant_past_future 34 | past = Instant.now - Duration.secs(1) 35 | 36 | assert past.in_past? 37 | refute past.in_future? 38 | 39 | future = Instant.now + Duration.secs(1) 40 | assert future.in_future? 41 | refute future.in_past? 42 | end 43 | 44 | def test_duration_zeros 45 | z = Duration.from_nanos(0) 46 | nz = Duration.from_nanos(1) 47 | 48 | assert z.zero? 49 | refute nz.zero? 50 | 51 | refute z.nonzero? 52 | assert nz.nonzero? 53 | end 54 | 55 | def test_instant_elapsed 56 | a = Instant.now - Duration.from_millis(100) 57 | elapsed = a.elapsed 58 | 59 | assert elapsed.nonzero? 60 | assert elapsed.positive? 61 | end 62 | 63 | def test_instant_sub 64 | now = Instant.now 65 | zero = now - now 66 | 67 | assert zero.zero? 68 | end 69 | 70 | def test_instant_to_s 71 | assert_match(/\A\d+.s\z/, Instant.now.to_s(0)) 72 | end 73 | 74 | def test_duration_equality 75 | a = Duration.from_secs(1) 76 | b = Duration.from_secs(2) 77 | duck_a = Class.new { def to_nanos() Duration.from_secs(1).to_nanos end }.new 78 | 79 | assert_equal a, Duration.from_secs(1) 80 | assert_equal a.hash, Duration.from_secs(1).hash 81 | assert a.eql?(Duration.from_secs(1)) 82 | 83 | assert_equal a, duck_a 84 | refute_equal b, duck_a 85 | refute a.eql?(duck_a) 86 | refute b.eql?(duck_a) 87 | 88 | refute_equal a, b 89 | assert a < b 90 | assert b > a 91 | 92 | assert_nil a <=> 'meep' 93 | end 94 | 95 | def test_duration_conversions 96 | secs = Duration.from_secs(10) 97 | assert_equal secs, Duration.from_secs(secs.to_secs) 98 | assert_equal secs, Duration.from_millis(secs.to_millis) 99 | assert_equal secs, Duration.from_micros(secs.to_micros) 100 | assert_equal secs, Duration.from_nanos(secs.to_nanos) 101 | 102 | assert_equal secs, Duration.secs(secs.secs) 103 | assert_equal secs, Duration.millis(secs.millis) 104 | assert_equal secs, Duration.micros(secs.micros) 105 | assert_equal secs, Duration.nanos(secs.nanos) 106 | end 107 | 108 | def test_duration_maths 109 | one_sec = Duration.from_secs(1) 110 | two_secs = Duration.from_secs(2) 111 | three_secs = Duration.from_secs(3) 112 | 113 | assert_equal one_sec * 2, two_secs 114 | assert_equal two_secs / 2, one_sec 115 | assert_equal one_sec + two_secs, three_secs 116 | assert_equal two_secs - one_sec, one_sec 117 | end 118 | 119 | def test_duration_measure 120 | elapsed = Duration.measure { "bleep" } 121 | assert_instance_of Duration, elapsed 122 | assert elapsed.positive? 123 | end 124 | 125 | def test_duration_with_measure 126 | res, elapsed = Duration.with_measure { "bloop" } 127 | assert_equal "bloop", res 128 | assert_instance_of Duration, elapsed 129 | assert elapsed.positive? 130 | end 131 | 132 | def test_type_errors 133 | type_error = begin 134 | RBS::Test::Tester::TypeError 135 | rescue NameError 136 | TypeError 137 | end 138 | assert_raises(type_error) { Instant.now.duration_since(0) } 139 | assert_raises(type_error) { Instant.now - 0 } 140 | assert_raises(type_error) { Instant.now + 0 } 141 | assert_raises(type_error) { Duration.secs(1) + 0 } 142 | assert_raises(type_error) { Duration.secs(1) - 0 } 143 | assert_raises(type_error) { Duration.secs(1) * "foo" } 144 | assert_raises(type_error) { Duration.secs(1) / "foo" } 145 | end 146 | 147 | def test_sleeps 148 | slept = Duration.zero 149 | old_sleep_function = Duration.sleep_function 150 | Duration.sleep_function = ->(secs) { slept += Duration.secs(secs);secs.to_i } 151 | ten_ms = Duration.from_millis(10) 152 | also_ten_ms = Object.new 153 | def also_ten_ms.to_nanos() 154 | Duration.from_millis(10).to_nanos 155 | end 156 | 157 | t = Instant.now 158 | a = t.sleep(ten_ms) 159 | t -= ten_ms 160 | b = t.sleep(also_ten_ms) 161 | 162 | assert((t - ten_ms).sleep.negative?) 163 | 164 | assert_includes 5..50, a.to_millis 165 | assert a > b 166 | assert b.negative? 167 | 168 | # Quick check of aliases 169 | assert_includes 5..50, Instant.now.sleep_millis(10).to_millis 170 | assert_includes 5..50, Instant.now.sleep_secs(0.01).to_millis 171 | Duration.sleep_function = old_sleep_function 172 | end 173 | 174 | def test_duration_unary 175 | one_sec = Duration.from_secs(1) 176 | minus_one_sec = Duration.from_secs(-1) 177 | 178 | assert_equal one_sec, minus_one_sec.abs 179 | assert_equal one_sec.abs, minus_one_sec.abs 180 | assert_equal(-one_sec, minus_one_sec) 181 | assert_equal one_sec, -minus_one_sec 182 | end 183 | 184 | def test_instant_hashing 185 | inst0 = Instant.now 186 | inst1 = inst0 + Duration.from_nanos(1) 187 | inst2 = inst0 + Duration.from_secs(1) 188 | inst3 = inst0 + Duration.from_secs(10) 189 | 190 | hash = {inst0 => 0, inst1 => 1, inst2 => 2, inst3 => 3} 191 | 192 | assert_equal hash[inst0], 0 193 | assert_equal hash[inst1], 1 194 | assert_equal hash[inst2], 2 195 | assert_equal hash[inst3], 3 196 | 197 | assert_equal hash.keys.sort, [inst0, inst1, inst2, inst3] 198 | end 199 | 200 | def test_duration_hashing 201 | dur0 = Duration.new 202 | dur1 = Duration.from_nanos(1) 203 | dur2 = Duration.from_secs(1) 204 | dur3 = Duration.from_secs(10) 205 | 206 | hash = {dur0 => 0, dur1 => 1, dur2 => 2, dur3 => 3} 207 | 208 | assert_equal hash[dur0], 0 209 | assert_equal hash[dur1], 1 210 | assert_equal hash[dur2], 2 211 | assert_equal hash[dur3], 3 212 | 213 | assert_equal hash.keys.sort, [dur0, dur1, dur2, dur3] 214 | end 215 | 216 | def test_duration_format 217 | assert_equal '1s', Duration.from_secs(1).to_s 218 | assert_equal '1.5s', Duration.from_secs(1.5).to_s 219 | assert_equal '1.25s', Duration.from_secs(1.25).to_s 220 | assert_equal '1.2s', Duration.from_secs(1.25).to_s(1) 221 | assert_equal '1.3s', Duration.from_secs(1.26).to_s(1) 222 | assert_equal '2s', Duration.from_secs(1.6).to_s(0) 223 | 224 | assert_equal '1ms', Duration.from_millis(1).to_s 225 | assert_equal '1.5ms', Duration.from_millis(1.5).to_s 226 | assert_equal '1.25ms', Duration.from_millis(1.25).to_s 227 | assert_equal '1.2ms', Duration.from_millis(1.25).to_s(1) 228 | assert_equal '1.3ms', Duration.from_millis(1.26).to_s(1) 229 | assert_equal '2ms', Duration.from_millis(1.6).to_s(0) 230 | 231 | assert_equal '1μs', Duration.from_micros(1).to_s 232 | assert_equal '1.5μs', Duration.from_micros(1.5).to_s 233 | assert_equal '1.25μs', Duration.from_micros(1.25).to_s 234 | assert_equal '1.2μs', Duration.from_micros(1.25).to_s(1) 235 | assert_equal '1.3μs', Duration.from_micros(1.26).to_s(1) 236 | assert_equal '2μs', Duration.from_micros(1.6).to_s(0) 237 | 238 | assert_equal '-1μs', Duration.from_micros(-1).to_s 239 | assert_equal '-1.5μs', Duration.from_micros(-1.5).to_s 240 | assert_equal '-1.25μs', Duration.from_micros(-1.25).to_s 241 | assert_equal '-1.2μs', Duration.from_micros(-1.25).to_s(1) 242 | assert_equal '-1.3μs', Duration.from_micros(-1.26).to_s(1) 243 | assert_equal '-2μs', Duration.from_micros(-1.6).to_s(0) 244 | 245 | assert_equal '1ns', Duration.from_nanos(1).to_s 246 | assert_equal '-1ns', Duration.from_nanos(-1).to_s 247 | end 248 | 249 | def test_duration_format_zero_stripping 250 | # Zeros should not be stripped if precision = 0 251 | assert_equal '100s', Duration.from_secs(100).to_s(0) 252 | assert_equal '100ns', Duration.from_nanos(100).to_s 253 | end 254 | 255 | def test_duration_to_s_precision 256 | duration = Duration.from_nanos(1111111111) 257 | assert_equal "1.111111111s", duration.to_s 258 | assert_equal 9, Duration.default_to_s_precision 259 | 260 | Duration.default_to_s_precision = 2 261 | assert_equal 2, Duration.default_to_s_precision 262 | assert_equal "1.11s", duration.to_s 263 | 264 | Duration.default_to_s_precision = 9 265 | end 266 | 267 | def test_duration_sleep_function 268 | old_sleep_function = Duration.sleep_function 269 | assert_equal Kernel.method(:sleep), old_sleep_function 270 | 271 | slept = 0 272 | Duration.sleep_function = ->(duration) { slept += duration;duration.to_i } 273 | 274 | Duration.secs(1).sleep 275 | Duration.millis(1).sleep 276 | assert_in_epsilon slept, 1.001 277 | 278 | Duration.sleep_function = old_sleep_function 279 | end 280 | 281 | def test_zero_constant 282 | assert_equal Duration.zero.object_id, Duration::ZERO.object_id 283 | assert_equal Duration.new.object_id, Duration::ZERO.object_id 284 | assert_equal Duration.secs(0).object_id, Duration::ZERO.object_id 285 | end 286 | 287 | def test_getres 288 | assert_instance_of Duration, Instant.clock_getres 289 | end 290 | 291 | def test_instant_clock_id 292 | old_clock_id = Instant.clock_id 293 | Instant.clock_id = Process::CLOCK_REALTIME 294 | assert_equal Process::CLOCK_REALTIME, Instant.clock_id 295 | 296 | assert_instance_of Instant, Instant.now 297 | Instant.clock_id = old_clock_id 298 | end 299 | 300 | def test_instant_monotonic_function 301 | old_fn = Instant.monotonic_function 302 | now = 0 303 | Instant.monotonic_function = ->() { now += 1 } 304 | assert_equal Duration.nanos(1), Instant.now.elapsed 305 | assert_equal 2, now 306 | Instant.monotonic_function = old_fn 307 | end 308 | 309 | def test_clock_name 310 | old_clock_id = Instant.clock_id 311 | Instant.clock_id = Process::CLOCK_REALTIME 312 | assert_equal :CLOCK_REALTIME, Instant.clock_name 313 | Instant.clock_id = old_clock_id 314 | end 315 | end 316 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | unless RUBY_ENGINE=='truffleruby' 4 | require 'simplecov' 5 | SimpleCov.start 6 | end 7 | 8 | require 'monotime' 9 | 10 | require 'minitest/autorun' 11 | --------------------------------------------------------------------------------