001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.command3;
006
007import static org.wpilib.units.Units.Microseconds;
008import static org.wpilib.units.Units.Milliseconds;
009
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.Collections;
013import java.util.HashSet;
014import java.util.LinkedHashMap;
015import java.util.LinkedHashSet;
016import java.util.List;
017import java.util.Map;
018import java.util.SequencedMap;
019import java.util.SequencedSet;
020import java.util.Set;
021import java.util.Stack;
022import java.util.function.Consumer;
023import java.util.stream.Collectors;
024import org.wpilib.annotation.NoDiscard;
025import org.wpilib.command3.button.CommandGenericHID;
026import org.wpilib.command3.proto.SchedulerProto;
027import org.wpilib.event.EventLoop;
028import org.wpilib.framework.TimedRobot;
029import org.wpilib.system.RobotController;
030import org.wpilib.util.ErrorMessages;
031import org.wpilib.util.protobuf.ProtobufSerializable;
032
033/**
034 * Manages the lifecycles of {@link Coroutine}-based {@link Command Commands}. Commands may be
035 * scheduled directly using {@link #schedule(Command)}, or be bound to {@link Trigger Triggers} to
036 * automatically handle scheduling and cancellation based on internal or external events. User code
037 * is responsible for calling {@link #run()} periodically to update trigger conditions and execute
038 * scheduled commands. Most often, this is done by overriding {@link TimedRobot#robotPeriodic()} to
039 * include a call to {@code Scheduler.getDefault().run()}:
040 *
041 * <pre>{@code
042 * public class Robot extends TimedRobot {
043 *   @Override
044 *   public void robotPeriodic() {
045 *     // Update the scheduler on every loop
046 *     Scheduler.getDefault().run();
047 *   }
048 * }
049 * }</pre>
050 *
051 * <h2>Danger</h2>
052 *
053 * <p>The scheduler <i>must</i> be used in a single-threaded program. Commands must be scheduled and
054 * canceled by the same thread that runs the scheduler, and cannot be run in a virtual thread.
055 *
056 * <p><strong>Using the commands framework in a multithreaded environment can cause crashes in the
057 * Java virtual machine at any time, including on an official field during a match.</strong> The
058 * Java JIT compilers make assumptions that rely on coroutines being used on a single thread.
059 * Breaking those assumptions can cause incorrect JIT code to be generated with undefined behavior,
060 * potentially causing control issues or crashes deep in JIT-generated native code.
061 *
062 * <p><strong>Normal concurrency constructs like locks, atomic references, and synchronized blocks
063 * or methods cannot save you.</strong>
064 *
065 * <h2>Lifecycle</h2>
066 *
067 * <p>The {@link #run()} method runs six steps:
068 *
069 * <ol>
070 *   <li>Cancel any commands bound to scopes that have gone inactive, such as having been scheduled
071 *       in an opmode that's no longer selected on the driverstation.
072 *   <li>Call {@link #sideload(Consumer) periodic sideload functions}.
073 *   <li>Poll all registered triggers to queue and cancel commands.
074 *   <li>Queue default commands for any mechanisms without a running command. The queued commands
075 *       can be superseded by any manual scheduling or commands scheduled by triggers in the next
076 *       run.
077 *   <li>Start all queued commands. This happens after all triggers are checked in case multiple
078 *       commands with conflicting requirements are queued in the same update; the last command to
079 *       be queued takes precedence over the rest.
080 *   <li>Loop over all running commands, mounting and calling each in turn until they either exit or
081 *       call {@link Coroutine#yield()}. Commands run in the order in which they were scheduled.
082 * </ol>
083 *
084 * <h2>Telemetry</h2>
085 *
086 * <p>There are two mechanisms for telemetry for a scheduler. A protobuf serializer can be used to
087 * take a snapshot of a scheduler instance, and report what commands are queued (scheduled but have
088 * not yet started to run), commands that are running (along with timing data for each command), and
089 * total time spent in the most recent {@link #run()} call. However, it cannot detect one-shot
090 * commands that are scheduled, run, and complete all in a single {@code run()} invocation -
091 * effectively, commands that never call {@link Coroutine#yield()} are invisible.
092 *
093 * <p>A second telemetry mechanism is provided by {@link #addEventListener(Consumer)}. The scheduler
094 * will issue events to all registered listeners when certain events occur (see {@link
095 * SchedulerEvent} for all event types). These events are emitted immediately and can be used to
096 * detect lifecycle events for all commands, including one-shots that would be invisible to the
097 * protobuf serializer. However, it is up to the user to log those events themselves.
098 */
099public final class Scheduler implements ProtobufSerializable {
100  /**
101   * The default command bindings for each mechanism. Binding lists are ordered by priority; the
102   * last element in the list is the highest priority default command to be used. Bindings need to
103   * be periodically checked and removed when they're inactive.
104   */
105  private final Map<Mechanism, List<Binding>> m_defaultCommandBindings = new LinkedHashMap<>();
106
107  /**
108   * All bindings attached to this scheduler. This lets us cancel commands tied to scopes that go
109   * inactive. Bindings need to be periodically checked and removed when they're inactive.
110   */
111  private final Collection<Binding> m_activeBindings = new ArrayList<>();
112
113  /** The set of commands scheduled since the start of the previous run. */
114  private final SequencedSet<CommandState> m_queuedToRun = new LinkedHashSet<>();
115
116  /**
117   * The states of all running commands (does not include on deck commands). We preserve insertion
118   * order to guarantee that child commands run after their parents.
119   */
120  private final SequencedMap<Command, CommandState> m_runningCommands = new LinkedHashMap<>();
121
122  /**
123   * The stack of currently executing commands. Child commands are pushed onto the stack and popped
124   * when they complete. Use {@link #currentState()} and {@link #currentCommand()} to get the
125   * currently executing command or its state.
126   */
127  private final Stack<CommandState> m_currentCommandAncestry = new Stack<>();
128
129  /** The periodic callbacks to run, outside of the command structure. */
130  private final List<Coroutine> m_periodicCallbacks = new ArrayList<>();
131
132  /** Event loop for trigger bindings. */
133  private final EventLoop m_eventLoop = new EventLoop();
134
135  /** The scope for continuations to yield to. */
136  private final ContinuationScope m_scope = new ContinuationScope("coroutine commands");
137
138  // Telemetry
139  /** Protobuf serializer for a scheduler. */
140  public static final SchedulerProto proto = new SchedulerProto();
141
142  private double m_lastRunTimeMs = -1;
143
144  private final Set<Consumer<? super SchedulerEvent>> m_eventListeners = new LinkedHashSet<>();
145
146  /** The default scheduler instance. */
147  private static final Scheduler s_defaultScheduler = new Scheduler();
148
149  /**
150   * Gets the default scheduler instance for use in a robot program. Unless otherwise specified,
151   * triggers and mechanisms will be registered with the default scheduler and require the default
152   * scheduler to run. However, triggers and mechanisms can be constructed to be registered with a
153   * specific scheduler instance, which may be useful for isolation for unit tests.
154   *
155   * @return the default scheduler instance.
156   */
157  @NoDiscard
158  public static Scheduler getDefault() {
159    return s_defaultScheduler;
160  }
161
162  /**
163   * Creates a new scheduler object. Note that most built-in constructs like {@link Trigger} and
164   * {@link CommandGenericHID} will use the {@link #getDefault() default scheduler instance} unless
165   * they were explicitly constructed with a different scheduler instance. Teams should use the
166   * default instance for convenience; however, new scheduler instances can be useful for unit
167   * tests.
168   *
169   * @return a new scheduler instance that is independent of the default scheduler instance.
170   */
171  @NoDiscard
172  public static Scheduler createIndependentScheduler() {
173    return new Scheduler();
174  }
175
176  /** Private constructor. Use static factory methods or the default scheduler instance. */
177  private Scheduler() {}
178
179  /**
180   * Sets the default command for a mechanism. The command must require that mechanism and cannot
181   * require any other mechanisms. If another default command has already been set for this
182   * mechanism, the one provided will supersede it.
183   *
184   * <p>If this is called inside a running opmode or in a running command, the default command
185   * setting will only apply while that opmode or command is active. When the opmode or command
186   * exits, the previous default command setting will be restored.
187   *
188   * <p>Commands running as default commands may call this method to change their mechanism's
189   * default command on the fly. The new default command will take effect at the end of the
190   * scheduler's loop cycle.
191   *
192   * @param mechanism the mechanism for which to set the default command
193   * @param defaultCommand the default command to execute on the mechanism
194   * @throws IllegalArgumentException if the command does not meet the requirements for being a
195   *     default command
196   */
197  @SuppressWarnings("PMD.CompareObjectsWithEquals")
198  public void setDefaultCommand(Mechanism mechanism, Command defaultCommand) {
199    if (!defaultCommand.requires(mechanism)) {
200      throw new IllegalArgumentException(
201          "A mechanism's default command must require that mechanism");
202    }
203
204    if (defaultCommand.requirements().size() > 1) {
205      throw new IllegalArgumentException(
206          "A mechanism's default command cannot require other mechanisms");
207    }
208
209    var currentCommand = currentCommand();
210    BindingScope scope = BindingScope.createNarrowestScope(this);
211
212    var binding =
213        new Binding(
214            scope,
215            BindingType.CONTINUOUSLY_SCHEDULE_WHILE_HIGH,
216            defaultCommand,
217            new Throwable().getStackTrace());
218
219    var currentDefaultCommand = getDefaultCommandFor(mechanism);
220    m_defaultCommandBindings.computeIfAbsent(mechanism, k -> new ArrayList<>()).add(binding);
221
222    if (currentCommand != null && currentCommand != currentDefaultCommand) {
223      // User called `setDefaultCommand` inside another command.
224      // Immediately reprocess the default commands for this mechanism to ensure it's in sync with
225      // the rest of the commands in the scheduler. This is required because we normally schedule
226      // the default commands at the start of the scheduler `run()` method, so the new default
227      // command wouldn't be handled until the next run (ie, the previous default command would
228      // still be active for the current iteration)
229      //
230      // Note that we cannot do this if the current default command is the caller because commands
231      // cannot be canceled while mounted.
232      processDefaultCommands(mechanism);
233    }
234  }
235
236  /**
237   * Gets the default command currently used for a mechanism.
238   *
239   * @param mechanism The mechanism
240   * @return The default command, or null if no default command was ever set
241   */
242  public Command getDefaultCommandFor(Mechanism mechanism) {
243    var bindings = m_defaultCommandBindings.getOrDefault(mechanism, Collections.emptyList());
244    if (bindings.isEmpty()) {
245      return null;
246    }
247
248    return bindings.getLast().command();
249  }
250
251  // package-private helper for unit test access
252  List<Binding> getDefaultCommandBindingsFor(Mechanism mechanism) {
253    return m_defaultCommandBindings.getOrDefault(mechanism, Collections.emptyList());
254  }
255
256  /**
257   * Adds a callback to run as part of the scheduler. The callback should not manipulate or control
258   * any mechanisms, but can be used to log information, update data (such as simulations or LED
259   * data buffers), or perform some other helpful task. The callback is responsible for managing its
260   * own control flow and end conditions. If you want to run a single task periodically for the
261   * entire lifespan of the scheduler, use {@link #addPeriodic(Runnable)}.
262   *
263   * <p><strong>Note:</strong> Like commands, any loops in the callback must appropriately yield
264   * control back to the scheduler with {@link Coroutine#yield} or risk stalling your program in an
265   * unrecoverable infinite loop!
266   *
267   * @param callback the callback to sideload
268   */
269  public void sideload(Consumer<Coroutine> callback) {
270    var coroutine = new Coroutine(this, m_scope, callback);
271    m_periodicCallbacks.add(coroutine);
272  }
273
274  /**
275   * Adds a task to run repeatedly for as long as the scheduler runs. This internally handles the
276   * looping and control yielding necessary for proper function. The callback will run at the same
277   * periodic frequency as the scheduler.
278   *
279   * <p>For example:
280   *
281   * <pre>{@code
282   * scheduler.addPeriodic(() -> leds.setData(ledDataBuffer));
283   * scheduler.addPeriodic(() -> {
284   *   SmartDashboard.putNumber("X", getX());
285   *   SmartDashboard.putNumber("Y", getY());
286   * });
287   * }</pre>
288   *
289   * @param callback the periodic function to run
290   */
291  public void addPeriodic(Runnable callback) {
292    sideload(
293        coroutine -> {
294          while (true) {
295            callback.run();
296            coroutine.yield();
297          }
298        });
299  }
300
301  /** Represents possible results of a command scheduling attempt. */
302  public enum ScheduleResult {
303    /** The command was successfully scheduled and added to the queue. */
304    SUCCESS,
305    /** The command is already scheduled or running. */
306    ALREADY_RUNNING,
307    /** The command is a lower priority and conflicts with a command that's already running. */
308    LOWER_PRIORITY_THAN_RUNNING_COMMAND,
309  }
310
311  /**
312   * Schedules a command to run. If one command schedules another (a "parent" and "child"), the
313   * child command will be canceled when the parent command completes. It is not possible to fork a
314   * child command and have it live longer than its parent.
315   *
316   * <p>Does nothing if the command is already scheduled or running, or requires at least one
317   * mechanism already used by a higher priority command.
318   *
319   * @param command the command to schedule
320   * @return the result of the scheduling attempt. See {@link ScheduleResult} for details.
321   * @throws IllegalArgumentException if scheduled by a command composition that has already
322   *     scheduled another command that shares at least one required mechanism
323   */
324  // Implementation detail: a child command will immediately start running when scheduled by a
325  // parent command, skipping the queue entirely. This avoids dead loop cycles where a parent
326  // schedules a child, appending it to the queue, then waits for the next cycle to pick it up and
327  // start it. With deeply nested commands, dead loops could quickly to add up and cause the
328  // innermost commands that actually _do_ something to start running hundreds of milliseconds after
329  // their root ancestor was scheduled.
330  public ScheduleResult schedule(Command command) {
331    // Get the narrowest binding scope.
332    // This prevents commands from outliving the opmodes that scheduled them, or from outliving
333    // their parents (eg if someone writes a command that manually calls schedule(Command) instead
334    // of using triggers to do so).
335    BindingScope scope = BindingScope.createNarrowestScope(this);
336
337    // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier
338    //       stack frame filtering and modification.
339    var binding =
340        new Binding(scope, BindingType.IMMEDIATE, command, new Throwable().getStackTrace());
341
342    return schedule(binding);
343  }
344
345  // Package-private for use by Trigger
346  ScheduleResult schedule(Binding binding) {
347    var command = binding.command();
348
349    if (isScheduledOrRunning(command)) {
350      return ScheduleResult.ALREADY_RUNNING;
351    }
352
353    if (lowerPriorityThanConflictingCommands(command)) {
354      return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND;
355    }
356
357    for (var scheduledState : m_queuedToRun) {
358      if (!command.conflictsWith(scheduledState.command())) {
359        // No shared requirements, skip
360        continue;
361      }
362      if (command.isLowerPriorityThan(scheduledState.command())) {
363        // Lower priority than an already-scheduled (but not yet running) command that requires at
364        // one of the same mechanism. Ignore it.
365        return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND;
366      }
367    }
368
369    // Track this binding so we can disable it when it's out of scope.
370    // Note that, even though triggers can clean themselves up, commands that are manually scheduled
371    // cannot do the same, so we have to track them in the scheduler.
372    m_activeBindings.add(binding);
373
374    // Evict conflicting on-deck commands
375    // We check above if the input command is lower priority than any of these,
376    // so at this point we're guaranteed to be >= priority than anything already on deck
377    evictConflictingOnDeckCommands(command);
378
379    // If the binding is scoped to a particular command, that command is the parent. If we're in the
380    // middle of a run cycle and running commands, the parent is whatever command is currently
381    // running. Otherwise, there is no parent command.
382    var parentCommand =
383        binding.scope() instanceof BindingScope.ForCommand scope
384            ? scope.command()
385            : currentCommand();
386    var state = new CommandState(command, parentCommand, buildCoroutine(command), binding);
387
388    emitScheduledEvent(command);
389
390    if (currentState() != null) {
391      // Scheduling a child command while running. Start it immediately instead of waiting a full
392      // cycle. This prevents issues with deeply nested command groups taking many scheduler cycles
393      // to start running the commands that actually _do_ things
394      evictConflictingRunningCommands(state);
395      m_runningCommands.put(command, state);
396      runCommand(state);
397    } else {
398      // Scheduling outside a command, add it to the pending set. If it's not overridden by another
399      // conflicting command being scheduled in the same scheduler loop, it'll be promoted and
400      // start to run when #runCommands() is called
401      m_queuedToRun.add(state);
402    }
403
404    return ScheduleResult.SUCCESS;
405  }
406
407  /**
408   * Checks if a command conflicts with and is a lower priority than any running command. Used when
409   * determining if the command can be scheduled.
410   */
411  private boolean lowerPriorityThanConflictingCommands(Command command) {
412    Set<CommandState> ancestors = new HashSet<>();
413    for (var state = currentState(); state != null; state = m_runningCommands.get(state.parent())) {
414      ancestors.add(state);
415    }
416
417    // Check for conflicts with the commands that are already running
418    for (var state : m_runningCommands.values()) {
419      if (ancestors.contains(state)) {
420        // Can't conflict with an ancestor command
421        continue;
422      }
423
424      var c = state.command();
425      if (c.conflictsWith(command) && command.isLowerPriorityThan(c)) {
426        return true;
427      }
428    }
429
430    return false;
431  }
432
433  private void evictConflictingOnDeckCommands(Command command) {
434    for (var iterator = m_queuedToRun.iterator(); iterator.hasNext(); ) {
435      var scheduledState = iterator.next();
436      var scheduledCommand = scheduledState.command();
437      if (scheduledCommand.conflictsWith(command)) {
438        // Remove the lower priority conflicting command from the on deck commands.
439        // We don't need to call removeOrphanedChildren here because it hasn't started yet,
440        // meaning it hasn't had a chance to schedule any children
441        iterator.remove();
442        emitInterruptedEvent(scheduledCommand, command);
443        emitCanceledEvent(scheduledCommand);
444      }
445    }
446  }
447
448  /**
449   * Cancels all running commands with which an incoming state conflicts. Ancestor commands of the
450   * incoming state will not be canceled.
451   */
452  @SuppressWarnings("PMD.CompareObjectsWithEquals")
453  private void evictConflictingRunningCommands(CommandState incomingState) {
454    // The set of root states with which the incoming state conflicts but does not inherit from
455    Set<CommandState> conflictingRootStates =
456        m_runningCommands.values().stream()
457            .filter(state -> incomingState.command().conflictsWith(state.command()))
458            .filter(state -> !isAncestorOf(state.command(), incomingState))
459            .map(
460                state -> {
461                  // Find the highest level ancestor of the conflicting command from which the
462                  // incoming state does _not_ inherit. If they're totally unrelated, this will
463                  // get the very root ancestor; otherwise, it'll return a direct sibling of the
464                  // incoming command
465                  CommandState root = state;
466                  while (root.parent() != null && root.parent() != incomingState.parent()) {
467                    root = m_runningCommands.get(root.parent());
468                  }
469                  return root;
470                })
471            .collect(Collectors.toSet());
472
473    // Cancel the root commands
474    for (var conflictingState : conflictingRootStates) {
475      emitInterruptedEvent(conflictingState.command(), incomingState.command());
476      cancel(conflictingState.command());
477    }
478  }
479
480  /**
481   * Checks if a particular command is an ancestor of another.
482   *
483   * @param ancestor the potential ancestor for which to search
484   * @param state the state to check
485   * @return true if {@code ancestor} is the direct parent or indirect ancestor, false if not
486   */
487  @SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.SimplifyBooleanReturns"})
488  private boolean isAncestorOf(Command ancestor, CommandState state) {
489    if (state.parent() == null) {
490      // No parent, cannot inherit
491      return false;
492    }
493    if (!m_runningCommands.containsKey(ancestor)) {
494      // The given ancestor isn't running
495      return false;
496    }
497    if (state.parent() == ancestor) {
498      // Direct child
499      return true;
500    }
501    // Check if the command's parent inherits from the given ancestor
502    return m_runningCommands.values().stream()
503        .filter(s -> state.parent() == s.command())
504        .anyMatch(s -> isAncestorOf(ancestor, s));
505  }
506
507  /**
508   * Cancels a command and any other command scheduled by it. This occurs immediately and does not
509   * need to wait for a call to {@link #run()}. Any command that it scheduled will also be canceled
510   * to ensure commands within compositions do not continue to run.
511   *
512   * <p>This has no effect if the given command is not currently scheduled or running.
513   *
514   * @param command the command to cancel
515   */
516  @SuppressWarnings("PMD.CompareObjectsWithEquals")
517  public void cancel(Command command) {
518    if (command == currentCommand()) {
519      throw new IllegalArgumentException(
520          "Command `" + command.name() + "` is mounted and cannot be canceled");
521    }
522
523    boolean running = isRunning(command);
524
525    // Evict the command. The next call to run() will schedule the default command for all its
526    // required mechanisms, unless another command requiring those mechanisms is scheduled between
527    // calling cancel() and calling run()
528    m_runningCommands.remove(command);
529    m_queuedToRun.removeIf(state -> state.command() == command);
530
531    if (running) {
532      // Only run the hook if the command was running. If it was on deck or not
533      // even in the scheduler at the time, then there's nothing to do
534      command.onCancel();
535      emitCanceledEvent(command);
536    }
537
538    // Clean up any orphaned child commands; their lifespan must not exceed the parent's
539    removeOrphanedChildren(command);
540  }
541
542  /**
543   * Updates the command scheduler. This will run operations in the following order:
544   *
545   * <ol>
546   *   <li>Cancel any commands bound to scopes that have gone inactive, such as having been
547   *       scheduled in an opmode that's no longer selected on the driverstation
548   *   <li>Run sideloaded functions from {@link #sideload(Consumer)} and {@link
549   *       #addPeriodic(Runnable)}
550   *   <li>Update trigger bindings to queue and cancel bound commands
551   *   <li>Queue default commands for mechanisms that do not have a queued or running command
552   *   <li>Promote queued commands to the running set
553   *   <li>For every command in the running set, mount and run that command until it calls {@link
554   *       Coroutine#yield()} or exits
555   * </ol>
556   *
557   * <p>This method is intended to be called in a periodic loop like {@link
558   * TimedRobot#robotPeriodic()}
559   */
560  public void run() {
561    final long startMicros = RobotController.getTime();
562
563    // Cancel any commands with stale binding scopes
564    cancelStaleBindings();
565
566    // Sideloads may change some state that affects triggers. Run them first.
567    runPeriodicSideloads();
568
569    // Poll triggers next to schedule and cancel commands
570    m_eventLoop.poll();
571
572    // Schedule default commands for any mechanisms that don't have a running command and didn't
573    // have a new command scheduled by a sideload function or a trigger
574    scheduleDefaultCommands();
575
576    // Move all scheduled commands to the running set
577    promoteScheduledCommands();
578
579    // Run every command in order until they call Coroutine.yield() or exit
580    runCommands();
581
582    final long endMicros = RobotController.getTime();
583    m_lastRunTimeMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds);
584  }
585
586  private void cancelStaleBindings() {
587    for (var iterator = m_activeBindings.iterator(); iterator.hasNext(); ) {
588      var binding = iterator.next();
589      if (binding.scope().active()) {
590        continue;
591      }
592      cancel(binding.command());
593      iterator.remove();
594    }
595  }
596
597  private void promoteScheduledCommands() {
598    // Clear any commands that conflict with the scheduled set
599    for (var queuedState : m_queuedToRun) {
600      evictConflictingRunningCommands(queuedState);
601    }
602
603    // Move any scheduled commands to the running set
604    for (var queuedState : m_queuedToRun) {
605      m_runningCommands.put(queuedState.command(), queuedState);
606    }
607
608    // Clear the set of on-deck commands,
609    // since we just put them all into the set of running commands
610    m_queuedToRun.clear();
611  }
612
613  private void runPeriodicSideloads() {
614    // Update periodic callbacks
615    for (Coroutine coroutine : m_periodicCallbacks) {
616      coroutine.mount();
617      try {
618        coroutine.runToYieldPoint();
619      } finally {
620        Continuation.mountContinuation(null);
621      }
622    }
623
624    // And remove any periodic callbacks that have completed
625    m_periodicCallbacks.removeIf(Coroutine::isDone);
626  }
627
628  private void runCommands() {
629    // Tick every command that hasn't been completed yet
630    // Run in reverse so parent commands can resume in the same loop cycle an awaited child command
631    // completes. Otherwise, parents could only resume on the next loop cycle, introducing a delay
632    // at every layer of nesting.
633    for (var state : List.copyOf(m_runningCommands.values()).reversed()) {
634      runCommand(state);
635    }
636  }
637
638  /**
639   * Mounts and runs a command until it yields or exits.
640   *
641   * @param state The command state to run
642   */
643  @SuppressWarnings("PMD.AvoidCatchingGenericException")
644  private void runCommand(CommandState state) {
645    final var command = state.command();
646    final var coroutine = state.coroutine();
647
648    if (!m_runningCommands.containsKey(command)) {
649      // Probably canceled by an owning composition, do not run
650      return;
651    }
652
653    var previousState = currentState();
654
655    m_currentCommandAncestry.push(state);
656    long startMicros = RobotController.getTime();
657    emitMountedEvent(command);
658    coroutine.mount();
659    try {
660      coroutine.runToYieldPoint();
661    } catch (RuntimeException e) {
662      // Command encountered an uncaught exception.
663      handleCommandException(state, e);
664    } finally {
665      long endMicros = RobotController.getTime();
666      double elapsedMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds);
667      state.setLastRuntimeMs(elapsedMs);
668
669      if (state.equals(currentState())) {
670        // Remove the command we just ran from the top of the stack
671        m_currentCommandAncestry.pop();
672      }
673
674      if (previousState != null) {
675        // Remount the parent command, if there is one
676        previousState.coroutine().mount();
677      } else {
678        Continuation.mountContinuation(null);
679      }
680    }
681
682    if (coroutine.isDone()) {
683      // Immediately check if the command has completed and remove any children commands.
684      // This prevents child commands from being executed one extra time in the run() loop
685      emitCompletedEvent(command);
686      m_runningCommands.remove(command);
687      removeOrphanedChildren(command);
688    } else {
689      // Yielded
690      emitYieldedEvent(command);
691    }
692  }
693
694  /**
695   * Handles uncaught runtime exceptions from a mounted and running command. The command's ancestor
696   * and child commands will all be canceled and the exception's backtrace will be modified to
697   * include the stack frames of the schedule call site.
698   *
699   * @param state The state of the command that encountered the exception.
700   * @param e The exception that was thrown.
701   * @throws RuntimeException rethrows the exception, with a modified backtrace pointing to the
702   *     schedule site
703   */
704  @SuppressWarnings("PMD.CompareObjectsWithEquals")
705  private void handleCommandException(CommandState state, RuntimeException e) {
706    var command = state.command();
707
708    // Fetch the root command
709    // (needs to be done before removing the failed command from the running set)
710    Command root = command;
711    while (getParentOf(root) != null) {
712      root = getParentOf(root);
713    }
714
715    // Remove it from the running set.
716    m_runningCommands.remove(command);
717
718    // Intercept the exception, inject stack frames from the schedule site, and rethrow it
719    var binding = state.binding();
720    e.setStackTrace(CommandTraceHelper.modifyTrace(e.getStackTrace(), binding.frames()));
721    emitCompletedWithErrorEvent(command, e);
722
723    // Clean up child commands after emitting the event so child Canceled events are emitted
724    // after the parent's CompletedWithError
725    removeOrphanedChildren(command);
726
727    // Bubble up to the root and cancel all commands between the root and this one
728    // Note: Because we remove the command from the running set above, we still need to
729    //       clean up all the failed command's children
730    if (root != null && root != command) {
731      cancel(root);
732    }
733
734    // Then rethrow the exception
735    throw e;
736  }
737
738  /**
739   * Gets the currently executing command state, or null if no command is currently executing.
740   *
741   * @return the currently executing command state
742   */
743  private CommandState currentState() {
744    if (m_currentCommandAncestry.isEmpty()) {
745      // Avoid EmptyStackException
746      return null;
747    }
748
749    return m_currentCommandAncestry.peek();
750  }
751
752  /**
753   * Gets the currently executing command, or null if no command is currently executing.
754   *
755   * @return the currently executing command
756   */
757  public Command currentCommand() {
758    var state = currentState();
759    if (state == null) {
760      return null;
761    }
762
763    return state.command();
764  }
765
766  private void scheduleDefaultCommands() {
767    m_defaultCommandBindings.keySet().forEach(this::processDefaultCommands);
768  }
769
770  private void processDefaultCommands(Mechanism mechanism) {
771    var bindings = m_defaultCommandBindings.get(mechanism);
772
773    // Remove default command bindings that are no longer active.
774    // If a default command is running when its scope goes inactive, also be sure to cancel it.
775    bindings.removeIf(
776        b -> {
777          if (!b.scope().active()) {
778            cancel(b.command());
779            return true;
780          }
781          return false;
782        });
783
784    if (bindings.isEmpty()) {
785      // Nothing to do. No active bindings remain.
786      return;
787    }
788
789    // Cancel any default command except the narrowest-scoped one (the last binding in the list)
790    for (int i = 0; i < bindings.size() - 1; i++) {
791      Command widerScopeDefaultCommand = bindings.get(i).command();
792      cancel(widerScopeDefaultCommand);
793    }
794
795    // Check if the mechanism is currently in use. We can queue the default command if it's not.
796    for (Command runningCommand : m_runningCommands.keySet()) {
797      if (runningCommand.requires(mechanism)) {
798        return;
799      }
800    }
801    for (CommandState queuedState : m_queuedToRun) {
802      if (queuedState.command().requires(mechanism)) {
803        return;
804      }
805    }
806
807    // Nothing currently running or queued that needs this mechanism. Queue the default command.
808    var defaultCommand = bindings.getLast();
809    schedule(defaultCommand);
810  }
811
812  /**
813   * Removes all commands descended from a parent command. This is used to ensure that any command
814   * scheduled within a composition or group cannot live longer than any ancestor.
815   *
816   * @param parent the root command whose descendants to remove from the scheduler
817   */
818  @SuppressWarnings("PMD.CompareObjectsWithEquals")
819  private void removeOrphanedChildren(Command parent) {
820    m_runningCommands.entrySet().stream()
821        .filter(e -> e.getValue().parent() == parent)
822        .toList() // copy to an intermediate list to avoid concurrent modification
823        .forEach(e -> cancel(e.getKey()));
824  }
825
826  /**
827   * Builds a coroutine object that the command will be bound to. The coroutine will be scoped to
828   * this scheduler object and cannot be used by another scheduler instance.
829   *
830   * @param command the command for which to build a coroutine
831   * @return the binding coroutine
832   */
833  private Coroutine buildCoroutine(Command command) {
834    return new Coroutine(this, m_scope, command::run);
835  }
836
837  /**
838   * Checks if a command is currently running.
839   *
840   * @param command the command to check
841   * @return true if the command is running, false if not
842   */
843  public boolean isRunning(Command command) {
844    return m_runningCommands.containsKey(command);
845  }
846
847  /**
848   * Checks if a command is currently scheduled to run, but is not yet running.
849   *
850   * @param command the command to check
851   * @return true if the command is scheduled to run, false if not
852   */
853  @SuppressWarnings("PMD.CompareObjectsWithEquals")
854  public boolean isScheduled(Command command) {
855    return m_queuedToRun.stream().anyMatch(state -> state.command() == command);
856  }
857
858  /**
859   * Checks if a command is currently scheduled to run, or is already running.
860   *
861   * @param command the command to check
862   * @return true if the command is scheduled to run or is already running, false if not
863   */
864  public boolean isScheduledOrRunning(Command command) {
865    return isScheduled(command) || isRunning(command);
866  }
867
868  /**
869   * Gets the set of all currently running commands. Commands are returned in the order in which
870   * they were scheduled. The returned set is read-only.
871   *
872   * @return the currently running commands
873   */
874  public Collection<Command> getRunningCommands() {
875    return Collections.unmodifiableSet(m_runningCommands.keySet());
876  }
877
878  /**
879   * Gets all the currently running commands that require a particular mechanism. Commands are
880   * returned in the order in which they were scheduled. The returned list is read-only.
881   *
882   * @param mechanism the mechanism to get the commands for
883   * @return the currently running commands that require the mechanism.
884   */
885  public List<Command> getRunningCommandsFor(Mechanism mechanism) {
886    return m_runningCommands.keySet().stream()
887        .filter(command -> command.requires(mechanism))
888        .toList();
889  }
890
891  /**
892   * Cancels all currently running and scheduled commands. All default commands will be scheduled on
893   * the next call to {@link #run()}, unless a higher priority command is scheduled or triggered
894   * after {@code cancelAll()} is used.
895   */
896  public void cancelAll() {
897    for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) {
898      var state = onDeckIter.next();
899      onDeckIter.remove();
900      emitCanceledEvent(state.command());
901    }
902
903    for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) {
904      var entry = liveIter.next();
905      liveIter.remove();
906      Command canceledCommand = entry.getKey();
907      canceledCommand.onCancel();
908      emitCanceledEvent(canceledCommand);
909    }
910  }
911
912  /**
913   * An event loop used by the scheduler to poll triggers that schedule or cancel commands. This
914   * event loop is always polled on every call to {@link #run()}. Custom event loops need to be
915   * bound to this one for synchronicity with the scheduler; however, they can always be polled
916   * manually before or after the call to {@link #run()}.
917   *
918   * @return the default event loop.
919   */
920  @NoDiscard
921  public EventLoop getDefaultEventLoop() {
922    return m_eventLoop;
923  }
924
925  /**
926   * For internal use.
927   *
928   * @return The commands that have been scheduled but not yet started.
929   */
930  @NoDiscard
931  public Collection<Command> getQueuedCommands() {
932    return m_queuedToRun.stream().map(CommandState::command).toList();
933  }
934
935  /**
936   * For internal use.
937   *
938   * @param command The command to check
939   * @return The command that forked the provided command. Null if the command is not a child of
940   *     another command.
941   */
942  public Command getParentOf(Command command) {
943    var state = m_runningCommands.get(command);
944    if (state == null) {
945      return null;
946    }
947    return state.parent();
948  }
949
950  /**
951   * Gets how long a command took to run in the previous cycle. If the command is not currently
952   * running, returns -1.
953   *
954   * @param command The command to check
955   * @return How long, in milliseconds, the command last took to execute.
956   */
957  public double lastCommandRuntimeMs(Command command) {
958    if (m_runningCommands.containsKey(command)) {
959      return m_runningCommands.get(command).lastRuntimeMs();
960    } else {
961      return -1;
962    }
963  }
964
965  /**
966   * Gets how long a command has taken to run, in aggregate, since it was most recently scheduled.
967   * If the command is not currently running, returns -1.
968   *
969   * @param command The command to check
970   * @return How long, in milliseconds, the command has taken to execute in total
971   */
972  public double totalRuntimeMs(Command command) {
973    if (m_runningCommands.containsKey(command)) {
974      return m_runningCommands.get(command).totalRuntimeMs();
975    } else {
976      // Not running; no data
977      return -1;
978    }
979  }
980
981  /**
982   * Gets the unique run id for a scheduled or running command. If the command is not currently
983   * scheduled or running, this function returns {@code 0}.
984   *
985   * @param command The command to get the run ID for
986   * @return The run of the command
987   */
988  @NoDiscard
989  @SuppressWarnings("PMD.CompareObjectsWithEquals")
990  public int runId(Command command) {
991    if (m_runningCommands.containsKey(command)) {
992      return m_runningCommands.get(command).id();
993    }
994
995    // Check scheduled commands
996    for (var scheduled : m_queuedToRun) {
997      if (scheduled.command() == command) {
998        return scheduled.id();
999      }
1000    }
1001
1002    return 0;
1003  }
1004
1005  /**
1006   * Gets how long the scheduler took to process its most recent {@link #run()} invocation, in
1007   * milliseconds. Defaults to -1 if the scheduler has not yet run.
1008   *
1009   * @return How long, in milliseconds, the scheduler last took to execute.
1010   */
1011  @NoDiscard
1012  public double lastRuntimeMs() {
1013    return m_lastRunTimeMs;
1014  }
1015
1016  // Event-base telemetry and helpers. The static factories are for convenience to automatically
1017  // set the timestamp instead of littering RobotController.getTime() everywhere.
1018
1019  private void emitScheduledEvent(Command command) {
1020    var event = new SchedulerEvent.Scheduled(command, RobotController.getTime());
1021    emitEvent(event);
1022  }
1023
1024  private void emitMountedEvent(Command command) {
1025    var event = new SchedulerEvent.Mounted(command, RobotController.getTime());
1026    emitEvent(event);
1027  }
1028
1029  private void emitYieldedEvent(Command command) {
1030    var event = new SchedulerEvent.Yielded(command, RobotController.getTime());
1031    emitEvent(event);
1032  }
1033
1034  private void emitCompletedEvent(Command command) {
1035    var event = new SchedulerEvent.Completed(command, RobotController.getTime());
1036    emitEvent(event);
1037  }
1038
1039  private void emitCompletedWithErrorEvent(Command command, Throwable error) {
1040    var event = new SchedulerEvent.CompletedWithError(command, error, RobotController.getTime());
1041    emitEvent(event);
1042  }
1043
1044  private void emitCanceledEvent(Command command) {
1045    var event = new SchedulerEvent.Canceled(command, RobotController.getTime());
1046    emitEvent(event);
1047  }
1048
1049  private void emitInterruptedEvent(Command command, Command interrupter) {
1050    var event = new SchedulerEvent.Interrupted(command, interrupter, RobotController.getTime());
1051    emitEvent(event);
1052  }
1053
1054  /**
1055   * Adds a listener to handle events that are emitted by the scheduler. Events are emitted when
1056   * certain actions are taken by user code or by internal processing logic in the scheduler.
1057   * Listeners should take care to be quick, simple, and not schedule or cancel commands, as that
1058   * may cause inconsistent scheduler behavior or even cause a program crash.
1059   *
1060   * <p>Listeners are primarily expected to be for data logging and telemetry. In particular, a
1061   * one-shot command (one that never calls {@link Coroutine#yield()}) will never appear in the
1062   * standard protobuf telemetry because it is scheduled, runs, and finishes all in a single
1063   * scheduler cycle. However, {@link SchedulerEvent.Scheduled},{@link SchedulerEvent.Mounted}, and
1064   * {@link SchedulerEvent.Completed} events will be emitted corresponding to those actions, and
1065   * user code can listen for and log such events.
1066   *
1067   * @param listener The listener to add. Cannot be null.
1068   * @throws NullPointerException if given a null listener
1069   */
1070  public void addEventListener(Consumer<? super SchedulerEvent> listener) {
1071    ErrorMessages.requireNonNullParam(listener, "listener", "addEventListener");
1072
1073    m_eventListeners.add(listener);
1074  }
1075
1076  private void emitEvent(SchedulerEvent event) {
1077    // TODO: Prevent listeners from interacting with the scheduler.
1078    //       Scheduling or canceling commands while the scheduler is processing will probably cause
1079    //       bugs in user code or even a program crash.
1080    for (var listener : m_eventListeners) {
1081      listener.accept(event);
1082    }
1083  }
1084}