Class: Monotonic::Timer

Inherits:
Object
  • Object
show all
Defined in:
lib/Monotonic/Timer.rb

Constant Summary collapse

CLOCK_NAMES =

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, which is the right answer for a timed block: a closed lid is not 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. This is what would be accepted, in order; which of them exists is the platform's business, and .clock_name reports what was found.

%i[CLOCK_UPTIME_RAW CLOCK_MONOTONIC]
CLOCK =

Read twice upon every measurement, against a floor of some tens of nanoseconds, so this is the one place here where a constant is worth the rigidity: a method call would be a measurable part of what it measures.

Process.const_get(CLOCK_NAMES.find{|name| Process.const_defined?(name)})
SAMPLES =

How many measurements of nothing .floor takes, and which of them it keeps.

1_000
PERCENTILE =
0.95

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(instants: nil) ⇒ Timer

Where the instants come from. Nothing supplied means the clock is read directly, which is what this has always done and is much the cheapest: a raw reading costs some 57ns against the 171ns of making a Monotonic::Time, and two are taken per measurement.

Supplying Monotonic::Time times upon CLOCK_MONOTONIC instead — coarser, at 1000ns against 42ns, but it maps back onto the wall clock, so a timing can be placed as well as measured. Anything answering .now will do, its instants needing only to subtract to something which can say itself in nanoseconds. That is what 0.7.0 took away without saying so, Timer having read Monotonic::Time until the two took different clocks.



75
76
77
# File 'lib/Monotonic/Timer.rb', line 75

def initialize(instants: nil)
  @instants = instants
end

Class Method Details

.clock_nameObject



29
30
31
# File 'lib/Monotonic/Timer.rb', line 29

def clock_name
  @clock_name ||= CLOCK_NAMES.find{|name| Process.const_defined?(name)}
end

.floorObject

What it costs to measure at all, in nanoseconds, measured by timing measurements of nothing. The clock's tick is not the limit: two readings must be taken, and taking them takes time, so an interval near this figure is mostly the measuring.

It is a floor and not an error bar. The typical cost is a tick or two; the tail is unbounded and one-sided, since the scheduler may take the processor away between the two readings and no single measurement can detect that it did. A high percentile is taken rather than a median, so that the ordinary case is covered rather than merely the best one. Repeat and take a robust statistic if the tail matters.



56
57
58
59
60
61
# File 'lib/Monotonic/Timer.rb', line 56

def floor
  @floor ||= (
    samples = SAMPLES.times.map{timer = new; timer.start; timer.stop; timer.total_nanoseconds}.sort
    samples[(samples.length * PERCENTILE).to_i]
  )
end

.resolutionObject

How finely this clock advances, in nanoseconds. Asked of the platform rather than claimed by the library, and worth asking: a reading is denominated in nanoseconds whether the clock affords them or not.



41
42
43
# File 'lib/Monotonic/Timer.rb', line 41

def resolution
  Process.clock_getres(CLOCK, :nanosecond)
end

.time(&block) ⇒ Object



33
34
35
36
# File 'lib/Monotonic/Timer.rb', line 33

def time(&block)
  timer = Timer.new
  timer.time(&block)
end

Instance Method Details

#measurement(drift: nil) ⇒ Object

The elapsed interval together with what it is worth. Not memoized: a timer which has not been stopped is still running, and so is its measurement. The floor is handed over as a method rather than a figure, so that a caller which only ever wants a number never sets the thousand null measurements going.



118
119
120
# File 'lib/Monotonic/Timer.rb', line 118

def measurement(drift: nil)
  Monotonic::Measurement.new(total_nanoseconds, floor: self.class.method(:floor), drift: drift)
end

#startObject



79
80
81
82
# File 'lib/Monotonic/Timer.rb', line 79

def start
  @finish_nanoseconds = nil
  @start_nanoseconds = now
end

#stopObject



84
85
86
# File 'lib/Monotonic/Timer.rb', line 84

def stop
  @finish_nanoseconds = now
end

#timeObject

A Monotonic::Measurement rather than a bare Float, so that what comes back says how much of itself is real. #total_time and #total_nanoseconds are still there for a number.



129
130
131
132
133
134
135
136
137
# File 'lib/Monotonic/Timer.rb', line 129

def time
  start
  begin
    yield self
  ensure
    stop
  end
  measurement
end

#to_durationObject

An elapsed interval is a duration, so here is one, and every unit follows from it exactly. #total_time is the same figure in seconds, that being the unit this library has always answered in.



105
106
107
# File 'lib/Monotonic/Timer.rb', line 105

def to_duration
  Duration::Nanoseconds.new(total_nanoseconds)
end

#to_sObject



122
123
124
# File 'lib/Monotonic/Timer.rb', line 122

def to_s
  measurement.to_s
end

#total_nanosecondsObject

Two exact integers differenced, which spends none of the reading. A pair of Floats would not begin to lose the clock at this resolution until some six years of uptime, but they would begin.

Raw readings difference to nanoseconds already. Instants difference to whatever their own subtraction gives, so it is asked to say itself in nanoseconds — which is why this could not be done before 0.8.0, where Monotonic::Time#- began answering with a duration rather than a bare Float of seconds.



97
98
99
100
# File 'lib/Monotonic/Timer.rb', line 97

def total_nanoseconds
  elapsed = finish_nanoseconds - @start_nanoseconds
  elapsed.respond_to?(:to_nanoseconds) ? elapsed.to_nanoseconds.to_i : elapsed
end

#total_timeObject



109
110
111
# File 'lib/Monotonic/Timer.rb', line 109

def total_time
  to_duration.to_seconds.to_f
end