package games.strategy.triplea.oddsCalculator.ta; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Territory; import games.strategy.engine.data.TerritoryEffect; import games.strategy.engine.data.Unit; import games.strategy.engine.framework.GameDataUtils; import games.strategy.util.CountUpAndDownLatch; /** * Concurrent wrapper class for the OddsCalculator. It spawns multiple worker threads and splits up the run count * across these workers. This is mainly to be used by AIs since they call the OddsCalculator a lot. */ public class ConcurrentOddsCalculator implements IOddsCalculator { private static final Logger s_logger = Logger.getLogger(ConcurrentOddsCalculator.class.getName()); private static final int MAX_THREADS = Math.max(1, Runtime.getRuntime().availableProcessors()); private int m_currentThreads = MAX_THREADS; private final ExecutorService m_executor; private final CopyOnWriteArrayList<OddsCalculator> m_workers = new CopyOnWriteArrayList<>(); // do not let calc be set up til data is set private volatile boolean m_isDataSet = false; // do not let calc start until it is set private volatile boolean m_isCalcSet = false; // shortcut everything if we are shutting down private volatile boolean m_isShutDown = false; // shortcut setting of previous game data if we are trying to set it to a new one, or shutdown private volatile int m_cancelCurrentOperation = 0; // do not let calcing happen while we are setting game data private final CountUpAndDownLatch m_latchSetData = new CountUpAndDownLatch(); // do not let setting of game data happen multiple times while we offload creating workers and copying data to a // different thread private final CountUpAndDownLatch m_latchWorkerThreadsCreation = new CountUpAndDownLatch(); // do not let setting of game data happen at same time private final Object m_mutexSetGameData = new Object(); // do not let multiple calculations or setting calc data happen at same time private final Object m_mutexCalcIsRunning = new Object(); private final List<OddsCalculatorListener> m_listeners = new ArrayList<>(); public ConcurrentOddsCalculator(final String threadNamePrefix) { m_executor = Executors.newFixedThreadPool(MAX_THREADS, new DaemonThreadFactory(true, threadNamePrefix + " ConcurrentOddsCalculator Worker")); s_logger.fine("Initialized executor thread pool with size: " + MAX_THREADS); } @Override public void setGameData(final GameData data) { // increment so that a new calc doesn't take place (since they all wait on this latch) m_latchSetData.increment(); // cancel any current setting of data --m_cancelCurrentOperation; // cancel any existing calcing (it won't stop immediately, just quicker) cancel(); synchronized (m_mutexSetGameData) { try { // since setting data takes place on a different thread, this is our token. wait on it since m_latchWorkerThreadsCreation.await(); // we could have exited the synchronized block already. } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } cancel(); m_isDataSet = false; m_isCalcSet = false; if (data == null || m_isShutDown) { m_workers.clear(); ++m_cancelCurrentOperation; // allow calcing and other stuff to go ahead m_latchSetData.countDown(); } else { ++m_cancelCurrentOperation; // increment our token, so that we can set the data in a different thread and return from this one m_latchWorkerThreadsCreation.increment(); m_executor.submit(() -> createWorkers(data)); } } } @Override public int getThreadCount() { return m_currentThreads; } // use both time and memory left to determine how many copies to make private static int getThreadsToUse(final long timeToCopyInMillis, final long memoryUsedBeforeCopy) { if (timeToCopyInMillis > 20000 || MAX_THREADS == 1) { // just use 1 thread if we took more than 20 seconds to copy return 1; } final Runtime runtime = Runtime.getRuntime(); final long usedMemoryAfterCopy = runtime.totalMemory() - runtime.freeMemory(); // we cannot predict how the gc works final long memoryLeftBeforeMax = runtime.maxMemory() - (Math.max(usedMemoryAfterCopy, memoryUsedBeforeCopy)); // make sure it is a decent size final long memoryUsedByCopy = Math.max(100000, (usedMemoryAfterCopy - memoryUsedBeforeCopy)); // regardless of how stupid the gc is // we leave some memory left over just in case final int numberOfTimesWeCanCopyMax = Math.max(1, (int) (Math.min(Integer.MAX_VALUE, (memoryLeftBeforeMax / memoryUsedByCopy)))); if (timeToCopyInMillis > 3000) { // use half the number of threads available if we took // more than 3 seconds to copy return Math.min(numberOfTimesWeCanCopyMax, Math.max(1, (MAX_THREADS / 2))); } // use all threads return Math.min(numberOfTimesWeCanCopyMax, MAX_THREADS); } private void createWorkers(final GameData data) { m_workers.clear(); if (data != null && m_cancelCurrentOperation >= 0) { // see how long 1 copy takes (some games can get REALLY big) final long startTime = System.currentTimeMillis(); final long startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); final GameData newData; try { // make first copy, then release lock on it so game can continue (ie: we don't want to lock on it while we copy // it 16 times, when once is enough) don't let the data change while we make the first copy data.acquireReadLock(); newData = GameDataUtils.cloneGameData(data, false); } finally { data.releaseReadLock(); } m_currentThreads = getThreadsToUse((System.currentTimeMillis() - startTime), startMemory); try { // make sure all workers are using the same data newData.acquireReadLock(); int i = 0; // we are already in 1 executor thread, so we have MAX_THREADS-1 threads left to use if (m_currentThreads <= 2 || MAX_THREADS <= 2) { // if 2 or fewer threads, do not multi-thread the copying (we have already copied it once above, so at most // only 1 more copy to // make) while (m_cancelCurrentOperation >= 0 && i < m_currentThreads) { // the last one will use our already copied data from above, without copying it again m_workers.add(new OddsCalculator(newData, (m_currentThreads == ++i))); } } else { // multi-thread our copying, cus why the heck not (it increases the speed of copying by about double) final CountDownLatch workerLatch = new CountDownLatch(m_currentThreads - 1); while (i < (m_currentThreads - 1)) { ++i; m_executor.submit(() -> { if (m_cancelCurrentOperation >= 0) { m_workers.add(new OddsCalculator(newData, false)); } workerLatch.countDown(); }); } // the last one will use our already copied data from above, without copying it again m_workers.add(new OddsCalculator(newData, true)); try { workerLatch.await(); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } } } finally { newData.releaseReadLock(); } } if (m_cancelCurrentOperation < 0 || data == null) { // we could have cancelled while setting data, so clear the workers again if so m_workers.clear(); m_isDataSet = false; } else { // should make sure that all workers have their game data set before we can call calculate and other things m_isDataSet = true; notifyListenersGameDataIsSet(); } // allow setting new data to take place if it is waiting on us m_latchWorkerThreadsCreation.countDown(); // allow calcing and other stuff to go ahead m_latchSetData.countDown(); s_logger.fine("Initialized worker thread pool with size: " + m_workers.size()); } @Override public void shutdown() { m_isShutDown = true; m_cancelCurrentOperation = Integer.MIN_VALUE / 2; cancel(); m_executor.shutdown(); synchronized (m_listeners) { m_listeners.clear(); } } @Override protected void finalize() throws Throwable { shutdown(); super.finalize(); } private void awaitLatch() { try { // there is a small chance calculate or setCalculateData or something could be called in between calls to // setGameData m_latchSetData.await(); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } } @Override public void setCalculateData(final PlayerID attacker, final PlayerID defender, final Territory location, final Collection<Unit> attacking, final Collection<Unit> defending, final Collection<Unit> bombarding, final Collection<TerritoryEffect> territoryEffects, int runCount) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); m_isCalcSet = false; final int workerNum = m_workers.size(); final int workerRunCount = Math.max(1, (runCount / Math.max(1, workerNum))); for (final OddsCalculator worker : m_workers) { if (!m_isDataSet || m_isShutDown) { // we could have attempted to set a new game data, while the old one was still being set, causing it to abort // with null data return; } worker.setCalculateData(attacker, defender, location, attacking, defending, bombarding, territoryEffects, (runCount <= 0 ? 0 : workerRunCount)); runCount -= workerRunCount; } if (!m_isDataSet || m_isShutDown || workerNum <= 0) { return; } m_isCalcSet = true; } } /** * Concurrently calculates odds using the OddsCalculatorWorker. It uses Executor to process the results. Then waits * for all the future * results and combines them together. */ @Override public AggregateResults calculate() throws IllegalStateException { synchronized (m_mutexCalcIsRunning) { awaitLatch(); final long start = System.currentTimeMillis(); // Create worker thread pool and start all workers int totalRunCount = 0; final List<Future<AggregateResults>> list = new ArrayList<>(); for (final OddsCalculator worker : m_workers) { if (!getIsReady()) { // we could have attempted to set a new game data, while the old one was still being set, causing it to abort // with null data return new AggregateResults(0); } if (!worker.getIsReady()) { throw new IllegalStateException("Called calculate before setting calculate data!"); } if (worker.getRunCount() > 0) { totalRunCount += worker.getRunCount(); final Future<AggregateResults> workerResult = m_executor.submit(worker); list.add(workerResult); } } // Wait for all worker futures to complete and combine results final AggregateResults results = new AggregateResults(totalRunCount); final Set<InterruptedException> interruptExceptions = new HashSet<>(); final Map<String, Set<ExecutionException>> executionExceptions = new HashMap<>(); for (final Future<AggregateResults> future : list) { try { final AggregateResults result = future.get(); results.addResults(result.getResults()); } catch (final InterruptedException e) { interruptExceptions.add(e); } catch (final ExecutionException e) { final String cause = e.getCause().getLocalizedMessage(); Set<ExecutionException> exceptions = executionExceptions.get(cause); if (exceptions == null) { exceptions = new HashSet<>(); } exceptions.add(e); executionExceptions.put(cause, exceptions); } } // we don't want to scare the user with 8+ errors all for the same thing if (!interruptExceptions.isEmpty()) { s_logger.log(Level.SEVERE, interruptExceptions.size() + " Battle results workers interrupted", interruptExceptions.iterator().next()); } if (!executionExceptions.isEmpty()) { Exception e = null; for (final Set<ExecutionException> entry : executionExceptions.values()) { if (!entry.isEmpty()) { e = entry.iterator().next(); s_logger.log(Level.SEVERE, entry.size() + " Battle results workers aborted by exception", e.getCause()); } } if (e != null) { throw new IllegalStateException(e.getCause()); } } results.setTime(System.currentTimeMillis() - start); return results; } } @Override public AggregateResults setCalculateDataAndCalculate(final PlayerID attacker, final PlayerID defender, final Territory location, final Collection<Unit> attacking, final Collection<Unit> defending, final Collection<Unit> bombarding, final Collection<TerritoryEffect> territoryEffects, final int runCount) { synchronized (m_mutexCalcIsRunning) { setCalculateData(attacker, defender, location, attacking, defending, bombarding, territoryEffects, runCount); return calculate(); } } @Override public boolean getIsReady() { return m_isDataSet && m_isCalcSet && !m_isShutDown; } @Override public int getRunCount() { int totalRunCount = 0; for (final OddsCalculator worker : m_workers) { totalRunCount += worker.getRunCount(); } return totalRunCount; } @Override public void setKeepOneAttackingLandUnit(final boolean bool) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setKeepOneAttackingLandUnit(bool); } } } @Override public void setAmphibious(final boolean bool) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setAmphibious(bool); } } } @Override public void setRetreatAfterRound(final int value) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setRetreatAfterRound(value); } } } @Override public void setRetreatAfterXUnitsLeft(final int value) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setRetreatAfterXUnitsLeft(value); } } } @Override public void setRetreatWhenOnlyAirLeft(final boolean value) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setRetreatWhenOnlyAirLeft(value); } } } @Override public void setAttackerOrderOfLosses(final String attackerOrderOfLosses) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setAttackerOrderOfLosses(attackerOrderOfLosses); } } } @Override public void setDefenderOrderOfLosses(final String defenderOrderOfLosses) { synchronized (m_mutexCalcIsRunning) { awaitLatch(); for (final OddsCalculator worker : m_workers) { worker.setDefenderOrderOfLosses(defenderOrderOfLosses); } } } // not on purpose, we need to be able to cancel at any time @Override public void cancel() { for (final OddsCalculator worker : m_workers) { worker.cancel(); } } @Override public void addOddsCalculatorListener(final OddsCalculatorListener listener) { synchronized (m_listeners) { m_listeners.add(listener); } } @Override public void removeOddsCalculatorListener(final OddsCalculatorListener listener) { synchronized (m_listeners) { m_listeners.remove(listener); } } private void notifyListenersGameDataIsSet() { synchronized (m_listeners) { for (final OddsCalculatorListener listener : m_listeners) { listener.dataReady(); } } } }