Class: Cyclotone::Scheduler

Inherits:
Object
  • Object
show all
Defined in:
lib/cyclotone/scheduler.rb

Defined Under Namespace

Classes: SystemClock

Constant Summary collapse

LOOKAHEAD =
0.3
INTERVAL =
0.05
DEFAULT_CPS =
Rational(9, 16)
DEFAULT_STOP_TIMEOUT =
1.0
SENT_RETAIN_CYCLES =
4

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backend:, cps: DEFAULT_CPS, lookahead: LOOKAHEAD, interval: INTERVAL, lookahead_cycles: nil, interval_cycles: nil, logger: nil, retry_failed: false, clock: nil) ⇒ Scheduler

Returns a new instance of Scheduler.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/cyclotone/scheduler.rb', line 22

def initialize(
  backend:,
  cps: DEFAULT_CPS,
  lookahead: LOOKAHEAD,
  interval: INTERVAL,
  lookahead_cycles: nil,
  interval_cycles: nil,
  logger: nil,
  retry_failed: false,
  clock: nil
)
  @backend = backend
  @cps = normalize_cps(cps)
  @lookahead = lookahead
  @interval = interval
  @lookahead_cycles = lookahead_cycles&.to_f
  @interval_cycles = interval_cycles&.to_f
  @logger = logger
  @retry_failed = retry_failed
  @clock = clock || SystemClock.new
  @mutex = Mutex.new
  @patterns = {}
  @sent = {}
  @running = false
  @thread = nil
  @last_error = nil
  @start_monotonic = monotonic_time
  @start_wall_time = wall_time
  @start_cycle = 0.0
  @last_cycle = 0.0
  @metrics = { ticks: 0, last_tick_duration: 0.0, max_tick_duration: 0.0 }
end

Instance Attribute Details

#backendObject

Returns the value of attribute backend.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def backend
  @backend
end

#cpsObject (readonly)

Returns the value of attribute cps.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def cps
  @cps
end

#intervalObject (readonly)

Returns the value of attribute interval.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def interval
  @interval
end

#interval_cyclesObject (readonly)

Returns the value of attribute interval_cycles.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def interval_cycles
  @interval_cycles
end

#lookaheadObject (readonly)

Returns the value of attribute lookahead.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def lookahead
  @lookahead
end

#lookahead_cyclesObject (readonly)

Returns the value of attribute lookahead_cycles.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def lookahead_cycles
  @lookahead_cycles
end

#metricsObject (readonly)

Returns the value of attribute metrics.



20
21
22
# File 'lib/cyclotone/scheduler.rb', line 20

def metrics
  @metrics
end

Instance Method Details

#current_cycle(now = monotonic_time) ⇒ Object



162
163
164
# File 'lib/cyclotone/scheduler.rb', line 162

def current_cycle(now = monotonic_time)
  @mutex.synchronize { time_to_cycle(now, @cps, @start_cycle, @start_monotonic) }
end

#last_errorObject



97
98
99
# File 'lib/cyclotone/scheduler.rb', line 97

def last_error
  @mutex.synchronize { @last_error }
end

#remove_pattern(slot_id) ⇒ Object



122
123
124
125
126
127
# File 'lib/cyclotone/scheduler.rb', line 122

def remove_pattern(slot_id)
  @mutex.synchronize do
    @patterns.delete(slot_id)
    @sent.delete_if { |key, _| key.first == slot_id }
  end
end

#render(duration:, at: 0.0) ⇒ Object

Raises:

  • (ArgumentError)


170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/cyclotone/scheduler.rb', line 170

def render(duration:, at: 0.0)
  duration_value = duration.to_f
  raise ArgumentError, "render duration must be non-negative" if duration_value.negative?

  state = snapshot_state.merge(start_wall_time: at.to_f)
  state[:backend].begin_capture(at: state[:start_wall_time]) if state[:backend].respond_to?(:begin_capture)

  logical_end = state[:start_cycle] + (duration_value * state[:cps])
  dispatch_until(logical_end, state)

  state[:backend].end_capture if state[:backend].respond_to?(:end_capture)
  state[:backend].write! if state[:backend].respond_to?(:write!)
  self
end

#reset_cyclesObject



144
145
146
# File 'lib/cyclotone/scheduler.rb', line 144

def reset_cycles
  set_cycle(0)
end

#running?Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/cyclotone/scheduler.rb', line 166

def running?
  @mutex.synchronize { @running }
end

#set_cycle(value) ⇒ Object



148
149
150
151
152
153
154
155
156
# File 'lib/cyclotone/scheduler.rb', line 148

def set_cycle(value)
  @mutex.synchronize do
    @start_cycle = value.to_f
    @start_monotonic = monotonic_time
    @start_wall_time = wall_time
    @last_cycle = value.to_f
    @sent.clear
  end
end

#setcps(value) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/cyclotone/scheduler.rb', line 129

def setcps(value)
  normalized_cps = normalize_cps(value)
  now = monotonic_time
  wall_now = wall_time

  @mutex.synchronize do
    current_cycle = time_to_cycle(now, @cps, @start_cycle, @start_monotonic)

    @start_cycle = current_cycle
    @start_monotonic = now
    @start_wall_time = wall_now
    @cps = normalized_cps
  end
end

#startObject



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/cyclotone/scheduler.rb', line 55

def start
  @mutex.synchronize do
    return self if @running

    @running = true
    @last_error = nil
    @thread = Thread.new do
      Thread.current.abort_on_exception = false

      while running?
        tick_started = monotonic_time

        begin
          tick(tick_started)
        rescue StandardError => error
          log_runtime_error(error)
        end

        record_tick_duration(monotonic_time - tick_started)
        sleep(current_interval)
      end
    end
  end

  self
end

#stop(timeout: DEFAULT_STOP_TIMEOUT) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/cyclotone/scheduler.rb', line 82

def stop(timeout: DEFAULT_STOP_TIMEOUT)
  thread = @mutex.synchronize do
    @running = false
    @thread
  end

  thread&.join(timeout)

  @mutex.synchronize do
    @thread = nil if @thread == thread && !thread&.alive?
  end

  self
end

#tick(now = monotonic_time) ⇒ Object



101
102
103
104
105
106
107
108
109
110
# File 'lib/cyclotone/scheduler.rb', line 101

def tick(now = monotonic_time)
  state = snapshot_state
  current_cycle = time_to_cycle(now, state[:cps], state[:start_cycle], state[:start_monotonic])
  logical_end = if lookahead_cycles
                  current_cycle + lookahead_cycles
                else
                  time_to_cycle(now + lookahead, state[:cps], state[:start_cycle], state[:start_monotonic])
                end
  dispatch_until(logical_end, state)
end

#update_pattern(slot_id, pattern, cps: nil, phase: 0) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/cyclotone/scheduler.rb', line 112

def update_pattern(slot_id, pattern, cps: nil, phase: 0)
  @mutex.synchronize do
    @patterns[slot_id] = {
      pattern: Pattern.ensure_pattern(pattern),
      cps: cps&.to_f,
      phase: Pattern.to_rational(phase)
    }
  end
end