How properly track "countdowns" for "lazy" events

Discussion in 'Resources' started by RawCode, Jan 10, 2014.

Thread Status:
Not open for further replies.
  1. Offline

    RawCode

    ps. my english is "not good", i will fix any grammar problems asap, but please use PMs for this, not rageposts.

    In this article i will explain and show how to implement efficient lazy countdown tracking without usage of Bukkit Tasks.
    I will track PvP combat state of players and will use my private plugin as source of code samples.

    Lazy is generic term, code wont actively perform any actions, actions will be performed only when required. If action not required right now it will be dalayed as long as possible.


    1) Static section and data storage:

    Code:java
    1. static long EPOCH_TIME_OFFSET = System.currentTimeMillis();
    2. static HashMap<String,_PvP_Wrapper> HASH_STORAGE = new HashMap<String,_PvP_Wrapper>();
    3. static byte GC_EVENT = 100;
    4. static int COUNTDOWN = 10000;
    5.  
    6. static class _PvP_Wrapper
    7. {
    8. public int TimeStamp;
    9. public String Source;
    10.  
    11. public _PvP_Wrapper(int i, String s){
    12. TimeStamp = i;
    13. Source = s;
    14. }
    15. }


    EPOCH_TIME_OFFSET used to compress current time into integer, it will work without issues as long as server not running for more then 68 years without restart.
    Also its possible to use this field to get uptime.

    HASH_STORAGE simple hashmap used to adress _PvP_Wrapper instance by string.

    GC_EVENT with magical value of 100 used for manual garbage collection of obsolete data.
    Yes yes i perfectly correct, our plugin will feature manual memory reclamation in java.

    _PvP_Wrapper storage for int and string.

    If you want to obfuscate your code you can use Object[] construction:
    Code:java
    1. static HashMap<String,Object[]> HASH_STORAGE = new HashMap<String,Object[]>();

    Object[] can hold reference to other Object[].
    Most IDEs will throw "rawtypes" warning for such declarations.

    2. Entry point:

    Code:java
    1. @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    2. public void EntityDamageEvent(EntityDamageByEntityEvent event) {
    3. if (!(event.getEntity() instanceof Player)) {
    4. return;
    5. }
    6. if (!(event.getDamager() instanceof Player)) {
    7. return;
    8. }
    9.  
    10. String Key = ((Player) event.getEntity()).getName().toLowerCase();
    11. int TimeStamp = (int)(System.currentTimeMillis() - EPOCH_TIME_OFFSET);
    12.  
    13. HASH_STORAGE.put(Key,new _PvP_Wrapper(TimeStamp,((Player) event.getDamager
    14. if (GC_EVENT-- == 0)PERFORM_GC_EVENT();
    15. }


    Its possible to check multiple conditions inside single "IF" with "&&", in current case with "||" and place return on same line, but this will make your code a lot harder to debug if you decide to.
    In code above i removed "Echo" calls before both returns, you can place them back.

    Code:java
    1. public static void Echo(String S){
    2. if (DEBUG)System.out.println(S);
    3. }


    Hashmap implementation check keys with "equals" method, it's not possible for players with same name in different case to join server at same time, but possible for player to change case of name and join back, equals method will return false in such case.
    To overcome this issue, you shoud always cast name into lowercase before using it as hashmap key or SQL key or any other key, in sample 10 seconds of countdown, but similar system can be used to store bans, money or something valueble.

    Hashmap replace objects sharing same key without any warnings or issues, there is no reason to check if key is already exists if you want to put object into hashmap.
    Value that existed before will be replaced and garbage collected (if no other fields hold reference to it).

    3. Usage:

    Code:java
    1. @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
    2. public void PlayerCommandPreprocessEvent(PlayerCommandPreprocessEvent event)
    3. {
    4.  
    5. String Key = event.getPlayer().getName().toLowerCase();
    6. _PvP_Wrapper Container = HASH_STORAGE.get(Key);
    7. if (Container == null) return;
    8.  
    9. int TimeOffset = (int)(System.currentTimeMillis() - EPOCH_TIME_OFFSET);
    10.  
    11. if (TimeOffset - Container.TimeStamp <= COUNTDOWN)
    12. {
    13. event.getPlayer().sendMessage(ChatColor.RED +
    14. "You cannot use any commands when fighting with " + Container.Source);
    15. event.setCancelled(true);
    16. return;
    17. }
    18. HASH_STORAGE.remove(Key);
    19. }


    When event we planning to block is occured, we perform hashmap check, if map does not contains record we return control, this means that player has no active countdowns.

    If record is present, we calculate TimeOffset and compare it with record, if difference equals or less then countdown, we throw warning and cancel event.
    If difference is greater - countdown already passed, we remove record from hashtable silently without altering event outcome.

    4. Garbage collection

    Code:java
    1. static void PERFORM_GC_EVENT(){
    2. GC_EVENT = 100;
    3. int TimeOffset = (int)(System.currentTimeMillis() - EPOCH_TIME_OFFSET);
    4. Iterator<_PvP_Wrapper> Source = HASH_STORAGE.values().iterator();
    5.  
    6. int LocalTime = 0;
    7. while (Source.hasNext())
    8. {
    9. LocalTime = Source.next().TimeStamp;
    10. if (TimeOffset - LocalTime >= COUNTDOWN){
    11. Source.remove();
    12. }
    13. }
    14. }


    As i stated above, our plugin is lazy, it will perform actions only when nessesary without any prefetch. If player not invoked any command after PvP combat, record will stay for 68 years.
    Some players may leave server permanently and never ever return.
    For such case we perform lazy task of garbage collection on ever 100th invocation of PvP damage event.
    100th invocation is not good for servers with online larger then 30 or for servers with intense PvP events, in such case recommended to GC every 10-20 minutes, in other case resource heavy GC operation will occur too ofter for no good reason.

    Code above will iterate all records that exists in hashmap and remove ones with countdown expired.

    Tasks will actively set values, ever if such actions not required now, lazy tracking wont.
    GC also shoud be perfromed before hashmap serialization on shutdown and after deserialization on load, in other case multiple obsolete records will consume space for no good reason till next GC event.
     
    GregMC, Wizehh and xTrollxDudex like this.
  2. Offline

    xTrollxDudex

  3. Offline

    mkremins

    For this particular use-case, as an alternative to iterating over the map periodically to remove expired records, you could probably get better performance and more elegant code by listening for PlayerQuitEvents and removing the quitting player's record (if it exists) from the hashmap at this point. This approach also gives you a way to catch and handle combat-logging players "for free".

    Aside from that, nice write-up – I see code of this nature popping up pretty frequently in Bukkit plugins, and it'd be great for server scalability if more plugin developers became aware of ways to avoid the overhead associated with a background timer task :)
     
  4. Offline

    RawCode

    mkremins
    This is sample, not complete plugin or complete solution (i will post complete plugin with some fixes on github later)
    Listening quit event is bad thing in most cases of countdown tracking, especially if players may notice that rejoining reset relatively long countdown on something.


    For this reason i included garbage collection in sample, that will garbage collect time to time only expired records, garbage collection in sample is not "smart" but its possible to perform GC based on number of players currently online, number of player changes (unique players leave\join), based of hashtable size and other factors.
    Basically there is no reason to perform any GC if players on server are same (number of player changes is zero).

    Also due to hashtable memory allocation there is no real reason to remove records for players who still online (they likely to get PvP damage again), players who left recently (they likely to join again and get PvP damage).

    Later i will update this article and include both benchmark results (with answers to questions like, what is faster - create new wrapper object or set both fields of existing one) and complete source code of plugin.

    xTrollxDudex
    There is no reason to use nanotime here, only reason to use nanotime is benchmarking and cryptography (each invocation of nanotime will return new nanotime).

    Also i dont "like" time "compression" by casting long to int, its very likely that such operation cost more resources then just using long on 64 bit machines.
    I will update article with benchmarks both for nanotime vs timemillis and casted int vs long.
     
  5. Offline

    Wizehh

    nice tutorial, this will help a lot of people.
     
  6. Offline

    Bloxcraft


    nanoTime() is very resource intensive to use, and you don't need the accuracy it provides here. currentTimeMillis() will work fine
     
  7. Offline

    xTrollxDudex

    It's precision, not accuracy.
    What nanoTime does is create a high precision measurement of time for resource intensivity, it is not technically "accurate".
     
  8. Offline

    RawCode

    xTrollxDudex

    Please explain why you want to track something with nanotime precision?
    Expecting benchmarking and cryptography.
     
  9. Offline

    xTrollxDudex

    RawCode
    I didn't say I used it...
     
Thread Status:
Not open for further replies.

Share This Page