// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. package org.wpilib.commands3; import static edu.wpi.first.units.Units.Microseconds; import static edu.wpi.first.units.Units.Milliseconds; import edu.wpi.first.util.ErrorMessages; import edu.wpi.first.util.protobuf.ProtobufSerializable; import edu.wpi.first.wpilibj.RobotController; import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.event.EventLoop; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.SequencedMap; import java.util.SequencedSet; import java.util.Set; import java.util.Stack; import java.util.function.Consumer; import java.util.stream.Collectors; import org.wpilib.annotation.NoDiscard; import org.wpilib.commands3.button.CommandGenericHID; import org.wpilib.commands3.proto.SchedulerProto; /** * Manages the lifecycles of {@link Coroutine}-based {@link Command Commands}. Commands may be * scheduled directly using {@link #schedule(Command)}, or be bound to {@link Trigger Triggers} to * automatically handle scheduling and cancellation based on internal or external events. User code * is responsible for calling {@link #run()} periodically to update trigger conditions and execute * scheduled commands. Most often, this is done by overriding {@link TimedRobot#robotPeriodic()} to * include a call to {@code Scheduler.getDefault().run()}: * *
{@code
 * public class Robot extends TimedRobot {
 *   @Override
 *   public void robotPeriodic() {
 *     // Update the scheduler on every loop
 *     Scheduler.getDefault().run();
 *   }
 * }
 * }
* *

Danger

* *

The scheduler must be used in a single-threaded program. Commands must be scheduled and * canceled by the same thread that runs the scheduler, and cannot be run in a virtual thread. * *

Using the commands framework in a multithreaded environment can cause crashes in the * Java virtual machine at any time, including on an official field during a match. The * Java JIT compilers make assumptions that rely on coroutines being used on a single thread. * Breaking those assumptions can cause incorrect JIT code to be generated with undefined behavior, * potentially causing control issues or crashes deep in JIT-generated native code. * *

Normal concurrency constructs like locks, atomic references, and synchronized blocks * or methods cannot save you. * *

Lifecycle

* *

The {@link #run()} method runs five steps: * *

    *
  1. Call {@link #sideload(Consumer) periodic sideload functions} *
  2. Poll all registered triggers to queue and cancel commands *
  3. Queue default commands for any mechanisms without a running command. The queued commands * can be superseded by any manual scheduling or commands scheduled by triggers in the next * run. *
  4. Start all queued commands. This happens after all triggers are checked in case multiple * commands with conflicting requirements are queued in the same update; the last command to * be queued takes precedence over the rest. *
  5. Loop over all running commands, mounting and calling each in turn until they either exit or * call {@link Coroutine#yield()}. Commands run in the order in which they were scheduled. *
* *

Telemetry

* *

There are two mechanisms for telemetry for a scheduler. A protobuf serializer can be used to * take a snapshot of a scheduler instance, and report what commands are queued (scheduled but have * not yet started to run), commands that are running (along with timing data for each command), and * total time spent in the most recent {@link #run()} call. However, it cannot detect one-shot * commands that are scheduled, run, and complete all in a single {@code run()} invocation - * effectively, commands that never call {@link Coroutine#yield()} are invisible. * *

A second telemetry mechanism is provided by {@link #addEventListener(Consumer)}. The scheduler * will issue events to all registered listeners when certain events occur (see {@link * SchedulerEvent} for all event types). These events are emitted immediately and can be used to * detect lifecycle events for all commands, including one-shots that would be invisible to the * protobuf serializer. However, it is up to the user to log those events themselves. */ public final class Scheduler implements ProtobufSerializable { private final Map m_defaultCommands = new LinkedHashMap<>(); /** The set of commands scheduled since the start of the previous run. */ private final SequencedSet m_queuedToRun = new LinkedHashSet<>(); /** * The states of all running commands (does not include on deck commands). We preserve insertion * order to guarantee that child commands run after their parents. */ private final SequencedMap m_runningCommands = new LinkedHashMap<>(); /** * The stack of currently executing commands. Child commands are pushed onto the stack and popped * when they complete. Use {@link #currentState()} and {@link #currentCommand()} to get the * currently executing command or its state. */ private final Stack m_currentCommandAncestry = new Stack<>(); /** The periodic callbacks to run, outside of the command structure. */ private final List m_periodicCallbacks = new ArrayList<>(); /** Event loop for trigger bindings. */ private final EventLoop m_eventLoop = new EventLoop(); /** The scope for continuations to yield to. */ private final ContinuationScope m_scope = new ContinuationScope("coroutine commands"); // Telemetry /** Protobuf serializer for a scheduler. */ public static final SchedulerProto proto = new SchedulerProto(); private double m_lastRunTimeMs = -1; private final Set> m_eventListeners = new LinkedHashSet<>(); /** The default scheduler instance. */ private static final Scheduler s_defaultScheduler = new Scheduler(); /** * Gets the default scheduler instance for use in a robot program. Unless otherwise specified, * triggers and mechanisms will be registered with the default scheduler and require the default * scheduler to run. However, triggers and mechanisms can be constructed to be registered with a * specific scheduler instance, which may be useful for isolation for unit tests. * * @return the default scheduler instance. */ @NoDiscard public static Scheduler getDefault() { return s_defaultScheduler; } /** * Creates a new scheduler object. Note that most built-in constructs like {@link Trigger} and * {@link CommandGenericHID} will use the {@link #getDefault() default scheduler instance} unless * they were explicitly constructed with a different scheduler instance. Teams should use the * default instance for convenience; however, new scheduler instances can be useful for unit * tests. * * @return a new scheduler instance that is independent of the default scheduler instance. */ @NoDiscard public static Scheduler createIndependentScheduler() { return new Scheduler(); } /** Private constructor. Use static factory methods or the default scheduler instance. */ private Scheduler() {} /** * Sets the default command for a mechanism. The command must require that mechanism, and cannot * require any other mechanisms. * * @param mechanism the mechanism for which to set the default command * @param defaultCommand the default command to execute on the mechanism * @throws IllegalArgumentException if the command does not meet the requirements for being a * default command */ public void setDefaultCommand(Mechanism mechanism, Command defaultCommand) { if (!defaultCommand.requires(mechanism)) { throw new IllegalArgumentException( "A mechanism's default command must require that mechanism"); } if (defaultCommand.requirements().size() > 1) { throw new IllegalArgumentException( "A mechanism's default command cannot require other mechanisms"); } m_defaultCommands.put(mechanism, defaultCommand); } /** * Gets the default command set for a mechanism. * * @param mechanism The mechanism * @return The default command, or null if no default command was ever set */ public Command getDefaultCommandFor(Mechanism mechanism) { return m_defaultCommands.get(mechanism); } /** * Adds a callback to run as part of the scheduler. The callback should not manipulate or control * any mechanisms, but can be used to log information, update data (such as simulations or LED * data buffers), or perform some other helpful task. The callback is responsible for managing its * own control flow and end conditions. If you want to run a single task periodically for the * entire lifespan of the scheduler, use {@link #addPeriodic(Runnable)}. * *

Note: Like commands, any loops in the callback must appropriately yield * control back to the scheduler with {@link Coroutine#yield} or risk stalling your program in an * unrecoverable infinite loop! * * @param callback the callback to sideload */ public void sideload(Consumer callback) { var coroutine = new Coroutine(this, m_scope, callback); m_periodicCallbacks.add(coroutine); } /** * Adds a task to run repeatedly for as long as the scheduler runs. This internally handles the * looping and control yielding necessary for proper function. The callback will run at the same * periodic frequency as the scheduler. * *

For example: * *

{@code
   * scheduler.addPeriodic(() -> leds.setData(ledDataBuffer));
   * scheduler.addPeriodic(() -> {
   *   SmartDashboard.putNumber("X", getX());
   *   SmartDashboard.putNumber("Y", getY());
   * });
   * }
* * @param callback the periodic function to run */ public void addPeriodic(Runnable callback) { sideload( coroutine -> { while (true) { callback.run(); coroutine.yield(); } }); } /** Represents possible results of a command scheduling attempt. */ public enum ScheduleResult { /** The command was successfully scheduled and added to the queue. */ SUCCESS, /** The command is already scheduled or running. */ ALREADY_RUNNING, /** The command is a lower priority and conflicts with a command that's already running. */ LOWER_PRIORITY_THAN_RUNNING_COMMAND, } /** * Schedules a command to run. If one command schedules another (a "parent" and "child"), the * child command will be canceled when the parent command completes. It is not possible to fork a * child command and have it live longer than its parent. * *

Does nothing if the command is already scheduled or running, or requires at least one * mechanism already used by a higher priority command. * * @param command the command to schedule * @return the result of the scheduling attempt. See {@link ScheduleResult} for details. * @throws IllegalArgumentException if scheduled by a command composition that has already * scheduled another command that shares at least one required mechanism */ // Implementation detail: a child command will immediately start running when scheduled by a // parent command, skipping the queue entirely. This avoids dead loop cycles where a parent // schedules a child, appending it to the queue, then waits for the next cycle to pick it up and // start it. With deeply nested commands, dead loops could quickly to add up and cause the // innermost commands that actually _do_ something to start running hundreds of milliseconds after // their root ancestor was scheduled. public ScheduleResult schedule(Command command) { // Note: we use a throwable here instead of Thread.currentThread().getStackTrace() for easier // stack frame filtering and modification. var binding = new Binding( BindingScope.global(), BindingType.IMMEDIATE, command, new Throwable().getStackTrace()); return schedule(binding); } // Package-private for use by Trigger ScheduleResult schedule(Binding binding) { var command = binding.command(); if (isScheduledOrRunning(command)) { return ScheduleResult.ALREADY_RUNNING; } if (lowerPriorityThanConflictingCommands(command)) { return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND; } for (var scheduledState : m_queuedToRun) { if (!command.conflictsWith(scheduledState.command())) { // No shared requirements, skip continue; } if (command.isLowerPriorityThan(scheduledState.command())) { // Lower priority than an already-scheduled (but not yet running) command that requires at // one of the same mechanism. Ignore it. return ScheduleResult.LOWER_PRIORITY_THAN_RUNNING_COMMAND; } } // Evict conflicting on-deck commands // We check above if the input command is lower priority than any of these, // so at this point we're guaranteed to be >= priority than anything already on deck evictConflictingOnDeckCommands(command); // If the binding is scoped to a particular command, that command is the parent. If we're in the // middle of a run cycle and running commands, the parent is whatever command is currently // running. Otherwise, there is no parent command. var parentCommand = binding.scope() instanceof BindingScope.ForCommand scope ? scope.command() : currentCommand(); var state = new CommandState(command, parentCommand, buildCoroutine(command), binding); emitScheduledEvent(command); if (currentState() != null) { // Scheduling a child command while running. Start it immediately instead of waiting a full // cycle. This prevents issues with deeply nested command groups taking many scheduler cycles // to start running the commands that actually _do_ things evictConflictingRunningCommands(state); m_runningCommands.put(command, state); runCommand(state); } else { // Scheduling outside a command, add it to the pending set. If it's not overridden by another // conflicting command being scheduled in the same scheduler loop, it'll be promoted and // start to run when #runCommands() is called m_queuedToRun.add(state); } return ScheduleResult.SUCCESS; } /** * Checks if a command conflicts with and is a lower priority than any running command. Used when * determining if the command can be scheduled. */ private boolean lowerPriorityThanConflictingCommands(Command command) { Set ancestors = new HashSet<>(); for (var state = currentState(); state != null; state = m_runningCommands.get(state.parent())) { ancestors.add(state); } // Check for conflicts with the commands that are already running for (var state : m_runningCommands.values()) { if (ancestors.contains(state)) { // Can't conflict with an ancestor command continue; } var c = state.command(); if (c.conflictsWith(command) && command.isLowerPriorityThan(c)) { return true; } } return false; } private void evictConflictingOnDeckCommands(Command command) { for (var iterator = m_queuedToRun.iterator(); iterator.hasNext(); ) { var scheduledState = iterator.next(); var scheduledCommand = scheduledState.command(); if (scheduledCommand.conflictsWith(command)) { // Remove the lower priority conflicting command from the on deck commands. // We don't need to call removeOrphanedChildren here because it hasn't started yet, // meaning it hasn't had a chance to schedule any children iterator.remove(); emitInterruptedEvent(scheduledCommand, command); emitCanceledEvent(scheduledCommand); } } } /** * Cancels all running commands with which an incoming state conflicts. Ancestor commands of the * incoming state will not be canceled. */ @SuppressWarnings("PMD.CompareObjectsWithEquals") private void evictConflictingRunningCommands(CommandState incomingState) { // The set of root states with which the incoming state conflicts but does not inherit from Set conflictingRootStates = m_runningCommands.values().stream() .filter(state -> incomingState.command().conflictsWith(state.command())) .filter(state -> !isAncestorOf(state.command(), incomingState)) .map( state -> { // Find the highest level ancestor of the conflicting command from which the // incoming state does _not_ inherit. If they're totally unrelated, this will // get the very root ancestor; otherwise, it'll return a direct sibling of the // incoming command CommandState root = state; while (root.parent() != null && root.parent() != incomingState.parent()) { root = m_runningCommands.get(root.parent()); } return root; }) .collect(Collectors.toSet()); // Cancel the root commands for (var conflictingState : conflictingRootStates) { emitInterruptedEvent(conflictingState.command(), incomingState.command()); cancel(conflictingState.command()); } } /** * Checks if a particular command is an ancestor of another. * * @param ancestor the potential ancestor for which to search * @param state the state to check * @return true if {@code ancestor} is the direct parent or indirect ancestor, false if not */ @SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.SimplifyBooleanReturns"}) private boolean isAncestorOf(Command ancestor, CommandState state) { if (state.parent() == null) { // No parent, cannot inherit return false; } if (!m_runningCommands.containsKey(ancestor)) { // The given ancestor isn't running return false; } if (state.parent() == ancestor) { // Direct child return true; } // Check if the command's parent inherits from the given ancestor return m_runningCommands.values().stream() .filter(s -> state.parent() == s.command()) .anyMatch(s -> isAncestorOf(ancestor, s)); } /** * Cancels a command and any other command scheduled by it. This occurs immediately and does not * need to wait for a call to {@link #run()}. Any command that it scheduled will also be canceled * to ensure commands within compositions do not continue to run. * *

This has no effect if the given command is not currently scheduled or running. * * @param command the command to cancel */ @SuppressWarnings("PMD.CompareObjectsWithEquals") public void cancel(Command command) { if (command == currentCommand()) { throw new IllegalArgumentException( "Command `" + command.name() + "` is mounted and cannot be canceled"); } boolean running = isRunning(command); // Evict the command. The next call to run() will schedule the default command for all its // required mechanisms, unless another command requiring those mechanisms is scheduled between // calling cancel() and calling run() m_runningCommands.remove(command); m_queuedToRun.removeIf(state -> state.command() == command); if (running) { // Only run the hook if the command was running. If it was on deck or not // even in the scheduler at the time, then there's nothing to do command.onCancel(); emitCanceledEvent(command); } // Clean up any orphaned child commands; their lifespan must not exceed the parent's removeOrphanedChildren(command); } /** * Updates the command scheduler. This will run operations in the following order: * *

    *
  1. Run sideloaded functions from {@link #sideload(Consumer)} and {@link * #addPeriodic(Runnable)} *
  2. Update trigger bindings to queue and cancel bound commands *
  3. Queue default commands for mechanisms that do not have a queued or running command *
  4. Promote queued commands to the running set *
  5. For every command in the running set, mount and run that command until it calls {@link * Coroutine#yield()} or exits *
* *

This method is intended to be called in a periodic loop like {@link * TimedRobot#robotPeriodic()} */ public void run() { final long startMicros = RobotController.getTime(); // Sideloads may change some state that affects triggers. Run them first. runPeriodicSideloads(); // Poll triggers next to schedule and cancel commands m_eventLoop.poll(); // Schedule default commands for any mechanisms that don't have a running command and didn't // have a new command scheduled by a sideload function or a trigger scheduleDefaultCommands(); // Move all scheduled commands to the running set promoteScheduledCommands(); // Run every command in order until they call Coroutine.yield() or exit runCommands(); final long endMicros = RobotController.getTime(); m_lastRunTimeMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds); } private void promoteScheduledCommands() { // Clear any commands that conflict with the scheduled set for (var queuedState : m_queuedToRun) { evictConflictingRunningCommands(queuedState); } // Move any scheduled commands to the running set for (var queuedState : m_queuedToRun) { m_runningCommands.put(queuedState.command(), queuedState); } // Clear the set of on-deck commands, // since we just put them all into the set of running commands m_queuedToRun.clear(); } private void runPeriodicSideloads() { // Update periodic callbacks for (Coroutine coroutine : m_periodicCallbacks) { coroutine.mount(); try { coroutine.runToYieldPoint(); } finally { Continuation.mountContinuation(null); } } // And remove any periodic callbacks that have completed m_periodicCallbacks.removeIf(Coroutine::isDone); } private void runCommands() { // Tick every command that hasn't been completed yet // Run in reverse so parent commands can resume in the same loop cycle an awaited child command // completes. Otherwise, parents could only resume on the next loop cycle, introducing a delay // at every layer of nesting. for (var state : List.copyOf(m_runningCommands.values()).reversed()) { runCommand(state); } } /** * Mounts and runs a command until it yields or exits. * * @param state The command state to run */ @SuppressWarnings("PMD.AvoidCatchingGenericException") private void runCommand(CommandState state) { final var command = state.command(); final var coroutine = state.coroutine(); if (!m_runningCommands.containsKey(command)) { // Probably canceled by an owning composition, do not run return; } var previousState = currentState(); m_currentCommandAncestry.push(state); long startMicros = RobotController.getTime(); emitMountedEvent(command); coroutine.mount(); try { coroutine.runToYieldPoint(); } catch (RuntimeException e) { // Command encountered an uncaught exception. handleCommandException(state, e); } finally { long endMicros = RobotController.getTime(); double elapsedMs = Milliseconds.convertFrom(endMicros - startMicros, Microseconds); state.setLastRuntimeMs(elapsedMs); if (state.equals(currentState())) { // Remove the command we just ran from the top of the stack m_currentCommandAncestry.pop(); } if (previousState != null) { // Remount the parent command, if there is one previousState.coroutine().mount(); } else { Continuation.mountContinuation(null); } } if (coroutine.isDone()) { // Immediately check if the command has completed and remove any children commands. // This prevents child commands from being executed one extra time in the run() loop emitCompletedEvent(command); m_runningCommands.remove(command); removeOrphanedChildren(command); } else { // Yielded emitYieldedEvent(command); } } /** * Handles uncaught runtime exceptions from a mounted and running command. The command's ancestor * and child commands will all be canceled and the exception's backtrace will be modified to * include the stack frames of the schedule call site. * * @param state The state of the command that encountered the exception. * @param e The exception that was thrown. * @throws RuntimeException rethrows the exception, with a modified backtrace pointing to the * schedule site */ @SuppressWarnings("PMD.CompareObjectsWithEquals") private void handleCommandException(CommandState state, RuntimeException e) { var command = state.command(); // Fetch the root command // (needs to be done before removing the failed command from the running set) Command root = command; while (getParentOf(root) != null) { root = getParentOf(root); } // Remove it from the running set. m_runningCommands.remove(command); // Intercept the exception, inject stack frames from the schedule site, and rethrow it var binding = state.binding(); e.setStackTrace(CommandTraceHelper.modifyTrace(e.getStackTrace(), binding.frames())); emitCompletedWithErrorEvent(command, e); // Clean up child commands after emitting the event so child Canceled events are emitted // after the parent's CompletedWithError removeOrphanedChildren(command); // Bubble up to the root and cancel all commands between the root and this one // Note: Because we remove the command from the running set above, we still need to // clean up all the failed command's children if (root != null && root != command) { cancel(root); } // Then rethrow the exception throw e; } /** * Gets the currently executing command state, or null if no command is currently executing. * * @return the currently executing command state */ private CommandState currentState() { if (m_currentCommandAncestry.isEmpty()) { // Avoid EmptyStackException return null; } return m_currentCommandAncestry.peek(); } /** * Gets the currently executing command, or null if no command is currently executing. * * @return the currently executing command */ public Command currentCommand() { var state = currentState(); if (state == null) { return null; } return state.command(); } private void scheduleDefaultCommands() { // Schedule the default commands for every mechanism that doesn't currently have a running or // scheduled command. m_defaultCommands.forEach( (mechanism, defaultCommand) -> { if (m_runningCommands.keySet().stream().noneMatch(c -> c.requires(mechanism)) && m_queuedToRun.stream().noneMatch(c -> c.command().requires(mechanism)) && defaultCommand != null) { // Nothing currently running or scheduled // Schedule the mechanism's default command, if it has one schedule(defaultCommand); } }); } /** * Removes all commands descended from a parent command. This is used to ensure that any command * scheduled within a composition or group cannot live longer than any ancestor. * * @param parent the root command whose descendants to remove from the scheduler */ @SuppressWarnings("PMD.CompareObjectsWithEquals") private void removeOrphanedChildren(Command parent) { m_runningCommands.entrySet().stream() .filter(e -> e.getValue().parent() == parent) .toList() // copy to an intermediate list to avoid concurrent modification .forEach(e -> cancel(e.getKey())); } /** * Builds a coroutine object that the command will be bound to. The coroutine will be scoped to * this scheduler object and cannot be used by another scheduler instance. * * @param command the command for which to build a coroutine * @return the binding coroutine */ private Coroutine buildCoroutine(Command command) { return new Coroutine(this, m_scope, command::run); } /** * Checks if a command is currently running. * * @param command the command to check * @return true if the command is running, false if not */ public boolean isRunning(Command command) { return m_runningCommands.containsKey(command); } /** * Checks if a command is currently scheduled to run, but is not yet running. * * @param command the command to check * @return true if the command is scheduled to run, false if not */ @SuppressWarnings("PMD.CompareObjectsWithEquals") public boolean isScheduled(Command command) { return m_queuedToRun.stream().anyMatch(state -> state.command() == command); } /** * Checks if a command is currently scheduled to run, or is already running. * * @param command the command to check * @return true if the command is scheduled to run or is already running, false if not */ public boolean isScheduledOrRunning(Command command) { return isScheduled(command) || isRunning(command); } /** * Gets the set of all currently running commands. Commands are returned in the order in which * they were scheduled. The returned set is read-only. * * @return the currently running commands */ public Collection getRunningCommands() { return Collections.unmodifiableSet(m_runningCommands.keySet()); } /** * Gets all the currently running commands that require a particular mechanism. Commands are * returned in the order in which they were scheduled. The returned list is read-only. * * @param mechanism the mechanism to get the commands for * @return the currently running commands that require the mechanism. */ public List getRunningCommandsFor(Mechanism mechanism) { return m_runningCommands.keySet().stream() .filter(command -> command.requires(mechanism)) .toList(); } /** * Cancels all currently running and scheduled commands. All default commands will be scheduled on * the next call to {@link #run()}, unless a higher priority command is scheduled or triggered * after {@code cancelAll()} is used. */ public void cancelAll() { for (var onDeckIter = m_queuedToRun.iterator(); onDeckIter.hasNext(); ) { var state = onDeckIter.next(); onDeckIter.remove(); emitCanceledEvent(state.command()); } for (var liveIter = m_runningCommands.entrySet().iterator(); liveIter.hasNext(); ) { var entry = liveIter.next(); liveIter.remove(); Command canceledCommand = entry.getKey(); canceledCommand.onCancel(); emitCanceledEvent(canceledCommand); } } /** * An event loop used by the scheduler to poll triggers that schedule or cancel commands. This * event loop is always polled on every call to {@link #run()}. Custom event loops need to be * bound to this one for synchronicity with the scheduler; however, they can always be polled * manually before or after the call to {@link #run()}. * * @return the default event loop. */ @NoDiscard public EventLoop getDefaultEventLoop() { return m_eventLoop; } /** * For internal use. * * @return The commands that have been scheduled but not yet started. */ @NoDiscard public Collection getQueuedCommands() { return m_queuedToRun.stream().map(CommandState::command).toList(); } /** * For internal use. * * @param command The command to check * @return The command that forked the provided command. Null if the command is not a child of * another command. */ public Command getParentOf(Command command) { var state = m_runningCommands.get(command); if (state == null) { return null; } return state.parent(); } /** * Gets how long a command took to run in the previous cycle. If the command is not currently * running, returns -1. * * @param command The command to check * @return How long, in milliseconds, the command last took to execute. */ public double lastCommandRuntimeMs(Command command) { if (m_runningCommands.containsKey(command)) { return m_runningCommands.get(command).lastRuntimeMs(); } else { return -1; } } /** * Gets how long a command has taken to run, in aggregate, since it was most recently scheduled. * If the command is not currently running, returns -1. * * @param command The command to check * @return How long, in milliseconds, the command has taken to execute in total */ public double totalRuntimeMs(Command command) { if (m_runningCommands.containsKey(command)) { return m_runningCommands.get(command).totalRuntimeMs(); } else { // Not running; no data return -1; } } /** * Gets the unique run id for a scheduled or running command. If the command is not currently * scheduled or running, this function returns {@code 0}. * * @param command The command to get the run ID for * @return The run of the command */ @NoDiscard @SuppressWarnings("PMD.CompareObjectsWithEquals") public int runId(Command command) { if (m_runningCommands.containsKey(command)) { return m_runningCommands.get(command).id(); } // Check scheduled commands for (var scheduled : m_queuedToRun) { if (scheduled.command() == command) { return scheduled.id(); } } return 0; } /** * Gets how long the scheduler took to process its most recent {@link #run()} invocation, in * milliseconds. Defaults to -1 if the scheduler has not yet run. * * @return How long, in milliseconds, the scheduler last took to execute. */ @NoDiscard public double lastRuntimeMs() { return m_lastRunTimeMs; } // Event-base telemetry and helpers. The static factories are for convenience to automatically // set the timestamp instead of littering RobotController.getTime() everywhere. private void emitScheduledEvent(Command command) { var event = new SchedulerEvent.Scheduled(command, RobotController.getTime()); emitEvent(event); } private void emitMountedEvent(Command command) { var event = new SchedulerEvent.Mounted(command, RobotController.getTime()); emitEvent(event); } private void emitYieldedEvent(Command command) { var event = new SchedulerEvent.Yielded(command, RobotController.getTime()); emitEvent(event); } private void emitCompletedEvent(Command command) { var event = new SchedulerEvent.Completed(command, RobotController.getTime()); emitEvent(event); } private void emitCompletedWithErrorEvent(Command command, Throwable error) { var event = new SchedulerEvent.CompletedWithError(command, error, RobotController.getTime()); emitEvent(event); } private void emitCanceledEvent(Command command) { var event = new SchedulerEvent.Canceled(command, RobotController.getTime()); emitEvent(event); } private void emitInterruptedEvent(Command command, Command interrupter) { var event = new SchedulerEvent.Interrupted(command, interrupter, RobotController.getTime()); emitEvent(event); } /** * Adds a listener to handle events that are emitted by the scheduler. Events are emitted when * certain actions are taken by user code or by internal processing logic in the scheduler. * Listeners should take care to be quick, simple, and not schedule or cancel commands, as that * may cause inconsistent scheduler behavior or even cause a program crash. * *

Listeners are primarily expected to be for data logging and telemetry. In particular, a * one-shot command (one that never calls {@link Coroutine#yield()}) will never appear in the * standard protobuf telemetry because it is scheduled, runs, and finishes all in a single * scheduler cycle. However, {@link SchedulerEvent.Scheduled},{@link SchedulerEvent.Mounted}, and * {@link SchedulerEvent.Completed} events will be emitted corresponding to those actions, and * user code can listen for and log such events. * * @param listener The listener to add. Cannot be null. * @throws NullPointerException if given a null listener */ public void addEventListener(Consumer listener) { ErrorMessages.requireNonNullParam(listener, "listener", "addEventListener"); m_eventListeners.add(listener); } private void emitEvent(SchedulerEvent event) { // TODO: Prevent listeners from interacting with the scheduler. // Scheduling or canceling commands while the scheduler is processing will probably cause // bugs in user code or even a program crash. for (var listener : m_eventListeners) { listener.accept(event); } } }