How to understand Spark reports to fix Minecraft Server lag

How to understand Spark reports to fix Minecraft Server lag

Posted on by

In Minecraft with tags Explainer, Minecraft Servers, Performance

Part 25 in a series on Running A Minecraft Server

1454 words, 5 minutes to read

Spark is currently the most popular tool for identifying performance issues on a Minecraft server, even recently replacing the old Timings system in Paper. While it’s more accurate than Timings, it can be more difficult to read and can lead to some of the same misinterpretations if you don’t fully understand how it works. If you need help with installing Spark, or the process of actually getting a report, Spark has excellent documentation.

How is it better than Timings?

Spark is a CPU sampler. As explained in my previous post on Timings, a sampler measures the time between different “sample points” in the code. The difference between Spark and Timings, however, is that Spark automatically handles these sample points. Timings is extremely limited in comparison, because all of these sample points must be manually written into the code. This means that while Timings can tell you what events on the server are slow, Spark can to an extent tell you why they are slow.

Due to the significantly larger number of sample points, it also means that Spark’s reports are more complex. Rather than just showing what plugins take up a percentage of time per event, Spark lets you step through how much time each part of the server’s code takes. This gives you a much more fine-grained view of what’s slow, but also means you need to have more contextual understanding to know what’s slow.

Reading the Spark report

At its core, Spark is made up of a waterfall graph that you can expand to show more details. The further right you go, the deeper in the code hierarchy you’re looking. If you expand a line, you’re viewing the code “inside” the line you’ve expanded.

The Spark waterfall graph
The Spark waterfall graph
Image transcript / description

Visible text

Server thread 100.00%
java.lang.Thread.run() 97.84%
java.lang.Thread.runWith() 97.84%
net.minecraft.server.MinecraftServer$$Lambda.0x00007efca4e08fd0.run() 97.84%
net.minecraft.server.MinecraftServer.lambda$spin$2() 97.84%
net.minecraft.server.MinecraftServer.runServer() 97.84%
net.minecraft.server.MinecraftServer.tickServer() 93.35%
net.minecraft.server.MinecraftServer.tickChildren() 93.25%
org.bukkit.craftbukkit.scheduler.CraftScheduler.mainThreadHeartbeat() 66.55%
org.bukkit.craftbukkit.scheduler.CraftTask.run() 58.80%
org.bukkit.craftbukkit.scheduler.CraftFuture.run() 7.70%

Description

A dark-themed Spark profiler “waterfall”/call-tree view shows the server’s main thread broken down into nested method calls, each line indented with expand/collapse triangles. Percentages appear at the end of each method name, starting with “Server thread 100.00%” and narrowing through MinecraftServer tick methods to CraftBukkit scheduler calls. On the far right, each row has a horizontal beige bar whose length corresponds to the percentage, forming a stacked waterfall-like column of bars. The highest rows have nearly full-width bars, while lower rows (like CraftFuture.run at 7.70%) have much shorter bars. Small circular info icons appear beside some entries.

For example, in the above image, the CraftScheduler.mainThreadHeartbeat() code contains CraftTask.run() and CraftFuture.run(). If you add up the 58.80% and 7.70%, you get 66.5%, which is0.05% shy of the total time (66.55%) that CraftScheduler.mainThreadHeartbeat() takes. This extra 0.05% is what we’d refer to as “self-time,” or extra time spent within that code itself rather than other code that’s triggered by it. Using this basic technique, you can narrow down the causes of lag to specific bits of code.

View Modes

Like Timings, Spark allows you to select three different view modes. “all,” “flat,” and “plugins.” There’s also a flame icon, allowing you to view the data in a flamegraph rather than the waterfall graph like the main view modes. The “all” view gives you a complete waterfall graph of the entire server, letting you drill down from the top. This was the view used in the above image.

The Spark view modes
The Spark view modes
Image transcript / description

Visible text

all
flat
plugins

Description

A dark gray horizontal toolbar shows three pill-shaped buttons labeled “all,” “flat,” and “plugins,” each preceded by an eye icon. The “all” button appears selected, with brighter white text and icon, while “flat” and “plugins” are dimmer gray. On the far right is a separate circular button containing a flame icon. The controls are evenly spaced with rounded corners and subtle shading against the dark background.

Flat View

The flat view is similar to the “all” view in that it doesn’t do any filtering, however rather than having to dig through each line of the graph, it flattens out all lines and sorts them. While all lines have been flattened, you’re still able to dig deeper into each individual one as necessary.

Spark’s flat view
Spark’s flat view
Image transcript / description

Visible text

Flat View
This view shows a flattened representation of the profile, where the top 250 method calls are listed.
Label: Percentage
The value displayed against each frame is the time divided by the total time as a percentage.
Display: Top Down
The call tree is 'normal' - expanding a node reveals the sub-methods that it calls.
Sort Mode: Total Time
Methods are sorted according to their 'total time' (the time spent executing code within the method and the time spent executing sub-calls)
Server thread 100.00%
net.minecraft.server. MinecraftServer.runServer() 97.84%
net.minecraft.server. MinecraftServer.lambda$spin$2() 97.84%
net.minecraft.server. MinecraftServer$$Lambda.0x00007efca4e08fd0.run() 97.84%
java.lang.Thread.run() 97.84%
java.lang.Thread.run() 97.84%
net.minecraft.server. MinecraftServer.tickServer() 93.35%
net.minecraft.server. MinecraftServer.tickChildren() 93.25%
org.bukkit.craftbukkit.scheduler. CraftScheduler.mainThreadHeartbeat() 66.55%
org.bukkit.plugin. SimplePluginManager.callEvent() 63.16%
io.papermc.paper.plugin.manager. PaperPluginManagerImpl.callEvent() 63.11%
io.papermc.paper.plugin.manager. PaperEventManager.callEvent() 63.09%

Description

A dark, code-editor-like interface shows a “Flat View” help section at the top, explaining profiling settings for label type, display mode, and sort mode. Below, a collapsible call tree lists methods under “Server thread 100.00%,” with each line showing a method name and an orange percentage value. Small disclosure triangles and circular info icons appear beside several entries, indicating expandable nodes and additional details. On the far right, a vertical stack of pale tan horizontal bars aligns with the method rows, visually representing relative time spent; the longest bars correspond to the highest percentages near the top.

The flat view also includes a number of different options. For example, by default it sorts them by the total time a line, and every line under it, takes. This sorting can instead be changed to sort by the amount of time that line itself takes. This is useful for finding cases where one specific part of the code is very slow, usually in cases where you have one very prominent source of lag. Generally, however, server slowdowns are a case of “death by a thousand cuts,” rather than one very slow thing.

Plugins View

The final view is the plugins view. While this view is very useful from a quick glance, it’s important to keep in mind that it has a significant limitation. Like Timings, the plugins that Spark lists here are categorised based on whether that plugin is mentioned in that branch of the graph. This means the same issue that Timings has, where it’ll “blame” a plugin (pictured below) that sets a lever, triggering a large redstone contraption, for the time that the redstone contraption takes.

Spark’s plugins view
Spark’s plugins view
Image transcript / description

Visible text

Plugins View
This view shows a filtered representation of the profile broken down by plugin.
Label: Percentage
The value displayed against each frame is the time divided by the total time as a percentage.
Merge Mode: Merge
Method calls with the same signature will be merged together, even though they may not have been invoked by the same calling method.
CraftBook (v3.10.12-SNAPSHOT;no_git_id)
Server thread 58.81%
com.sk89q.craftbook.core.st.MechanicClock.run() 58.31%
com.sk89q.craftbook.core.st.SelfTriggeringManager.think() 58.31%
org.bukkit.plugin.SimplePluginManager.callEvent() 58.11%
io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent() 58.11%
io.papermc.paper.plugin.manager.PaperEventManager.callEvent() 58.11%
org.bukkit.plugin.RegisteredListener.callEvent() 58.09%
org.bukkit.plugin.RegisteredListener.getPlugin() 0.00%
com.sk89q.craftbook.core.st.SelfTriggeringManager.areAdjacentChunksLoaded() 0.01%
org.bukkit.craftbukkit.CraftWorld.isChunkLoaded() 0.01%
java.util.HashSet.toArray() 0.00%

Description

A dark-themed Spark profiling interface shows the “Plugins View” page with explanatory text at the top describing the view, a “Label: Percentage” setting, and “Merge Mode: Merge.” Below, a section titled “CraftBook (v3.10.12-SNAPSHOT;no_git_id)” displays a collapsible call tree for the “Server thread,” with nested method calls indented and each line annotated with a percentage. The main chain highlights CraftBook methods leading into Bukkit/Paper event-calling methods, with most entries around 58% and a few near 0%. On the far right, a vertical column of horizontal beige bars aligns with the call-tree rows, visually representing the relative time percentages.

Luckily, Spark provides more information than Timings. While Timings would just provide that basic level of information, with Spark you can dig deeper. If you dig down and start seeing something that appears entirely unrelated to the plugin, such as redstone updates, chunk generation, hoppers, etc, that can indicate that it’s not necessarily the plugin that’s directly causing issues, but something downstream from the plugin. That’s not to say that the plugin is definitely not at fault, but it provides significantly more context as to where the slowdown is actually taking place.

Flamegraph

The flamegraph mode, selected with the flame icon next to the main view mode selector, allows visualising the data in a different format. Rather than the waterfall graph that shows the hierarchy of the code and allows you to dig deeper, the flame graph shows everything at once. A flamegraph is good for getting a quick overview of what's slow, but isn't as useful for digging into the details. However, in some situations a quick glance might be all you need.

Spark’s flamegraph view
Spark’s flamegraph view
Image transcript / description

Visible text

Server thread
java.lang.Thread.run()
java.lang.Thread.runWith()
nms.MinecraftServer.run()
nms.MinecraftServer.lambda$spin$2()
nms.MinecraftServer.runServer()
nms.MinecraftServer.tickServer()
nms.MinecraftServer.tickChildren()
obc.scheduler.CraftScheduler.mainThreadHeartbeat()
obc.scheduler.CraftTask.run()
com.sk89q.craftbook.core.st.MechanicClock.run()
com.sk89q.craftbook.core.st.SelfTriggeringManager.think()
org.bukkit.plugin.SimplePluginManager.callEvent()
io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEven…
io.papermc.paper.plugin.manager.PaperEventManager.callEvent()
org.bukkit.plugin.RegisteredListener.callEvent()
co.aikar.timings.TimedEventExecutor.execute()
org.bukkit.plugin.EventExecutor$2.execute()
com.destroystokyo.paper.event.executor.asm.generated.Generated…
com.sk89q.craftbook.mechanics.ic.ICMechanic.onThink()

Description

A wide flamegraph fills the screen on a dark gray background. The top stack is a long orange bar labeled “Server thread,” with successive orange/yellow frames beneath it showing Java and Minecraft server methods (e.g., Thread.run, MinecraftServer.runServer, tickServer, tickChildren). Lower in the stack, the graph transitions into olive and then many narrow teal blocks, indicating deeper call stacks and many small samples; several teal blocks are truncated with ellipses. The widest mid-level frame is “obc.scheduler.CraftScheduler.mainThreadHeartbeat()”, followed by plugin/event-related calls (Bukkit/Paper) and CraftBook methods near the bottom. Most activity appears concentrated in the left and center, while the far right shows thin vertical slivers of teal frames.

Each line in the flamegraph represents a different part of the code, with the width (and colour) representing how much time that part took. The wider the line and hotter the colour, the more time it took. Each following line then allows you to see what other parts of code that part called, and how much time they took. For example, in the above image the MinecraftServer.tickChildren() box took up most of the time on the 8th line, with the CraftScheduler.mainThreadHeartbeat() and ServerLevel.tick() boxes on the 9th line being the source of most of it. If you want to focus on a very specific area, clicking on a box will focus it and make it take the full width of the chart, allowing you to see what's below it easier.

Limitations

While significantly fewer than Timings, Spark does sadly have a few limitations.

Event counts

Spark doesn’t measure how many times something was called, only how much time was spent doing it. This means that if a giant hopper lag machine is passing thousands of items around, it might spam a protection plugin like WorldGuard with item movement events. Each individual item check might be taking far less than a millisecond but doing it thousands of times per tick still ends up causing a slowdown. As you can’t see how many times this was called, you have no way to tell if something is actually slow or whether it’s just being inundated.

Lag spikes

If you’re dealing with intermittent lag spikes, Spark by default might make it fairly difficult to actually see in the report. The results that Spark shows you are the amount of time each line of the graph took across the entire duration that the sampler was running for. This means if you run it for one minute, and there’s a lag spike during only one tick, the data from that one tick will likely be drowned out by all the other data.

The generally recommended way to alleviate this is to re-run Spark with the --only-ticks-over [time] flag. This flag basically only includes results in the report if the tick they were a part of took over the amount of time in milliseconds given to the flag. The Spark docs provide some good information on how to find a good time value to use, but there are also a few good rules of thumb to go by to see only ticks slower than average. As the Minecraft server ideally runs at 20 ticks per second, this means the average tick should take less than or equal to 1000/20, or 50, milliseconds.

Contextual knowledge

Another limitation is less to do with Spark, and more to do with reading it. While Spark provides more information, this information might not actually be useful if you’re not a developer. If you don’t have contextual knowledge of what different parts of the Minecraft server, as well as plugins, are doing, it’s harder to actually interpret the report and connect the dots. While it’s very useful to be able to do baseline interpretation of the report yourself so that you know who to talk to about it, it’s still a task that is better suited for people familiar with the code showing up in the report.

If you’re seeing a plugin mentioned a lot, the best people to interpret that report are the developers of that plugin. If not, you should reach out to your server software’s support instead.

Conclusion

Spark is a significant improvement over Timings and has even replaced it as the performance tool bundled with Paper. It’s important to be able to interpret these to an extent, however the developers behind the relevant software are better placed to interpret the context around those areas of the code.