Skip to content

Link: with alternatives as plain edges, fernflower's StructClass.create exceeds the work ceiling through a callee #89

Closed

Description

@bnaudts

With alternatives as plain edges ("alternatives are not aliases", #72), fernflower's StructClass.create(DataInputFullStream, boolean, LazyLoader) exceeds the work ceiling, is degraded to a shallow summary, and so becomes nonModifying=false. Without the change it is analysed in full and non-modifying.

create marks no alternatives in its own statements: the retry without them (LinkComputerImpl.doMethod, which saves DomHelper.findGeneralStatement) does not trigger. So the extra cost comes in through a callee whose summary changed under the change, and makes create's own link graph heavier.

What is known

  • A/B on laser1, PARALLEL=1, NOALTSOURCES NOLIBRARYRETURN against neither: the only fernflower FPDUMP difference, and the only degraded method that differs (17 without, 18 with).
  • Every attempt is a work ceiling ([work ceiling] in the "Cycle protection tripped" line), in every iteration.
  • A class isolate of StructClass, and of DomHelper together with its reached types, doesn't reproduce such blowups: the callees there are stubs, or too close (maddi#180 for the isolate's own defects).

Next step

Find the callee: log LINKWORK for create's callees on both sides (a run with the link computer's debug logging limited to the org.jetbrains.java.decompiler.struct package) and compare their summaries. Then decide between a rule that keeps that shape grouped and a retry that also covers a method whose callees used alternatives.

Activity

  1. bnaudts commented on Nov 3, 2026

    @bnaudts
    Author

    Found and fixed on ws/maddi-null-89 (9d4c768), on top of the #72 stack rebased onto devel (ws/maddi-null-72stack). Not merged yet; guava is being measured.

    The callee

    StructClass.create marks no alternatives itself, and without them it already uses 53–60% of the work ceiling. One callee's summary changed: GenericMain.parseClassSignature, through GenericType.parse. Logging every recipient of alternatives (a probe, not committed) showed two kinds marked there, neither of them aliasing the method can observe:

    • a value without identity: type = getType(c) (one of CodeConstants' ints), wildcard = switch (..) { .. };
    • a call result whose sources are all objects the callee created: descriptor.superclass = GenericType.parse(superCl) takes one of four new GenericType(..) (oc:122, oc:126, oc:129, oc:133). As plain edges the result's fields went from ≺ oc (field of) to ≈ oc (shares fields with), both ways, and create's closure grew from 9.1k to 12.9k edges.

    The fix

    Both keep their group (Graph.aliasingIsMoot, gates NOALTPRIMITIVE, NOALTFRESH). guava's TypeResolver case has the caller's own locals as sources and is unaffected (TestCallResults.alternativeSources).

    fernflower, laser1, PARALLEL=1 alternatives off rules off both rules primitive only fresh only
    StructClass.create 53% of ceiling trips 53% trips 53%
    trips 262 280 275 280 275
    retries without alternatives 0 273 117 273 137
    run time 29 min 43 min 35 min 43 min 37 min

    With both rules, fernflower's verdicts equal those with alternatives off.

    Still open

    • The retry is costly. About a dozen methods trip with alternatives, are retried without them, and trip again, in every iteration (117 retries). Remembering that a method degrades either way would save most of the remaining 6 minutes.
    • SWITCHFORK=1 on fernflower moves 2 summary lines and no trips.
  2. bnaudts commented on Nov 3, 2026

    @bnaudts
    Author

    Guava check, KOTLIN-pass nullability on guava with every verdict and local dumped (PARALLEL=1, Mac):

    leg time trips retries on the work ceiling
    A: alternatives off (NOALTSOURCES) 18 min 19 0
    B: alternatives + the fix (9d4c768) 21 min 19 0

    Of 69,598 verdict and local lines, 2 differ:

    • TypeResolver.resolveParameterizedType: resolvedOwner is nullable with the fix (A: not null), resolvedRawType stays not null. The case the alternatives were introduced for holds under the fix.
    • ImmutableMap.copyOf(Iterable): the elements of entryArray move from nullable (A) to not null (B).

    From the earlier laser1 run, at modification level (FPDUMP): the fix changes nothing compared with the change before it (R vs B, 0 lines). Alternatives themselves change 252 lines against A, e.g. ImmutableSet.SetBuilderImpl.build() and three ContiguousSet.*Impl methods become non-modifying. SWITCHFORK=1 changes 44 more lines (B vs S).

    The 19 trips are the same in both legs, so they are not caused by alternatives.

  3. bnaudts commented on Nov 5, 2026

    @bnaudts
    Author

    Merged into devel (fix 9d4c768, gates NOALTPRIMITIVE and NOALTFRESH). The remaining work-ceiling retries are #92.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions