monotonic.rb 0.6.7 → 0.7.1

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: a9992e2194aa62416707e8ee507f68f870e4eea0cbee2770879017d07d2ff7e7
4
- data.tar.gz: 8221c6232e0c2f1df21d9d5faaf42c7292ed1052801c8fe7f8cd6b819fdd4bde
3
+ metadata.gz: a74e6c5bfeba5fef4c029e5b2ce7495f9299445b884d89077be212da9be8ca49
4
+ data.tar.gz: 40d70e63f0f9f422a636171dbe17aa8ac48244b586ce4018d29d0e503f3f4b04
5
5
  SHA512:
6
- metadata.gz: 7a827ddd7420e8ff5911ca543cc5dbf46765063f521fefb55f0be0f883fe73c21b2552f43d5e6890e6a01e826c335c8d86ddfdb8db087983f25fd029d91e3cf0
7
- data.tar.gz: 35d7a1115045c486bd49074f31098a6d0c6d2e21fa795a0f07fd842120a1b3a407c9f3b76456b31dd8a185c4659212ba8bd85e08cd7968d7bd33beede84c8a19
6
+ metadata.gz: 4ba8a7204e07558e06f2aa83a9bb5d32f2be409b25538c5e5bd6df0bdbabdf81ea68415bbfe0e75a99ea3cd01ebfe2e460d803a4df4fd6d46f23e5998a343503
7
+ data.tar.gz: af817c6ec0e76e7692836341daec031afd16d5028dd2a898b0be9ebfbf3c575fad32dbb8985fe5c8497933945272f0a2aa233a42541d6c72adae0b1dab82f0fe
data/CHANGELOG CHANGED
@@ -1,5 +1,36 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260812
4
+
5
+ 0.7.1: + VERSION test.
6
+
7
+ 1. + test/Monotonic/VERSION_test.rb: Ensure that VERSION is a string, that it has three dotted numbers, and that it matches the newest entry in the CHANGELOG. A release edits both files and this now ensures that one moves with the other. Modelled upon moby.rb's.
8
+ 2. ~ Monotonic::VERSION: /0.7.0/0.7.1/
9
+
10
+
11
+ ## 20260811
12
+
13
+ 0.7.0: Read the clock exactly, and read a clock worth reading exactly.
14
+
15
+ 1. ~ lib/Monotonic/Time.rb: Process.clock_gettime is now asked for :nanosecond, where it answers with an Integer, rather than for seconds, where it answers with a Float. A Float's mantissa is of a fixed width wherever it sits, so its absolute resolution falls away as the uptime grows: around 2.2e-16s near 1.0, but only 4.7e-10s at a month of uptime.
16
+ 2. + Monotonic::Time#nanoseconds_since_boot: the reading itself, exact.
17
+ 3. ~ Monotonic::Time#seconds_since_boot: /attr_reader/derived from the nanosecond reading/, so that the rounding happens where it is asked for rather than where the clock is read. It returns a Float as before.
18
+ 4. + Monotonic::NANOSECONDS_PER_SECOND, being needed by both Time and Timer.
19
+ 5. + Monotonic::Timer#total_nanoseconds: an elapsed interval as an exact Integer. This is where the reading was formerly spent, an interval being the difference of two Floats drawn from an uptime many orders of magnitude larger than itself.
20
+ 6. ~ Monotonic::Timer#total_time: derived from #total_nanoseconds. It returns a Float as before.
21
+ 7. ~ Monotonic::Timer#total_time: - the branch upon @finish_time, which moved to a private #finish_nanoseconds, so that the running and stopped cases are distinguished in one place rather than in each caller.
22
+ 8. + Monotonic::Timer.clock_name and Monotonic::Time.clock_name, and Monotonic::Timer::CLOCK, being CLOCK_UPTIME_RAW where it is defined and CLOCK_MONOTONIC otherwise. An interval is not an instant and wants a different clock: upon Darwin CLOCK_UPTIME_RAW advances in some 42ns against the 1000ns of CLOCK_MONOTONIC, and holds while the machine sleeps, a closed lid being no part of execution. Linux has no such clock, but its CLOCK_MONOTONIC already holds while suspended and advances finely, so the fallback carries the same meaning and not merely the same name. Exactness alone was worth little upon the old clock, whose 1000ns tick a Float would not have troubled for 142 years of uptime; upon 42ns it begins to tell after six.
23
+ 9. ~ Monotonic::Timer: - its use of Monotonic::Time, since the two now read different clocks. @start_time and @finish_time become @start_nanoseconds and @finish_nanoseconds, read through a private #now.
24
+ 10. ~ Monotonic::Time::CLOCK, being CLOCK_MONOTONIC still, and now named. #to_time maps an instant back onto the wall clock by way of the boot time, so the clock wanted there is the one tracking time since boot as the wall clock understands it, sleep and all.
25
+ 11. + Monotonic::Time.resolution and Monotonic::Timer.resolution, from Process.clock_getres. How finely a clock advances is a property of the processor and the operating system, so it is asked rather than tabulated, and a README sentence about one platform cannot stand in for it. A reading is denominated in nanoseconds whether or not the clock affords them; this is the method which says what a reading is worth.
26
+ 12. ~ .clock_name is a method upon both classes rather than a CLOCK_NAME constant, since which clock is read is something found by asking the platform, as .resolution is, and not a property this library declares. A constant would also have frozen a contestable decision: sleep is excluded from a timed interval here, and nothing could have said otherwise. CLOCK stays a constant, being read twice upon every measurement against a floor of some tens of nanoseconds, and is now commented as such.
27
+ 13. ~ test/Monotonic/Time_test.rb: ~ the initialize test, which read @seconds_since_boot, to read @nanoseconds_since_boot.
28
+ 14. + test/Monotonic/Time_test.rb: tests for .clock_name, .resolution and #nanoseconds_since_boot, and that #seconds_since_boot is that reading expressed in seconds.
29
+ 15. + test/Monotonic/Timer_test.rb: tests for #total_nanoseconds, including that it holds while the timer is stopped, and for the clock: which one is chosen, that CLOCK follows .clock_name, that it reports its resolution, and that it is no coarser than the one Monotonic::Time reads.
30
+ 16. ~ README.md: + #nanoseconds_since_boot, #total_nanoseconds and .resolution to the description and the usage. The description now distinguishes denomination from resolution, asking for :nanosecond settling only what the figure counts, and points at .resolution rather than making a claim about any one platform.
31
+ 17. ~ Monotonic::VERSION: /0.6.7/0.7.0/
32
+
33
+
3
34
  ## 20260716
4
35
 
5
36
  0.6.7: Block exception bug fix, relicensing, and gem packaging changes.
data/README.md CHANGED
@@ -1,24 +1,49 @@
1
1
  # monotonic.rb
2
2
 
3
+
3
4
  ## Description
4
5
 
5
6
  Create accurate timings of excution in Ruby.
6
7
 
8
+ What accurate means is to capture many digits as an operating system is willing to afford. Hence, the clock is read in nanoseconds by default. If a number in seconds is requested, then that is derived from nanoseconds rather than read directly, so the rounding happens upon request and not before.
9
+
10
+ It tells most upon an elapsed interval, that being the difference of two readings drawn from an uptime many orders of magnitude larger than itself. A `Float` carries a mantissa of fixed width wherever it sits, so its absolute resolution falls away as the uptime grows: around 2.2e-16s near 1.0, but only 4.7e-10s after a month of uptime. Differencing two exact integers spends none of the reading, however long the machine has been up.
11
+
12
+ Which clock is read follows from that. `Monotonic::Timer` measures intervals and so takes the finest available which holds while the machine sleeps, a closed lid being no part of execution: `CLOCK_UPTIME_RAW` upon Darwin, where it advances in some 42ns against the 1000ns of `CLOCK_MONOTONIC`, and `CLOCK_MONOTONIC` elsewhere, which upon Linux already holds while suspended and advances finely. `Monotonic::Time` is an instant rather than an interval, and `#to_time` maps it back onto the wall clock, so it stays upon `CLOCK_MONOTONIC`, sleep and all.
13
+
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
+
16
+ ```ruby
17
+ Monotonic::Timer.resolution
18
+ # => 42
19
+ Monotonic::Time.resolution
20
+ # => 1000
21
+ ```
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.
24
+
25
+
7
26
  ## Installation
8
27
 
9
28
  Add this line to your application's Gemfile:
29
+
10
30
  ```ruby
11
31
  gem 'monotonic.rb'
12
32
  ```
33
+
13
34
  And then execute:
35
+
14
36
  ```bash
15
- $ bundle
37
+ $ bundle install
16
38
  ```
39
+
17
40
  Or install it yourself as:
41
+
18
42
  ```bash
19
43
  $ gem install monotonic.rb
20
44
  ```
21
45
 
46
+
22
47
  ## Usage
23
48
 
24
49
  ### Monotonic::Time
@@ -26,6 +51,8 @@ Or install it yourself as:
26
51
  ```ruby
27
52
  require 'monotonic.rb'
28
53
  monotonic_time = Monotonic::Time.new
54
+ monotonic_time.nanoseconds_since_boot
55
+ # => 2614365376498000
29
56
  monotonic_time.seconds_since_boot
30
57
  # => 1208799.325906
31
58
  monotonic_time + Monotonic::Time.now
@@ -47,6 +74,8 @@ Or install it yourself as:
47
74
  i = 0
48
75
  1_000_000.times{puts i += 1}
49
76
  timer.stop
77
+ timer.total_nanoseconds
78
+ # => 27734000
50
79
  timer.total_time
51
80
  # => 7.166559999808669
52
81
  ```
@@ -90,6 +119,7 @@ Or install it yourself as:
90
119
  # => 7.033131000120193
91
120
  ```
92
121
 
122
+
93
123
  ## Contributing
94
124
 
95
125
  1. Fork it ( https://github.com/thoran/monotonic.rb/fork )
@@ -4,34 +4,70 @@
4
4
  require 'sys-uptime'
5
5
 
6
6
  module Monotonic
7
+ NANOSECONDS_PER_SECOND = 1_000_000_000
8
+
7
9
  class Time
8
10
  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
+ def clock_name
21
+ :CLOCK_MONOTONIC
22
+ end
23
+
9
24
  def now
10
25
  self.new
11
26
  end
27
+
28
+ # How finely this clock advances, in nanoseconds. It is asked rather than
29
+ # tabulated, being a property of the processor and the operating system
30
+ # 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
+ def resolution
34
+ Process.clock_getres(CLOCK, :nanosecond)
35
+ end
12
36
  end # class << self
13
37
 
14
- attr_reader :seconds_since_boot
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
+ attr_reader :nanoseconds_since_boot
43
+
44
+ # The clock is read in nanoseconds because it answers there with an Integer,
45
+ # which is exact and stays exact however long the machine has been up.
46
+ # Seconds are derived rather than read, so that the reading loses nothing and
47
+ # the rounding happens where it is asked for.
48
+ def seconds_since_boot
49
+ @nanoseconds_since_boot / NANOSECONDS_PER_SECOND.to_f
50
+ end
15
51
 
16
52
  def initialize
17
53
  @boot_time = Sys::Uptime.boot_time
18
- @seconds_since_boot = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ @nanoseconds_since_boot = Process.clock_gettime(CLOCK, :nanosecond)
19
55
  end
20
56
 
21
57
  def +(monotonic_time_addend)
22
- @seconds_since_boot + monotonic_time_addend.seconds_since_boot
58
+ seconds_since_boot + monotonic_time_addend.seconds_since_boot
23
59
  end
24
60
 
25
61
  def -(monotonic_time_subtrahend)
26
- @seconds_since_boot - monotonic_time_subtrahend.seconds_since_boot
62
+ seconds_since_boot - monotonic_time_subtrahend.seconds_since_boot
27
63
  end
28
64
 
29
65
  def to_s
30
- "#{@seconds_since_boot} seconds since boot."
66
+ "#{seconds_since_boot} seconds since boot."
31
67
  end
32
68
 
33
69
  def to_time
34
- @boot_time + @seconds_since_boot
70
+ @boot_time + seconds_since_boot
35
71
  end
36
72
  end
37
73
  end
@@ -6,27 +6,57 @@ require_relative './Time'
6
6
  module Monotonic
7
7
  class Timer
8
8
  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
+ def clock_name
20
+ @clock_name ||= %i[CLOCK_UPTIME_RAW CLOCK_MONOTONIC].find{|name| Process.const_defined?(name)}
21
+ end
22
+
9
23
  def time(&block)
10
24
  timer = Timer.new
11
25
  timer.time(&block)
12
26
  end
27
+
28
+ # How finely this clock advances, in nanoseconds. Asked of the platform
29
+ # rather than claimed by the library, and worth asking: a reading is
30
+ # denominated in nanoseconds whether the clock affords them or not.
31
+ def resolution
32
+ Process.clock_getres(CLOCK, :nanosecond)
33
+ end
13
34
  end # class << self
14
35
 
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
+
15
42
  def start
16
- @finish_time = nil
17
- @start_time = Monotonic::Time.now
43
+ @finish_nanoseconds = nil
44
+ @start_nanoseconds = now
18
45
  end
19
46
 
20
47
  def stop
21
- @finish_time = Monotonic::Time.now
48
+ @finish_nanoseconds = now
49
+ end
50
+
51
+ # Two exact integers differenced, which spends none of the reading. A pair
52
+ # of Floats would not begin to lose the clock at this resolution until some
53
+ # six years of uptime, but they would begin.
54
+ def total_nanoseconds
55
+ finish_nanoseconds - @start_nanoseconds
22
56
  end
23
57
 
24
58
  def total_time
25
- if @finish_time
26
- @finish_time - @start_time
27
- else
28
- Monotonic::Time.now - @start_time
29
- end
59
+ total_nanoseconds / NANOSECONDS_PER_SECOND.to_f
30
60
  end
31
61
 
32
62
  def time
@@ -38,5 +68,16 @@ module Monotonic
38
68
  end
39
69
  total_time
40
70
  end
71
+
72
+ private
73
+
74
+ # A timer which has not been stopped is still running, so the finish is now.
75
+ def finish_nanoseconds
76
+ @finish_nanoseconds || now
77
+ end
78
+
79
+ def now
80
+ Process.clock_gettime(CLOCK, :nanosecond)
81
+ end
41
82
  end
42
83
  end
@@ -2,5 +2,5 @@
2
2
  # Monotonic::VERSION
3
3
 
4
4
  module Monotonic
5
- VERSION = '0.6.7'
5
+ VERSION = '0.7.1'
6
6
  end
@@ -12,16 +12,47 @@ describe Monotonic::Time do
12
12
  .must_equal(Sys::Uptime.boot_time)
13
13
  end
14
14
 
15
- it "the time spent in the block is returned as the value of the block" do
16
- expect(subject.instance_variable_get(:@seconds_since_boot).round(2)) \
17
- .must_equal(Process.clock_gettime(Process::CLOCK_MONOTONIC).round(2))
15
+ it "the reading is taken from the monotonic clock in nanoseconds" do
16
+ expect((subject.instance_variable_get(:@nanoseconds_since_boot) / 1_000_000_000.0).round(2)) \
17
+ .must_equal((Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) / 1_000_000_000.0).round(2))
18
+ end
19
+ end
20
+
21
+ describe ".clock_name" do
22
+ it "is the clock which tracks time since boot as the wall clock understands it" do
23
+ expect(Monotonic::Time.clock_name).must_equal(:CLOCK_MONOTONIC)
24
+ end
25
+
26
+ it "names the clock which is read" do
27
+ expect(Monotonic::Time::CLOCK).must_equal(Process.const_get(Monotonic::Time.clock_name))
28
+ end
29
+ end
30
+
31
+ describe ".resolution" do
32
+ it "returns how finely the clock advances, in nanoseconds" do
33
+ expect(Monotonic::Time.resolution) \
34
+ .must_equal(Process.clock_getres(Process::CLOCK_MONOTONIC, :nanosecond))
35
+ end
36
+
37
+ it "returns an instance of integer" do
38
+ expect(Monotonic::Time.resolution.class).must_equal(Integer)
39
+ end
40
+ end
41
+
42
+ describe "#nanoseconds_since_boot" do
43
+ it "returns an instance of integer" do
44
+ expect((subject.nanoseconds_since_boot).class).must_equal(Integer)
18
45
  end
19
46
  end
20
47
 
21
48
  describe "#seconds_since_boot" do
22
- it "returns an instance of string" do
49
+ it "returns an instance of float" do
23
50
  expect((subject.seconds_since_boot).class).must_equal(Float)
24
51
  end
52
+
53
+ it "is the nanoseconds reading expressed in seconds" do
54
+ expect(subject.seconds_since_boot).must_equal(subject.nanoseconds_since_boot / 1_000_000_000.0)
55
+ end
25
56
  end
26
57
 
27
58
  describe "#+" do
@@ -107,4 +107,53 @@ describe Monotonic::Timer do
107
107
  expect(timer.total_time.round).must_equal(1)
108
108
  end
109
109
  end
110
+
111
+ context "the clock" do
112
+ it "is the finest available which holds while the machine sleeps" do
113
+ expect(Monotonic::Timer.clock_name) \
114
+ .must_equal(Process.const_defined?(:CLOCK_UPTIME_RAW) ? :CLOCK_UPTIME_RAW : :CLOCK_MONOTONIC)
115
+ end
116
+
117
+ it "is asked of the platform rather than declared" do
118
+ expect(Monotonic::Timer::CLOCK).must_equal(Process.const_get(Monotonic::Timer.clock_name))
119
+ end
120
+
121
+ it "reports its resolution rather than leaving it to be assumed" do
122
+ expect(Monotonic::Timer.resolution) \
123
+ .must_equal(Process.clock_getres(Monotonic::Timer::CLOCK, :nanosecond))
124
+ end
125
+
126
+ it "is no coarser than the one Monotonic::Time reads" do
127
+ expect(Monotonic::Timer.resolution).must_be :<=, Monotonic::Time.resolution
128
+ end
129
+ end
130
+
131
+ context "in nanoseconds" do
132
+ it "returns an instance of integer" do
133
+ timer = Monotonic::Timer.new
134
+ timer.start
135
+ sleep 1
136
+ timer.stop
137
+ expect(timer.total_nanoseconds.class).must_equal(Integer)
138
+ end
139
+
140
+ it "is the total time expressed in nanoseconds" do
141
+ timer = Monotonic::Timer.new
142
+ timer.start
143
+ sleep 1
144
+ timer.stop
145
+ expect(timer.total_nanoseconds / 1_000_000_000.0).must_equal(timer.total_time)
146
+ end
147
+
148
+ it "holds while the timer is stopped and runs on while it is not" do
149
+ timer = Monotonic::Timer.new
150
+ timer.start
151
+ timer.stop
152
+ held = timer.total_nanoseconds
153
+ sleep 1
154
+ expect(timer.total_nanoseconds).must_equal(held)
155
+ timer.start
156
+ expect(timer.total_nanoseconds).must_be :>, 0
157
+ end
158
+ end
110
159
  end
@@ -0,0 +1,21 @@
1
+ require_relative '../../lib/monotonic.rb'
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest-spec-context'
5
+
6
+ describe Monotonic do
7
+ describe "VERSION" do
8
+ it "is a string" do
9
+ _(Monotonic::VERSION).must_be_instance_of String
10
+ end
11
+
12
+ it "is three numbers separated by dots" do
13
+ _(Monotonic::VERSION).must_match(/\A\d+\.\d+\.\d+\z/)
14
+ end
15
+
16
+ it "matches the newest entry in the CHANGELOG" do
17
+ changelog = File.read(File.expand_path('../../CHANGELOG', __dir__))
18
+ _(changelog[/^(\d+\.\d+\.\d+):/, 1]).must_equal Monotonic::VERSION
19
+ end
20
+ end
21
+ 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.6.7
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -82,6 +82,7 @@ files:
82
82
  - monotonic.rb.gemspec
83
83
  - test/Monotonic/Time_test.rb
84
84
  - test/Monotonic/Timer_test.rb
85
+ - test/Monotonic/VERSION_test.rb
85
86
  homepage: https://github.com/thoran/monotonic.rb
86
87
  licenses:
87
88
  - MIT
@@ -100,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
101
  - !ruby/object:Gem::Version
101
102
  version: '0'
102
103
  requirements: []
103
- rubygems_version: 4.0.16
104
+ rubygems_version: 4.0.18
104
105
  specification_version: 4
105
106
  summary: Monotonic timing made easy.
106
107
  test_files: []