mojira.dev
MC-265805

Executing a tail-recursive function consumes memory resources linearly with recursion depth

The bug

Before 23w41a, tail-recursive functions, which call themselves with /function at the end of their body, worked quite efficiently. They consume only a constant amount of memory resources regardless of the depth of the recursion.
Since 23w41a, however, even tail-recursive functions consume more memory resources for each recursive call.

How to reproduce

  1. /gamerule maxCommandChainLength 2147483647
  2. /function mc-265805:f

    → Eventually, the game crashes due to java.lang.OutOfMemoryError: Java heap space

data/mc-265805/functions/f.mcfunction

function mc-265805:f

Code analysis

net/minecraft/server/commands/FunctionCommand.java

public static <T extends ExecutionCommandSource<T>> void queueFunctions(
    Collection<CommandFunction<T>> functions,
    @Nullable CompoundTag arguments,
    T sender,
    T senderForExecution,
    ExecutionControl<T> control,
    FunctionCommand.Callbacks<T> callbacks
) throws CommandSyntaxException {
    CommandDispatcher<T> dispatcher = sender.dispatcher();
    MutableInt accumulatedReturnValue = new MutableInt();

    for(CommandFunction<T> function : functions) {
        ResourceLocation id = function.id();

        try {
            T listener = senderForExecution.clearCallbacks().withReturnValueConsumer(returnValue -> {
                int value = accumulatedReturnValue.addAndGet(returnValue);
                callbacks.signalResult(sender, id, value);
                sender.storeResults(true, value);
            });
            InstantiatedFunction<T> instantiatedFunction = function.instantiate(arguments, dispatcher, listener);
            control.queueNext(new CallFunction<>(instantiatedFunction).bind(listener));
        } catch (FunctionInstantiationException e) {
            throw ERROR_FUNCTION_INSTANTATION_FAILURE.create(id, e.messageComponent());
        }
    }
}

Because listener's return value consumer captures the original sender, JVM cannot release sender while listener is alive. In the case of recursive calls, a listener becomes the sender of another function call, chaining to the initial sender. Although calling a tail-recursive function still consumes only a constant amount of the command queue space, it now consumes a linear amount of memory resources due to the chain of execution command sources.

Heap dump analysis

Using VisualVM and a deobfuscated server.jar of 23w41a, a heap dump was obtained when the heap was almost full. According to the heap dump, the heap was almost fully occupied by the following three objects:

  • net.minecraft.commands.CommandSourceStack: 30.6%

  • net.minecraft.server.commands.FunctionCommand$$Lambda+0x000001c05ebefc78: 30.6%

  • org.apache.commons.lang3.mutable.MutableInt: 30.6%

Attachments

Comments 0

No comments.

intsuc

boq

Confirmed

Platform

Important

Commands, Performance

23w41a, 23w42a, 23w43a

23w44a

Retrieved