Making Time Calculations with Java Timestamps: The best performance-friendly solution to cooldowns

Awhile ago, while trying to become more active in the Spigot community offering help and discussing different methods of accomplishing things, I email subscribed to the Plugin Development section. I now get an email for every post, which occurs atleast once every few minutes. This means I'll probably have to get a new hard drive months before I normally would've, but atleast I can catch posts that are intriguing or spark a train of thought in some way.

For some strange reason, atleast one post a day is someone trying to add a cooldown effect to some tool or ability - basically, a player must wait x seconds before being able to use a feature again. 

People don't seem to be able to use the search feature on the Spigot Forums before posting.

People don't seem to be able to use the search feature on the Spigot Forums before posting.

I can't help myself from responding to them as I see them, so instead I'm making this handy-dandy post with everything you need to know about how to create cooldowns, common pitfalls to avoid, and case examples.

Option #1: Repeating or delayed tasks

This is what I see getting most suggested after using timestamps. Basically, when the player uses the feature for the first time, a variable is set putting a player's UUID and the amount of seconds until they can use that feature again into a map, and a repeating task is started using Bukkit's Scheduler. In that task, the variable is decreased by 1 every second. If the player tries to use the feature, it will get the seconds from the list and if it's more than 0 it'll tell the player it has to wait <x> seconds. 

Why this is an awful method

When using countdowns of any kind, it's always tempting to use repeating tasks. It looks like it makes sense, right? In reality, it's not very performance friendly. First, it's not a good idea to have too many tasks running at a time, as a thread is created for every task. Second, you're changing a variable every second. That doesn't affect performance too much, but there are methods that are much more performance friendly. The main issue, though, is that tasks are just a little weird to deal with anyway. They need to be cancelled when no longer used, and a whole class is needed aswell, with a reference to either the task ID or it's actual object (thought it can also be cancelled from within the task itself). Using tasks should be a last resort, they just aren't a very code-clean method.

Option #2: Timestamps

The only reason you don't see timestamps as a solution to time tracking is that they can be a little difficult to wrap your noggin around the first time you hear about it - it's a little less beginner friendly, but much, much cleaner once you get the concept.

What are timestamps?

Timestamps are very simple by themselves. Simply put, it's the amount of milliseconds that have passed since January 1st, 1970. It's used by all systems. For example, your system clock runs off of that timestamp, and simply translates that into whatever format it requires. You can get the current system timestamp by using System.currentTimeMillis(). As the variable won't fit in an int type variable, it must be stored as a Long.

How it works

When a player uses the feature, their ID and the timestamp at that time is stored in a map inside a manager class. When a player attempts to use the feature again, you can use the equation <currentTimestamp> - <lastFeatureUseTimestamp> to get how many milliseconds have passed since the feature has been last used. From there, you can convert that into whatever time format you want to check it against the required time before the player can use it again.

In order to make time conversions easier, Java has a util class called TimeUnit. To convert milliseconds to seconds, for example, you can use the method TimeUnit.MILLISECONDS.toSeconds(<ms>). 

As you can tell, it is very performance friendly, so much more so than the previous method with repeating tasks. The variable is only set once, and it's even only checked or changed when the player attempts to use the feature. 

Potential issues and how to fix/avoid them

The only issue with this method is that it becomes a memory leak. It's very easy to solve, however, with these two fixes:

  1. On PlayerQuitEvent, remove them from the cooldown manager map
  2. If they use the feature and the cooldown has expired, remove them from the map

You'll notice how much easier it is to handle cooldowns, and with this method you also have the option to display it in whatever format you want.