Skip to content

server micro optimizations - #194

Open
Nrleryxx wants to merge 9 commits into
mainfrom
mega-optimizations
Open

server micro optimizations#194
Nrleryxx wants to merge 9 commits into
mainfrom
mega-optimizations

Conversation

@Nrleryxx

@Nrleryxx Nrleryxx commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

A batch of performance optimizations, all original work (not taken from Lithium or other forks):

  • ServerLevel: avoid stream + list allocation in wakeUpAllPlayers, skip thunder work when thunder cannot happen (with !isThundering() short-circuit first), skip the profiler random call when thunder is disabled
  • Level: skip the
    emoveAll scan when no block entity ticker was removed
  • HopperBlockEntity: cache the suck AABB instead of allocating it per item entity
  • BaseSpawner: hoist the nearby entity query box out of the spawn loop
  • GameEventDispatcher: reuse a single listener visitor + dispatch list across event posts instead of allocating per post
  • EuclideanGameEventListenerRegistry: nullable range check to avoid Optional allocations per listener per event
  • Villager: skip gossip decay for villagers without gossip
  • BlockBehaviour: hasBlockEntity precomputed + made final
  • Entity: event-driven isInTickList instead of a set lookup

@Nrleryxx
Nrleryxx requested a review from MartijnMuijsers July 31, 2026 17:06
@Nrleryxx Nrleryxx self-assigned this Jul 31, 2026
@Nrleryxx Nrleryxx added the type: optimization optimization related PRs label Jul 31, 2026

@MartijnMuijsers MartijnMuijsers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great changes!

Are some of the changes from Lithium? If so, can you indicate in the PR description?

entity -> {
if (!entity.isRemoved()) {
if (!tickRateManager.isEntityFrozen(entity)) {
- profiler.push("checkDespawn");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is completely pointless.

In case of InactiveProfiler, push and pop are no-op, so they won't have any effect.

And JIT will quickly realize (during a run where profiler is disabled, which is essentially always) that there is only 1 runtime instance of profiler and it will completely delete these lines.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, removed it - reverted to the vanilla push/pop. The JIT devirtualizes InactiveProfiler and eliminates these branches when profiling is off anyway.


public void tickChunk(final LevelChunk chunk, final int tickSpeed) {
+ // Gale start - Do less work - Skip the whole chunk tick when there is nothing to do
+ if (tickSpeed <= 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when tickSpeed <= 0 this is technically a tiny tiny tiny improvement.

But if tickSpeed > 0 we have a tiny tiny tiny extra check.

Which of these is more likely? I strongly argue tickSpeed > 0 will be set for by far most servers.
I think optimizing for tickSpeed <= 0 at a smaller cost of tickSpeed > 0 is still not worth it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, tickSpeed > 0 is the common case. Removed the early return.


public void tickThunder(final LevelChunk chunk) {
+ // Gale start - Do less work - Skip the profiler and random calls when thunder cannot happen
+ if (!this.isRaining() || this.paperConfig().environment.disableThunder || !this.isThundering() || this.spigotConfig.thunderChance <= 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, because we have a diff here now anyway, putting !this.isThundering() first is fastest since that one has the highest chance of short-circuiting.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - !isThundering() now comes first for the best short-circuit.

- entity.tickCount++;
+ entity.setTickCount(entity.tickCount + 1); // Gale - Event-driven - Cat.canRemoveWhenFarAway, Ocelot.canRemoveWhenFarAway
entity.totalEntityAge++; // Paper - age-like counter for all entities
+ boolean profilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Gale - Do less work - Avoid profiler supplier allocation and calls when profiling is disabled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, pointless.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix applied - removed the profilerActive skip here and in tickPassenger too.

- this.gossips.decay();
+ // Gale start - Do less work - Skip gossip decay for villagers without any gossip
+ if (!this.gossips.gossips.isEmpty()) {
+ this.gossips.decay();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move line one tab the left is preferred since we have a proximity diff on it anyway

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - moved it one tab left so the wrapped line stays at its original indentation.

if (this.listenersToRemove.remove(listener)) {
iterator.remove();
} else {
- Optional<Vec3> optionalPosition = getPostableListenerPosition(this.level, sourcePosition, listener);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to make a gale$getPostableListenerPositionNullable that returns @Nullable Vec3. Then copy the implementation from getPostableListenerPosition into gale$getPostableListenerPositionNullable but make it nullable. Then replace the implementation of getPostableListenerPosition by calling gale$getPostableListenerPositionNullable with Optional.ofNullable (so that the method still exists).

This way we have a clearer and more traceable diff

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as suggested - gale returns @nullable Vec3, and getPostableListenerPosition is now a thin Optional.ofNullable wrapper around it.


public class GameEventDispatcher {
+ // Gale start - Do less work - Reuse a single visitor instance and dispatch list across event posts
+ private List<GameEvent.ListenerInfo> eventsToDispatch = Lists.newArrayList();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Candidate for my new class huh :p please remind me of this later in case I forget

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted, I'll remind you when you build that class. :) With the new structure the visitor and the dispatch list are the only per-post allocations left.

- for (int section = sectionMinY; section <= sectionMaxY; section++) {
- applicable |= chunk.getListenerRegistry(section).visitInRangeListeners(gameEvent, position, context, visitListeners);
+ try {
+ for (int chunkX = sectionMinX; chunkX <= sectionMaxX; chunkX++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move these to the left to reduce the diff?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - the try/finally indentation is gone and the loop is back to vanilla in the diff (only the visitListeners -> this.visitor line changed). Stale-entry clearing moved to the start of post() instead, so the exception-safety is preserved.

boolean delay = false;
RandomSource random = level.getRandom();
SpawnData nextSpawnData = this.getOrCreateNextSpawnData(level, random, pos);
-

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RIP empty line? xD

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored. RIP empty line 2026-2026 xD

@@ -1498,11 +_,13 @@
// Paper start - Fix MC-117075 use removeAll
final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<@Nullable TickingBlockEntity> toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
toRemove.add(null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a reason for the toRemove.add(null) above, did you look into this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That line is Paper's, not ours - it's part of Paper's MC-117075 fix (blockEntityTickers can contain nulls, so null is added to the reference set so removeAll strips them too). It only appears as an addition here because our removeAny change pulled that region into the patch as context. Our change in this method is just the removeAny guard.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a reason for the toRemove.add(null) above, did you look into this?

I don't know why they add this add(null) and expect removeAll to also remove null blockEntity from the tick list, because I don't see there is a place to add null to the list (If I missed some places, correct me((( ).

This line of change was added in this commit PaperMC/Paper-archive@6f064f9#diff-477840aa089f974ea049572e2fc59e5dec986abc597ea9b1ce0f1487caad587c

And Machine Maker didn't leave a note to explain why he added add(null) compared to the original patch.

If the removeAny here is false, it will not remove null block entity from the list, but it can still possibly be removed in future ticks; not sure whether it's a big issue. (If we really have null elements, if they don't exist, then it's fine I think)

@Nrleryxx

Copy link
Copy Markdown
Member Author

None of the changes here are from Lithium - all of them are original work. Added a short summary of the changes to the PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: optimization optimization related PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants