Class: Musa::Clock::Timer

Inherits:
Object show all
Defined in:
lib/musa-dsl/transport/timer.rb

Overview

High-precision timer for generating regular ticks.

Timer uses Ruby's monotonic clock (Process::CLOCK_MONOTONIC) for drift-free timing. It compensates for processing delays and reports when the system cannot keep up with the requested tick rate.

Precision Features

  • Monotonic clock: Immune to system time changes
  • Drift compensation: Calculates exact next tick time
  • Overload detection: Reports delayed ticks when processing is slow
  • Correction parameter: Fine-tune timing for specific systems

Usage Pattern

Timer is typically used internally by TimerClock, not directly. It runs in a loop, yielding for each tick and managing precise sleep intervals.

Timing Algorithm

  1. Record next expected tick time
  2. Yield (caller processes tick)
  3. Add period to next_moment
  4. Calculate sleep time = (next_moment + correction) - current_time
  5. Sleep if positive, warn if negative (delayed)

How TimerClock uses it (a reading: run blocks until stopped):

timer = Timer.new(0.02083, logger: logger)  # ~48 ticks/second
timer.run { sequencer.tick }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tick_period_in_seconds, correction: nil, stop: nil, delayed_ticks_error: nil, logger: nil, do_log: nil) ⇒ Timer

Creates a new precision timer.

Examples:

120 BPM, 24 ticks per beat

period = 60.0 / (120 * 24)  # 0.02083 seconds
timer = Timer.new(period, logger: Musa::Logger::Logger.new)
timer.period  # => 1/48r

Parameters:

  • tick_period_in_seconds (Numeric)

    time between ticks in seconds

  • correction (Numeric, nil) (defaults to: nil)

    timing correction offset in seconds (for calibration)

  • stop (Boolean, nil) (defaults to: nil)

    initial stopped state (true = paused)

  • delayed_ticks_error (Numeric, nil) (defaults to: nil)

    threshold for error-level logging (default: 1.0 tick)

  • logger (Logger, nil) (defaults to: nil)

    logger for timing warnings/errors

  • do_log (Boolean, nil) (defaults to: nil)

    enable logging



54
55
56
57
58
59
60
61
62
63
# File 'lib/musa-dsl/transport/timer.rb', line 54

def initialize(tick_period_in_seconds, correction: nil, stop: nil, delayed_ticks_error: nil, logger: nil, do_log: nil)
  @period = tick_period_in_seconds.rationalize
  @correction = (correction || 0r).rationalize
  @stop = stop || false
  @terminate = false

  @delayed_ticks_error = delayed_ticks_error || 1.0
  @logger = logger
  @do_log = do_log
end

Instance Attribute Details

#periodRational

The period between ticks in seconds.

Returns:



39
40
41
# File 'lib/musa-dsl/transport/timer.rb', line 39

def period
  @period
end

Instance Method Details

#continuevoid

Note:

Resets timing baseline to prevent tick accumulation

This method returns an undefined value.

Resumes the timer after being stopped.

Resets the next tick moment to avoid a burst of catchup ticks, then wakes the timer thread.

See Also:



141
142
143
144
145
# File 'lib/musa-dsl/transport/timer.rb', line 141

def continue
  @stop = false
  @next_moment = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  @thread.run
end

#run { ... } ⇒ void

Note:

This method blocks the current thread until #terminate is called

Note:

Uses monotonic clock for drift-free timing

This method returns an undefined value.

Runs the timer loop, yielding for each tick.

This method blocks and runs until terminated. For each tick:

  1. Yields to caller if not stopped
  2. Calculates next tick time
  3. Sleeps precisely until next tick
  4. Logs warnings if timing cannot be maintained

When stopped (@stop = true), the thread sleeps until #continue is called. When terminated (@terminate = true), the loop exits and this method returns.

Yields:

  • Called once per tick for processing



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/musa-dsl/transport/timer.rb', line 81

def run
  @thread = Thread.current
  @terminate = false

  @next_moment = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  loop do
    break if @terminate

    unless @stop
      # Process the tick
      yield

      # Calculate next tick moment (compensates for processing time)
      @next_moment += @period
      to_sleep = (@next_moment + @correction) - Process.clock_gettime(Process::CLOCK_MONOTONIC)

      # Log timing issues if enabled
      if @do_log && to_sleep.negative? & @logger
        tick_errors = -to_sleep / @period
        if tick_errors >= @delayed_ticks_error
          @logger.error "Timer delayed #{tick_errors.round(2)} ticks (#{-to_sleep.round(3)}s)"
        else
          @logger.warn "Timer delayed #{tick_errors.round(2)} ticks (#{-to_sleep.round(3)}s)"
        end
      end

      # Sleep precisely until next tick (if not already late)
      sleep to_sleep if to_sleep > 0.0
    end

    # When stopped, sleep thread until continue or terminate is called
    if @stop
      break if @terminate
      sleep
    end
  end
end

#stopvoid

This method returns an undefined value.

Pauses the timer without terminating the loop.

The timer thread sleeps until #continue is called. Ticks are not generated while stopped.

See Also:



128
129
130
# File 'lib/musa-dsl/transport/timer.rb', line 128

def stop
  @stop = true
end

#terminatevoid

Note:

This wakes the thread if sleeping and causes #run to return

This method returns an undefined value.

Terminates the timer loop permanently.

Unlike #stop which pauses the timer (allowing #continue to resume), terminate causes the #run loop to exit completely. Use this for clean shutdown.



157
158
159
160
# File 'lib/musa-dsl/transport/timer.rb', line 157

def terminate
  @terminate = true
  @thread&.wakeup
end