/* * JaamSim Discrete Event Simulation * Copyright (C) 2002-2014 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.events; import java.util.ArrayList; /** * The EventManager is responsible for scheduling future events, controlling * conditional event evaluation, and advancing the simulation time. Events are * scheduled in based on: * <ul> * <li>1 - The execution time scheduled for the event * <li>2 - The priority of the event (if scheduled to occur at the same time) * <li>3 - If both 1) and 2) are equal, the user specified FIFO or LIFO order * </ul> * <p> * The event time is scheduled using a backing long value. Double valued time is * taken in by the scheduleWait function and scaled to the nearest long value * using the simTimeFactor. * <p> * Most EventManager functionality is available through static methods that rely * on being in a running model context which will access the eventmanager that is * currently running, think of it like a thread-local variable for all model threads. */ public final class EventManager { public final String name; private final Object lockObject; // Object used as global lock for synchronization private final EventTree eventTree; private volatile boolean executeEvents; private boolean processRunning; private final ArrayList<ConditionalEvent> condEvents; private long currentTick; // Master simulation time (long) private long nextTick; // The next tick to execute events at private long targetTick; // the largest time we will execute events for (run to time) private double ticksPerSecond; // The number of discrete ticks per simulated second private double secsPerTick; // The length of time in seconds each tick represents // Real time execution state private long realTimeTick; // the simulation tick corresponding to the wall-clock millis value private long realTimeMillis; // the wall-clock time in millis private volatile boolean executeRealTime; // TRUE if the simulation is to be executed in Real Time mode private volatile boolean rebaseRealTime; // TRUE if the time keeping for Real Time model needs re-basing private volatile double realTimeFactor; // target ratio of elapsed simulation time to elapsed wall clock time private EventTimeListener timelistener; private EventErrorListener errListener; private EventTraceListener trcListener; /** * Allocates a new EventManager with the given parent and name * * @param parent the connection point for this EventManager in the tree * @param name the name this EventManager should use */ public EventManager(String name) { // Basic initialization this.name = name; lockObject = new Object(); // Initialize and event lists and timekeeping variables currentTick = 0; nextTick = 0; setTickLength(1e-6d); eventTree = new EventTree(); condEvents = new ArrayList<>(); executeEvents = false; processRunning = false; executeRealTime = false; realTimeFactor = 1; rebaseRealTime = true; setTimeListener(null); setErrorListener(null); } public final void setTimeListener(EventTimeListener l) { synchronized (lockObject) { if (l != null) timelistener = l; else timelistener = new NoopListener(); timelistener.tickUpdate(currentTick); } } public final void setErrorListener(EventErrorListener l) { synchronized (lockObject) { if (l != null) errListener = l; else errListener = new NoopListener(); } } public final void setTraceListener(EventTraceListener l) { synchronized (lockObject) { trcListener = l; } } public void clear() { synchronized (lockObject) { currentTick = 0; nextTick = 0; targetTick = Long.MAX_VALUE; timelistener.tickUpdate(currentTick); rebaseRealTime = true; eventTree.runOnAllNodes(new KillAllEvents()); eventTree.reset(); clearFreeList(); for (int i = 0; i < condEvents.size(); i++) { condEvents.get(i).target.kill(); if (condEvents.get(i).handle != null) { condEvents.get(i).handle.event = null; } } condEvents.clear(); } } private static class KillAllEvents implements EventNode.Runner { @Override public void runOnNode(EventNode node) { Event each = node.head; while (each != null) { if (each.handle != null) { each.handle.event = null; each.handle = null; } each.target.kill(); each = each.next; } } } private boolean executeTarget(Process cur, ProcessTarget t) { try { // If the event has a captured process, pass control to it Process p = t.getProcess(); if (p != null) { p.setNextProcess(cur); p.wake(); threadWait(cur); return true; } // Execute the method t.process(); // Notify the event manager that the process has been completed if (trcListener != null) { cur.beginCallbacks(); trcListener.traceProcessEnd(this, currentTick); cur.endCallbacks(); } if (cur.hasNext()) { cur.wakeNextProcess(); return false; } else { return true; } } catch (Throwable e) { // This is how kill() is implemented for sleeping processes. if (e instanceof ThreadKilledException) return false; // Tear down any threads waiting for this to finish Process next = cur.forceKillNext(); while (next != null) { next = next.forceKillNext(); } executeEvents = false; processRunning = false; errListener.handleError(this, e, currentTick); return false; } } /** * Main event execution method the eventManager, this is the only entrypoint * for Process objects taken out of the pool. */ final void execute(Process cur, ProcessTarget t) { synchronized (lockObject) { // This occurs in the startProcess or interrupt case where we start // a process with a target already assigned if (t != null) { executeTarget(cur, t); return; } if (processRunning) return; processRunning = true; timelistener.timeRunning(true); // Loop continuously while (true) { EventNode nextNode = eventTree.getNextNode(); if (nextNode == null || currentTick >= targetTick) { executeEvents = false; } if (!executeEvents) { processRunning = false; timelistener.timeRunning(false); return; } // If the next event is at the current tick, execute it if (nextNode.schedTick == currentTick) { // Remove the event from the future events Event nextEvent = nextNode.head; ProcessTarget nextTarget = nextEvent.target; if (trcListener != null) { cur.beginCallbacks(); trcListener.traceEvent(this, currentTick, nextNode.schedTick, nextNode.priority, nextTarget); cur.endCallbacks(); } removeEvent(nextEvent); // the return from execute target informs whether or not this // thread should grab an new Event, or return to the pool if (executeTarget(cur, nextTarget)) continue; else return; } // If the next event would require us to advance the time, check the // conditonal events if (eventTree.getNextNode().schedTick > nextTick) { if (condEvents.size() > 0) { evaluateConditions(cur); if (!executeEvents) continue; } // If a conditional event was satisfied, we will have a new event at the // beginning of the eventStack for the current tick, go back to the // beginning, otherwise fall through to the time-advance nextTick = eventTree.getNextNode().schedTick; if (nextTick == currentTick) continue; } // Advance to the next event time if (executeRealTime) { // Loop until the next event time is reached long realTick = this.calcRealTimeTick(); if (realTick < nextTick && realTick < targetTick) { // Update the displayed simulation time currentTick = realTick; timelistener.tickUpdate(currentTick); //Halt the thread for 20ms and then reevaluate the loop try { lockObject.wait(20); } catch( InterruptedException e ) {} continue; } } // advance time if (targetTick < nextTick) currentTick = targetTick; else currentTick = nextTick; timelistener.tickUpdate(currentTick); } } } private void evaluateConditions(Process cur) { // Protecting the conditional evaluate() callbacks and the traceWaitUntilEnded callback cur.beginCallbacks(); try { for (int i = 0; i < condEvents.size();) { ConditionalEvent c = condEvents.get(i); if (c.c.evaluate()) { condEvents.remove(i); EventNode node = getEventNode(currentTick, 0); Event evt = getEvent(); evt.node = node; evt.target = c.target; evt.handle = c.handle; if (evt.handle != null) { // no need to check the handle.isScheduled as we just unscheduled it above // and we immediately switch it to this event evt.handle.event = evt; } if (trcListener != null) trcListener.traceWaitUntilEnded(this, currentTick, c.target); node.addEvent(evt, true); continue; } i++; } } catch (Throwable e) { executeEvents = false; processRunning = false; errListener.handleError(this, e, currentTick); } cur.endCallbacks(); } /** * Return the simulation time corresponding the given wall clock time * @param simTime = the current simulation time used when setting a real-time basis * @return simulation time in seconds */ private long calcRealTimeTick() { long curMS = System.currentTimeMillis(); if (rebaseRealTime) { realTimeTick = currentTick; realTimeMillis = curMS; rebaseRealTime = false; } double simElapsedsec = ((curMS - realTimeMillis) * realTimeFactor) / 1000.0d; long simElapsedTicks = secondsToNearestTick(simElapsedsec); return realTimeTick + simElapsedTicks; } /** // Pause the current active thread and restart the next thread on the // active thread list. For this case, a future event or conditional event // has been created for the current thread. Called by // eventManager.scheduleWait() and related methods, and by // eventManager.waitUntil(). // restorePreviousActiveThread() * Must hold the lockObject when calling this method. */ private void captureProcess(Process cur) { // if we don't wake a new process, take one from the pool Process next = cur.preCapture(); if (next == null) { processRunning = false; Process.processEvents(this); } else { next.wake(); } threadWait(cur); cur.postCapture(); } /** * Calculate the time for an event taking into account numeric overflow. * Must hold the lockObject when calling this method */ private long calculateEventTime(long waitLength) { // Test for negative duration schedule wait length if(waitLength < 0) throw new ProcessError("Negative duration wait is invalid, waitLength = " + waitLength); // Check for numeric overflow of internal time long nextEventTime = currentTick + waitLength; if (nextEventTime < 0) nextEventTime = Long.MAX_VALUE; return nextEventTime; } /** * Pause the execution of the current Process and schedule it to wake up at a future * time in the controlling EventManager, * @throws ProcessError if called outside of a Process context * * @param waitLength the number of ticks in the future to wake at * @param eventPriority the priority of the scheduled wakeup event * @param fifo break ties with previously scheduled events using FIFO/LIFO ordering * @param t the process target to run when the event is executed * @param handle an optional handle to hold onto the scheduled event */ public static final void waitTicks(long ticks, int priority, boolean fifo, EventHandle handle) { Process cur = Process.current(); cur.evt().waitTicks(cur, ticks, priority, fifo, handle); } /** * Pause the execution of the current Process and schedule it to wake up at a future * time in the controlling EventManager, * @throws ProcessError if called outside of a Process context * * @param secs the number of seconds in the future to wake at * @param eventPriority the priority of the scheduled wakeup event * @param fifo break ties with previously scheduled events using FIFO/LIFO ordering * @param t the process target to run when the event is executed * @param handle an optional handle to hold onto the scheduled event */ public static final void waitSeconds(double secs, int priority, boolean fifo, EventHandle handle) { Process cur = Process.current(); long ticks = cur.evt().secondsToNearestTick(secs); cur.evt().waitTicks(cur, ticks, priority, fifo, handle); } /** * Schedules a future event to occur with a given priority. Lower priority * events will be executed preferentially over higher priority. This is * by lower priority events being placed higher on the event stack. * @param ticks the number of discrete ticks from now to schedule the event. * @param priority the priority of the scheduled event: 1 is the highest priority (default is priority 5) */ private void waitTicks(Process cur, long ticks, int priority, boolean fifo, EventHandle handle) { synchronized (lockObject) { cur.checkCallback(); long nextEventTime = calculateEventTime(ticks); WaitTarget t = new WaitTarget(cur); EventNode node = getEventNode(nextEventTime, priority); Event evt = getEvent(); evt.node = node; evt.target = t; evt.handle = handle; if (handle != null) { if (handle.isScheduled()) throw new ProcessError("Tried to schedule using an EventHandle already in use"); handle.event = evt; } if (trcListener != null) { cur.beginCallbacks(); trcListener.traceWait(this, currentTick, nextEventTime, priority, t); cur.endCallbacks(); } node.addEvent(evt, fifo); captureProcess(cur); } } /** * Find an eventNode in the list, if a node is not found, create one and * insert it. */ private EventNode getEventNode(long tick, int prio) { return eventTree.createOrFindNode(tick, prio); } private Event freeEvents = null; private Event getEvent() { if (freeEvents != null) { Event evt = freeEvents; freeEvents = evt.next; return evt; } return new Event(); } private void clearFreeList() { freeEvents = null; } public static final void waitUntil(Conditional cond, EventHandle handle) { Process cur = Process.current(); cur.evt().waitUntil(cur, cond, handle); } /** * Used to achieve conditional waits in the simulation. Adds the calling * thread to the conditional stack, then wakes the next waiting thread on * the thread stack. */ private void waitUntil(Process cur, Conditional cond, EventHandle handle) { synchronized (lockObject) { cur.checkCallback(); WaitTarget t = new WaitTarget(cur); ConditionalEvent evt = new ConditionalEvent(cond, t, handle); if (handle != null) { if (handle.isScheduled()) throw new ProcessError("Tried to waitUntil using a handle already in use"); handle.event = evt; } condEvents.add(evt); if (trcListener != null) { cur.beginCallbacks(); trcListener.traceWaitUntil(this, currentTick); cur.endCallbacks(); } captureProcess(cur); } } public static final void scheduleUntil(ProcessTarget t, Conditional cond, EventHandle handle) { Process cur = Process.current(); cur.evt().schedUntil(cur, t, cond, handle); } private void schedUntil(Process cur, ProcessTarget t, Conditional cond, EventHandle handle) { synchronized (lockObject) { cur.checkCallback(); ConditionalEvent evt = new ConditionalEvent(cond, t, handle); if (handle != null) { if (handle.isScheduled()) throw new ProcessError("Tried to scheduleUntil using a handle already in use"); handle.event = evt; } condEvents.add(evt); if (trcListener != null) { cur.beginCallbacks(); trcListener.traceWaitUntil(this, currentTick); cur.endCallbacks(); } } } public static final void startProcess(ProcessTarget t) { Process cur = Process.current(); cur.evt().start(cur, t); } private void start(Process cur, ProcessTarget t) { Process newProcess = Process.allocate(this, cur, t); // Notify the eventManager that a new process has been started synchronized (lockObject) { cur.checkCallback(); if (trcListener != null) { cur.beginCallbacks(); trcListener.traceProcessStart(this, t, currentTick); cur.endCallbacks(); } // Transfer control to the new process newProcess.wake(); threadWait(cur); } } /** * Remove an event from the eventList, must hold the lockObject. * @param idx * @return */ private void removeEvent(Event evt) { EventNode node = evt.node; node.removeEvent(evt); if (node.head == null) { if (!eventTree.removeNode(node.schedTick, node.priority)) throw new ProcessError("Tried to remove an eventnode that could not be found"); } // Clear the event to reuse it evt.node = null; evt.target = null; if (evt.handle != null) { evt.handle.event = null; evt.handle = null; } evt.next = freeEvents; freeEvents = evt; } private ProcessTarget rem(EventHandle handle) { BaseEvent base = handle.event; ProcessTarget t = base.target; handle.event = null; base.handle = null; if (base instanceof Event) { removeEvent((Event)base); } else { condEvents.remove(base); } return t; } /** * Removes the event held in the EventHandle and disposes of it, the ProcessTarget is not run. * If the handle does not currently hold a scheduled event, this method simply returns. * @throws ProcessError if called outside of a Process context */ public static final void killEvent(EventHandle handle) { Process cur = Process.current(); cur.evt().killEvent(cur, handle); } /** * Removes an event from the pending list without executing it. */ private void killEvent(Process cur, EventHandle handle) { synchronized (lockObject) { cur.checkCallback(); // no handle given, or Handle was not scheduled, nothing to do if (handle == null || handle.event == null) return; if (trcListener != null) { cur.beginCallbacks(); trcKill(handle.event); cur.endCallbacks(); } ProcessTarget t = rem(handle); t.kill(); } } private void trcKill(BaseEvent event) { if (event instanceof Event) { EventNode node = ((Event)event).node; trcListener.traceKill(this, currentTick, node.schedTick, node.priority, event.target); } else { trcListener.traceKill(this, currentTick, -1, -1, event.target); } } /** * Interrupts the event held in the EventHandle and immediately runs the ProcessTarget. * If the handle does not currently hold a scheduled event, this method simply returns. * @throws ProcessError if called outside of a Process context */ public static final void interruptEvent(EventHandle handle) { Process cur = Process.current(); cur.evt().interruptEvent(cur, handle); } /** * Removes an event from the pending list and executes it. */ private void interruptEvent(Process cur, EventHandle handle) { synchronized (lockObject) { cur.checkCallback(); // no handle given, or Handle was not scheduled, nothing to do if (handle == null || handle.event == null) return; if (trcListener != null) { cur.beginCallbacks(); trcInterrupt(handle.event); cur.endCallbacks(); } ProcessTarget t = rem(handle); Process proc = t.getProcess(); if (proc == null) proc = Process.allocate(this, cur, t); proc.setNextProcess(cur); proc.wake(); threadWait(cur); } } private void trcInterrupt(BaseEvent event) { if (event instanceof Event) { EventNode node = ((Event)event).node; trcListener.traceInterrupt(this, currentTick, node.schedTick, node.priority, event.target); } else { trcListener.traceInterrupt(this, currentTick, -1, -1, event.target); } } public void setExecuteRealTime(boolean useRealTime, double factor) { executeRealTime = useRealTime; realTimeFactor = factor; if (useRealTime) rebaseRealTime = true; } /** * Locks the calling thread in an inactive state to the global lock. * When a new thread is created, and the current thread has been pushed * onto the inactive thread stack it must be put to sleep to preserve * program ordering. * <p> * The function takes no parameters, it puts the calling thread to sleep. * This method is NOT static as it requires the use of wait() which cannot * be called from a static context * <p> * There is a synchronized block of code that will acquire the global lock * and then wait() the current thread. */ private void threadWait(Process cur) { // Ensure that the thread owns the global thread lock try { /* * Halt the thread and only wake up by being interrupted. * * The infinite loop is _absolutely_ necessary to prevent * spurious wakeups from waking us early....which causes the * model to get into an inconsistent state causing crashes. */ while (true) { lockObject.wait(); } } // Catch the exception when the thread is interrupted catch( InterruptedException e ) {} if (cur.shouldDie()) throw new ThreadKilledException("Thread killed"); } public static final double calcSimTime(double secs) { EventManager evt = Process.current().evt(); long ticks = evt.secondsToNearestTick(secs) + evt.currentTick; if (ticks < 0) ticks = Long.MAX_VALUE; return evt.ticksToSeconds(ticks); } public static final long calcSimTicks(double secs) { EventManager evt = Process.current().evt(); long ticks = evt.secondsToNearestTick(secs) + evt.currentTick; if (ticks < 0) ticks = Long.MAX_VALUE; return ticks; } public void scheduleProcessExternal(long waitLength, int eventPriority, boolean fifo, ProcessTarget t, EventHandle handle) { synchronized (lockObject) { long schedTick = calculateEventTime(waitLength); EventNode node = getEventNode(schedTick, eventPriority); Event evt = getEvent(); evt.node = node; evt.target = t; evt.handle = handle; if (handle != null) { if (handle.isScheduled()) throw new ProcessError("Tried to schedule using an EventHandle already in use"); handle.event = evt; } if (trcListener != null) trcListener.traceSchedProcess(this, currentTick, schedTick, eventPriority, t); node.addEvent(evt, fifo); } } /** * Schedule a future event in the controlling EventManager for the current Process. * @throws ProcessError if called outside of a Process context * * @param waitLength the number of ticks in the future to schedule this event * @param eventPriority the priority of the scheduled event * @param fifo break ties with previously scheduled events using FIFO/LIFO ordering * @param t the process target to run when the event is executed * @param handle an optional handle to hold onto the scheduled event */ public static final void scheduleTicks(long waitLength, int eventPriority, boolean fifo, ProcessTarget t, EventHandle handle) { Process cur = Process.current(); cur.evt().scheduleTicks(cur, waitLength, eventPriority, fifo, t, handle); } /** * Schedule a future event in the controlling EventManager for the current Process. * @throws ProcessError if called outside of a Process context * * @param secs the number of seconds in the future to schedule this event * @param eventPriority the priority of the scheduled event * @param fifo break ties with previously scheduled events using FIFO/LIFO ordering * @param t the process target to run when the event is executed * @param handle an optional handle to hold onto the scheduled event */ public static final void scheduleSeconds(double secs, int eventPriority, boolean fifo, ProcessTarget t, EventHandle handle) { Process cur = Process.current(); long ticks = cur.evt().secondsToNearestTick(secs); cur.evt().scheduleTicks(cur, ticks, eventPriority, fifo, t, handle); } private void scheduleTicks(Process cur, long waitLength, int eventPriority, boolean fifo, ProcessTarget t, EventHandle handle) { cur.checkCallback(); long schedTick = calculateEventTime(waitLength); EventNode node = getEventNode(schedTick, eventPriority); Event evt = getEvent(); evt.node = node; evt.target = t; evt.handle = handle; if (handle != null) { if (handle.isScheduled()) throw new ProcessError("Tried to schedule using an EventHandle already in use"); handle.event = evt; } if (trcListener != null) { cur.beginCallbacks(); trcListener.traceSchedProcess(this, currentTick, schedTick, eventPriority, t); cur.endCallbacks(); } node.addEvent(evt, fifo); } /** * Sets the value that is tested in the doProcess loop to determine if the * next event should be executed. If set to false, the eventManager will * execute a threadWait() and wait until an interrupt is generated. It is * guaranteed in this state that there is an empty thread stack and the * thread referenced in activeThread is the eventManager thread. */ public void pause() { executeEvents = false; } /** * Sets the value that is tested in the doProcess loop to determine if the * next event should be executed. Generates an interrupt of activeThread * in case the eventManager thread has already been paused and needs to * resume the event execution loop. This prevents the model being resumed * from an inconsistent state. */ public void resume(long targetTicks) { synchronized (lockObject) { targetTick = targetTicks; rebaseRealTime = true; if (executeEvents) return; executeEvents = true; Process.processEvents(this); } } @Override public String toString() { return name; } /** * Returns whether or not we are currently running in a Process context * that has a controlling EventManager. * @return true if we are in a Process context, false otherwise */ public static final boolean hasCurrent() { return (Thread.currentThread() instanceof Process); } /** * Returns the controlling EventManager for the current Process. * @throws ProcessError if called outside of a Process context */ public static final EventManager current() { return Process.current().evt(); } /** * Returns the current simulation tick for the current Process. * @throws ProcessError if called outside of a Process context */ public static final long simTicks() { return Process.current().evt().currentTick; } /** * Returns the current simulation time in seconds for the current Process. * @throws ProcessError if called outside of a Process context */ public static final double simSeconds() { return Process.current().evt()._simSeconds(); } private double _simSeconds() { return currentTick * secsPerTick; } public final void setTickLength(double tickLength) { secsPerTick = tickLength; ticksPerSecond = Math.round(1e9d / secsPerTick) / 1e9d; globalsecsPerTick = secsPerTick; globalticksPerSecond = ticksPerSecond; } /** * Convert the number of seconds rounded to the nearest tick. */ public final long secondsToNearestTick(double seconds) { return Math.round(seconds * ticksPerSecond); } /** * Convert the number of ticks into a value in seconds. */ public final double ticksToSeconds(long ticks) { return ticks * secsPerTick; } /** * This whole block is a temporary crutch until we decide how access to time conversion * should be exposed. */ private static double globalsecsPerTick = 1e-6d; private static double globalticksPerSecond = Math.round(globalsecsPerTick) / 1e9d; /** * Convert the number of seconds rounded to the nearest tick. The same as EventManager.secondsToNearestTick() */ public static final long secsToNearestTick(double seconds) { return Math.round(seconds * globalticksPerSecond); } /** * Convert the number of ticks into a value in seconds. The same as EventManager.ticksToSeconds() */ public static final double ticksToSecs(long ticks) { return ticks * globalsecsPerTick; } }