Skip to content

[1.21.1] fml.toml -> maxThreads not respected for Common and Sided setup #428

Description

@alexoj

Minecraft Version: 1.21.1

NeoForge Version: 21.1.219

Steps to Reproduce:

  1. Set maxThreads to 1 in fml.toml.
  2. Expect to see any deadlock issues during initialization (due to faulty mods) fixed since maxThreads is 1.
  3. See in a thread dump that Common and Sided Setup run with more than one thread anyway.

Description of issue:
maxThreads settings is not respected during the Common and Sided Setup phases of initialization. This is due to the tasks being submitted to the wrong executor:

In ClientModLoader.java the game calls NeoForge's resource loading hook which starts Common and Sided setup:

  private static CompletableFuture<Void> onResourceReload(final PreparableReloadListener.PreparationBarrier stage, final ResourceManager resourceManager, final ProfilerFiller prepareProfiler, final ProfilerFiller executeProfiler, final Executor asyncExecutor, final Executor syncExecutor) {
        return CompletableFuture.runAsync(() -> startModLoading(syncExecutor, asyncExecutor), ModWorkManager.parallelExecutor())
                .thenCompose(stage::wait)
                .thenRunAsync(() -> finishModLoading(syncExecutor, asyncExecutor), ModWorkManager.parallelExecutor());
    }

However, note that the executor passed to startModLoading(), which later calls load() which submits the mod setup tasks, is not the executor that has a configurable thread count (which is ModWorkManager.parallelExecutor()), but instead, asyncExecutor is passed instead, which seems to be a thread pool from the vanilla game.

ModWorkManager.parallelExecutor() is passed instead to runAsync, so the function that submits the tasks runs in ModWorkManager.parallelExecutor(), but the tasks themselves run in the vanilla game's executor.

This means that regardless of the max thread count configured for ModWorkManager.parallelExecutor(), mods will be loaded in the vanilla game's executor which has as many threads as CPUs are available.

This makes mitigating the classloader deadlock problems of the modpack by reducing the thread count impossible.

Swapping the two executors, that is, leaving the code like this, fixed the issue for me:

  private static CompletableFuture<Void> onResourceReload(final PreparableReloadListener.PreparationBarrier stage, final ResourceManager resourceManager, final ProfilerFiller prepareProfiler, final ProfilerFiller executeProfiler, final Executor asyncExecutor, final Executor syncExecutor) {
        return CompletableFuture.runAsync(() -> startModLoading(syncExecutor, ModWorkManager.parallelExecutor()), asyncExecutor)
                .thenCompose(stage::wait)
                .thenRunAsync(() -> finishModLoading(syncExecutor, ModWorkManager.parallelExecutor()), asyncExecutor);
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions