monotonic.rb 0.7.3 → 0.9.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 +4 -4
- data/CHANGELOG +45 -1
- data/README.md +101 -56
- data/lib/Monotonic/Measurement.rb +118 -0
- data/lib/Monotonic/Time.rb +45 -31
- data/lib/Monotonic/Timer.rb +88 -21
- data/lib/Monotonic/VERSION.rb +1 -1
- data/monotonic.rb.gemspec +5 -3
- data/test/Monotonic/Measurement_test.rb +118 -0
- data/test/Monotonic/Time_test.rb +25 -4
- data/test/Monotonic/Timer_test.rb +55 -4
- data/test/gemspec_test.rb +1 -1
- metadata +31 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02f12d8ccd007cc410e5ebeb524fd6761088aebe771409a81323fd331e479e61
|
|
4
|
+
data.tar.gz: 9bfd7ff12760b554b0921e535803c4e3e07c0c0cdb166b8133f1ab710a6a1d10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb3b665160980c7d6319db88518e9bd6f74a5e7eade1035a61e62c6aaa88e06110b382f02900f4e61fc7509cb7d9167084d5650c95858912c4a5b5bb2ffdcd43
|
|
7
|
+
data.tar.gz: 9c7c70bb79a05df1608516272583870cd7aab4229e13dce3d626b38fd3426a58c3529bfe2e8ddd2607f3995508c3c1cdcda287212002349db0fabea114a373b5
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20260816
|
|
4
|
+
|
|
5
|
+
0.9.0: Use measurand gem, + Monotonic::Measurement and Monotonic::Timer.floor.
|
|
6
|
+
|
|
7
|
+
1. + measurand as a dependency, constrained to >= 0.1.0 where Measurand#place became public. How well a figure is known is its business, and it was being hand-rolled here: combining an absolute floor with a relative drift by addition gave 109.49ns where the correct combination in quadrature gives 87.78ns, a quarter out.
|
|
8
|
+
2. + Monotonic::Timer.floor: what it costs to measure at all, in nanoseconds, measured by timing measurements of nothing and remembered. The clock's tick is not the limit, an interval wanting two readings and the taking of them taking time. A high percentile rather than a median, so that the ordinary case is covered and not merely the best one. It is a floor and not an error bar: the tail is unbounded and one-sided, the scheduler being free to take the processor away between the two readings, and no single measurement can detect that it did.
|
|
9
|
+
3. + Monotonic::Measurement: an elapsed interval together with what it is worth. #to_s shows only as many digits as the uncertainty reaches, the least significant place being settled by Measurand#place upon the Particle Data Group convention, so 1265625ns against a floor of 84ns prints as 1.26562ms and the same count measured coarsely prints as 1.27ms. The unit ladder is powers of a thousand and stops at seconds, the scheme being a decimal-places one and minutes being sexagesimal. Nothing is capped, a day resolved to a hundred nanoseconds printing every figure it has: the absurdity is the mismatch between instrument and question, and is better seen than rounded away.
|
|
10
|
+
4. + Monotonic::Measurement#uncertainty, #measurand, #unit_name, #decimals, #nanoseconds, #floor, #drift: every figure the rendering rests upon, so that a string can be walked back to the clock which produced it.
|
|
11
|
+
5. + Monotonic::Measurement#floor takes a number or anything answering to #call, and Monotonic::Timer hands over its .floor method rather than its value. Measuring the floor is a thousand null measurements, and a caller which only wants a number should not set them going: a timed block costs 0.008ms where it would cost 0.307ms, the floor being measured when doubt is first asked about.
|
|
12
|
+
6. + Monotonic::Measurement drift, optional and nil by default: the relative term, being the rate at which the clock's own oscillator wanders. It cannot be measured from within, weighing a clock wanting a better clock and this being the best one here, so it is supplied by a caller or not at all.
|
|
13
|
+
7. + Monotonic::Measurement#to_i and #to_f, both in nanoseconds, its own unit, as Duration::Minutes answers 5 rather than 300; #to_duration for every other unit; and #<=> with Comparable, against another measurement only, a bare number having no unit to compare against.
|
|
14
|
+
8. ~ BREAKING: Monotonic::Timer#time and .time return a Monotonic::Measurement rather than a Float of seconds. What comes back now says how much of itself is real, which a bare Float could not: 0.001295791 shows nine figures upon an instrument which resolves five. #total_time and #total_nanoseconds are unchanged for a number.
|
|
15
|
+
9. + Monotonic::Timer#measurement and #to_s.
|
|
16
|
+
10. + test/Monotonic/Measurement_test.rb: the unit chosen, the digits allowed, the uncertainty and that it combines in quadrature, the comparison, the conversions, the lazy floor, and that every figure the rendering rests upon is reachable.
|
|
17
|
+
11. + test/Monotonic/Timer_test.rb: the floor, and that a timed block returns a measurement which carries the interval and the floor.
|
|
18
|
+
12. ~ test/gemspec_test.rb: the runtime dependency list.
|
|
19
|
+
13. ~ README.md: + the floor and the measurement, and what a timed block now hands back.
|
|
20
|
+
14. ~ Monotonic::VERSION: /0.8.0/0.9.0/
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## 20260816
|
|
24
|
+
|
|
25
|
+
0.8.0: Use duration.rb gem.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
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.
|
|
29
|
+
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.
|
|
30
|
+
4. + Monotonic::Timer#to_duration, being a Duration::Nanoseconds. An elapsed interval is a duration, and every unit follows from this one exactly.
|
|
31
|
+
5. ~ Monotonic::Timer#total_time: derived from #to_duration. It returns a Float of seconds as before.
|
|
32
|
+
6. ~ Monotonic::Time#seconds_since_boot: converted by a duration rather than by division. It returns a Float as before.
|
|
33
|
+
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.
|
|
34
|
+
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.
|
|
35
|
+
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.
|
|
36
|
+
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.
|
|
37
|
+
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.
|
|
38
|
+
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.
|
|
39
|
+
13. ~ test/gemspec_test.rb: the runtime dependency list.
|
|
40
|
+
14. ~ README.md: + what belongs to duration.rb and why, and the usage for the arithmetic and #to_duration.
|
|
41
|
+
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.
|
|
42
|
+
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.
|
|
43
|
+
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.
|
|
44
|
+
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.
|
|
45
|
+
19. ~ Monotonic::VERSION: /0.7.3/0.8.0/
|
|
46
|
+
|
|
47
|
+
|
|
3
48
|
## 20260816
|
|
4
49
|
|
|
5
50
|
0.7.3: Require Monotonic::Time from the gem, and stop every instant fetching the boot time.
|
|
@@ -13,7 +58,6 @@
|
|
|
13
58
|
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.
|
|
14
59
|
8. ~ Monotonic::VERSION: /0.7.2/0.7.3/
|
|
15
60
|
|
|
16
|
-
|
|
17
61
|
## 20260812
|
|
18
62
|
|
|
19
63
|
0.7.2: + gemspec test.
|
data/README.md
CHANGED
|
@@ -14,35 +14,62 @@ 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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.
|
|
24
26
|
|
|
25
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.
|
|
26
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
|
+
```
|
|
37
|
+
|
|
38
|
+
A timed block hands back a `Monotonic::Measurement` rather than a bare number, so that what comes back says how much of itself is real. `Monotonic::Timer.floor` is what it costs to measure at all, taken by timing measurements of nothing, and the measurement shows only as many digits as that floor reaches — the doubt itself being [measurand](https://github.com/thoran/measurand)'s business, which combines an absolute floor with a relative drift in quadrature rather than by addition.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Monotonic::Timer.floor
|
|
42
|
+
# => 84
|
|
43
|
+
measurement = Monotonic::Timer.time{sleep 0.001}
|
|
44
|
+
measurement.to_s
|
|
45
|
+
# => "1.26562 ms"
|
|
46
|
+
measurement.uncertainty
|
|
47
|
+
# => 84.0
|
|
48
|
+
measurement.to_duration.to_microseconds.to_f
|
|
49
|
+
# => 1265.625
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Nothing is capped: a day resolved to a hundred nanoseconds prints every figure it has, the absurdity being the mismatch between what the instrument resolves and what was asked of it, which is better seen than rounded away. The floor is a floor and not an error bar — the tail is unbounded and one-sided, the scheduler being free to take the processor away between the two readings — so repeat and take a robust statistic where that matters.
|
|
53
|
+
|
|
27
54
|
|
|
28
55
|
## Installation
|
|
29
56
|
|
|
30
57
|
Add this line to your application's Gemfile:
|
|
31
58
|
|
|
32
59
|
```ruby
|
|
33
|
-
|
|
60
|
+
gem 'monotonic.rb'
|
|
34
61
|
```
|
|
35
62
|
|
|
36
63
|
And then execute:
|
|
37
64
|
|
|
38
65
|
```bash
|
|
39
|
-
|
|
66
|
+
$ bundle install
|
|
40
67
|
```
|
|
41
68
|
|
|
42
69
|
Or install it yourself as:
|
|
43
70
|
|
|
44
71
|
```bash
|
|
45
|
-
|
|
72
|
+
$ gem install monotonic.rb
|
|
46
73
|
```
|
|
47
74
|
|
|
48
75
|
|
|
@@ -51,74 +78,92 @@ Or install it yourself as:
|
|
|
51
78
|
### Monotonic::Time
|
|
52
79
|
|
|
53
80
|
```ruby
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
require 'monotonic.rb'
|
|
82
|
+
|
|
83
|
+
monotonic_time = Monotonic::Time.new
|
|
84
|
+
monotonic_time.nanoseconds_since_boot
|
|
85
|
+
# => 2614365376498000
|
|
86
|
+
|
|
87
|
+
monotonic_time.seconds_since_boot
|
|
88
|
+
# => 1208799.325906
|
|
89
|
+
|
|
90
|
+
monotonic_time + Duration::Seconds.new(30)
|
|
91
|
+
# => #<Monotonic::Time>, thirty seconds later
|
|
92
|
+
|
|
93
|
+
Monotonic::Time.now - monotonic_time
|
|
94
|
+
# => #<Duration::Nanoseconds @nanoseconds=44104999>
|
|
95
|
+
|
|
96
|
+
monotonic_time + Monotonic::Time.now
|
|
97
|
+
# => TypeError: an instant plus an instant is not an instant
|
|
98
|
+
|
|
99
|
+
monotonic_time.to_s
|
|
100
|
+
# => "1164320.268127 seconds since boot."
|
|
101
|
+
|
|
102
|
+
monotonic_time.to_time
|
|
103
|
+
# => 2021-06-07 09:27:08 8249692651179/8388608000000 +1000
|
|
68
104
|
```
|
|
69
105
|
|
|
70
106
|
### Monotonic::Timer without a block
|
|
71
107
|
|
|
72
108
|
```ruby
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
require 'monotonic.rb'
|
|
110
|
+
|
|
111
|
+
timer = Monotonic::Timer.new
|
|
112
|
+
timer.start
|
|
113
|
+
i = 0
|
|
114
|
+
1_000_000.times{puts i += 1}
|
|
115
|
+
timer.stop
|
|
116
|
+
timer.total_nanoseconds
|
|
117
|
+
# => 27734000
|
|
118
|
+
|
|
119
|
+
timer.to_duration
|
|
120
|
+
# => #<Duration::Nanoseconds @nanoseconds=27734000>
|
|
121
|
+
|
|
122
|
+
timer.to_duration.to_microseconds.to_f
|
|
123
|
+
# => 27734.0
|
|
124
|
+
|
|
125
|
+
timer.total_time
|
|
126
|
+
# => 0.027734
|
|
83
127
|
```
|
|
84
128
|
|
|
85
129
|
### Monotonic::Timer with a block
|
|
86
130
|
|
|
87
131
|
```ruby
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
require 'monotonic.rb'
|
|
133
|
+
|
|
134
|
+
measurement = Monotonic::Timer.time do
|
|
135
|
+
i = 0
|
|
136
|
+
1_000_000.times{puts i += 1}
|
|
137
|
+
end
|
|
138
|
+
measurement.to_s
|
|
139
|
+
# => "6.97582 s"
|
|
95
140
|
```
|
|
96
141
|
|
|
97
142
|
### Monotonic::Timer with a block and block variable
|
|
98
143
|
|
|
99
144
|
```ruby
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
require 'monotonic.rb'
|
|
146
|
+
measurement = Monotonic::Timer.time do |timer|
|
|
147
|
+
i = 0
|
|
148
|
+
500_000.times{puts i += 1}
|
|
149
|
+
p timer.total_time
|
|
150
|
+
500_000.times{puts i += 1}
|
|
151
|
+
end
|
|
152
|
+
measurement.to_s
|
|
153
|
+
# => "6.97582 s"
|
|
109
154
|
```
|
|
110
155
|
|
|
111
156
|
### Monotonic::Timer with a block on a timer instance
|
|
112
157
|
|
|
113
158
|
```ruby
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
159
|
+
require 'monotonic.rb'
|
|
160
|
+
timer = Monotonic::Timer.new
|
|
161
|
+
measurement = timer.time do
|
|
162
|
+
i = 0
|
|
163
|
+
1_000_000.times{puts i += 1}
|
|
164
|
+
end
|
|
165
|
+
measurement.to_s
|
|
166
|
+
# => "7.03313 s"
|
|
122
167
|
```
|
|
123
168
|
|
|
124
169
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Monotonic/Measurement.rb
|
|
2
|
+
# Monotonic::Measurement
|
|
3
|
+
|
|
4
|
+
require 'duration.rb'
|
|
5
|
+
require 'measurand'
|
|
6
|
+
|
|
7
|
+
module Monotonic
|
|
8
|
+
# An elapsed interval together with what it is worth. The count alone says
|
|
9
|
+
# nothing of how much of itself is real, so #to_s shows only as many digits as
|
|
10
|
+
# the uncertainty supports. Nothing is capped: a day resolved to a hundred
|
|
11
|
+
# nanoseconds prints every figure it has, which looks absurd and is — the
|
|
12
|
+
# absurdity being the mismatch between what the instrument resolves and what
|
|
13
|
+
# was asked of it, and better seen than rounded away.
|
|
14
|
+
#
|
|
15
|
+
# The doubt is a Measurand's business and the unit is a Duration's; what is
|
|
16
|
+
# left here is the ladder between them and the string at the end of it.
|
|
17
|
+
class Measurement
|
|
18
|
+
include Comparable
|
|
19
|
+
|
|
20
|
+
# Powers of a thousand, and no further. The scheme is a decimal-places one,
|
|
21
|
+
# so minutes and hours have no place in it: 90s is plainer than 1.5min, and
|
|
22
|
+
# a run of a day reads as 86400s rather than reaching for a unit which does
|
|
23
|
+
# not divide by ten.
|
|
24
|
+
UNITS = [[1, 'ns'], [1_000, 'us'], [1_000_000, 'ms'], [1_000_000_000, 's']]
|
|
25
|
+
|
|
26
|
+
attr_reader :nanoseconds
|
|
27
|
+
attr_reader :drift
|
|
28
|
+
|
|
29
|
+
# The floor may be given as a number or as anything which answers to #call,
|
|
30
|
+
# so that measuring it can wait until somebody asks about doubt.
|
|
31
|
+
# Monotonic::Timer hands over its .floor method rather than its value, and a
|
|
32
|
+
# caller which only wants a number never sets the thousand null measurements
|
|
33
|
+
# going.
|
|
34
|
+
def floor
|
|
35
|
+
@floor = @floor.respond_to?(:call) ? @floor.call : @floor
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The floor is absolute and tells upon short intervals; drift is relative
|
|
39
|
+
# and tells upon long ones, being the rate at which the clock's own
|
|
40
|
+
# oscillator wanders. Combining them is not addition — they are independent,
|
|
41
|
+
# so they go in quadrature — which is why the arithmetic is Measurand's and
|
|
42
|
+
# not done here.
|
|
43
|
+
def measurand
|
|
44
|
+
@measurand ||= (
|
|
45
|
+
absolute = Measurand.new(@nanoseconds, floor)
|
|
46
|
+
@drift ? absolute * Measurand.relative(1, @drift) : absolute
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def uncertainty
|
|
51
|
+
measurand.uncertainty
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def unit
|
|
55
|
+
@unit ||= UNITS.reverse.find{|scale, _| @nanoseconds.abs >= scale} || UNITS.first
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def scale
|
|
59
|
+
unit.first
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def unit_name
|
|
63
|
+
unit.last
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The least significant digit worth showing is the one the uncertainty
|
|
67
|
+
# reaches, which Measurand settles by the Particle Data Group convention and
|
|
68
|
+
# reports as #place, a power of ten. Rendering in a coarser unit moves it
|
|
69
|
+
# along by however many tens that unit is worth. A measurand with no
|
|
70
|
+
# uncertainty has no last real digit, and answers nil.
|
|
71
|
+
def decimals
|
|
72
|
+
@decimals ||= (
|
|
73
|
+
place = measurand.place
|
|
74
|
+
place ? [Math.log10(scale) - place, 0].max.to_i : Math.log10(scale).to_i
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_duration
|
|
79
|
+
Duration::Nanoseconds.new(@nanoseconds)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Both answer in nanoseconds, this measurement's own unit, as
|
|
83
|
+
# Duration::Minutes answers 5 rather than 300. Every other unit comes from
|
|
84
|
+
# #to_duration, which names the one it is asked for. A number handed out
|
|
85
|
+
# without its unit stated must at least always mean the same thing.
|
|
86
|
+
def to_i
|
|
87
|
+
@nanoseconds
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_f
|
|
91
|
+
@nanoseconds.to_f
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_s
|
|
95
|
+
format("%.#{decimals}f #{unit_name}", @nanoseconds.to_f / scale)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Against another measurement only. A bare number has no unit, and comparing
|
|
99
|
+
# against one would have to assume which was meant, so nil is returned and ==
|
|
100
|
+
# is false, as Duration::Common does.
|
|
101
|
+
def <=>(other)
|
|
102
|
+
return nil unless other.is_a?(Monotonic::Measurement)
|
|
103
|
+
@nanoseconds <=> other.nanoseconds
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def inspect
|
|
107
|
+
"#<#{self.class} #{self} (#{measurand} ns)>"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def initialize(nanoseconds, floor:, drift: nil)
|
|
113
|
+
@nanoseconds = nanoseconds
|
|
114
|
+
@floor = floor
|
|
115
|
+
@drift = drift
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/Monotonic/Time.rb
CHANGED
|
@@ -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
|
|
@@ -25,27 +26,14 @@ module Monotonic
|
|
|
25
26
|
self.new
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
# Invariant for the life of the process, coming from kern.boottime, which
|
|
29
|
-
# does not move. It was asked afresh upon every instance and cost 1740ns
|
|
30
|
-
# of the 1947ns an instant took to make.
|
|
31
|
-
def boot_time
|
|
32
|
-
@boot_time ||= Sys::Uptime.boot_time
|
|
33
|
-
end
|
|
34
|
-
|
|
35
29
|
# How finely this clock advances, in nanoseconds. It is asked rather than
|
|
36
30
|
# tabulated, being a property of the processor and the operating system
|
|
37
31
|
# and not of this library: upon macOS CLOCK_MONOTONIC answers 1000 here.
|
|
38
|
-
# A reading is denominated in nanoseconds whatever the answer, so this is
|
|
39
|
-
# the method which says what a reading is worth.
|
|
40
32
|
def resolution
|
|
41
33
|
Process.clock_getres(CLOCK, :nanosecond)
|
|
42
34
|
end
|
|
43
35
|
end # class << self
|
|
44
36
|
|
|
45
|
-
# Read upon every instance, so a constant rather than a lookup. It follows
|
|
46
|
-
# .clock_name, and so must come after it.
|
|
47
|
-
CLOCK = Process.const_get(clock_name)
|
|
48
|
-
|
|
49
37
|
attr_reader :nanoseconds_since_boot
|
|
50
38
|
|
|
51
39
|
# The clock is read in nanoseconds because it answers there with an Integer,
|
|
@@ -53,19 +41,45 @@ module Monotonic
|
|
|
53
41
|
# Seconds are derived rather than read, so that the reading loses nothing and
|
|
54
42
|
# the rounding happens where it is asked for.
|
|
55
43
|
def seconds_since_boot
|
|
56
|
-
@nanoseconds_since_boot
|
|
44
|
+
Duration::Nanoseconds.new(@nanoseconds_since_boot).to_seconds.to_f
|
|
57
45
|
end
|
|
58
46
|
|
|
59
|
-
def initialize
|
|
60
|
-
@nanoseconds_since_boot =
|
|
47
|
+
def initialize(nanoseconds_since_boot = Process.clock_gettime(CLOCK, :nanosecond))
|
|
48
|
+
@nanoseconds_since_boot = nanoseconds_since_boot
|
|
61
49
|
end
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
|
|
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)
|
|
65
71
|
end
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def to_s
|
data/lib/Monotonic/Timer.rb
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
# Monotonic/Timer.rb
|
|
2
2
|
# Monotonic::Timer
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require 'duration.rb'
|
|
5
|
+
require_relative './Measurement'
|
|
5
6
|
|
|
6
7
|
module Monotonic
|
|
7
8
|
class Timer
|
|
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 such
|
|
13
|
+
# clock, but its CLOCK_MONOTONIC already holds while suspended and advances
|
|
14
|
+
# finely, so the fallback carries the same meaning and not merely the same
|
|
15
|
+
# name. This is what would be accepted, in order; which of them exists is
|
|
16
|
+
# the platform's business, and .clock_name reports what was found.
|
|
17
|
+
CLOCK_NAMES = %i[CLOCK_UPTIME_RAW CLOCK_MONOTONIC]
|
|
18
|
+
|
|
19
|
+
# Read twice upon every measurement, against a floor of some tens of
|
|
20
|
+
# nanoseconds, so this is the one place here where a constant is worth the
|
|
21
|
+
# rigidity: a method call would be a measurable part of what it measures.
|
|
22
|
+
CLOCK = Process.const_get(CLOCK_NAMES.find{|name| Process.const_defined?(name)})
|
|
23
|
+
|
|
24
|
+
# How many measurements of nothing .floor takes, and which of them it keeps.
|
|
25
|
+
SAMPLES = 1_000
|
|
26
|
+
PERCENTILE = 0.95
|
|
27
|
+
|
|
8
28
|
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
29
|
def clock_name
|
|
20
|
-
@clock_name ||=
|
|
30
|
+
@clock_name ||= CLOCK_NAMES.find{|name| Process.const_defined?(name)}
|
|
21
31
|
end
|
|
22
32
|
|
|
23
33
|
def time(&block)
|
|
@@ -31,13 +41,40 @@ module Monotonic
|
|
|
31
41
|
def resolution
|
|
32
42
|
Process.clock_getres(CLOCK, :nanosecond)
|
|
33
43
|
end
|
|
44
|
+
|
|
45
|
+
# What it costs to measure at all, in nanoseconds, measured by timing
|
|
46
|
+
# measurements of nothing. The clock's tick is not the limit: two readings
|
|
47
|
+
# must be taken, and taking them takes time, so an interval near this
|
|
48
|
+
# figure is mostly the measuring.
|
|
49
|
+
#
|
|
50
|
+
# It is a floor and not an error bar. The typical cost is a tick or two;
|
|
51
|
+
# the tail is unbounded and one-sided, since the scheduler may take the
|
|
52
|
+
# processor away between the two readings and no single measurement can
|
|
53
|
+
# detect that it did. A high percentile is taken rather than a median, so
|
|
54
|
+
# that the ordinary case is covered rather than merely the best one.
|
|
55
|
+
# Repeat and take a robust statistic if the tail matters.
|
|
56
|
+
def floor
|
|
57
|
+
@floor ||= (
|
|
58
|
+
samples = SAMPLES.times.map{timer = new; timer.start; timer.stop; timer.total_nanoseconds}.sort
|
|
59
|
+
samples[(samples.length * PERCENTILE).to_i]
|
|
60
|
+
)
|
|
61
|
+
end
|
|
34
62
|
end # class << self
|
|
35
63
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
64
|
+
# Where the instants come from. Nothing supplied means the clock is read
|
|
65
|
+
# directly, which is what this has always done and is much the cheapest: a
|
|
66
|
+
# raw reading costs some 57ns against the 171ns of making a Monotonic::Time,
|
|
67
|
+
# and two are taken per measurement.
|
|
68
|
+
#
|
|
69
|
+
# Supplying Monotonic::Time times upon CLOCK_MONOTONIC instead — coarser, at
|
|
70
|
+
# 1000ns against 42ns, but it maps back onto the wall clock, so a timing can
|
|
71
|
+
# be placed as well as measured. Anything answering .now will do, its
|
|
72
|
+
# instants needing only to subtract to something which can say itself in
|
|
73
|
+
# nanoseconds. That is what 0.7.0 took away without saying so, Timer having
|
|
74
|
+
# read Monotonic::Time until the two took different clocks.
|
|
75
|
+
def initialize(instants: nil)
|
|
76
|
+
@instants = instants
|
|
77
|
+
end
|
|
41
78
|
|
|
42
79
|
def start
|
|
43
80
|
@finish_nanoseconds = nil
|
|
@@ -51,14 +88,44 @@ module Monotonic
|
|
|
51
88
|
# Two exact integers differenced, which spends none of the reading. A pair
|
|
52
89
|
# of Floats would not begin to lose the clock at this resolution until some
|
|
53
90
|
# six years of uptime, but they would begin.
|
|
91
|
+
#
|
|
92
|
+
# Raw readings difference to nanoseconds already. Instants difference to
|
|
93
|
+
# whatever their own subtraction gives, so it is asked to say itself in
|
|
94
|
+
# nanoseconds — which is why this could not be done before 0.8.0, where
|
|
95
|
+
# Monotonic::Time#- began answering with a duration rather than a bare Float
|
|
96
|
+
# of seconds.
|
|
54
97
|
def total_nanoseconds
|
|
55
|
-
finish_nanoseconds - @start_nanoseconds
|
|
98
|
+
elapsed = finish_nanoseconds - @start_nanoseconds
|
|
99
|
+
elapsed.respond_to?(:to_nanoseconds) ? elapsed.to_nanoseconds.to_i : elapsed
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# An elapsed interval is a duration, so here is one, and every unit follows
|
|
103
|
+
# from it exactly. #total_time is the same figure in seconds, that being
|
|
104
|
+
# the unit this library has always answered in.
|
|
105
|
+
def to_duration
|
|
106
|
+
Duration::Nanoseconds.new(total_nanoseconds)
|
|
56
107
|
end
|
|
57
108
|
|
|
58
109
|
def total_time
|
|
59
|
-
|
|
110
|
+
to_duration.to_seconds.to_f
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The elapsed interval together with what it is worth. Not memoized: a timer
|
|
114
|
+
# which has not been stopped is still running, and so is its measurement.
|
|
115
|
+
# The floor is handed over as a method rather than a figure, so that a caller
|
|
116
|
+
# which only ever wants a number never sets the thousand null measurements
|
|
117
|
+
# going.
|
|
118
|
+
def measurement(drift: nil)
|
|
119
|
+
Monotonic::Measurement.new(total_nanoseconds, floor: self.class.method(:floor), drift: drift)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def to_s
|
|
123
|
+
measurement.to_s
|
|
60
124
|
end
|
|
61
125
|
|
|
126
|
+
# A Monotonic::Measurement rather than a bare Float, so that what comes back
|
|
127
|
+
# says how much of itself is real. #total_time and #total_nanoseconds are
|
|
128
|
+
# still there for a number.
|
|
62
129
|
def time
|
|
63
130
|
start
|
|
64
131
|
begin
|
|
@@ -66,7 +133,7 @@ module Monotonic
|
|
|
66
133
|
ensure
|
|
67
134
|
stop
|
|
68
135
|
end
|
|
69
|
-
|
|
136
|
+
measurement
|
|
70
137
|
end
|
|
71
138
|
|
|
72
139
|
private
|
|
@@ -77,7 +144,7 @@ module Monotonic
|
|
|
77
144
|
end
|
|
78
145
|
|
|
79
146
|
def now
|
|
80
|
-
Process.clock_gettime(CLOCK, :nanosecond)
|
|
147
|
+
@instants ? @instants.now : Process.clock_gettime(CLOCK, :nanosecond)
|
|
81
148
|
end
|
|
82
149
|
end
|
|
83
150
|
end
|
data/lib/Monotonic/VERSION.rb
CHANGED
data/monotonic.rb.gemspec
CHANGED
|
@@ -35,9 +35,11 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
Dir['test/**/*.rb'],
|
|
36
36
|
].flatten
|
|
37
37
|
|
|
38
|
-
spec.dependencies =
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
spec.dependencies = [
|
|
39
|
+
['duration.rb', '>= 0.4.0'],
|
|
40
|
+
['measurand', '>= 0.1.0'],
|
|
41
|
+
'sys-uptime'
|
|
42
|
+
]
|
|
41
43
|
|
|
42
44
|
spec.development_dependencies = %w{
|
|
43
45
|
minitest
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require_relative '../../lib/monotonic.rb'
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest-spec-context'
|
|
5
|
+
|
|
6
|
+
describe Monotonic::Measurement do
|
|
7
|
+
def measurement(nanoseconds, floor: 84, drift: nil)
|
|
8
|
+
Monotonic::Measurement.new(nanoseconds, floor: floor, drift: drift)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe "the unit" do
|
|
12
|
+
it "is the largest which leaves a mantissa of at least one" do
|
|
13
|
+
_(measurement(999).unit_name).must_equal('ns')
|
|
14
|
+
_(measurement(1_000).unit_name).must_equal('us')
|
|
15
|
+
_(measurement(1_265_625).unit_name).must_equal('ms')
|
|
16
|
+
_(measurement(1_000_000_000).unit_name).must_equal('s')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "stops at seconds, the ladder being powers of a thousand" do
|
|
20
|
+
_(measurement(86_400_000_000_000).unit_name).must_equal('s')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "is nanoseconds for nothing at all" do
|
|
24
|
+
_(measurement(0).to_s).must_equal('0 ns')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "the digits" do
|
|
29
|
+
it "reach as far as the uncertainty does and no further" do
|
|
30
|
+
_(measurement(1_265_625).to_s).must_equal('1.26562 ms')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "are fewer upon a coarser instrument" do
|
|
34
|
+
_(measurement(1_265_625, floor: 100_000).to_s).must_equal('1.27 ms')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "are none where the interval is near the floor" do
|
|
38
|
+
_(measurement(999).to_s).must_equal('999 ns')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "are not capped, a day resolved to a hundred nanoseconds saying so" do
|
|
42
|
+
_(measurement(86_400_000_000_000).to_s).must_equal('86400.00000000 s')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "the uncertainty" do
|
|
47
|
+
it "is the floor alone when no drift is supplied" do
|
|
48
|
+
_(measurement(1_000_000).uncertainty).must_equal(84)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "combines a drift in quadrature rather than by addition" do
|
|
52
|
+
combined = measurement(1_000_000_000, drift: 1e-6).uncertainty
|
|
53
|
+
_(combined).must_be_close_to(Math.sqrt(84**2 + 1_000**2), 0.01)
|
|
54
|
+
_(combined).wont_be_close_to(84 + 1_000, 1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "takes digits away as it grows" do
|
|
58
|
+
_(measurement(7_166_559_999, drift: 20e-6).decimals) \
|
|
59
|
+
.must_be(:<, measurement(7_166_559_999).decimals)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "comes from a Measurand, which owns the arithmetic" do
|
|
63
|
+
_(measurement(1_000_000).measurand).must_be_instance_of(Measurand)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "the floor" do
|
|
68
|
+
it "may be a number" do
|
|
69
|
+
_(measurement(1_000_000, floor: 84).floor).must_equal(84)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "may be something callable, and is not called until doubt is asked about" do
|
|
73
|
+
called = false
|
|
74
|
+
subject = Monotonic::Measurement.new(1_000_000, floor: ->{called = true; 84})
|
|
75
|
+
subject.to_i
|
|
76
|
+
subject.to_duration
|
|
77
|
+
_(called).must_equal(false)
|
|
78
|
+
subject.uncertainty
|
|
79
|
+
_(called).must_equal(true)
|
|
80
|
+
_(subject.floor).must_equal(84)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "comparison" do
|
|
85
|
+
it "orders one measurement against another" do
|
|
86
|
+
_(measurement(999)).must_be(:<, measurement(1_000))
|
|
87
|
+
_([measurement(1_000), measurement(999)].sort.map(&:nanoseconds)).must_equal([999, 1_000])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "is nil against a bare number, which has no unit" do
|
|
91
|
+
_(measurement(999) <=> 999).must_be_nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "is therefore not equal to a bare number" do
|
|
95
|
+
_(measurement(999) == 999).must_equal(false)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe "#to_duration" do
|
|
100
|
+
it "hands the interval to duration.rb, whose business the units are" do
|
|
101
|
+
_(measurement(1_500_000_000).to_duration).must_be_instance_of(Duration::Nanoseconds)
|
|
102
|
+
_(measurement(1_500_000_000).to_duration.to_seconds.to_f).must_equal(1.5)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe "#to_i and #to_f" do
|
|
107
|
+
it "both answer in nanoseconds, the measurement's own unit" do
|
|
108
|
+
_(measurement(1_274_625).to_i).must_equal(1_274_625)
|
|
109
|
+
_(measurement(1_274_625).to_f).must_equal(1_274_625.0)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "agree with one another, and with #nanoseconds" do
|
|
113
|
+
subject = measurement(1_274_625)
|
|
114
|
+
_(subject.to_f).must_equal(subject.to_i.to_f)
|
|
115
|
+
_(subject.to_i).must_equal(subject.nanoseconds)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/test/Monotonic/Time_test.rb
CHANGED
|
@@ -55,14 +55,35 @@ describe Monotonic::Time do
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
describe "#+" do
|
|
58
|
-
it "returns
|
|
59
|
-
expect((subject +
|
|
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)
|
|
60
73
|
end
|
|
61
74
|
end
|
|
62
75
|
|
|
63
76
|
describe "#-" do
|
|
64
|
-
it "returns
|
|
65
|
-
expect((subject - Monotonic::Time.now).class).must_equal(
|
|
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)
|
|
66
87
|
end
|
|
67
88
|
end
|
|
68
89
|
|
|
@@ -9,7 +9,7 @@ describe Monotonic::Timer do
|
|
|
9
9
|
block_time = Monotonic::Timer.time do |timer|
|
|
10
10
|
sleep 3
|
|
11
11
|
end
|
|
12
|
-
expect(block_time.round).must_equal(3)
|
|
12
|
+
expect(block_time.to_duration.to_seconds.to_f.round).must_equal(3)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# Test introduced in 0.6.5 to demonstrate that the bug had been fixed.
|
|
@@ -18,7 +18,7 @@ describe Monotonic::Timer do
|
|
|
18
18
|
sleep 3
|
|
19
19
|
4
|
|
20
20
|
end
|
|
21
|
-
expect(block_time.round).must_equal(3)
|
|
21
|
+
expect(block_time.to_duration.to_seconds.to_f.round).must_equal(3)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Test introduced in 0.6.7 to demonstrate that the bug had been fixed.
|
|
@@ -51,7 +51,7 @@ describe Monotonic::Timer do
|
|
|
51
51
|
timer.start
|
|
52
52
|
sleep 1
|
|
53
53
|
end
|
|
54
|
-
expect(block_time.round).must_equal(1)
|
|
54
|
+
expect(block_time.to_duration.to_seconds.to_f.round).must_equal(1)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -88,7 +88,7 @@ describe Monotonic::Timer do
|
|
|
88
88
|
expect(timer.total_time.round).must_equal(0)
|
|
89
89
|
sleep 1
|
|
90
90
|
end
|
|
91
|
-
expect(time.round).must_equal(1)
|
|
91
|
+
expect(time.to_duration.to_seconds.to_f.round).must_equal(1)
|
|
92
92
|
expect(timer.total_time.round).must_equal(1)
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -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 measurand sys-uptime})
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
it "declares its development dependencies" do
|
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.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -9,6 +9,34 @@ 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
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: measurand
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.1.0
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: sys-uptime
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,11 +103,13 @@ files:
|
|
|
75
103
|
- Gemfile
|
|
76
104
|
- README.md
|
|
77
105
|
- Rakefile
|
|
106
|
+
- lib/Monotonic/Measurement.rb
|
|
78
107
|
- lib/Monotonic/Time.rb
|
|
79
108
|
- lib/Monotonic/Timer.rb
|
|
80
109
|
- lib/Monotonic/VERSION.rb
|
|
81
110
|
- lib/monotonic.rb
|
|
82
111
|
- monotonic.rb.gemspec
|
|
112
|
+
- test/Monotonic/Measurement_test.rb
|
|
83
113
|
- test/Monotonic/Time_test.rb
|
|
84
114
|
- test/Monotonic/Timer_test.rb
|
|
85
115
|
- test/Monotonic/VERSION_test.rb
|