Skip to content
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

AI Attack & getSpellAbilityToPlay Timeout #6577

Open
wants to merge 45 commits into
base: master
Choose a base branch
from

Conversation

kevlahnota
Copy link
Contributor

No description provided.

@kevlahnota kevlahnota changed the title AI Attack Timeout AI Attack & getSpellAbilityToPlay Timeout Nov 14, 2024
@Hanmac
Copy link
Contributor

Hanmac commented Nov 14, 2024

@kevlahnota about the AI timeout part, how about separating it into extra Threads per creature?

threadSafeIterable would that change make problems for this ConcurrentModificationException?

When Touching FCollection, how about looking at #3397 ?
Especially looking at SetUniqueList

@kevlahnota
Copy link
Contributor Author

@kevlahnota about the AI timeout part, how about separating it into extra Threads per creature?

threadSafeIterable would that change make problems for this ConcurrentModificationException?

When Touching FCollection, how about looking at #3397 ? Especially looking at SetUniqueList

I changed the FCollection LinkedList to Lists.newCopyOnWriteArrayList() so it's already thread safe, threadSafeIterable returns the list directly from FCollection, also it uses Sets.newConcurrentHashSet(), (I think this is a shortcut to ConcurrentHashmap.newkeyset).

@Agetian
Copy link
Contributor

Agetian commented Nov 14, 2024

I agree with @tool4ever's observations, and after these tweaks it looks like a very useful and nice option! :)

@Hanmac
Copy link
Contributor

Hanmac commented Nov 14, 2024

I changed the FCollection LinkedList to Lists.newCopyOnWriteArrayList() so it's already thread safe, threadSafeIterable returns the list directly from FCollection, also it uses Sets.newConcurrentHashSet(), (I think this is a shortcut to ConcurrentHashmap.newkeyset).

I just thought we could clean up some code if we make:
FCollection = new SetUniqueList(Lists.newCopyOnWriteArrayList(), Sets.newConcurrentHashSet())

less own code means less errors ;P

@Jetz72
Copy link
Contributor

Jetz72 commented Nov 14, 2024

I changed the FCollection LinkedList to Lists.newCopyOnWriteArrayList() so it's already thread safe

Isn't that going to be a massive hit to performance for a lot of the single-threaded code? CopyOnWriteArrayList really does copy the whole list every time you add or remove list items. Adding a single item goes from O(1) to O(N), and adding iteratively goes from O(M) to O(N * M). Using it for every single FCollection might be excessive.

@kevlahnota
Copy link
Contributor Author

kevlahnota commented Nov 14, 2024

I changed the FCollection LinkedList to Lists.newCopyOnWriteArrayList() so it's already thread safe

Isn't that going to be a massive hit to performance for a lot of the single-threaded code? CopyOnWriteArrayList really does copy the whole list every time you add or remove list items. Adding a single item goes from O(1) to O(N), and adding iteratively goes from O(M) to O(N * M). Using it for every single FCollection might be excessive.

I did profile it and seems ok so far. I don't know any concurrent list that is available. How does the change react to your machine when using it with lots of token/card generation?

@kevlahnota
Copy link
Contributor Author

@Hanmac I encountered concurrent modification on Card.class for the Iterable getchangedcardtypes. Is it safe to return ImmutableList.copyOf instead of Iterables.unmodifiableIterable?

@Hanmac
Copy link
Contributor

Hanmac commented Nov 15, 2024

@Hanmac I encountered concurrent modification on Card.class for the Iterable getchangedcardtypes. Is it safe to return ImmutableList.copyOf instead of Iterables.unmodifiableIterable?

I saw the error too on sentry but I can't explain it yet

Do you got a case to reproduce that?

If yes I could make a short hot fix

Replace copyonwritearray to a custom implementation for concurrent modification exception. A little bit slower but supports iterator operations.
Added FCollectionTest
@kevlahnota
Copy link
Contributor Author

I did profile it and seems ok so far. I don't know any concurrent list that is available. How does the change react to your machine when using it with lots of token/card generation?

Nothing significant in gameplay, though it's hard to pin down what's causing sluggishness just by watching, and I haven't had much luck finding a reasonably priced profiler on Linux. I ran some basic benchmarks - creating and destroying a ton of tokens, shuffling a deck a bunch of times. Destroying things seems unaffected funnily enough, but other operations do experience exponential slowdown with higher numbers and the CopyOnWriteArrayList.

Creating a deck and shuffling it 10000 times

ArrayList

100 card deck

Create: 0.15320649
Shuffle: 0.413653924

1000 card deck

Create: 0.42519795
Shuffle: 2.900546915

3000 card deck

Create: 0.937716733
Shuffle x 10000: 9.418635845

CopyOnWriteArrayList

100 card deck

Create: 0.155789148
Shuffle: 0.755845171

1000 card deck

Create: 0.466518411
Shuffle: 15.762016363

3000 card deck

Create: 3.240357319
Shuffle: 2:04.585301495

Creating and destroying tokens

ArrayList

100 tokens

Create: 0.464475841
Destroy: 0.11633868

1000 tokens

Create: 7.275628844
Destroy: 6.799610756

3000 tokens

Create: 1:03.685734948
Destroy: 3:49.847811285

CopyOnWriteArrayList

100 tokens

Create: 0.45074327
Destroy: 0.112953013

1000 tokens

Create: 12.786269518
Destroy: 7.194827712

3000 tokens

Create: 3:39.469679407
Destroy: 4:03.47748994

Obviously these numbers are far above what you could expect to have in an actual game, but Forge already has a number of issues with scaling to larger board states and this would compound that somewhat. A custom implementation may be better than the out-of-the-box list. You could also just make separate thread-safe collections for concurrent operations to minimize the impact, perhaps either a subclass of FCollection or a method that instructs it to switch its underlying list to a thread-safe one when it's needed.

Hmm FCollection needs List without duplicates (the cards has its own id, so they're all unique I guess), @Hanmac suggested the SetUniqueList but it's another addition that needs to be tested. Eitherway, the copyonwriteareaylist works fine on less than 1000 objects but will suffer when used on generating ai genetic decks (but that generation is slow even before the change of fcollection)

I replaced the copyonwritearraylist, I think it's slower but its a good tradeoff for those using iterator functions

@Jetz72
Copy link
Contributor

Jetz72 commented Nov 20, 2024

I replaced the copyonwritearraylist, I think it's slower but its a good tradeoff for those using iterator functions

Seems like a worthy trade-off. Performance is important but I think full support for the collection interface is more important for an implementation this widely used.

@kevlahnota
Copy link
Contributor Author

@Hanmac @Agetian @tool4ever is there any changes needed? FCollection and ManaPool shouldn't throw ConcurrentModification anymore and seems to work fine.

@tool4ever
Copy link
Contributor

Well my main concern isn't really addressed:
while you might prevent plain out crashing you're still creating race conditions if you allow multiple threads to access the manapool concurrently. Either the floating mana or the arrays for conversion will have a chance of getting messed up. I don't see how you're working around this unless you mark canPlayAndPayFor or something as synchronized.
(And no, I can't give you a fully working example because that's just the nature of nondeterminism)

Also the "dumb check" you had to add clearly hints that something with your custom data alternative doesn't fully work and will break some existing AI behaviour.

@kevlahnota
Copy link
Contributor Author

kevlahnota commented Nov 22, 2024

Well my main concern isn't really addressed: while you might prevent plain out crashing you're still creating race conditions if you allow multiple threads to access the manapool concurrently. Either the floating mana or the arrays for conversion will have a chance of getting messed up. I don't see how you're working around this unless you mark canPlayAndPayFor or something as synchronized. (And no, I can't give you a fully working example because that's just the nature of nondeterminism)

Also the "dumb check" you had to add clearly hints that something with your custom data alternative doesn't fully work and will break some existing AI behaviour.

Ok I'll revert the Manapool and implement a lock similar to what I did to FCollection and remove the dumb check and restore the sort above it (I still think that's is the cause the of the issue) to satisfy your concern.

remove ManaPoolTest for concurrency
refactor timeout for chooseSpellAbilityToPlayFromList
@tool4ever
Copy link
Contributor

👍
(I think some parts still look a bit like duct tape but thanks for all the work. And maybe the concurrent SA approach can be reused later for simulation together with normal one, we could talk on Discord in case more AI stuff is on your roadmap)

@@ -43,12 +44,40 @@ public static <T> FCollection<T> getEmpty() {
/**
* The {@link Set} representation of this collection.
*/
private final Set<T> set = Sets.newHashSet();
private Set<T> SET;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question, why don't you use the default value anymore? I mean there shouldn't be a time where the set/list is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everytime a new cardcollection is created and isn't used because of invalid condition saves a bit of memory. On my local copy when i profile it (Card, player, spellability, etc), unused vars and not initialized if not needed produce good results.

Copy link
Contributor

@Jetz72 Jetz72 Nov 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that the result of a bunch of fields using them as the default value? Like these in Card.java?

    private List<GameCommand> leavePlayCommandList = Lists.newArrayList();
    private final List<GameCommand> untapCommandList = Lists.newArrayList();
    private final List<GameCommand> changeControllerCommandList = Lists.newArrayList();
    private final List<GameCommand> unattachCommandList = Lists.newArrayList();
    private final List<GameCommand> faceupCommandList = Lists.newArrayList();
    private final List<GameCommand> facedownCommandList = Lists.newArrayList();
    private final List<GameCommand> phaseOutCommandList = Lists.newArrayList();
    private final List<Object[]> staticCommandList = Lists.newArrayList();

Might be a good idea in the future to give the same lazy-init treatment to other commonly used classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants