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}