Fabric Mod Bisect Tool

Anyone who has run a large Minecraft modpack has been there. The game crashes, the log is unhelpful, and you have a hundred mods to blame. The usual approach is to manually disable half your mods, try again, and repeat until you find the culprit. It works, but it is tedious and easy to get wrong. I wrote this tool to automate that process properly.

The source and prebuilt binaries for Windows, Linux, and macOS are on GitHub.

How It Works

The core of the tool is a bisection search. It splits the pool of candidate mods in half, asks you to run the game and report whether the issue occurred, then eliminates whichever half is clean. Each round cuts the search space in half, so even a modpack with hundreds of mods converges to a result in a handful of steps. The user interface walks you through each test one at a time, so there is no guesswork involved.

One thing that makes this more than a simple binary search is dependency handling. Mods often require other mods to be present, and naively enabling a subset of mods can produce crashes that are unrelated to the actual conflict. The tool includes a dependency resolver that automatically activates any required dependencies when a mod is selected for testing, keeping each test valid. It also reads bundled JARs so that if a conflict is caused by a library shipped inside another mod, the tool still points to the right culprit.

Some conflicts only surface when two or more mods are active together. The bisection engine handles this by continuing to narrow down the candidate set until it isolates the exact combination responsible. After a conflict is found, the tool can set those mods aside and resume searching the rest of the pool, finding any number of separate, unrelated conflicts in one session.

The algorithm described in the appendix (IMCS) is my own approach, designed specifically for this use case. It is optimized for situations where only a small number of conflicts exist within a large set of components, focusing on minimizing the number of required test runs by isolating one conflicting element at a time. In contrast, delta debugging (ddmin) and QuickXplain (QXP) have a lot of overhead in such scenarios, but perform better when the number of conflicts is large.

Managing the Search

The tool gives you fine-grained control over which mods participate in the search. A mod can be force-enabled if it must always be present for the issue to reproduce, force-disabled if you already know it is safe to exclude, or omitted to remove it from the candidate pool while still allowing the resolver to activate it as a dependency for other mods. A history page logs every test and its outcome so you can review how the tool arrived at its conclusion, and a live log page exposes internal diagnostics for bug reports.

The tool also supports a dependency override file for cases where a mod's metadata is missing or incorrect. Overrides can be placed next to the executable, in the Minecraft config folder, or left to the tool's own built-in list, with a clear priority order between them.

Setup screen where the mods folder is loaded
The setup screen, where the mods folder is loaded and analyzed before the search begins.
Main screen showing the candidate mod list
The main screen, showing the current candidates and controls for starting a test.
Test in progress screen
The test screen, shown while a specific subset of mods is enabled and the game is running.
Result screen showing the conflicting mods
The result screen, listing the conflicting mods and offering the option to continue searching for further conflicts.

Future Work

One limitation is that a test result is not always simply "Good" or "Fail". In practice, other issues can mask the real problem and lead to indeterminate outcomes, for example due to missing or incorrect dependency metadata. The tool currently does not handle such cases, but I have a potential approach in mind to address this in the future.


Appendix

Algorithm: Iterative Minimal Conflict Search (IMCS)

The Iterative Minimal Conflict Search (IMCS) algorithm is a novel, highly efficient method for identifying a 1-minimal conflict set from a larger collection of components. Building upon the core principles of binary search and iterative component isolation, IMCS significantly refines traditional bisection techniques. Unlike the classic ddmin algorithm, which struggles with multi-component conflicts due to its exponential increase in test calls when faced with union issues, IMCS maintains stable and predictable O(p log n) performance. While sharing the same optimal theoretical complexity as QuickXplain (QXP), IMCS distinguishes itself by adopting a "lean start" strategy, precisely targeting individual conflict elements and avoiding QXP's upfront speculative tests. This results in superior practical performance and significantly lower variance for sparse problems, making IMCS ideally suited for real-world troubleshooting scenarios.

1. Objective

To efficiently identify a 1-minimal conflict set of size p from a larger superset of n components. A conflict set is defined as the smallest subset of components that causes a system failure (or a designated undesirable outcome) when tested together.

2. Core Principle

The IMCS algorithm operates on a "lean start, iterative isolation" principle. It fundamentally avoids the high overhead of speculative testing on large component sets. Instead, it executes a series of highly efficient, independent binary searches. Each search is tasked with identifying exactly one new conflict element that contributes to the system's failure. This iterative process guarantees stable, predictable performance and is mathematically optimized for sparse conflicts (where p is much smaller than n), which is the common scenario in troubleshooting complex systems.

3. Algorithm Description

The algorithm consists of a main procedure, FindConflictSet, and a recursive helper, FindNextConflictElement.

Definitions:
InitialCandidates
The initial superset of all n components.
ConflictSet
The set of components confirmed to be part of the minimal conflict set.
CandidateSet
The set of components currently being considered for inclusion in the ConflictSet.
StableSet
A set of components that has been tested together in the current search context and found to be stable (i.e., does not cause a failure). This set serves as the baseline for subsequent tests.
test(S)
A black-box function that returns FAIL if the system exhibits the undesirable outcome when configured with set S of components, and GOOD otherwise.
Implicit Definitions:
ClearedSet
An implicit subset of StableSet (specifically, StableSet \ ConflictSet) comprising components that have been tested and found not to contribute to the current system failure in their respective search contexts.
Main Procedure: FindConflictSet
function FindConflictSet(InitialCandidates):
  ConflictSet  {}
  CandidateSet  InitialCandidates
  loop indefinitely:
    // Find the next single component that, in conjunction with the current ConflictSet, contributes to the failure.
    nextElement  FindNextConflictElement(StableSet=ConflictSet, CandidateSet=CandidateSet)

    // If no additional conflict element can be found, the process is complete.
    if nextElement is null:
      break

    // Add the found element to the confirmed ConflictSet and remove it from the candidate pool.
    ConflictSet  ConflictSet  {nextElement}
    CandidateSet  CandidateSet \ {nextElement}

    // Optimization: Test if the current ConflictSet is already a complete, minimal set.
    // If it causes failure, we can terminate early without searching for more components.
    if test(ConflictSet) is FAIL: 
      break

  return ConflictSet
Helper Procedure: FindNextConflictElement
function FindNextConflictElement(StableSet, CandidateSet):
  // Base Case 1: No more candidates to test.
  if CandidateSet is empty:
    return null

  // Base Case 2: Handles the initial call if CandidateSet has only one element.
  if size(CandidateSet) = 1:
    let c be the single element in CandidateSet
    if test(StableSet  {c}) is FAIL:
      return c
    else:
      return null

  // Recursive Step: Divide and conquer.
  Split CandidateSet into two halves, C₁ and C₂.

  // Test the first half in conjunction with the current StableSet.
  if test(StableSet  C₁) is FAIL:
    // The next conflict element is in C₁.
    // Optimization: If C₁ is a single element, it must be the one.
    if size(C₁) = 1:
      return the single element in C₁
    else:
      return FindNextConflictElement(StableSet, C₁)

  // Otherwise, the first half is safe. Add it to the StableSet and search C₂.
  else:
    newStableSet  StableSet  C₁
    // Optimization: The next conflict element might be in C₂.
    // If C₂ is a single element, test it directly.
    if size(C₂) = 1:
      let c be the single element in C₂
      if test(newStableSet  {c}) is FAIL:
        return c
      else:
        return null // This was the last possible conflict element.
    else:
      return FindNextConflictElement(newStableSet, C₂)

4. Complexity Analysis

Time Complexity: O(p log n)

The algorithm's total cost is dominated by the p calls to the FindNextConflictElement procedure. Each call performs a binary search on a diminishing set of candidates (from n down to n-p+1), with a cost of O(log n). Therefore, the total time complexity is O(p log n).

Space Complexity: O(n)

The algorithm requires storing the set of candidates, which is initially of size n. The recursion depth of the helper function is O(log n).

5. Extension: Finding All Independent Conflicts (IMCS_Enumerator)

The core IMCS algorithm finds a single conflict set. The IMCS_Enumerator is a meta-procedure that extends this to discover all independent minimal conflict sets in a system that may have multiple unrelated faults.

A persistent, cross-iteration test cache ("knowledge base") is not used. Such a cache is unworkable in practice for two fundamental reasons. First, a FAIL result is only relevant to its specific set of components; once a conflict element from that set is found and removed, that exact test can never be run again, rendering the cached result useless. Second, a GOOD result is context-dependent on the user's current focus; caching it could incorrectly mask a different, independent issue in a subsequent search. Therefore, the only knowledge that can be safely and usefully persisted between iterations is the reduction of the candidate pool itself.

Meta-Procedure: IMCS_Enumerator
function IMCS_Enumerator(InitialCandidates):
  AllConflictSets  []
  CandidateSet  InitialCandidates

  loop indefinitely:
    // Find the next conflict set using a fresh IMCS run. This ensures that
    // knowledge from previous runs does not incorrectly influence the current search.
    newConflictSet  FindConflictSet(CandidateSet)

    // If IMCS returns an empty set, no more conflicts exist among the candidates.
    if newConflictSet is empty:
      break

    // A new independent conflict has been found.
    add newConflictSet to AllConflictSets
    
    // The only safe and persistent knowledge transfer is shrinking the problem space
    // by removing the components of the just-found conflict.
    CandidateSet  CandidateSet \ newConflictSet

  return AllConflictSets

6. Capabilities and Limitations

The IMCS algorithm suite is highly optimized for a specific class of diagnostic problems.

Capabilities & Strengths
  1. Optimized for Sparse Conflicts: The algorithm's primary strength is its exceptional O(p log n) performance and low variance when finding a small number (p) of conflict elements within a large set (n). This makes it ideal for real-world troubleshooting.
  2. Black-Box Operation: IMCS requires no internal knowledge of the system being tested. It operates purely on the GOOD/FAIL outcome of tests, making it universally applicable.
  3. Low Overhead & High Stability: The "lean start" strategy avoids wasteful speculative tests, and the iterative nature results in extremely stable, predictable performance with minimal variance, as confirmed by benchmarks.
  4. Complete Conflict Enumeration:The IMCS_Enumerator extension provides a state-of-the-art method for enumerating all separate, unrelated conflict sets efficiently.
Limitations
  1. Finding Non-Minimal Supersets: IMCS is designed to find only the smallest set that causes a failure (1-minimal). It will not report larger sets that also fail but contain non-essential components.
  2. Conflict Prioritization: The algorithm finds conflict sets in an order determined by the binary search path, not by any measure of severity or probability.
  3. Dense Conflicts: As demonstrated by benchmarks against QuickXplain, IMCS is less efficient for "dense" problems where p is a large fraction of n. In such scenarios, algorithms that leverage information reuse more aggressively may perform better.
  4. Non-Deterministic Systems: The algorithm relies on the system behaving deterministically. If a test on the same set of components can produce both GOOD and FAIL results, IMCS may fail to find a consistent conflict set or may terminate with an incorrect result.