-
Notifications
You must be signed in to change notification settings - Fork 664
[cmd] Commands v3 framework #6518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[cmd] Commands v3 framework #6518
Conversation
We've discussed putting a command-based rewrite in its own package instead of wpilibjN. For example, |
It's too early to be bikeshedding 😄 My goal here is to inform future command framework changes, not to have a ready-to-go implementation. (Though I do agree that |
Any reason for |
It's clearer about what it does without needing to assume a baseline knowledge of multithreading concepts. And it's apt if async functions/continuations/coroutines (or wherever else you'd like to call them) are taught as pausable functions |
I strongly disagree that "pausable functions" is the term that should be used -- it's not a term that is commonly used and will likely cause confusion. Therefore I maintain my suggestion to name it Command-based historically has a problem of being very DSL-y and magic incantations; this is a good opportunity to take a big step to using common industry terms. Let's take that step. |
What happens if AsyncCommand.pause() is called from outside an AsyncCommand virtual thread? |
With the current impl, it would suspend the entire carrier thread. Throwing could be better. |
This seems like it could also support differing command interruption semantics in addition to priorities. If we have both of those features, we can very naturally represent "default commands" as a minimum-priority command with suspend or reschedule behavior on interruption. |
This is a good argument. I'll note that virtual thread pausing does not behave like
As written, it would just block the calling thread. Yotam's suggestion of throwing would certainly be better, since it makes no sense to pause outside a command.
I like this idea. How would a resume-after-interruption command look in user code? |
I think we should re-purpose the So, it'd look like: run(...).onInterrupt(InterruptBehavior.kSuspend);
run(...).onInterrupt(() -> { ... }, InterruptBehavior.kSuspend); Suspended commands will be re-scheduled as soon as their requirements become available. The implementation that allows this could also allow us to eventually support queued commands. |
Reminder that commands are interrupted by interrupting their vthread, which triggers an Maybe we could have a This is not a complete and correct implementation, but for the sake of example: // AsyncScheduler.java
public void pause(AsyncCommand command, Measure<Time> time) {
long ms = (long) time.in(Milliseconds);
boolean suspendOnInterrupt = command.interruptBehavior() == kSuspend;
do {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
if (suspendOnInterrupt)
continue;
throw e;
}
} while (command.priority() < getHighestPriorityCommandUsingAnyOf(command.requirements()).priority());
} |
This is very similar syntax to my python coroutine command impl. encoders.reset()
while getDistance() < distance:
yield
tankDrive(1, 1)
stop() It does active cooperation with yield. I've been mulling over doing yield based coroutines vs async/await based coroutines throughout wpilib and I think there needs to be more exploring do on which is better long-term (idk if java has async/await keywords incoming but it would be nice if the python/java apis were similar). |
Java isn't getting async/await keywords, the best we have is Futures (more or less analogous to JavaScript promises). That's error prone since it's totally valid to call a method that returns a Future and then not await its result. A yield-based API seems less prone to unintended behavior |
wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj3/command/async/AsyncScheduler.java
Outdated
Show resolved
Hide resolved
3661407
to
d570544
Compare
Virtual threads ended up being too prone to deadlocking and inconsistent timing, since it's at the mercy of the JVM's thread scheduler for when parked vthreads are resumed. I was consistently seeing sleep times of 20ms end up taking ~25ms, with the worst outlier at ~38ms. I've pushed changes using Java's AsyncCommand cmd = someResource.run(() -> {
var timer = new Timer();
timer.start();
while (!timer.hasElapsed(5.000)) {
AsyncCommand.yield();
doSomething();
}
}).named("Demo"); One potential issue here is that there's no way to run cleanup logic on command interrupts. Continuations will just be kicked out of the scheduler if their command is canceled. |
19198c4
to
1d70f6d
Compare
1b5aa09
to
c5dc29b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I'm not sure about the protobuf/codegen/cmake changes, so if someone else can review that it'd be great.
- Suspend/resume needs some more discussion. Currently it's only in the design doc, so no need to block this PR on that. Removing it from the doc would be good, but not a PR blocker imo.
- This comment: #6518 (comment)
Other than those three points, looks good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone with more experience with protobuf/cmake/codegen etc can look over these areas of this PR, that'd be great.
Other than that, this PR is good to be merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Root CMakeLists.txt needs to have commandsv3 added as a subdirectory under the WITH_WPILIB check. Codegen looks good, if duplicative, and protobuf looks mostly good, although I would suggest having a static proto field for Mechanism. I'll also note our implementation of Protobuf clears the message beforehand, which likely makes the clears that we do here unnecessary.
This is a deliberate choice. |
Command makes sense to not have a static proto field, but Mechanism just has a string, independent of the scheduler, so I figured it might be a better fit. I won't push too hard in either direction though. It saves, what, a handful of allocations per loop cycle? |
616f3c0
to
c269e52
Compare
Yeah, that's negligible and I don't want people to try serializing bare mechanisms, especially if we expand the serialization to include scheduler-specific data such as default commands (which are scheduler-specific) |
99607e4
to
c965607
Compare
c965607
to
4a89bf1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing, otherwise, LGTM!
The framework fundamentally relies on the continuation API added in Java 21 (which is currently internal to the JDK). Continuations allow for call stacks to be saved to the heap and resumed later
The async framework allows command bodies to be written in an imperative style:
Which would look something like this using the current functional framework:
Commands could be written as thin wrappers around regular imperative methods:
However, an async command will need to be actively cooperative and periodically call
coroutine.yield()
in loops to yield control back to the command scheduler to let it process other commands.There are also some other additions like priority levels (as opposed to a blanket yes/no for ignoring incoming commands), factories requiring names be provided for commands, and the scheduler tracking all running commands and not just the highest-level groups. However, those changes aren't unique to an async framework, and could just as easily be used in a traditional command framework.
Links:
JEP 425 - Virtual Threads
Scheduler.java
ParallelGroup.java
Sequence.java