/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.shared.event; import aphelion.shared.event.promise.AbstractPromise; import aphelion.shared.event.promise.Promise; import aphelion.shared.swissarmyknife.LinkedListEntry; import aphelion.shared.swissarmyknife.LinkedListHead; import aphelion.shared.swissarmyknife.ThreadSafe; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; /** A simple event loop that runs on a fixed interval (ticks). * If a tick suddenly takes too much time to run or if the clock changes, the loop * will catch up to the proper number of iterations. * Methods are not safe to call from different threads (unless noted otherwise). * @author Joris */ public class TickedEventLoop implements Workable, Timerable, Deadlock.DeadlockTicker { private static final Logger log = Logger.getLogger("aphelion.eventloop"); public final long TICK; // length of a tick in nanoseconds public ClockSource clockSource; private long nano = 0; private long tick = 0; private final boolean syncedByOtherLoop; private boolean setup = false; private boolean breakdown = false; private volatile boolean interrupted = false; private long loop_nanoTime; private long loop_systemNanoTime; Thread myThread; volatile long deadlock_tick = 0; // can be updated as often as desired volatile long deadlock_tick_lastseen = -1; // tick and loop events are not added to or removed frequently private final ArrayList<TickEvent> tickEvents = new ArrayList<>(8); private final ArrayList<LoopEvent> loopEvents = new ArrayList<>(8); // Timer events are added and removed frequently private final LinkedListHead<TimerData> timerEvents = new LinkedListHead<>(); // Tasks that have not yet been started private final LinkedBlockingDeque<WorkerTask> tasks; /** Tasks that have been completed, will be fired as callbacks the next tick. */ private final LinkedBlockingDeque<TaskCompleteEntry> completedTasks; /** Same as the completedTasks, however this list is intended to be used by the consumer thread. * This is to prevent dead locks */ private final LinkedList<TaskCompleteEntry> completedTasks_local; // stuff added by the same thread that is consuming private final WorkerThread[] workerThreads; /** * @param tickLength How long does a tick last in nanoseconds * @param workerThreads How many worker threads to spawn (used by addWorkerTask). 0 to not spawn any threads * @param clockSource An interface providing relative time */ public TickedEventLoop(long tickLength, int workerThreads, ClockSource clockSource) { this.clockSource = clockSource; if (this.clockSource == null) { this.clockSource = new DefaultClockSource(); } this.TICK = tickLength; syncedByOtherLoop = false; if (workerThreads > 0) { this.tasks = new LinkedBlockingDeque<>(); } else { this.tasks = null; } this.completedTasks = new LinkedBlockingDeque<>(); this.completedTasks_local = new LinkedList<>(); // Fire a maximum of 32 callbacks a time this.workerThreads = new WorkerThread[workerThreads]; for (int i = 0; i < workerThreads; ++i) { this.workerThreads[i] = new WorkerThread(this, tasks); this.workerThreads[i].setDaemon(true); } } /** Construct and synchronize with an other event loop. * This loop will have the same clock source and tick length. * This loop will begin with the same tick count and will tick at the same time. * * When you use this constructor to synchronize loops across threads, make sure * that the clock source returns the same value on all threads! (this is a known issue * with System.nanoTime on old windows systems. * @param other * @param workerThreads How many worker threads to spawn (used by addWorkerTask). 0 to not spawn any threads */ public TickedEventLoop(TickedEventLoop other, int workerThreads) { if (!other.setup) { throw new IllegalArgumentException("Given loop has not been setup"); } this.clockSource = other.clockSource; this.TICK = other.TICK; this.nano = other.nano; this.tick = other.tick; syncedByOtherLoop = true; if (workerThreads > 0) { this.tasks = new LinkedBlockingDeque<>(); // with 4 threads, queue a maximum of 128 tasks } else { this.tasks = null; } this.completedTasks = new LinkedBlockingDeque<>(); this.completedTasks_local = new LinkedList<>(); this.workerThreads = new WorkerThread[workerThreads]; for (int i = 0; i < workerThreads; ++i) { this.workerThreads[i] = new WorkerThread(this, tasks); this.workerThreads[i].setDaemon(true); } } /** The nanoTime at which the current loop began. * Using clockSource.nanoTime * @return nanoseconds */ public long getLoopNanoTime() { return loop_nanoTime; } /** The nanoTime at which the current loop began. * Using System.nanoTime() * @return nanoseconds */ public long getLoopSystemNanoTime() { return loop_systemNanoTime; } /** Replace the current clock source with a new one. * Any difference between the new and old clock source is corrected * so that no extra ticks occur. * @param clockSource */ public void setClockSource(ClockSource clockSource) { long oldTime = this.clockSource.nanoTime(); long newTime = clockSource.nanoTime(); nano += newTime - oldTime; this.clockSource = clockSource; } @ThreadSafe @Override public final void tickDeadlock() { ++this.deadlock_tick; } /** When this EventLoop is part of another loop, use this method before starting the loop */ public void setup() { if (setup) { throw new IllegalStateException(); } myThread = Thread.currentThread(); Deadlock.add(this); for (int i = 0; i < workerThreads.length; ++i) { workerThreads[i].start(); } setup = true; interrupted = false; // If not 0, the "TickedEventLoop other" constructor was used. if (!syncedByOtherLoop) { nano = nanoTime(); tick = 0; } breakdown = false; } /** When this EventLoop is part of another loop, use this method after ending the loop */ public void breakdown() { if (!setup) { throw new IllegalStateException(); } if (breakdown) { throw new IllegalStateException(); } Deadlock.remove(this); breakdown = true; setup = false; interrupted = true; for (int i = 0; i < workerThreads.length; ++i) { workerThreads[i].interrupt(); } for (int i = 0; i < workerThreads.length; ++i) { while (true) { try { workerThreads[i].join(); break; } catch (InterruptedException ex) { } } } } /** When this EventLoop is part of another loop, * call this method on every iteration. * As often as you want, but preferably at least once per TICK. */ public void loop() { assert isLoopThreadCurrent(); loop(false); } @SuppressWarnings("unchecked") private void loop(boolean internal) { assert setup; assert !breakdown; tickDeadlock(); if (!internal) { // Check for completed work while (true) { TaskCompleteEntry t; t = completedTasks_local.poll(); if (t == null) { t = completedTasks.poll(); } if (t == null) { break; } if (t.task != null) { if (t.task.error != null) { t.task.promise.reject(t.task.error); } else { t.task.promise.resolve(t.task.ret); } } if (t.runFromMain != null) { t.runFromMain.run(); } tickDeadlock(); } } loop_nanoTime = nanoTime(); loop_systemNanoTime = this.clockSource instanceof DefaultClockSource ? loop_nanoTime : System.nanoTime(); for (LoopEvent event : loopEvents) { event.loop(loop_systemNanoTime, loop_nanoTime); tickDeadlock(); } // if nanoTime() wraps, this calculation will still be correct thanks to overflow // // 32 bit example: // Suppose the previous time (nanos) is 2147483547 (Integer.MAX_VALUE) // Time = 2147483647: 2147483647 - 2147483547 = 100 // Time = 2147483647+1 = -2147483648: -2147483648 - 2147483547 = 101 // Time = 2147483647+2 = -2147483647: -2147483647 - 2147483547 = 102 long delta = loop_nanoTime - nano; while (delta >= TICK && !interrupted) { delta -= TICK; nano += TICK; ++tick; //System.out.printf("%d: %4d => %4d\n", Objects.hashCode(this), (nano/1_000_000L), tick); tick(); tickDeadlock(); } } /** Synchronize the loop so that the tick "tick" would have occurred at tickAtNano. * The difference should not be too large. * @param tickNano The nano time at which tick should have occurred. * This value is relative to the ClockSource * @param tick The tick count at tickAtNano */ public void synchronize(long tickNano, long tick) { long currentTickTime = this.nano - (this.tick - tick) * TICK; this.nano = this.nano - (currentTickTime - tickNano); // suppose: // TICK = 10ms // tick 20 occurred at 40'000ms // tick 21 occurred at 40'010ms // tick 22 occurred at 40'020ms (current tick) // synchronize(40'050, 20) is called: // currentTickTime = 40'020 - (22 - 20) * 10 = 40'000 // this.nanos = 40'020 - (40'000 - 40'050) = 40070 // tick 22 is now at 40'070ms } /** Blocks until stop() is called */ @SuppressWarnings("unchecked") public void run() { setup(); while (!interrupted) { loop(true); try { // Check for completed work boolean first = true; while (true) { TaskCompleteEntry t; t = completedTasks_local.poll(); if (t == null) { if (first) { t = completedTasks.poll(); first = false; } else { // Sleep for 1ms to avoid trashing the cpu // This is the only place where we sleep t = completedTasks.poll(1, TimeUnit.MILLISECONDS); } } if (t == null) { break; } if (t.task != null) { if (t.task.error != null) { t.task.promise.reject(t.task.error); } else { t.task.promise.resolve(t.task.ret); } } if (t.runFromMain != null) { t.runFromMain.run(); } } } catch (InterruptedException ex) { interrupt(); log.log(Level.WARNING, "Thread interrupted, stopping loop. ", ex); return; } } breakdown(); } @ThreadSafe public void interrupt() { interrupted = true; } /** Has this loop been interrupted?. * Unlike Thread.interrupted(), this method has no side effects. * @return */ @ThreadSafe public boolean isInterruped() { return interrupted; } /** The current nanoTime as given by the clock source. * This value may change by a large amount if the clock source was just replaced. * @return nanotime (10^-9 seconds) */ public long nanoTime() { return this.clockSource.nanoTime(); } /** Returns the nanoTime of the current tick. * If a new clock source was just set, this value may change by a large * amount (even for the same tick). * @return nanotime (10^-9 seconds) */ public long currentNano() { return nano; } public long currentTick() { return tick; } private void tick() { LinkedListEntry<TimerData> timerEntry, timerEntryNext; TimerData timer; long now; now = currentTick(); // Tick events for (TickEvent event : tickEvents) { event.tick(now); } // Timers timerEntry = timerEvents.first; while (timerEntry != null) { timerEntryNext = timerEntry.next; timer = timerEntry.data; if (now >= timer.nextRun) { timer.nextRun = now + timer.interval; if (!timer.event.timerElapsed(now)) { timerEntry.remove(); continue; } } timerEntry = timerEntryNext; } } /** Adds an event that will be fired every tick. * @param event */ public void addTickEvent(TickEvent event) { if (event == null) { throw new IllegalArgumentException(); } tickEvents.add(event); } /** Adds an event that will be fired every tick. * @param events */ public void addTickEvent(TickEvent[] events) { for (TickEvent event : events) { addTickEvent(event); } } /** Adds an event that will be fired every tick before any event that has already been registered. * @param event */ public void prependTickEvent(TickEvent event) { if (event == null) { throw new IllegalArgumentException(); } tickEvents.add(0, event); } /** Adds an event that will be fired every tick before any event that has already been registered. * @param events */ public void prependTickEvent(TickEvent[] events) { for (TickEvent event : events) { prependTickEvent(event); } } /** Removes a tick event that was previously added. * @param event * @return true if the tick event was present */ public boolean removeTickEvent(TickEvent event) { for (int a = tickEvents.size()-1; a >= 0; --a) { if (tickEvents.get(a) == event) { tickEvents.remove(a); return true; } } return false; } /** Removes a tick event that was previously added. * @param events */ public void removeTickEvent(TickEvent[] events) { for (TickEvent event : events) { removeTickEvent(event); } } /** Adds an event that will be fired every loop. * @param event */ public void addLoopEvent(LoopEvent event) { if (event == null) { throw new IllegalArgumentException(); } loopEvents.add(event); } /** Adds an event that will be fired every loop. * @param events */ public void addLoopEvent(LoopEvent[] events) { for (LoopEvent event : events) { addLoopEvent(event); } } /** Removes a loop event that was previously added. * @param event * @return True if the tick event was present. */ public boolean removeLoopEvent(LoopEvent event) { for (int a = loopEvents.size()-1; a >= 0; --a) { if (loopEvents.get(a) == event) { loopEvents.remove(a); return true; } } return false; } /** Removes a loop event that was previously added. * @param events */ public void removeLoopEvent(LoopEvent[] events) { for (LoopEvent event : events) { removeLoopEvent(event); } } /** Adds an event that will be fired every X ticks. * @param interval The interval in ticks * @param event The callback to fire */ @Override public void addTimerEvent(long interval, TimerEvent event) { TimerData timer; timer = new TimerData(); timer.event = event; timer.interval = interval; timer.nextRun = currentTick() + interval; timerEvents.appendData(timer); } /** Removes a timer event that was previously added. * @param event * @return true if the timer event was present */ @Override public boolean removeTimerEvent(TimerEvent event) { LinkedListEntry<TimerData> entry; entry = timerEvents.first; while (entry != null) { if (entry.data.event == event) { entry.remove(); return true; } entry = entry.next; } return false; } @ThreadSafe public boolean isLoopThreadCurrent() { return myThread.equals(Thread.currentThread()); } private static class TimerData { TimerEvent event; long nextRun; long interval; // in ticks } @Override @SuppressWarnings("unchecked") public AbstractPromise addWorkerTask(WorkerTask task, Object argument) throws IllegalStateException { if (workerThreads.length <= 0) { throw new IllegalStateException("There are no worker threads present"); } task.argument = argument; task.promise = new Promise(this); tasks.add(task); // throws an exception if full return task.promise; } @ThreadSafe @Override public void runOnMain(Runnable runnable) { addCompletedTask(new TaskCompleteEntry(runnable)); } @ThreadSafe @Override public void taskCompleted(WorkerTask task) { addCompletedTask(new TaskCompleteEntry(task)); } @ThreadSafe private void addCompletedTask(TaskCompleteEntry t) { // myThread is the one consuming. // So do not using a blocking queue, this would cause a dead lock if (isLoopThreadCurrent()) { completedTasks_local.add(t); } else { try { while (!completedTasks.offer(t, 1, TimeUnit.MILLISECONDS)); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } static final class TaskCompleteEntry { final WorkerTask task; final Runnable runFromMain; TaskCompleteEntry(WorkerTask task) { this.task = task; this.runFromMain = null; } TaskCompleteEntry(Runnable runFromMain) { this.task = null; this.runFromMain = runFromMain; } } }