Thread: Accurate EventManager, an fast and accurate alternative to unreliable process timers.

Page 1 of 37 12311 ... LastLast
Results 1 to 10 of 367
  1. #1 Accurate EventManager, an fast and accurate alternative to unreliable process timers. 
    Programmer, Contributor, RM and Veteran




    Join Date
    Mar 2007
    Posts
    5,147
    Thanks given
    2,656
    Thanks received
    3,731
    Rep Power
    5000
    This tutorial is now obsolete! See my new Cycle-based Task Manager for a better alternative!

    Description: Adds an accurate, fast and stable event manager, an alternative to process().

    Assumed Knowledge: How to read, adding new classes, how to modify existing classes, common sense.

    Tested Server: Winterlove, should work in all.

    Files/Classes modified: server, client

    Files/Classes added: EventManager, EventContainer, Event

    Difficulty: If you can read and have common sense - easy.

    Welcome to my second large tutorial, which is going to teach you why process is a failure for timing-critical code, and how to create a fast and stable EventManager, which is a great alternative to using process().

    This tutorial is going to be split into two parts: the first part is going to explain why process() is an unreliable method for timing, and why it causes so much lag. The second part is going to explain how to add an accurate, lag-free and stable alternative to using process() timers.

    As usual, please backup your server before making such a big and complicated change.

    Now I got that over with, we can begin with the real stuff:

    PART 1: WHY PROCESS IS UNRELIABLE FOR TIMING

    You may be wondering why I say process() is unreliable for timing. Process() is called every 500ms. The main server loop ensures this. If you have a look at the code, you even see it takes into account the previous processing time.

    For instance on a large server, a situation like this happens:

    1. Players are being processed, there are hundreds so it takes a long time. (E.g. 200 ms).

    2. Server sleeps for 300 ms

    That means the total execution time of the loop would have been 500ms, meaning a stable call to the player processing every 500 ms.

    Now we are going to step through your server and see what actually happens when the players are processed.

    In the server class, you will see this call in the main server loop:

    Code:
    playerHandler.process();
    That's being called every 500ms and runs in a stable loop.

    So lets go to the PlayerHandler class, and see what the process() there is actually doing.

    It is pretty complicated and scary, but we are looking for this part:

    Code:
    for(int i = 0; i < MAXIMUM_PLAYERS; i++) {
    			if(players[i] == null) continue;
    
    			players[i].actionAmount--;
    
    			players[i].preProcessing();
    			while(players[i].process());
    			players[i].postProcessing();
    If you can see, players[i].process is being called in a while loop. That means it could be called multiple times. So our process() method in the client class, which some people use for timing could actually be run more than once every 500ms, making unreliable timers! Your timers could be running twice, three times or four times as fast!

    Now, we are going to look at the process() method in the client class. Specifically, the end of it.

    The part we are looking at is this:

    Code:
    if(disconnected) return false;
    		try {
    			parseOutgoingPackets();
    			if(timeOutCounter++ > 20) {
    				Misc.println("Client lost connection: timeout");
    				disconnected = true;
    				return false;
    			}
    			if(in == null) return false;
    
    			int avail = in.available();
    			if(avail == 0) return false;
    
    			if(packetType == -1) {
    				packetType = in.read() & 0xff;
    				if(inStreamDecryption != null)
    					packetType = packetType - inStreamDecryption.getNextKey() & 0xff;
    				packetSize = packetSizes[packetType];
    				avail--;
                }
    			if(packetSize == -1) {
    				if(avail > 0) {
    					// this is a variable size packet, the next byte containing the length of said
    					packetSize = in.read() & 0xff;
    					avail--;
    				}
    				else return false;
    			}
    			if(avail < packetSize) return false;	// packet not completely arrived here yet
    
    			fillInStream(packetSize);
                timeOutCounter = 0;			// reset
    
    			parseIncomingPackets();		// method that does actually interprete these packets
    
    			packetType = -1;
    		} catch(java.lang.Exception __ex) {
    			Misc.println("BlakeScape Server: Exception!");
    			__ex.printStackTrace(); 
    			disconnected = true;
    		}
    		return true;
    As it runs on a while loop, whenever true is returned, it runs a second time. If you see, true is returned whenever a whole packet is read.

    That means, a clever bot (or player), could send a huge amount of packets in 500 ms. Maybe 10. Maybe 20. And then that would cause process() to be called 10 or 20 times a second. Maybe every 30, 40 or 50!

    In fact, you don't even need a bot to do this. Mass clicking will do. So that means, your process() timers could be running anywhere from 10x - 50x faster than they should with a clever player!

    That is why process() is a complete failure for putting timers. In fact, in my opinion, you should only put packet processing code there.

    Also, if you put lots of timers (that don't run often), there is little point. Why have it consuming CPU power when you could put them in a list (array) of events?

    And now we move onto the next part, how to use a stable event manager for timers.

    This uses some more complicated parts of java, including inner classes, so it may seem confusing at first. However, with practice, this becomes a much easier way than using process timers.

    PART 2: ADDING A STABLE AND RELIABLE EVENTMANAGER

    I have programmed a stable and reliable event manager. It does not consume CPU when no events are running (through the use of wait), and waits until an event needs to be run.

    It runs in its own thread so does not interrupt player processing and this also means you can have timers that run faster than 500ms.

    The timers don't need to run in multiples of 500ms as well, so they could run in 600ms, 700ms, etc!

    Note: There is already an event manager in RuneFusion, however this is far superior to it, so you may wish to consider replacing it. There is also already an event manager in RS2D.

    Step 1: The Event Manager class

    The event manager class is the core of the event system. It runs in its own thread, and it executes events that need to be executed.

    Here is the code for it:

    Code:
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Manages events which will be run in the future.
     * Has its own thread since some events may need to be ran faster than the cycle time
     * in the main thread.
     * 
     * @author Graham
     *
     */
    public class EventManager implements Runnable {
    	
    	/**
    	 * A reference to the singleton;
    	 */
    	private static EventManager singleton = null;
    	
    	/**
    	 * A list of events that are being executed.
    	 */
    	private List<EventContainer> events;
    	
    	/**
    	 * Initialise the event manager.
    	 */
    	private EventManager() {
    		events = new ArrayList<EventContainer>();
    	}
    	
    	/**
    	 * The event manager thread. So we can interrupt it and end it nicely on shutdown.
    	 */
    	private Thread thread;
    	
    	/**
    	 * Gets the event manager singleton. If there is no singleton, the singleton is created.
    	 * @return The event manager singleton.
    	 */
    	public static EventManager getSingleton() {
    		if(singleton == null) {
    			singleton = new EventManager();
    			singleton.thread = new Thread(singleton);
    			singleton.thread.start();
    		}
    		return singleton;
    	}
    	
    	/**
    	 * Initialises the event manager (if it needs to be).
    	 */
    	public static void initialise() {
    		getSingleton();
    	}
    	
    	/**
    	 * The waitFor variable is multiplied by this before the call to wait() is made.
    	 * We do this because other events may be executed after waitFor is set (and take time).
    	 * We may need to modify this depending on event count? Some proper tests need to be done.
    	 */
    	private static final double WAIT_FOR_FACTOR = 0.5;
    
    	@Override
    	/**
    	 * Processes events. Works kinda like newer versions of cron.
    	 */
    	public synchronized void run() {
    		long waitFor = -1;
    		List<EventContainer> remove = new ArrayList<EventContainer>();
    		
    		while(true) {
    			
    			// reset wait time
    			waitFor = -1;
    			
    			// process all events
    			for(EventContainer container : events) {
    				if(container.isRunning()) {
    					if((System.currentTimeMillis() - container.getLastRun()) >= container.getTick()) {
    						container.execute();
    					}
    					if(container.getTick() < waitFor || waitFor == -1) {
    						waitFor = container.getTick();
    					}
    				} else {
    					// add to remove list
    					remove.add(container);
    				}
    			}
    			
    			// remove events that have completed
    			for(EventContainer container : remove) {
    				events.remove(container);
    			}
    			remove.clear();
    			
    			// no events running
    			try {
    				if(waitFor == -1) {
    					wait(); // wait with no timeout
    				} else {
    					// an event is running, wait for that time or until a new event is added
    					int decimalWaitFor = (int)(Math.ceil(waitFor*WAIT_FOR_FACTOR));
    					wait(decimalWaitFor);
    				}
    			} catch(InterruptedException e) {
    				break; // stop running
    			}
    		}
    	}
    	
    	/**
    	 * Adds an event.
    	 * @param event The event to add.
    	 * @param tick The tick time.
    	 */
    	public synchronized void addEvent(Event event, int tick) {
    		events.add(new EventContainer(event,tick));
    		notify();
    	}
    	
    	/**
    	 * Shuts the event manager down.
    	 */
    	public void shutdown() {
    		this.thread.interrupt();
    	}
    
    }
    Now I will explain what each part does:

    Code:
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Manages events which will be run in the future.
     * Has its own thread since some events may need to be ran faster than the cycle time
     * in the main thread.
     * 
     * @author Graham
     *
     */
    public class EventManager implements Runnable {
    All the standard stuff.

    Code:
    	/**
    	 * A reference to the singleton;
    	 */
    	private static EventManager singleton = null;
    We have one event manager for the whole server, so use the singleton design pattern, instead of making it all static. It basically means, to access the event manager, you can do this:

    Code:
    EventManager.getSingleton().doSomething();
    Instead of (the bad):

    Code:
    server.eventManager.doSomething();
    Next:

    Code:
    	/**
    	 * A list of events that are being executed.
    	 */
    	private List<EventContainer> events;
    We need a list of events that are still be executed, so the definition for that list is there.

    Code:
    	/**
    	 * Initialise the event manager.
    	 */
    	private EventManager() {
    		events = new ArrayList<EventContainer>();
    	}
    That is the constructor, it just sets up the event list. It is set to private because it needs to be created through the getSingleton() method.

    Code:
    	/**
    	 * The event manager thread. So we can interrupt it and end it nicely on shutdown.
    	 */
    	private Thread thread;
    Because it runs in a thread we need to be able to stop it on server shutdown by interrupting it, so we store the reference to its thread here.

    Code:
    	/**
    	 * Gets the event manager singleton. If there is no singleton, the singleton is created.
    	 * @return The event manager singleton.
    	 */
    	public static EventManager getSingleton() {
    		if(singleton == null) {
    			singleton = new EventManager();
    			singleton.thread = new Thread(singleton);
    			singleton.thread.start();
    		}
    		return singleton;
    	}
    This function gets the singleton (explained above). If the singleton has not been created, it is created and the thread is started.

    Code:
    	/**
    	 * Initialises the event manager (if it needs to be).
    	 */
    	public static void initialise() {
    		getSingleton();
    	}
    This code can be used to initialise the event manager without getting a reference back to the singleton.

    Code:
    	/**
    	 * The waitFor variable is multiplied by this before the call to wait() is made.
    	 * We do this because other events may be executed after waitFor is set (and take time).
    	 * We may need to modify this depending on event count? Some proper tests need to be done.
    	 */
    	private static final double WAIT_FOR_FACTOR = 0.5;
    The wait for factor is kinda complicated to explain, I will explain it later. You may need to tweak this if you have a lot of events running simultaneously, but 0.5 seems a good value.

    Now we get onto the juicy stuff:

    Code:
    	@Override
    	/**
    	 * Processes events. Works kinda like newer versions of cron.
    	 */
    	public synchronized void run() {
    		long waitFor = -1;
    		List<EventContainer> remove = new ArrayList<EventContainer>();
    This is the run() method of the thread.

    The waitFor variable is used to store how long the thread should wait for while it is not executing events. It is set to -1 by default so it is actually calculated.

    The remove list is a list where we put old events (you cannot remove items from a list while you are iterating through it).

    Now onto the loop:

    Code:
    		while(true) {
    			
    			// reset wait time
    			waitFor = -1;
    The wait time needs to be reset since it needs to be calculated every loop.

    Code:
    			
    			// process all events
    			for(EventContainer container : events) {
    That code will iterate through running events.

    Code:
    				if(container.isRunning()) {
    					if((System.currentTimeMillis() - container.getLastRun()) >= container.getTick()) {
    						container.execute();
    					}
    This is where it gets complicated. This calculates when the event was last ran, and if it needs to be executed, it is executed.

    Code:
    					if(container.getTick() < waitFor || waitFor == -1) {
    						waitFor = container.getTick();
    					}
    Then it sets the waitFor variable if we need to wait() for a shorter time, or if it has not been calculated yet.

    Code:
    				} else {
    					// add to remove list
    					remove.add(container);
    				}
    			}
    If the event is not running, it is added to the remove list.

    Code:
    			// remove events that have completed
    			for(EventContainer container : remove) {
    				events.remove(container);
    			}
    			remove.clear();
    Now we finished iterating through the list we can remove elements from it.

    Code:
    			// no events running
    			try {
    				if(waitFor == -1) {
    					wait(); // wait with no timeout
    If wait was not set, no events are running, so we wait with no timeout.

    Code:
    				} else {
    					// an event is running, wait for that time or until a new event is added
    					int decimalWaitFor = (int)(Math.ceil(waitFor*WAIT_FOR_FACTOR));
    					wait(decimalWaitFor);
    				}
    Otherwise we wait for the specified amount of time.

    Code:
    			} catch(InterruptedException e) {
    				break; // stop running
    			}
    This piece of code checks if we were interrupted. When a thread is interrupted, it is basically a request for it to exit. So we nicely break the loop which will stop the thread.

    Code:
    		}
    	}
    Need I say?

    Code:
    	/**
    	 * Adds an event.
    	 * @param event The event to add.
    	 * @param tick The tick time.
    	 */
    	public synchronized void addEvent(Event event, int tick) {
    		events.add(new EventContainer(event,tick));
    		notify();
    	}
    This part of the code adds an event to the events list, and then calls notify() to make wait() end early. This will then make it recalculate all the timing etc etc.

    DUE TO THE NATURE OF THE CODE, YOU CANNOT CALL ADDEVENT INSIDE AN EVENT.

    Code:
    	/**
    	 * Shuts the event manager down.
    	 */
    	public void shutdown() {
    		this.thread.interrupt();
    	}
    
    }
    The last method is used to shut the event manager down nicely.

    Step 2: Adding the Event interface

    Here is the code for the Event interface:

    Code:
    /**
     * A simple interface for an event.
     * @author Graham
     *
     */
    public interface Event {
    	
    	/**
    	 * Called when the event is executed.
    	 * @param container The event container, so the event can dynamically change the tick time etc.
    	 */
    	public void execute(EventContainer container);
    
    }
    Pretty simple, isn't it?

    An interface cannot be created. We can't do Event e = new Event();. But we can implement an interface. One interface you probably know about is the Runnable interface, used for making threads.

    If a class implements an interface, it can be used where that interface is used. Which is why you can do new Thread(whatever). Because, the constructor to the Thread class looks like this:

    Code:
    Thread(Runnable r)
    It's a very useful concept, so I suggest you read more (this is not vital).

    However, an interface cannot hold data (such as the event timing code), so we have a container class:

    Code:
    /**
     * Holds extra data for an event (for example the tick time etc).
     * @author Graham
     *
     */
    public class EventContainer {
    	
    	/**
    	 * The tick time in milliseconds.
    	 */
    	private int tick;
    	
    	/**
    	 * The actual event.
    	 */
    	private Event event;
    	
    	/**
    	 * A flag which specifies if the event is running;
    	 */
    	private boolean isRunning;
    	
    	/**
    	 * When this event was last run.
    	 */
    	private long lastRun;
    	
    	/**
    	 * The event container.
    	 * @param evt
    	 * @param tick
    	 */
    	protected EventContainer(Event evt, int tick) {
    		this.tick = tick;
    		this.event = evt;
    		this.isRunning = true;
    		this.lastRun = System.currentTimeMillis();
    		// can be changed to 0 if you want events to run straight away
    	}
    	
    	/**
    	 * Stops this event.
    	 */
    	public void stop() {
    		this.isRunning = false;
    	}
    	
    	/**
    	 * Returns the is running flag.
    	 * @return
    	 */
    	public boolean isRunning() {
    		return this.isRunning;
    	}
    	
    	/**
    	 * Returns the tick time.
    	 * @return
    	 */
    	public int getTick() {
    		return this.tick;
    	}
    	
    	/**
    	 * Executes the event!
    	 */
    	public void execute() {
    		this.lastRun = System.currentTimeMillis();
    		this.event.execute(this);
    	}
    	
    	/**
    	 * Gets the last run time.
    	 * @return
    	 */
    	public long getLastRun() {
    		return this.lastRun;
    	}
    
    }
    This code is all fairly simple, and you should understand it.

    Finally, we need to integrate the code with the server class!

    This is really simple! Under your main method (should look like this):

    Code:
    public static void main(java.lang.String args[]) {
    Add the following snippet of code:

    Code:
    		EventManager.initialise();
    And then under:

    Code:
    		// shut down the server
    		playerHandler.destruct();
    		clientHandler.killServer();
    We need to shutdown the event manager:

    Code:
    		EventManager.getSingleton().shutdown();
    Now, the event manager is integrated, and I will show you how to use it!

    PART 3: HOW TO USE THE EVENT MANAGER

    This is very easy, all the other difficult code there was behind the scenes stuff.

    To add a new event, (for example hitting a dummy), add it under the appropriate case in the switch statement.

    Code:
    EventManager.getSingleton().addEvent(
        new Event() {
            public void execute(EventContainer c) {
                sendMessage("You hit the dummy.");
                // you could add animations here, addSkillXP, whatever you want
                c.stop(); // stops the event from running
            }
        }, 2000); // executes after 2,000 ms = 2 seconds
    };
    You can also add events that repeat, perhaps you want a global messages every 60 seconds?

    In that case, add this to your server startup code:

    Code:
    EventManager.getSingleton().addEvent(
        new Event() {
            public void execute(EventContainer c) {
                PlayerHandler.messageToAll = "Visit our forums at www.blahblah.com!";
                // c.stop(); commented out as not needed
            }
        }, 60000); // executes every 60,000 ms = 60 seconds
    };
    That's the end of the tutorial!

    Credits: 100% Me.

    I would appreciate it if you gave some mention to me in your server credits, since this took a long time to write.

    Thank you for reading, and enjoy your new stable event system. This would work well with my non threaded fix, found here http://www.rune-server.org/showthread.php?t=82322!
    .
    Reply With Quote  
     

  2. Thankful user:


  3. #2  
    Registered Member

    Join Date
    Jun 2007
    Posts
    2,237
    Thanks given
    267
    Thanks received
    411
    Rep Power
    1283
    The problem here is.

    If you click on a dummy, for example you wait 2 seconds then preform the action
    Don't worry, Be happy.
    Reply With Quote  
     

  4. Thankful user:


  5. #3  
    Programmer, Contributor, RM and Veteran




    Join Date
    Mar 2007
    Posts
    5,147
    Thanks given
    2,656
    Thanks received
    3,731
    Rep Power
    5000
    Quote Originally Posted by surfer25 View Post
    The problem here is.

    If you click on a dummy, for example you wait 2 seconds then preform the action
    That's just an example usage.

    You would want to start the animation first, and then set the event running, and then stop the animation, give xp etc afterwards.

    It would be more suited to things like woodcutting, mining, fighting etc I guess, but I'm not posting a full example.

    Anyway, this ain't about the example, it's about the eventmanager really and why process() is bad.
    .
    Reply With Quote  
     

  6. Thankful user:


  7. #4  
    SERGEANT OF THE MASTER SERGEANTS MOST IMPORTANT PERSON OF EXTREME SERGEANTS TO THE MAX!

    cube's Avatar
    Join Date
    Jun 2007
    Posts
    8,871
    Thanks given
    1,854
    Thanks received
    4,745
    Rep Power
    5000
    Wow....

    I'm definently going to use this

    EDIT: This must be stickied !

    Attached image

    Reply With Quote  
     

  8. Thankful user:


  9. #5  
    Registered Member

    Join Date
    Jun 2007
    Posts
    2,237
    Thanks given
    267
    Thanks received
    411
    Rep Power
    1283
    Quote Originally Posted by Graham View Post
    That's just an example usage.

    You would want to start the animation first, and then set the event running, and then stop the animation, give xp etc afterwards.

    It would be more suited to things like woodcutting, mining, fighting etc I guess, but I'm not posting a full example.

    Anyway, this ain't about the example, it's about the eventmanager really and why process() is bad.
    I understand that, and honestly you deserve alot for you work here.
    But i was pointing out a small flaw

    But fantastic work none the less
    Don't worry, Be happy.
    Reply With Quote  
     

  10. Thankful user:


  11. #6  
    Programmer, Contributor, RM and Veteran




    Join Date
    Mar 2007
    Posts
    5,147
    Thanks given
    2,656
    Thanks received
    3,731
    Rep Power
    5000
    Thanks for the positive comments everyone. If people use this in a live server, I would like to know how they get on with it, etc. Also, I may be adding a download with this included already just like I did with my non thread-per-client tutorial, although again, that all depends on feedback and how easy people think it is.
    .
    Reply With Quote  
     

  12. Thankful user:


  13. #7  
    Registered Member
    Join Date
    Nov 2007
    Age
    30
    Posts
    517
    Thanks given
    1
    Thanks received
    3
    Rep Power
    73
    This is really nice, you have good tutorials.
    Spoiler for Show GIFs:

    Reply With Quote  
     

  14. Thankful user:


  15. #8  
    Programmer, Contributor, RM and Veteran




    Join Date
    Mar 2007
    Posts
    5,147
    Thanks given
    2,656
    Thanks received
    3,731
    Rep Power
    5000
    Quote Originally Posted by Zombiedevice View Post
    This is really nice, you have good tutorials.
    Thanks.

    It's sad to see things like this get pushed down the page because a lot of people just don't understand how things work in their server (like why process() fails etc).

    Btw, just to let you all know, I'm working on a server with all of this integrated (and non thread-per-client etc). Should be interesting when I'm done.
    .
    Reply With Quote  
     

  16. Thankful user:


  17. #9  
    Optimist

    Vice's Avatar
    Join Date
    Nov 2007
    Age
    28
    Posts
    3,263
    Thanks given
    3
    Thanks received
    59
    Rep Power
    2536
    This is amazing to be honest, sticky material...

    2k posts
    Jack
    Scotland
    Undergraduate - BSc Computing Science
    Reply With Quote  
     

  18. Thankful user:


  19. #10  
    stunning ko
    Guest
    wow impressive man..!
    Reply With Quote  
     

  20. Thankful user:


Page 1 of 37 12311 ... LastLast

Thread Information
Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)


User Tag List

Posting Permissions
  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •