You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Parallel analysis: verdicts around cycles depend on thread order #82
With PARALLEL > 1 (the default is min(8, cpus−2)), the modification analysis can give different verdicts depending on thread timing. The link cache (#76) makes this visible, but did not cause it.
Measured during #76 (FPDUMP with FPDUMP_PARAMS=1, 8 threads):
corpus
no cache vs cache
cache vs cache (two runs)
no cache vs no cache
guava (oracle)
30 lines
14 lines
2 lines
timefold
22 lines
20 lines
0 lines
langchain4j
0
–
–
With PARALLEL=1, the cache and no-cache runs are byte-identical on all three corpora. So the cache is exact, and the differences come from thread order. Without the cache, the timing happens to be stable enough to hide them. Cache hits change the timing.
Where
The differing lines are independence and eventual verdicts on types and methods inside cycles. In guava: CaseFormat, CharMatcher.And, FluentIterable.$1/$2, and the util.concurrent futures. This suggests that the fixpoint around cycles (cycle-breaking activation, or the order in which an SCC's members see each other's intermediate values) can settle in more than one place, depending on which member is computed first.
Next steps
Reproduce with the cache on, comparing two 8-thread runs, and diff the FPDUMPs to pick one small cycle (CharMatcher.And is a candidate).
Find which verdict decision depends on the order: probably an "activate cycle breaking" decision taken from a partial view of the SCC, or a latest-wins write racing with a reader.
Goal: verdicts that do not depend on PARALLEL or on timing, so that a parallel FPDUMP A/B is a valid exactness test again.
A second reproduction, single-threaded: the worklist's view of eventual writes
While working on #76 option C (verification passes that find changes on no element), we found 13 sites that write a summary property with a plain set() and only increment the change counter: the eventual verdicts in TypeEventualAnalyzerImpl, SourceContractMaterializer's contracts, AbstractMethodAnalyzerImpl's eventual merges, and DynamicImmutabilityInference. TolerantWrite never sees these writes, so the worklist does not wake their dependents. A full verification pass then counts changes with an empty residue, and the next full pass repeats.
The fix restores TolerantWrite.markChanged(property, target) and calls it at those 13 sites, leaving the writes themselves unchanged. With PARALLEL=1, compared against the run without the fix:
langchain4j
guava
timefold
FPDUMP diff
0
8 lines
0
verification passes with empty residue
1 → 0
4 → 0
1 → 0
full passes
20 → 19
28 → 26
25 → 25
The analyzer suite passes, 648/648. The guava diff is in eventual verdicts:
CharMatcher.Is, CharMatcher.IsNot: eventual null → @Immutable(after="first,original,predicate,second"). These labels are fields of other CharMatcher classes, not of these two.
Without threads, the only difference is when dependents re-run. So eventual verdicts depend on the iteration history, not only on the state the analysis converges to: they are written once, and some are held back during a warm-up period. This is the same weakness this issue describes for thread order.
Parked. The gain is small, about 2 full passes on guava (roughly 30 s of 724 s with the link cache). The code is on the local branch park/76-markchanged in ws/maddi-all/maddi-mod (on top of the link-cache commit), and the diff is below. Once eventual verdicts no longer depend on history, landing it should be result-neutral. The new log line ("Verification-pass changes outside any summary, on: …") names any remaining gap.
diff
diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java
index c478378a..186d0e2b 100644
--- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java+++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java@@ -289,6 +289,7 @@ public class AbstractMethodAnalyzerImpl extends CommonAnalyzerImpl implements Ab
}
methodInfo.analysis().set(EVENTUAL_METHOD, fromImplementations);
DECIDE.debug("AM: Decide eventual of abstract method {} = {}", methodInfo, fromImplementations);
+ TolerantWrite.markChanged(EVENTUAL_METHOD, methodInfo);
propertyChanges.incrementAndGet();
}
}
@@ -359,6 +360,7 @@ public class AbstractMethodAnalyzerImpl extends CommonAnalyzerImpl implements Ab
if (agreed != null) {
methodInfo.analysis().set(EVENTUALLY_NON_MODIFYING_METHOD, new ValueImpl.SetOfStringsImpl(Set.copyOf(agreed)));
DECIDE.debug("AM: Decide eventually-non-modifying of abstract method {} after {}", methodInfo, agreed);
+ TolerantWrite.markChanged(EVENTUALLY_NON_MODIFYING_METHOD, methodInfo);
propertyChanges.incrementAndGet();
}
}
@@ -491,6 +493,7 @@ public class AbstractMethodAnalyzerImpl extends CommonAnalyzerImpl implements Ab
if (agreed != null) {
pi.analysis().set(EVENTUALLY_UNMODIFIED_PARAMETER, new ValueImpl.SetOfStringsImpl(Set.copyOf(agreed)));
DECIDE.debug("AM: Decide eventually-unmodified of abstract method parameter {} after {}", pi, agreed);
+ TolerantWrite.markChanged(EVENTUALLY_UNMODIFIED_PARAMETER, pi);
propertyChanges.incrementAndGet();
}
}
diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java
index fac0bbfa..d8d1b5b0 100644
--- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java+++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java@@ -31,6 +31,7 @@ import io.codelaser.maddi.cst.api.info.TypeInfo;
import io.codelaser.maddi.cst.api.variable.FieldReference;
import io.codelaser.maddi.cst.impl.analysis.PropertyImpl;
import io.codelaser.maddi.cst.impl.analysis.ValueImpl;
+import io.codelaser.maddi.modification.common.util.TolerantWrite;
import java.util.ArrayList;
import java.util.List;
@@ -123,6 +124,7 @@ public class DynamicImmutabilityInference {
if (meet.isAtLeastImmutableHC()) {
fieldInfo.analysis().set(PropertyImpl.IMMUTABLE_FIELD, meet);
CommonAnalyzerImpl.DECIDE.debug("DII: Decide dynamic immutable of field {} = {}", fieldInfo, meet);
+ TolerantWrite.markChanged(PropertyImpl.IMMUTABLE_FIELD, fieldInfo);
propertyChanges.incrementAndGet();
}
}
diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java
index f706d659..323dcc3a 100644
--- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java+++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java@@ -914,6 +914,13 @@ public class IteratingAnalyzerImpl extends CommonAnalyzerImpl implements Iterati
LOGGER.info("Verification-pass residue ({} elements): {}", residue.size(), residue.stream()
.map(Info::fullyQualifiedName).sorted().limit(12)
.reduce((a, b) -> a + ", " + b).orElse("-"));
+ if (residue.isEmpty()) {+ // changes on no summary: a write the worklist cannot see (a plain set() without+ // TolerantWrite.markChanged), so the next full pass repeats; name where they landed+ LOGGER.info("Verification-pass changes outside any summary, on: {}",+ singleIterationAnalyzer.changedInfos().stream().map(Info::fullyQualifiedName)+ .sorted().limit(12).reduce((a, b) -> a + ", " + b).orElse("-"));+ }
verifying = false;
} else if (dirty.isEmpty() || propertiesChanged == 0) {
if (incremental) {
diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java
index 5f4df373..a6f89724 100644
--- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java+++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java@@ -154,6 +154,7 @@ public class SourceContractMaterializer {
pi.analysis().set(
io.codelaser.maddi.cst.impl.analysis.PropertyImpl.IGNORE_MODIFICATIONS_PARAMETER, bool);
CommonAnalyzerImpl.DECIDE.debug("SCM: Inherited @IgnoreModifications on {} from {}", pi, opi);
+ TolerantWrite.markChanged(io.codelaser.maddi.cst.impl.analysis.PropertyImpl.IGNORE_MODIFICATIONS_PARAMETER, pi);
propertyChanges.incrementAndGet();
break;
}
@@ -201,6 +202,7 @@ public class SourceContractMaterializer {
CommonAnalyzerImpl.DECIDE.debug("SCM: Contracted {} of {} = true, from the class-level"
+ " disclaimer on {}", IGNORE_MODIFICATIONS_FIELD, fieldInfo,
fieldType);
+ TolerantWrite.markChanged(IGNORE_MODIFICATIONS_FIELD, fieldInfo);
propertyChanges.incrementAndGet();
return;
}
@@ -246,6 +248,7 @@ public class SourceContractMaterializer {
// MUTABLE is the default: writing it would record an "annotation" indistinguishable from silence
info.analysis().set(property, immutable);
CommonAnalyzerImpl.DECIDE.debug("SCM: Contracted {} of {} = {}", property, info, immutable);
+ TolerantWrite.markChanged(property, info);
propertyChanges.incrementAndGet();
}
}
@@ -258,6 +261,7 @@ public class SourceContractMaterializer {
if (contractReader.contracts(info).get(property) instanceof Value.Bool bool && bool.isTrue()) {
info.analysis().set(property, bool);
CommonAnalyzerImpl.DECIDE.debug("SCM: Contracted {} of {} = {}", property, info, bool);
+ TolerantWrite.markChanged(property, info);
propertyChanges.incrementAndGet();
}
}
diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java
index e0676e69..77ebf3ab 100644
--- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java+++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java@@ -17,6 +17,7 @@
package io.codelaser.maddi.modification.analyzer.impl;
+import io.codelaser.maddi.modification.common.util.TolerantWrite;
import io.codelaser.maddi.modification.prepwork.variable.MethodLinkedVariables;
import io.codelaser.maddi.modification.link.impl.MethodLinkedVariablesImpl;
import io.codelaser.maddi.modification.prepwork.variable.Link;
@@ -182,6 +183,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
methodInfo.analysis().set(EVENTUAL_METHOD, eventual);
DECIDE.debug("TE: {} eventual of method {} = {}", contracted ? "Contracted" : "Decide",
methodInfo, eventual);
+ TolerantWrite.markChanged(EVENTUAL_METHOD, methodInfo);
propertyChanges.incrementAndGet();
eventualCluster.commitAssumptionBuffer();
} else {
@@ -205,6 +207,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
methodInfo.analysis().set(EVENTUALLY_NON_MODIFYING_METHOD,
new ValueImpl.SetOfStringsImpl(Set.copyOf(labels)));
DECIDE.debug("TE: Decide eventually-non-modifying of method {} after {}", methodInfo, labels);
+ TolerantWrite.markChanged(EVENTUALLY_NON_MODIFYING_METHOD, methodInfo);
propertyChanges.incrementAndGet();
eventualCluster.commitAssumptionBuffer();
} else {
@@ -227,6 +230,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
pi.analysis().set(EVENTUALLY_UNMODIFIED_PARAMETER,
new ValueImpl.SetOfStringsImpl(Set.copyOf(labels)));
DECIDE.debug("TE: Decide eventually-unmodified of parameter {} after {}", pi, labels);
+ TolerantWrite.markChanged(EVENTUALLY_UNMODIFIED_PARAMETER, pi);
propertyChanges.incrementAndGet();
eventualCluster.commitAssumptionBuffer();
} else {
@@ -280,6 +284,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
Value.EventuallyImmutable contracted = eventuallyImmutable(typeInfo);
if (contracted.isEventual()) {
typeInfo.analysis().set(EVENTUALLY_IMMUTABLE_TYPE, contracted);
+ TolerantWrite.markChanged(EVENTUALLY_IMMUTABLE_TYPE, typeInfo);
propertyChanges.incrementAndGet();
return; // contracts win
}
@@ -506,6 +511,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
typeInfo.analysis().set(EVENTUALLY_IMMUTABLE_TYPE, value);
}
DECIDE.debug("TE: Decide eventual immutability of type {} = {}", typeInfo, value);
+ TolerantWrite.markChanged(EVENTUALLY_IMMUTABLE_TYPE, typeInfo);
propertyChanges.incrementAndGet();
}
diff --git a/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java b/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java
index 969af75f..7fc2cc68 100644
--- a/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java+++ b/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java@@ -79,7 +79,18 @@ public final class TolerantWrite {
}
}
- /** Bookkeeping for a deliberate overwrite made outside this class: counts it and wakes the target's dependents. */+ /**+ * Bookkeeping for a write made outside this class (a plain {@code set()} of a summary property): counts it and+ * wakes the target's dependents. Without it, the worklist never sees the change, and only a full verification+ * pass does: it finds changes on no element, and the next full pass repeats (CodeLaser/maddi-mod#76).+ */+ public static void markChanged(Property property, Object context) {+ CHANGES.computeIfAbsent(property.key(), _ -> new java.util.concurrent.atomic.LongAdder()).increment();+ if (context != null && !(context instanceof String) && !INTERNAL_PROPERTIES.contains(property.key())) {+ CHANGED_TARGETS.add(context);+ }+ }+
/**
* The write of an ANALYSIS-ORDER recomputation (the method's slot in the main loop, or the link computer's
* own doType slot): the latest computation wins over an equal-keyed stored value with different content.
Six verdicts vary, and each comes out differently in a different 8-thread run (so these are races, not two stable outcomes): CaseFormat.$4 eventual, the independence of three constructor parameters (Equivalence.EquivalentToPredicate, SortedMultisets.ElementSet, InterruptibleTask.Blocker) and of two fields (CycleDetectingReentrantReadWriteLock.writeLock, Striped.WeakSafeLock.strongReference).
Cause, shown for EquivalentToPredicate: in every 8-thread run, never sequentially, its parameter is set to @Independent by cycle breaking (TypeModIndyAnalyzerImpl.doIndependent: "decide the never-decided"), 1–3 times. Whether a later honest computation overwrites that default then decides the final verdict. Cycle breaking settles whatever is undecided at the moment an element is reached; in a parallel pass that moment depends on the threads.
Once cycle breaking is active, every pass runs sequentially; the passes before stay parallel. 4× PARALLEL=8 on laser1, 445–478 s each:
all six verdicts above now equal the sequential run's, in all four runs;
two variants remain (p8a = p8b, p8c = p8d), in type independence only: five util.concurrent futures (TimeoutFuture, AbstractCatchingFuture, AbstractTransformFuture, FluentFuture.TrustedFuture, TrustedListenableFutureTask) and SpliteratorTester.GeneralSpliteratorOfObject.
Every run activates cycle breaking 4 times: after the first certification and again after each MODREACH re-derivation round (cycleBreakingActive = false, IteratingAnalyzerImpl ~l.811). The parallel passes in between take a varying number of iterations (first activation at 19 sequentially, at 20–24 in parallel). In some runs those passes decide TimeoutFuture's independence honestly (@Dependent) before the last activation; in others it stays undecided and cycle breaking sets @Independent (as sequentially). So a parallel pass that is not cycle breaking also leaks order: a value that one order decides and another leaves undecided means some input is read in a state that is not monotone (written, then cleared or overwritten, within a pass).
Next: find which input TimeoutFuture's type independence reads in the passes before the 4th activation, and whether it is overwritten mid-pass. Separately: the cost of SEQCYCLEBREAKING against an ungated 8-thread run on the same machine.
Second source found and fixed: the abstract-supertype shortcut in type independence (e7efb20, gate NOSUPERWAIT)
A per-pass trace (EC_TYPE_DEBUG now also prints type independence per pass, 9f6e3b2) showed where the two remaining variants split: in the pass after a MODREACH reset, FluentFuture.TrustedFuture reads its supertype FluentFuture before that has a verdict. TypeIndependentAnalyzerImpl.independentSuper then falls back to the min over the supertype's concrete methods, ignoring its fields, abstract methods and own supertypes. Read early, that gives @Independent; one iteration later the supertype's own verdict, @Dependent, is there. An @Independent type is never revisited, so the early read stuck and flowed down to TimeoutFuture and four more futures (and the same for SpliteratorTester.GeneralSpliterator's subtypes).
Fix: a source supertype is waited for; the shortcut stays for library supertypes, which get no verdict of their own.
What the fix changes (sequential, before vs after): 646 verdicts, all type independence, all downward; no immutability, method or parameter verdict moves.
Examples: util.concurrent.AbstractCatchingFuture (an AbstractFuture subclass) and anonymous ForwardingObject subclasses in the tests → @Dependent; base.Equivalence.Equals/Identity → @Independent(hc=true). Many are anonymous test classes.
Open:
corpus effect (ratchets, oracle) of the 646 changes: for a maddi-all measurement before merging;
SEQCYCLEBREAKING costs about 50% (8 threads on laser1: 295–307 s without, 445–478 s with); a cheaper deterministic form is the next step;
CBSWEEP (2cbbdc3, opt-in) is an alternative to SEQCYCLEBREAKING that keeps the method loop parallel. Under cycle breaking, the per-element loop runs without it. Only on a full pass whose loop changed nothing does a sequential sweep in analysis order apply cycle breaking to the fields and types. That state is the fixpoint without cycle breaking, so it does not depend on thread timing.
guava, CBSWEEP=1, on the Mac:
run
time
verdicts
3 runs at 8 threads
396–419 s
byte-identical
1 sequential run
1205 s
3 lines differ from the 8-thread runs
Sequential vs parallel: the 3 lines are eventual verdicts of three anonymous CaseFormat constants (after="wordBoundary" in the sequential run, null at 8 threads). With SEQCYCLEBREAKING, sequential and parallel were identical, so this is still to trace.
Cost: the Mac's earlier ungated 8-thread runs took 363–381 s, but at 3601648, before the supertype fix. That suggests about +10%, against about +50% for SEQCYCLEBREAKING. Not yet measured at a single commit.
Analyzer suite with CBSWEEP=1 (laser1): 680 tests, 2 skipped, 0 failed.
Verdicts compared with SEQCYCLEBREAKING: 249 lines change, mostly upward. Upward changes need a soundness check (corpus).
change
count
field independence @Dependent → @Independent
189
type independence @Dependent → @Independent
23
type independence @Independent → @Independent(hc=true)
18
type immutability @FinalFields → @Immutable(hc=true)
7
field independence @Dependent → @Independent(hc=true)
7
type independence @Dependent → @Independent(hc=true)
6
eventual and other
6
The likely reading: the old scheme applied defaults while the loop was still deciding these values.
Regression test for the supertype fix: TestSubtypeWaitsForAbstractSupertype (737913b). It fails with NOSUPERWAIT=1.
CBSWEEP with 3e9b0a4 (type analyzers out of the parallel loop) is now thread-count independent on guava.
Cause of the earlier sequential-vs-parallel difference: the eventual assumption ledger records whether a candidate already had its verdict when a member leaned on it. Built inside the parallel loop, that depended on timing. CaseFormat.$0 and $1 recorded CaseFormat at 8 threads and were retracted with it by cascade. Every ledger write comes from the type analyzers, which under CBSWEEP now run only in the sequential second type pass.
Run on the Mac (laser1 was full), same commit:
run
time
verdicts
analyzer suite, CBSWEEP=1
680 tests, 2 skipped, 0 failed
CBSWEEP=1, 8 threads, 2 runs
409, 444 s
identical
CBSWEEP=1, sequential
1320 s
identical to the 8-thread runs
ungated, 8 threads
372 s
—
Cost: +10–19% against 1 ungated sample on a busy Mac; SEQCYCLEBREAKING cost about +50%.
Fixed in devel: CBSWEEP is now the default (c3ba74b), and NOCBSWEEP=1 turns it off. Under cycle breaking, the defaults are decided in a sequential sweep at the fixpoint, and the type analyzers run outside the parallel loop. guava gives identical verdicts at 8 threads and sequentially.
Activity
bnaudts commentedon Oct 29, 2026
A second reproduction, single-threaded: the worklist's view of eventual writes
While working on #76 option C (verification passes that find changes on no element), we found 13 sites that write a summary property with a plain
set()and only increment the change counter: the eventual verdicts in TypeEventualAnalyzerImpl, SourceContractMaterializer's contracts, AbstractMethodAnalyzerImpl's eventual merges, and DynamicImmutabilityInference. TolerantWrite never sees these writes, so the worklist does not wake their dependents. A full verification pass then counts changes with an empty residue, and the next full pass repeats.The fix restores
TolerantWrite.markChanged(property, target)and calls it at those 13 sites, leaving the writes themselves unchanged. WithPARALLEL=1, compared against the run without the fix:The analyzer suite passes, 648/648. The guava diff is in eventual verdicts:
CharMatcher.Is,CharMatcher.IsNot: eventual null →@Immutable(after="first,original,predicate,second"). These labels are fields of other CharMatcher classes, not of these two.ImmutableMultimap.KeysSerializedForm: null →@Immutable(hc=true)(after="multimap")BenchmarkHelpers.SampleMatcherConfig:@Immutable(hc=true)(after="matcher")→ nullWithout threads, the only difference is when dependents re-run. So eventual verdicts depend on the iteration history, not only on the state the analysis converges to: they are written once, and some are held back during a warm-up period. This is the same weakness this issue describes for thread order.
Parked. The gain is small, about 2 full passes on guava (roughly 30 s of 724 s with the link cache). The code is on the local branch
park/76-markchangedin ws/maddi-all/maddi-mod (on top of the link-cache commit), and the diff is below. Once eventual verdicts no longer depend on history, landing it should be result-neutral. The new log line ("Verification-pass changes outside any summary, on: …") names any remaining gap.diff
Link cache: skip a method's link computation when nothing it read cha…
bnaudts commentedon Nov 3, 2026
Re-measured on devel + #89/#80 (ws/maddi-null), guava
TestGuava, FPDUMP + FPDUMP_PARAMSSix verdicts vary, and each comes out differently in a different 8-thread run (so these are races, not two stable outcomes):
CaseFormat.$4eventual, the independence of three constructor parameters (Equivalence.EquivalentToPredicate,SortedMultisets.ElementSet,InterruptibleTask.Blocker) and of two fields (CycleDetectingReentrantReadWriteLock.writeLock,Striped.WeakSafeLock.strongReference).Cause, shown for
EquivalentToPredicate: in every 8-thread run, never sequentially, its parameter is set to@Independentby cycle breaking (TypeModIndyAnalyzerImpl.doIndependent: "decide the never-decided"), 1–3 times. Whether a later honest computation overwrites that default then decides the final verdict. Cycle breaking settles whatever is undecided at the moment an element is reached; in a parallel pass that moment depends on the threads.Experiment:
SEQCYCLEBREAKING(6742432, opt-in)Once cycle breaking is active, every pass runs sequentially; the passes before stay parallel. 4× PARALLEL=8 on laser1, 445–478 s each:
util.concurrentfutures (TimeoutFuture,AbstractCatchingFuture,AbstractTransformFuture,FluentFuture.TrustedFuture,TrustedListenableFutureTask) andSpliteratorTester.GeneralSpliteratorOfObject.Every run activates cycle breaking 4 times: after the first certification and again after each MODREACH re-derivation round (
cycleBreakingActive = false, IteratingAnalyzerImpl ~l.811). The parallel passes in between take a varying number of iterations (first activation at 19 sequentially, at 20–24 in parallel). In some runs those passes decideTimeoutFuture's independence honestly (@Dependent) before the last activation; in others it stays undecided and cycle breaking sets@Independent(as sequentially). So a parallel pass that is not cycle breaking also leaks order: a value that one order decides and another leaves undecided means some input is read in a state that is not monotone (written, then cleared or overwritten, within a pass).Next: find which input
TimeoutFuture's type independence reads in the passes before the 4th activation, and whether it is overwritten mid-pass. Separately: the cost ofSEQCYCLEBREAKINGagainst an ungated 8-thread run on the same machine.bnaudts commentedon Nov 3, 2026
Second source found and fixed: the abstract-supertype shortcut in type independence (e7efb20, gate
NOSUPERWAIT)A per-pass trace (
EC_TYPE_DEBUGnow also prints type independence per pass, 9f6e3b2) showed where the two remaining variants split: in the pass after a MODREACH reset,FluentFuture.TrustedFuturereads its supertypeFluentFuturebefore that has a verdict.TypeIndependentAnalyzerImpl.independentSuperthen falls back to the min over the supertype's concrete methods, ignoring its fields, abstract methods and own supertypes. Read early, that gives@Independent; one iteration later the supertype's own verdict,@Dependent, is there. An@Independenttype is never revisited, so the early read stuck and flowed down toTimeoutFutureand four more futures (and the same forSpliteratorTester.GeneralSpliterator's subtypes).Fix: a source supertype is waited for; the shortcut stays for library supertypes, which get no verdict of their own.
guava, laser1 (TestGuava, FPDUMP + FPDUMP_PARAMS):
SEQCYCLEBREAKINGWhat the fix changes (sequential, before vs after): 646 verdicts, all type independence, all downward; no immutability, method or parameter verdict moves.
@Independent→@Dependent@Independent→@Independent(hc=true)@Independent(hc=true)→@Dependent@Independent(except=…)/(hc, except=…)→@DependentExamples:
util.concurrent.AbstractCatchingFuture(anAbstractFuturesubclass) and anonymousForwardingObjectsubclasses in the tests →@Dependent;base.Equivalence.Equals/Identity→@Independent(hc=true). Many are anonymous test classes.Open:
SEQCYCLEBREAKINGcosts about 50% (8 threads on laser1: 295–307 s without, 445–478 s with); a cheaper deterministic form is the next step;Predefinedflip is one of these two mechanisms.Analyzer: SEQCYCLEBREAKING runs the passes after cycle breaking activ…
Analyzer: EC_TYPE_DEBUG also traces type independence per pass, with …
Analyzer: an undecided abstract source supertype is waited for, not j…
bnaudts commentedon Nov 4, 2026
CBSWEEP(2cbbdc3, opt-in) is an alternative toSEQCYCLEBREAKINGthat keeps the method loop parallel. Under cycle breaking, the per-element loop runs without it. Only on a full pass whose loop changed nothing does a sequential sweep in analysis order apply cycle breaking to the fields and types. That state is the fixpoint without cycle breaking, so it does not depend on thread timing.guava,
CBSWEEP=1, on the Mac:CaseFormatconstants (after="wordBoundary"in the sequential run,nullat 8 threads). WithSEQCYCLEBREAKING, sequential and parallel were identical, so this is still to trace.SEQCYCLEBREAKING. Not yet measured at a single commit.CBSWEEP=1(laser1): 680 tests, 2 skipped, 0 failed.Verdicts compared with
SEQCYCLEBREAKING: 249 lines change, mostly upward. Upward changes need a soundness check (corpus).@Dependent→@Independent@Dependent→@Independent@Independent→@Independent(hc=true)@FinalFields→@Immutable(hc=true)@Dependent→@Independent(hc=true)@Dependent→@Independent(hc=true)The likely reading: the old scheme applied defaults while the loop was still deciding these values.
Regression test for the supertype fix:
TestSubtypeWaitsForAbstractSupertype(737913b). It fails withNOSUPERWAIT=1.Analyzer: CBSWEEP, cycle breaking as a sequential sweep over the fixp…
Test: a subtype waits for its undecided abstract supertype's independ…
Analyzer: under CBSWEEP the type analyzers run only in the sequential…
bnaudts commentedon Nov 4, 2026
CBSWEEPwith 3e9b0a4 (type analyzers out of the parallel loop) is now thread-count independent on guava.Cause of the earlier sequential-vs-parallel difference: the eventual assumption ledger records whether a candidate already had its verdict when a member leaned on it. Built inside the parallel loop, that depended on timing.
CaseFormat.$0and$1recordedCaseFormatat 8 threads and were retracted with it by cascade. Every ledger write comes from the type analyzers, which underCBSWEEPnow run only in the sequential second type pass.Run on the Mac (laser1 was full), same commit:
CBSWEEP=1CBSWEEP=1, 8 threads, 2 runsCBSWEEP=1, sequentialSEQCYCLEBREAKINGcost about +50%.CBSWEEP: 520 lines change, mostly upward.@Dependent→@Independent@Dependent→@Independent@Independent→@Independent(hc=true)@FinalFields→@Immutable(hc=true)@Dependent→@Independent(hc=true)@Dependent→@Independent(hc=true)Analyzer: a CBSWEEP sweep repeats in place until it changes nothing (#82
bnaudts commentedon Nov 5, 2026
Fixed in devel: CBSWEEP is now the default (c3ba74b), and
NOCBSWEEP=1turns it off. Under cycle breaking, the defaults are decided in a sequential sweep at the fixpoint, and the type analyzers run outside the parallel loop. guava gives identical verdicts at 8 threads and sequentially.Analyzer: CBSWEEP is the default, NOCBSWEEP turns it off (#82)
TestAnalysisEarlyCutoffPrototype: a Reader is the firewall case, now …
Cycle-breaking sweep: a field waits for its content type before takin…