clockblocks.scheduler.Scheduler
- class clockblocks.scheduler.Scheduler(timing_policy: float = 0.98, precise_timing: bool = False, spin_guard_duration: float = 0.0005, daemon: bool = True, time_backend: TimingBackend | None = None)[source]
Bases:
ThreadThe background timing thread for one clock family. Internal: user code need never touch it directly. Each master clock mints and owns its own Scheduler, children share their master’s, and the public timing knobs are surfaced on
Clock(e.g.timing_policy, fast-forward).Runs a single loop over a time-ordered heap of events, sleeping until the next is due and then firing it. Utilizes two locks for concurrency safety:
_queue_change_condition, for anything that touches the event Queue, and_execution_lockwhich is held throughout an event’s action. See inline commentary inrun().In the clock system each event wakes a clock and parks until the user code runs up to its next
wait(). Tempo changes reschedule pending events usingreschedule(), and killing a clock removes them viaremove_events().- Parameters:
timing_policy – Bounds how far each wait may deviate from its nominal length in order to track the absolute schedule. 0 -> Absolute timing (cut/extend the wait as needed to match ideal time since start). 1 -> Relative timing (wait exactly the nominal delay, drift never corrected). 0.98 (default) -> a wait may be compressed to 98% (or stretched to 102%) of its nominal length to correct any accumulated drift.
precise_timing – When True, close the final approach to each event with a busy-spin instead of resting on the (jittery) OS wait timeout, hitting the event time to within microseconds. Costs one core for at most
spin_guard_durationseconds per event — seerun()STEP 1c.spin_guard_duration – Width (seconds) of the busy-spin guard band used when
precise_timingis on. The coarse OS wait stops this far short of the deadline and the remainder is spun out. Default 500µs.daemon – run as a daemon thread (the default) so it can’t keep the process alive on its own.
time_backend – The
TimingBackendsupplying “now” and the sleep condition. Defaults to realperf_countertime; tests passCompressedTimeto run faster.
Methods
held()Context manager that: 1) ensures that the scheduler is not executing scheduled actions — waiting if one is in flight, then preventing others from starting; 2) rouses the scheduler and updates its committed time to the current projected time.
Whether a fast-forward goal is currently set.
kill()Stop the scheduler.
lag()How far behind the absolute schedule the scheduler is currently running, in seconds: the real time that has elapsed minus the ideal time that should have elapsed.
A wall-clock-interpolated estimate of where the scheduler is right now, as opposed to
time, which returns the time of the last executed event.remove_events(matches)Remove all queued events that satisfy matches(event).
reschedule(matches, recompute)Walk the heap and recompute t for any event that satisfies matches(event).
run()Main scheduler loop: wait until the next queued action is due, then execute it, forever.
schedule_action(t, action[, priority, metadata])Schedule the given action to be executed at time 't' (in seconds since scheduler start).
set_fast_forward_goal(goal)Set the fast-forward goal (a scheduler-time, float('inf') for indefinite, or None to stop) and wake the run loop so it acts on the change immediately rather than after its current timed wait.
Return the actual time elapsed on the schedule.
Inherited Methods
getName()Return a string used for identification purposes only.
isDaemon()Return whether this thread is a daemon.
is_alive()Return whether the thread is alive.
join([timeout])Wait until the thread terminates.
setDaemon(daemonic)Set whether this thread is a daemon.
setName(name)Set the name string for this thread.
start()Start the thread's activity.
Attributes
A boolean value indicating whether this thread is a daemon thread.
Thread identifier of this thread or None if it has not been started.
A string used for identification purposes only.
Native integral thread ID of this thread, or None if it has not been started.
The scheduler's ideal time (the time that should have passed by schedule).
- property time: float
The scheduler’s ideal time (the time that should have passed by schedule).
This is event-quantized, not wall-clock-interpolated: it is bumped to each event’s scheduled time as that event executes (see
_execute_event()), so between events it holds the most recently executed event’s time. Seetimefor what that means for reads taken between events / from other threads. Deterministic, hence a read-only property; for a smoothly-advancing wall-clock sample of the position between events, seeprojected_time().
- projected_time() float[source]
A wall-clock-interpolated estimate of where the scheduler is right now, as opposed to
time, which returns the time of the last executed event.Between events,
timestays put while real time passes; this fills the gap by interpolating from the last event toward the next, so continuous readers (e.g. parameter automation) see smooth motion. The estimate depends ontiming_policy, which decides when the next event is planned to arrive.This is an estimate, and not monotonic. It resets on deliberate schedule changes (a fast-forward, a live timing_policy retune), and booking an event below the current projection pulls it back to that event. (This kind of makes sense, since the free-climb overshot a point where something needed to happen.) In both cases the cause of the backward step is that we are collapsing the gap between a stale committed time and an overly optimistic projected time.
To avoid this issue entirely, use the
held()context manager, which first rouses the scheduler to the projected position.Refer to
diagrams/projectedTimeExplanationfor how this resolves under the various scenarios (ahead of schedule, behind, empty queue, etc.).
- wall_time() float[source]
Return the actual time elapsed on the schedule. Returns 0 until timing is anchored by the first event that requires a real wait (see
run()), then advances in real time. Also resets to ideal time after fast-forwarding, erasing any prior lag.Measured through the
TimingBackend, which defaults totime.perf_counter. perf_counter is monotonic and high-resolution, so this is true elapsed real time, never affected by NTP corrections (slews or steps) to CLOCK_REALTIME. Under a compressed backend it is scaled in step with everything else.
- lag() float[source]
How far behind the absolute schedule the scheduler is currently running, in seconds: the real time that has elapsed minus the ideal time that should have elapsed.
Positive means events are firing late. It grows when an event’s action takes longer than the gap to the next event, and (under a relative timing policy) is allowed to persist rather than being chased down. Fast-forwarding resets it to zero, since
_reanchor_timing()re-anchors_start_timeto put us exactly on the absolute schedule.
- set_fast_forward_goal(goal: float | None) None[source]
Set the fast-forward goal (a scheduler-time, float(‘inf’) for indefinite, or None to stop) and wake the run loop so it acts on the change immediately rather than after its current timed wait.
- held()[source]
Context manager that: 1) ensures that the scheduler is not executing scheduled actions — waiting if one is in flight, then preventing others from starting; 2) rouses the scheduler and updates its committed time to the current projected time. Code under this context manager therefore acts like an immediately scheduled action, holding exclusive access to an up-to-date scheduler.
Implemented by taking _execution_lock, which the run loop holds for the full duration of each action. So don’t call this from within a scheduled action: you’d block waiting for that action to finish, but it can’t finish while it’s stuck waiting here.
In the context of the clock system, this context manager should be used any time you want to take clock-related actions from a non-clock thread. However, since it’s implemented by taking the same _execution_lock held during scheduled actions, it should never be used within a scheduled action: the action would block waiting for itself to finish. For this reason we have
hold_scheduler(), which safely wraps this method by detecting whether we are running from a clock and no-op’ing in that case.
- remove_events(matches: Callable[[QueueEvent], bool]) int[source]
Remove all queued events that satisfy matches(event). Returns the number removed. Used when a clock is killed and its pending wakeups (and pending forks) need to be cancelled.
- reschedule(matches: Callable[[QueueEvent], bool], recompute: Callable[[QueueEvent], float]) None[source]
Walk the heap and recompute t for any event that satisfies matches(event). Used when a tempo change makes previously-scheduled wakeups stale.
- schedule_action(t: float, action: Callable, priority: Tuple[int, ...] = (0,), metadata: Any = None) None[source]
Schedule the given action to be executed at time ‘t’ (in seconds since scheduler start).
- run() None[source]
Main scheduler loop: wait until the next queued action is due, then execute it, forever.
The two subtle parts — how the wait is structured around _queue_change_condition so a queue change can’t be lost, and why execution is wrapped in _execution_lock — are explained inline at STEP 1 and STEP 2 below.
Note that the clock starts in an unanchored state (_start_time = _last_event_time = None) This is intentional: we don’t want the code that runs before the first future action is scheduled (which can be lengthy initial setup) to cause us to start off way behind schedule. The timing is anchored (via _reanchor_timing) when the first future event is pulled from the queue.
- property daemon
A boolean value indicating whether this thread is a daemon thread.
This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.
The entire Python program exits when only daemon threads are left.
- getName()
Return a string used for identification purposes only.
This method is deprecated, use the name attribute instead.
- property ident
Thread identifier of this thread or None if it has not been started.
This is a nonzero integer. See the get_ident() function. Thread identifiers may be recycled when a thread exits and another thread is created. The identifier is available even after the thread has exited.
- isDaemon()
Return whether this thread is a daemon.
This method is deprecated, use the daemon attribute instead.
- is_alive()
Return whether the thread is alive.
This method returns True just before the run() method starts until just after the run() method terminates. See also the module function enumerate().
- join(timeout=None)
Wait until the thread terminates.
This blocks the calling thread until the thread whose join() method is called terminates – either normally or through an unhandled exception or until the optional timeout occurs.
When the timeout argument is present and not None, it should be a floating point number specifying a timeout for the operation in seconds (or fractions thereof). As join() always returns None, you must call is_alive() after join() to decide whether a timeout happened – if the thread is still alive, the join() call timed out.
When the timeout argument is not present or None, the operation will block until the thread terminates.
A thread can be join()ed many times.
join() raises a RuntimeError if an attempt is made to join the current thread as that would cause a deadlock. It is also an error to join() a thread before it has been started and attempts to do so raises the same exception.
- property name
A string used for identification purposes only.
It has no semantics. Multiple threads may be given the same name. The initial name is set by the constructor.
- property native_id
Native integral thread ID of this thread, or None if it has not been started.
This is a non-negative integer. See the get_native_id() function. This represents the Thread ID as reported by the kernel.
- setDaemon(daemonic)
Set whether this thread is a daemon.
This method is deprecated, use the .daemon property instead.
- setName(name)
Set the name string for this thread.
This method is deprecated, use the name attribute instead.
- start()
Start the thread’s activity.
It must be called at most once per thread object. It arranges for the object’s run() method to be invoked in a separate thread of control.
This method will raise a RuntimeError if called more than once on the same thread object.