allwpilib icon indicating copy to clipboard operation
allwpilib copied to clipboard

[🛠️] Change Command Scheduler to fix iterator invalidation bugs and get rid of "hacks" in C++ and Java

Open kytpbs opened this issue 1 year ago • 22 comments

scheduledCommands.erase() was being called in for(Command* command : scheduledCommands)

this now gets put into a temporary vector (at the size of the set, I do not think all the commands will end in the same for loop but nevertheless it's the size of the set) and erases them after the for loop is done.

Found together with @PeterJohnson.

kytpbs avatar May 07 '24 00:05 kytpbs

Wasn't the iteration being done via an iterator (and then the removal also being done through the iterator)?

Sort of. It used a range for, which has an internal iterator. That iterator is invalidated by SmallSet when the current object is removed, which means the for loop next iteration is incrementing from an invalid iterator (UB). Even manually managing the iterators doesn't work with SmallSet, because it can use a vector implementation, which invalidates all iterators, not just the current one. So the only other possible fix would be to use std::set AND manually manage the iterators instead of using range-for.

Java doesn't have this issue--iterator.remove() updates itself in such a way that hasNext() on it is safe for the next loop iteration.

PeterJohnson avatar May 07 '24 04:05 PeterJohnson

SmallSet doesn't have an Iterator.remove like Java?

Starlight220 avatar May 07 '24 05:05 Starlight220

No. Neither does std::set.

PeterJohnson avatar May 07 '24 05:05 PeterJohnson

/format

kytpbs avatar May 07 '24 12:05 kytpbs

The way robot.py did it seems like a better way to mitigate this issue, and SmallSet<Command*>(scheduledCommands) should create a copy of the smallSet, and I don't think the smallSet will be big enough that this will have a performance impact.

What do you guys think?

kytpbs avatar May 16 '24 02:05 kytpbs

I agree that copying scheduledCommands would be the cleaner solution. Some things to remember for whoever implements that:

  • Make the same change to Java (I think iterating over m_scheduledCommands.toArray(new Command[0]) would be the most performant way to copy the set)
  • Add a check to the start of the run loop to skip commands that are no longer scheduled
  • Remove the inRunLoop, toSchedule, toCancelCommands, and toCancelInterruptors member variables

KangarooKoala avatar May 16 '24 16:05 KangarooKoala

Whatever behavior is being chosen, if it's going to being a part of the "spec", there should be tests that fail currently and pass afterwards added.

TheTripleV avatar May 16 '24 17:05 TheTripleV

/format

kytpbs avatar May 21 '24 22:05 kytpbs

/format

kytpbs avatar May 22 '24 00:05 kytpbs

Set.copyOf is memory allocation heavy; this is what the implementation is:

return (Set<E>)Set.of(new HashSet<>(coll).toArray());

So it first creates a HashSet, then creates an array of its elements, then creates a Set from that array.

For how we're using it here, toArray() would be more efficient. As a further optimization we could even avoid allocations most of the time by smartly reusing the array.

PeterJohnson avatar May 23 '24 14:05 PeterJohnson

Note that toArray(T[]) will automatically try to reuse the passed array, so a simple way to reduce allocations would be to update the array with m_composedCommandsCopy = m_composedCommands.toArray(m_composedCommandsCopy). (There's some options we could explore such as the initial array size and growing the array by more than is initially required, but I don't know if it would necessarily be worth it)

KangarooKoala avatar May 23 '24 19:05 KangarooKoala

Note that toArray(T[]) will automatically try to reuse the passed array, so a simple way to reduce allocations would be to update the array with m_composedCommandsCopy = m_composedCommands.toArray(m_composedCommandsCopy). (There's some options we could explore such as the initial array size and growing the array by more than is initially required, but I don't know if it would necessarily be worth it)

Note also toArray() will null-fill the end elements, not shrink the array, so you will need to look for the first null and exit the loop early if doing this.

PeterJohnson avatar May 24 '24 17:05 PeterJohnson

synced with main with rebase.

Will write new tests and better optimize currently working on it

kytpbs avatar Oct 10 '24 23:10 kytpbs

fix formatting with rebase + force-push: (I forget to run wpiformat all the time...)

kytpbs avatar Oct 11 '24 14:10 kytpbs

Update the checking for null and isScheduled as discussed in discord with a amend + force-push:

kytpbs avatar Oct 11 '24 17:10 kytpbs

oops did command.isScheduled() instead of isScheduled(command) what a rookie mistake, fixing with force push now

kytpbs avatar Oct 11 '24 18:10 kytpbs

I have added 3 different tests testing weird cases of using the scheduler inside the commands. all 3 fail on the main branch and pass on this branch

What other tests should I add? @Starlight220?

kytpbs avatar Oct 12 '24 02:10 kytpbs

@KangarooKoala: Should we move some of these tests to SchedulingRecursionTest?

I say we move test cancelNextCommandTest for sure and also scheduleCommandInCommand since they are both similar to #4259 but keep commandKnowsWhenEndedTest since it feels more like a CommandScheduler test more than a scheduling recursion. Any objections?

kytpbs avatar Oct 12 '24 21:10 kytpbs

Force pushing to rebase with upstream/main...

kytpbs avatar Dec 16 '24 14:12 kytpbs

Oop, left some errors while rebasing, force-pushing again to fix those ASAP

kytpbs avatar Dec 16 '24 14:12 kytpbs

Unintentionally removed ownedCommands, adding it back with rebase now

kytpbs avatar Dec 16 '24 15:12 kytpbs