monotonic.rb 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf995a6ab7105dc05b1b11b660fc4a28b34f07d784c5c13af2b83e46c1d5b949
4
- data.tar.gz: 871dc0fc14b0771d4ddc00d7717787df34ace4d98b248e8613ba24918c40e641
3
+ metadata.gz: 735b3915ec7e7b95ef1fde8effe2d2723fea5eb027f9af87d1bea9dc8552048a
4
+ data.tar.gz: 8497225d7afb17a9e6c5f1c6a7c79510986fc74f731c81c06c4b0529ca1fa1d1
5
5
  SHA512:
6
- metadata.gz: 809325b41986a73bea427d0685a4b63f42a6a89a090716edda20e400c5f9a0b91ec8ba28e1844ef63470cdef8368b913f3479488e449076abd2f63321c9aaa42
7
- data.tar.gz: 744d49e5aaa3028ac0fbf9180e146dd73ceb5c2ea6da50fcb33e72ef5659586aacff6786be1526244ab195f6e144a9b8a75b6fe488ed7a1cbd398e273eaa9ae2
6
+ metadata.gz: a9a637b1664afe2b51d8b2f6abedace3e2dd6faf150d5767da5a6add304acba0554db879f72f16867856219eee41da3ed1042418e832a76722928dea62e27511
7
+ data.tar.gz: fed34f80416fa88c2b31fce83a9f21576a62d678c74ca0202bedf550149842de31cd121e55cec8f377f1354b4748cd4f818333b9aed726d778310630f9c8854b
data/CHANGELOG CHANGED
@@ -1,5 +1,43 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260816
4
+
5
+ 0.8.0: Use duration.rb gem.
6
+
7
+ 1. + duration.rb as a dependency, constrained to >= 0.4.0 where sys-uptime is unconstrained. Units and the arithmetic between them are its business, and were being hand-rolled here. 0.4.0 is where Duration::Nanoseconds arrived, which this gem names, and is the true minimum rather than a guess: the whole API used here exercises clean against it, and everything between is additive.
8
+ 2. ~ monotonic.rb.gemspec: spec.dependencies takes a name and a requirement as well as a bare name, the splat in #dependencies= already allowing for it. sys-uptime stays unconstrained as before.
9
+ 3. - Monotonic::NANOSECONDS_PER_SECOND, introduced at 0.7.0 and by now divided by in four places. It existed only because the conversion was hand-rolled; a duration converts by exact ratios and stays a Rational until to_f is asked for, where the constant dropped to Float a step early.
10
+ 4. + Monotonic::Timer#to_duration, being a Duration::Nanoseconds. An elapsed interval is a duration, and every unit follows from this one exactly.
11
+ 5. ~ Monotonic::Timer#total_time: derived from #to_duration. It returns a Float of seconds as before.
12
+ 6. ~ Monotonic::Time#seconds_since_boot: converted by a duration rather than by division. It returns a Float as before.
13
+ 7. ~ BREAKING: Monotonic::Time#- returns a Duration::Nanoseconds where it returned a Float of seconds. The difference of two instants is a duration, and that is the reason a monotonic clock is read at all. Given a duration instead it returns the earlier instant.
14
+ 8. ~ BREAKING: Monotonic::Time#+ takes a Duration and returns a later Monotonic::Time. Given another instant it raises TypeError, where it formerly returned the sum of two seconds-since-boot figures: a quantity which moves when the epoch moves is not a quantity, and the README demonstrated it producing 2417598.681896, which was nothing at all. Ruby's own Time raises upon time + time for the same reason.
15
+ 9. ~ Monotonic::Time#initialize takes an optional nanoseconds-since-boot, defaulting to the clock, so that #+ and #- can answer with an instant other than now.
16
+ 10. ~ lib/Monotonic/Timer.rb: the constants moved above the methods, CLOCK no longer following .clock_name. + Monotonic::Timer::CLOCK_NAMES, declaring what would be accepted and in what order, so that CLOCK derives from it rather than from a method and need not come after one.
17
+ 11. ~ test/Monotonic/Time_test.rb: #+ and #- rewritten for the arithmetic above, including that an instant plus an instant raises and a bare number is refused.
18
+ 12. + test/Monotonic/Timer_test.rb: #to_duration, that it carries the same figure as #total_nanoseconds and converts to the same seconds as #total_time.
19
+ 13. ~ test/gemspec_test.rb: the runtime dependency list.
20
+ 14. ~ README.md: + what belongs to duration.rb and why, and the usage for the arithmetic and #to_duration.
21
+ 15. ~ Monotonic::Time#+ and #-: dispatch upon what the argument answers to rather than upon its class. One question tells the two cases apart — does this know where it sits since boot? — and it is asked of the object: an instant subtracts to a duration, and anything converting to nanoseconds shifts the instant. The refusal of instant plus instant is asked of the same duck, since leaving it to Monotonic::Time happening not to answer #to_nanoseconds would be an accident rather than a rule. A bare number still raises TypeError, the guard asking respond_to? rather than is_a?: duck typing forbids asking after a class, not asking after a capability. Ruby raises TypeError for an unsuitable operand throughout — Time, Integer, Float, Rational — and NoMethodError is a NameError, the family meaning that something named does not exist, where here the caller named #- which does. The message carries the protocol a duck must satisfy, so nothing is lost by not letting it escape. The parameters are named for their arithmetic role rather than for a type: /monotonic_time_addend/addend/ and /monotonic_time_subtrahend/subtrahend/, the type half of each having been an unenforced claim in any case, the old #- never having checked what it was given.
22
+ 16. + Monotonic::Timer#initialize takes instants:, defaulting to nil, which reads the clock directly as before. Supplied with Monotonic::Time it times upon CLOCK_MONOTONIC instead: coarser, at 1000ns against 42ns, but placeable against the wall clock through #to_time. Anything answering .now will do. This is what 0.7.0 took away without saying so, Timer having read Monotonic::Time as its substrate since 0.1.0 until the two took different clocks — and it could not be given back before now, Monotonic::Time#- having answered with a bare Float of seconds until this release, so that an interval taken from it would have been reported as nanoseconds and been wrong by a factor of a billion.
23
+ 17. ~ Monotonic::Timer#total_nanoseconds asks the elapsed figure to say itself in nanoseconds where it can, a raw difference already being in them and an instant's difference being a duration.
24
+ 18. + test/Monotonic/Timer_test.rb: which source is read, that either answers in nanoseconds, and that a supplied source times upon its own clock, the figure falling upon a multiple of that clock's resolution.
25
+ 19. ~ Monotonic::VERSION: /0.7.3/0.8.0/
26
+
27
+
28
+ ## 20260816
29
+
30
+ 0.7.3: Require Monotonic::Time from the gem, and stop every instant fetching the boot time.
31
+
32
+ 1. ~ lib/monotonic.rb: + require_relative Monotonic/Time. It had reached a caller only by way of lib/Monotonic/Timer.rb requiring a file which Timer stopped using at 0.7.0, when the two took different clocks. Delete that stray line and Monotonic::Time would have vanished from the published gem, and nothing said so.
33
+ 2. ~ lib/Monotonic/Timer.rb: - require_relative ./Time, which nothing there has wanted since 0.7.0.
34
+ 3. + test/loading_test.rb: that requiring the gem defines Monotonic::Time and Monotonic::Timer, and the version. In a process of its own, this one having loaded every file directly and so being unable to tell what the gem alone would bring. The same hole as VERSION.rb before 0.7.1: a file nothing names is a file nothing checks.
35
+ 4. ~ Monotonic::Time#to_time: asks Sys::Uptime for the boot time rather than every instant keeping one. Only this method wanted it, and asking cost 1740ns of the 1947ns an instant took to make, so an instant now costs 194ns. Memoising it was tried and is wrong: Darwin computes kern.boottime from the wall clock and the uptime, so it moves as the clock is disciplined, and a kept copy would fix the origin while the mapping drifted from it.
36
+ 5. ~ test/Monotonic/Time_test.rb: the #initialize test asserted @boot_time was set, under a description copied from Timer's about blocks. It now asserts that an instant keeps the reading and nothing else. + that #to_time lands upon the wall clock, which nothing checked.
37
+ 6. ~ README.md: what Monotonic::Time is for, since 0.7.0 left it with no caller in the library and said only that Timer had stopped using it. It is the point type: the difference of two instants is what a monotonic clock is read for, and #to_time places one against the wall clock.
38
+ 7. + .gitignore: the standard list, as duration.rb and namo carry verbatim, in place of the three lines here. It is not among spec.files, so nothing in the published gem changes by it.
39
+ 8. ~ Monotonic::VERSION: /0.7.2/0.7.3/
40
+
3
41
  ## 20260812
4
42
 
5
43
  0.7.2: + gemspec test.
data/README.md CHANGED
@@ -14,13 +14,26 @@ Which clock is read follows from that. `Monotonic::Timer` measures intervals and
14
14
  How finely a clock advances is the platform's business and not this library's to claim, so it is asked rather than tabulated:
15
15
 
16
16
  ```ruby
17
- Monotonic::Timer.resolution
18
- # => 42
19
- Monotonic::Time.resolution
20
- # => 1000
17
+ Monotonic::Timer.resolution
18
+ # => 42
19
+ Monotonic::Time.resolution
20
+ # => 1000
21
21
  ```
22
22
 
23
- A reading is denominated in nanoseconds whether or not the clock affords them, so that is the method which says what a reading is worth. Two figures give the sense of it upon the finer clock: reading it costs about 34ns against the 42ns it takes to advance, the two being close enough that there is little to be had by going finer, and a pair of `Float`s would not begin to lose it until some six years of uptime — but they would begin.
23
+ A reading is denominated in nanoseconds whether or not the clock affords it, so that is the method which says what a reading is worth. Two figures give the sense of it upon the finer clock: reading it costs about 34ns against the 42ns it takes to advance, the two being close enough that there is little to be had by going finer, and a pair of `Float`s would not begin to lose it until some six years of uptime — but they would begin.
24
+
25
+ Units and the arithmetic between them belong to [duration.rb](https://github.com/thoran/duration.rb), which this gem depends upon rather than reimplementing. An elapsed interval is a duration, so `Monotonic::Timer#to_duration` hands one back and every unit follows from it exactly, a Rational until `to_f` is asked for. An instant is not a duration: `Monotonic::Time#-` gives the duration between two instants, `#+` takes a duration and gives a later instant, and adding one instant to another raises — a quantity which moves when you move the epoch is not a quantity, and Ruby's own `Time` refuses it for the same reason.
26
+
27
+ What `Monotonic::Time` is for follows from that. It is the point type: `Monotonic::Timer` used it as its own substrate until 0.7.0, when the two took different clocks, and what remains to it is what a point is good for — the difference of two instants, which is the reason a monotonic clock is read at all, and `#to_time`, which places one against the wall clock by way of the boot time.
28
+
29
+ Where a timing is taken from can be chosen. `Monotonic::Timer.new` reads the clock directly, as it always has and much the cheapest; `Monotonic::Timer.new(instants: Monotonic::Time)` times upon `CLOCK_MONOTONIC` instead, coarser but placeable against the wall clock. Anything answering `.now` will do.
30
+
31
+ ```ruby
32
+ timer = Monotonic::Timer.new(instants: Monotonic::Time)
33
+ timer.start; sleep 0.002; timer.stop
34
+ timer.total_nanoseconds
35
+ # => 2508000, upon a clock which ticks in whole microseconds
36
+ ```
24
37
 
25
38
 
26
39
  ## Installation
@@ -28,19 +41,19 @@ A reading is denominated in nanoseconds whether or not the clock affords them, s
28
41
  Add this line to your application's Gemfile:
29
42
 
30
43
  ```ruby
31
- gem 'monotonic.rb'
44
+ gem 'monotonic.rb'
32
45
  ```
33
46
 
34
47
  And then execute:
35
48
 
36
49
  ```bash
37
- $ bundle install
50
+ $ bundle install
38
51
  ```
39
52
 
40
53
  Or install it yourself as:
41
54
 
42
55
  ```bash
43
- $ gem install monotonic.rb
56
+ $ gem install monotonic.rb
44
57
  ```
45
58
 
46
59
 
@@ -49,74 +62,92 @@ Or install it yourself as:
49
62
  ### Monotonic::Time
50
63
 
51
64
  ```ruby
52
- require 'monotonic.rb'
53
- monotonic_time = Monotonic::Time.new
54
- monotonic_time.nanoseconds_since_boot
55
- # => 2614365376498000
56
- monotonic_time.seconds_since_boot
57
- # => 1208799.325906
58
- monotonic_time + Monotonic::Time.now
59
- # => 2417598.681896
60
- monotonic_time - Monotonic::Time.now
61
- # => -0.044104999862611294
62
- monotonic_time.to_s
63
- # => "1164320.268127 seconds since boot."
64
- monotonic_time.to_time
65
- # => 2021-06-07 09:27:08 8249692651179/8388608000000 +1000
65
+ require 'monotonic.rb'
66
+
67
+ monotonic_time = Monotonic::Time.new
68
+ monotonic_time.nanoseconds_since_boot
69
+ # => 2614365376498000
70
+
71
+ monotonic_time.seconds_since_boot
72
+ # => 1208799.325906
73
+
74
+ monotonic_time + Duration::Seconds.new(30)
75
+ # => #<Monotonic::Time>, thirty seconds later
76
+
77
+ Monotonic::Time.now - monotonic_time
78
+ # => #<Duration::Nanoseconds @nanoseconds=44104999>
79
+
80
+ monotonic_time + Monotonic::Time.now
81
+ # => TypeError: an instant plus an instant is not an instant
82
+
83
+ monotonic_time.to_s
84
+ # => "1164320.268127 seconds since boot."
85
+
86
+ monotonic_time.to_time
87
+ # => 2021-06-07 09:27:08 8249692651179/8388608000000 +1000
66
88
  ```
67
89
 
68
90
  ### Monotonic::Timer without a block
69
91
 
70
92
  ```ruby
71
- require 'monotonic.rb'
72
- timer = Monotonic::Timer.new
73
- timer.start
74
- i = 0
75
- 1_000_000.times{puts i += 1}
76
- timer.stop
77
- timer.total_nanoseconds
78
- # => 27734000
79
- timer.total_time
80
- # => 7.166559999808669
93
+ require 'monotonic.rb'
94
+
95
+ timer = Monotonic::Timer.new
96
+ timer.start
97
+ i = 0
98
+ 1_000_000.times{puts i += 1}
99
+ timer.stop
100
+ timer.total_nanoseconds
101
+ # => 27734000
102
+
103
+ timer.to_duration
104
+ # => #<Duration::Nanoseconds @nanoseconds=27734000>
105
+
106
+ timer.to_duration.to_microseconds.to_f
107
+ # => 27734.0
108
+
109
+ timer.total_time
110
+ # => 0.027734
81
111
  ```
82
112
 
83
113
  ### Monotonic::Timer with a block
84
114
 
85
115
  ```ruby
86
- require 'monotonic.rb'
87
- time = Monotonic::Timer.time do
88
- i = 0
89
- 1_000_000.times{puts i += 1}
90
- end
91
- time
92
- # => 6.975823000073433
116
+ require 'monotonic.rb'
117
+
118
+ time = Monotonic::Timer.time do
119
+ i = 0
120
+ 1_000_000.times{puts i += 1}
121
+ end
122
+ time
123
+ # => 6.975823000073433
93
124
  ```
94
125
 
95
126
  ### Monotonic::Timer with a block and block variable
96
127
 
97
128
  ```ruby
98
- require 'monotonic.rb'
99
- time = Monotonic::Timer.time do |timer|
100
- i = 0
101
- 500_000.times{puts i += 1}
102
- p timer.total_time
103
- 500_000.times{puts i += 1}
104
- end
105
- time
106
- # => 6.975823000073433
129
+ require 'monotonic.rb'
130
+ time = Monotonic::Timer.time do |timer|
131
+ i = 0
132
+ 500_000.times{puts i += 1}
133
+ p timer.total_time
134
+ 500_000.times{puts i += 1}
135
+ end
136
+ time
137
+ # => 6.975823000073433
107
138
  ```
108
139
 
109
140
  ### Monotonic::Timer with a block on a timer instance
110
141
 
111
142
  ```ruby
112
- require 'monotonic.rb'
113
- timer = Monotonic::Timer.new
114
- time = timer.time do
115
- i = 0
116
- 1_000_000.times{puts i += 1}
117
- end
118
- time
119
- # => 7.033131000120193
143
+ require 'monotonic.rb'
144
+ timer = Monotonic::Timer.new
145
+ time = timer.time do
146
+ i = 0
147
+ 1_000_000.times{puts i += 1}
148
+ end
149
+ time
150
+ # => 7.033131000120193
120
151
  ```
121
152
 
122
153
 
@@ -1,22 +1,23 @@
1
1
  # Monotonic/Time.rb
2
2
  # Monotonic::Time
3
3
 
4
+ require 'duration.rb'
4
5
  require 'sys-uptime'
5
6
 
6
7
  module Monotonic
7
- NANOSECONDS_PER_SECOND = 1_000_000_000
8
-
9
8
  class Time
9
+ # An instant, which #to_time maps back onto the wall clock by way of the
10
+ # boot time, so the clock wanted here is the one which tracks time since
11
+ # boot as the wall clock understands it. That is CLOCK_MONOTONIC, sleep and
12
+ # all. Monotonic::Timer measures intervals rather than instants and chooses
13
+ # a finer clock of its own.
14
+ #
15
+ # Unlike Timer's, this one is chosen rather than found, there being no
16
+ # alternative which would still answer to #to_time. .clock_name is a method
17
+ # all the same, so that the two classes answer the question alike.
18
+ CLOCK = Process::CLOCK_MONOTONIC
19
+
10
20
  class << self
11
- # An instant, which #to_time maps back onto the wall clock by way of the
12
- # boot time, so the clock wanted here is the one which tracks time since
13
- # boot as the wall clock understands it. That is CLOCK_MONOTONIC, sleep
14
- # and all. Monotonic::Timer measures intervals rather than instants and
15
- # chooses a finer clock of its own.
16
- #
17
- # Unlike Timer's, this one is chosen rather than found, there being no
18
- # alternative which would still answer to #to_time. It is a method all
19
- # the same, so that the two classes answer the question alike.
20
21
  def clock_name
21
22
  :CLOCK_MONOTONIC
22
23
  end
@@ -28,17 +29,11 @@ module Monotonic
28
29
  # How finely this clock advances, in nanoseconds. It is asked rather than
29
30
  # tabulated, being a property of the processor and the operating system
30
31
  # and not of this library: upon macOS CLOCK_MONOTONIC answers 1000 here.
31
- # A reading is denominated in nanoseconds whatever the answer, so this is
32
- # the method which says what a reading is worth.
33
32
  def resolution
34
33
  Process.clock_getres(CLOCK, :nanosecond)
35
34
  end
36
35
  end # class << self
37
36
 
38
- # Read upon every instance, so a constant rather than a lookup. It follows
39
- # .clock_name, and so must come after it.
40
- CLOCK = Process.const_get(clock_name)
41
-
42
37
  attr_reader :nanoseconds_since_boot
43
38
 
44
39
  # The clock is read in nanoseconds because it answers there with an Integer,
@@ -46,28 +41,58 @@ module Monotonic
46
41
  # Seconds are derived rather than read, so that the reading loses nothing and
47
42
  # the rounding happens where it is asked for.
48
43
  def seconds_since_boot
49
- @nanoseconds_since_boot / NANOSECONDS_PER_SECOND.to_f
44
+ Duration::Nanoseconds.new(@nanoseconds_since_boot).to_seconds.to_f
50
45
  end
51
46
 
52
- def initialize
53
- @boot_time = Sys::Uptime.boot_time
54
- @nanoseconds_since_boot = Process.clock_gettime(CLOCK, :nanosecond)
47
+ def initialize(nanoseconds_since_boot = Process.clock_gettime(CLOCK, :nanosecond))
48
+ @nanoseconds_since_boot = nanoseconds_since_boot
55
49
  end
56
50
 
57
- def +(monotonic_time_addend)
58
- seconds_since_boot + monotonic_time_addend.seconds_since_boot
51
+ # One question tells the two cases apart, and it is asked of the object
52
+ # rather than of its class: does this know where it sits since boot? If it
53
+ # does it is an instant, and if it converts to nanoseconds it is a duration.
54
+ # Nothing here names a class, so anything answering the same messages will
55
+ # do.
56
+ #
57
+ # An instant plus a duration is an instant. An instant plus an instant is
58
+ # nothing at all: it depends upon where the epoch was arbitrarily put, and a
59
+ # quantity which moves when you move the origin is not a quantity. Ruby's
60
+ # own Time refuses it, with "time + time?", and so does this. The refusal is
61
+ # asked of the duck too, since leaving it to Monotonic::Time happening not to
62
+ # answer #to_nanoseconds would be an accident rather than a rule.
63
+ def +(addend)
64
+ if addend.respond_to?(:nanoseconds_since_boot)
65
+ raise TypeError, "can't add #{addend.class} to #{self.class}: an instant plus an instant is not an instant"
66
+ end
67
+ unless addend.respond_to?(:to_nanoseconds)
68
+ raise TypeError, "can't add #{addend.class} to #{self.class}: expected something answering to #to_nanoseconds"
69
+ end
70
+ self.class.new(@nanoseconds_since_boot + addend.to_nanoseconds.to_i)
59
71
  end
60
72
 
61
- def -(monotonic_time_subtrahend)
62
- seconds_since_boot - monotonic_time_subtrahend.seconds_since_boot
73
+ # Minus an instant it is the duration between them; minus a duration it is
74
+ # the earlier instant. The first is why a monotonic clock is read at all.
75
+ def -(subtrahend)
76
+ if subtrahend.respond_to?(:nanoseconds_since_boot)
77
+ Duration::Nanoseconds.new(@nanoseconds_since_boot - subtrahend.nanoseconds_since_boot)
78
+ elsif subtrahend.respond_to?(:to_nanoseconds)
79
+ self.class.new(@nanoseconds_since_boot - subtrahend.to_nanoseconds.to_i)
80
+ else
81
+ raise TypeError, "can't subtract #{subtrahend.class} from #{self.class}: expected an instant, or something answering to #to_nanoseconds"
82
+ end
63
83
  end
64
84
 
65
85
  def to_s
66
86
  "#{seconds_since_boot} seconds since boot."
67
87
  end
68
88
 
89
+ # The boot time is asked for here rather than kept upon every instant. It
90
+ # cost 1740ns of the 1947ns an instant took to make, and only this method
91
+ # wants it — so an instant is now cheap to take, and the mapping still
92
+ # uses the boot time as it stands rather than as it stood, Darwin moving
93
+ # kern.boottime as the wall clock is disciplined.
69
94
  def to_time
70
- @boot_time + seconds_since_boot
95
+ Sys::Uptime.boot_time + seconds_since_boot
71
96
  end
72
97
  end
73
98
  end
@@ -1,23 +1,28 @@
1
1
  # Monotonic/Timer.rb
2
2
  # Monotonic::Timer
3
3
 
4
- require_relative './Time'
4
+ require 'duration.rb'
5
5
 
6
6
  module Monotonic
7
7
  class Timer
8
+ # An interval is not an instant, and wants a different clock. Upon Darwin
9
+ # CLOCK_UPTIME_RAW advances in some 42ns against the 1000ns of
10
+ # CLOCK_MONOTONIC, and holds while the machine sleeps, which is the right
11
+ # answer for a timed block: a closed lid is not execution. Linux has no such
12
+ # clock, but its CLOCK_MONOTONIC already holds while suspended and advances
13
+ # finely, so the fallback carries the same meaning and not merely the same
14
+ # name. This is what would be accepted, in order; which of them exists is
15
+ # the platform's business, and .clock_name reports what was found.
16
+ CLOCK_NAMES = %i[CLOCK_UPTIME_RAW CLOCK_MONOTONIC]
17
+
18
+ # Read twice upon every measurement, against a floor of some tens of
19
+ # nanoseconds, so this is the one place here where a constant is worth the
20
+ # rigidity: a method call would be a measurable part of what it measures.
21
+ CLOCK = Process.const_get(CLOCK_NAMES.find{|name| Process.const_defined?(name)})
22
+
8
23
  class << self
9
- # An interval is not an instant, and wants a different clock. Upon Darwin
10
- # CLOCK_UPTIME_RAW advances in some 42ns against the 1000ns of
11
- # CLOCK_MONOTONIC, and holds while the machine sleeps, which is the right
12
- # answer for a timed block: a closed lid is not execution. Linux has no
13
- # such clock, but its CLOCK_MONOTONIC already holds while suspended and
14
- # advances finely, so the fallback carries the same meaning and not merely
15
- # the same name.
16
- #
17
- # Which clocks exist is the platform's business, so this is asked rather
18
- # than declared, as .resolution is.
19
24
  def clock_name
20
- @clock_name ||= %i[CLOCK_UPTIME_RAW CLOCK_MONOTONIC].find{|name| Process.const_defined?(name)}
25
+ @clock_name ||= CLOCK_NAMES.find{|name| Process.const_defined?(name)}
21
26
  end
22
27
 
23
28
  def time(&block)
@@ -33,11 +38,20 @@ module Monotonic
33
38
  end
34
39
  end # class << self
35
40
 
36
- # Read twice upon every measurement, against a floor of some tens of
37
- # nanoseconds, so this is the one place here where a constant is worth the
38
- # rigidity: a method call would be a measurable part of what it measures.
39
- # It follows .clock_name, and so must come after it.
40
- CLOCK = Process.const_get(clock_name)
41
+ # Where the instants come from. Nothing supplied means the clock is read
42
+ # directly, which is what this has always done and is much the cheapest: a
43
+ # raw reading costs some 57ns against the 171ns of making a Monotonic::Time,
44
+ # and two are taken per measurement.
45
+ #
46
+ # Supplying Monotonic::Time times upon CLOCK_MONOTONIC instead — coarser, at
47
+ # 1000ns against 42ns, but it maps back onto the wall clock, so a timing can
48
+ # be placed as well as measured. Anything answering .now will do, its
49
+ # instants needing only to subtract to something which can say itself in
50
+ # nanoseconds. That is what 0.7.0 took away without saying so, Timer having
51
+ # read Monotonic::Time until the two took different clocks.
52
+ def initialize(instants: nil)
53
+ @instants = instants
54
+ end
41
55
 
42
56
  def start
43
57
  @finish_nanoseconds = nil
@@ -51,12 +65,26 @@ module Monotonic
51
65
  # Two exact integers differenced, which spends none of the reading. A pair
52
66
  # of Floats would not begin to lose the clock at this resolution until some
53
67
  # six years of uptime, but they would begin.
68
+ #
69
+ # Raw readings difference to nanoseconds already. Instants difference to
70
+ # whatever their own subtraction gives, so it is asked to say itself in
71
+ # nanoseconds — which is why this could not be done before 0.8.0, where
72
+ # Monotonic::Time#- began answering with a duration rather than a bare Float
73
+ # of seconds.
54
74
  def total_nanoseconds
55
- finish_nanoseconds - @start_nanoseconds
75
+ elapsed = finish_nanoseconds - @start_nanoseconds
76
+ elapsed.respond_to?(:to_nanoseconds) ? elapsed.to_nanoseconds.to_i : elapsed
77
+ end
78
+
79
+ # An elapsed interval is a duration, so here is one, and every unit follows
80
+ # from it exactly. #total_time is the same figure in seconds, that being
81
+ # the unit this library has always answered in.
82
+ def to_duration
83
+ Duration::Nanoseconds.new(total_nanoseconds)
56
84
  end
57
85
 
58
86
  def total_time
59
- total_nanoseconds / NANOSECONDS_PER_SECOND.to_f
87
+ to_duration.to_seconds.to_f
60
88
  end
61
89
 
62
90
  def time
@@ -77,7 +105,7 @@ module Monotonic
77
105
  end
78
106
 
79
107
  def now
80
- Process.clock_gettime(CLOCK, :nanosecond)
108
+ @instants ? @instants.now : Process.clock_gettime(CLOCK, :nanosecond)
81
109
  end
82
110
  end
83
111
  end
@@ -2,5 +2,5 @@
2
2
  # Monotonic::VERSION
3
3
 
4
4
  module Monotonic
5
- VERSION = '0.7.2'
5
+ VERSION = '0.8.0'
6
6
  end
data/lib/monotonic.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # monotonic.rb
2
2
  # Monotonic
3
3
 
4
+ require_relative './Monotonic/Time'
4
5
  require_relative './Monotonic/Timer'
5
6
  require_relative './Monotonic/VERSION'
data/monotonic.rb.gemspec CHANGED
@@ -35,9 +35,10 @@ Gem::Specification.new do |spec|
35
35
  Dir['test/**/*.rb'],
36
36
  ].flatten
37
37
 
38
- spec.dependencies = %w{
39
- sys-uptime
40
- }
38
+ spec.dependencies = [
39
+ ['duration.rb', '>= 0.4.0'],
40
+ 'sys-uptime'
41
+ ]
41
42
 
42
43
  spec.development_dependencies = %w{
43
44
  minitest
@@ -7,9 +7,8 @@ describe Monotonic::Time do
7
7
  subject{Monotonic::Time.now}
8
8
 
9
9
  describe "#initialize" do
10
- it "the time spent in the block is returned as the value of the block" do
11
- expect(subject.instance_variable_get(:@boot_time)) \
12
- .must_equal(Sys::Uptime.boot_time)
10
+ it "keeps the reading and nothing else, the boot time being wanted only by #to_time" do
11
+ expect(subject.instance_variables).must_equal([:@nanoseconds_since_boot])
13
12
  end
14
13
 
15
14
  it "the reading is taken from the monotonic clock in nanoseconds" do
@@ -56,14 +55,35 @@ describe Monotonic::Time do
56
55
  end
57
56
 
58
57
  describe "#+" do
59
- it "returns an instance of string" do
60
- expect((subject + Monotonic::Time.now).class).must_equal(Float)
58
+ it "returns a later instant when given a duration" do
59
+ expect((subject + Duration::Seconds.new(5)).class).must_equal(Monotonic::Time)
60
+ end
61
+
62
+ it "advances by exactly that duration" do
63
+ expect((subject + Duration::Seconds.new(5)).nanoseconds_since_boot - subject.nanoseconds_since_boot) \
64
+ .must_equal(5_000_000_000)
65
+ end
66
+
67
+ it "refuses another instant, there being no such quantity" do
68
+ expect{subject + Monotonic::Time.now}.must_raise(TypeError)
69
+ end
70
+
71
+ it "refuses a bare number, which has no unit" do
72
+ expect{subject + 5}.must_raise(TypeError)
61
73
  end
62
74
  end
63
75
 
64
76
  describe "#-" do
65
- it "returns an instance of time" do
66
- expect((subject - Monotonic::Time.now).class).must_equal(Float)
77
+ it "returns the duration between two instants" do
78
+ expect((subject - Monotonic::Time.now).class).must_equal(Duration::Nanoseconds)
79
+ end
80
+
81
+ it "returns an earlier instant when given a duration" do
82
+ expect((subject - Duration::Seconds.new(5)).class).must_equal(Monotonic::Time)
83
+ end
84
+
85
+ it "refuses a bare number, which has no unit" do
86
+ expect{subject - 5}.must_raise(TypeError)
67
87
  end
68
88
  end
69
89
 
@@ -77,5 +97,9 @@ describe Monotonic::Time do
77
97
  it "returns an instance of time" do
78
98
  expect(subject.to_time.class).must_equal(Time)
79
99
  end
100
+
101
+ it "lands upon the wall clock, the boot time being asked for as it stands" do
102
+ expect((subject.to_time - Time.now).abs).must_be :<, 0.01
103
+ end
80
104
  end
81
105
  end
@@ -128,6 +128,57 @@ describe Monotonic::Timer do
128
128
  end
129
129
  end
130
130
 
131
+ context "where the instants come from" do
132
+ it "reads the clock directly when nothing is supplied" do
133
+ timer = Monotonic::Timer.new
134
+ timer.start
135
+ timer.stop
136
+ expect(timer.total_nanoseconds.class).must_equal(Integer)
137
+ end
138
+
139
+ it "takes them from whatever is supplied, in nanoseconds still" do
140
+ timer = Monotonic::Timer.new(instants: Monotonic::Time)
141
+ timer.start
142
+ sleep 0.01
143
+ timer.stop
144
+ expect(timer.total_nanoseconds.class).must_equal(Integer)
145
+ expect(timer.total_nanoseconds).must_be :>, 5_000_000
146
+ end
147
+
148
+ it "times upon that source's clock rather than its own" do
149
+ timer = Monotonic::Timer.new(instants: Monotonic::Time)
150
+ timer.start
151
+ sleep 0.01
152
+ timer.stop
153
+ expect(timer.total_nanoseconds % Monotonic::Time.resolution).must_equal(0)
154
+ end
155
+ end
156
+
157
+ context "as a duration" do
158
+ it "hands back a Duration::Nanoseconds" do
159
+ timer = Monotonic::Timer.new
160
+ timer.start
161
+ timer.stop
162
+ expect(timer.to_duration.class).must_equal(Duration::Nanoseconds)
163
+ end
164
+
165
+ it "carries the same figure as #total_nanoseconds" do
166
+ timer = Monotonic::Timer.new
167
+ timer.start
168
+ sleep 0.01
169
+ timer.stop
170
+ expect(timer.to_duration.to_i).must_equal(timer.total_nanoseconds)
171
+ end
172
+
173
+ it "converts exactly, #total_time being the same in seconds" do
174
+ timer = Monotonic::Timer.new
175
+ timer.start
176
+ sleep 0.01
177
+ timer.stop
178
+ expect(timer.to_duration.to_seconds.to_f).must_equal(timer.total_time)
179
+ end
180
+ end
181
+
131
182
  context "in nanoseconds" do
132
183
  it "returns an instance of integer" do
133
184
  timer = Monotonic::Timer.new
data/test/gemspec_test.rb CHANGED
@@ -19,7 +19,7 @@ describe 'monotonic.rb.gemspec' do
19
19
  end
20
20
 
21
21
  it "declares its runtime dependencies" do
22
- _(spec.runtime_dependencies.map(&:name).sort).must_equal(%w{sys-uptime})
22
+ _(spec.runtime_dependencies.map(&:name).sort).must_equal(%w{duration.rb sys-uptime})
23
23
  end
24
24
 
25
25
  it "declares its development dependencies" do
@@ -0,0 +1,23 @@
1
+ require_relative '../lib/monotonic.rb'
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest-spec-context'
5
+
6
+ # In its own process, since this one has loaded every file directly and so could
7
+ # not tell what requiring the gem alone would bring.
8
+ describe 'loading' do
9
+ def ruby(source)
10
+ lib = File.expand_path('../lib', __dir__)
11
+ IO.popen(['ruby', "-I#{lib}", '-e', source], err: [:child, :out]){|io| io.read}.strip
12
+ end
13
+
14
+ it "defines both classes upon requiring the gem" do
15
+ expect(ruby('require "monotonic.rb"; print [defined?(Monotonic::Time), defined?(Monotonic::Timer)].inspect')) \
16
+ .must_equal('["constant", "constant"]')
17
+ end
18
+
19
+ it "defines the version too" do
20
+ expect(ruby('require "monotonic.rb"; print Monotonic::VERSION')) \
21
+ .must_equal(Monotonic::VERSION)
22
+ end
23
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monotonic.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: duration.rb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.4.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.4.0
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: sys-uptime
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -84,6 +98,7 @@ files:
84
98
  - test/Monotonic/Timer_test.rb
85
99
  - test/Monotonic/VERSION_test.rb
86
100
  - test/gemspec_test.rb
101
+ - test/loading_test.rb
87
102
  homepage: https://github.com/thoran/monotonic.rb
88
103
  licenses:
89
104
  - MIT