Describe the Issue
MethodProfile.TypeProfile.toTypeProfile() can hand the compiler a JavaTypeProfile whose per-type
probabilities sum to slightly more than 1.0. JVMCI rejects that in AbstractJavaProfile's constructor
(jdk/vm/ci/meta/AbstractJavaProfile.java:51,
assert totalProbablility() >= 0 && totalProbablility() <= 1.0001), and the rejection reaches the user as
jdk.graal.compiler.java.BytecodeParser$BytecodeParserError: java.lang.AssertionError at parsing <method>
The compilation is aborted and the method stays interpreted. Enough of these in a row trip the
Systemic Graal compilation failure detected warning, which is how I noticed it.
This is a lost-update race, not floating-point accumulation. File and line references below are
against master at c6d3d9795c9, file
substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/profile/MethodProfile.java.
Mechanism
incrementTypeProfile (MethodProfile.java:730-752) writes two plain, non-atomic fields, in this order:
counts[i]++ — MethodProfile.java:745 — only on the path where the observed type matched slot i
counter++ — MethodProfile.java:751 — on every call ("Always update the total count, even if
recording the type failed")
Slot claiming is done with a CAS, but neither counter update is atomic.
toTypeProfile() (MethodProfile.java:754-780) then computes, per type:
double p = counts[i];
p = p / counter; // MethodProfile.java:766-767
totalProbability += p;
Two things go wrong:
-
The denominator is re-read from the live field on every iteration. counter is still being
incremented by profiling threads while the loop runs, so the probabilities that end up in one
JavaTypeProfile are not even normalized against a common total.
-
counter is the more contended of the two fields, so it loses proportionally more updates.
Every profiled execution of the bci writes counter; a given counts[i] is written only by the
executions that observed type i. On a polymorphic site under n threads, counter takes n-way
contention while each counts[i] takes only its own share, so lost updates are biased against the
denominator. Σ counts[i] > counter is therefore reachable, and then Σ p > 1.
Nothing downstream clamps it: notRecordedTypeProbability is floored at 0
(MethodProfile.java:775) but the over-unity ptypes array is passed to the JavaTypeProfile
constructor as-is.
getProbability (MethodProfile.java:713) divides by the same live counter and can likewise return
a probability above 1.
The observation
The recorded probability sum in the failure I hit was 1.0007639419404126. That is bit-exactly
1310.0 / 1309.0 — the only ratio of small integers in that neighbourhood that reproduces the value.
So the counts summed to 1310 while the denominator read 1309: a single lost increment on counter,
in a profile that had just matured past JITProfileMatureInvocationThreshold (default 1000,
substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/profile/InterpreterProfilingOptions.java:34).
One lost update, not an accumulation of rounding error.
Using the latest version of GraalVM can resolve many issues.
GraalVM Version
Observed on Oracle GraalVM 25.2.4+7.1 (build 25.0.4+7-LTS-jvmci-25.2-b20).
Code inspected on oracle/graal master c6d3d9795c9.
Operating System and Version
linux-amd64 (Linux 6.6 kernel, WSL2).
Troubleshooting Confirmation
Run Command
Native image built with -H:+RuntimeClassLoading -H:+GraalJITCompileAtRuntime, running a
multi-threaded workload (JDK HttpServer plus HttpClient in one process) long enough for the
affected call sites to mature past the profiling threshold.
Expected Behavior
toTypeProfile() always produces a JavaTypeProfile whose per-type probabilities sum to at most 1.0,
regardless of concurrent profiling activity, so JVMCI's invariant holds and the compilation proceeds.
Actual Behavior
The sum can exceed 1.0. With assertions enabled the AbstractJavaProfile check fires, BytecodeParser
wraps it as BytecodeParserError, the compilation is discarded and the method stays interpreted.
In a product build the assertion is not active, so the malformed profile is not rejected at all — the
compiler consumes type probabilities that sum above 1. That is the more concerning half of this, since
it is silent.
Steps to Reproduce
I want to be straightforward about this: I do not have a reliable runnable reproducer, and I do not
think a short one exists. The window is a lost update on a non-atomic long field, and the profile
has to be read out during the race.
- The workload that produced the observation above was a long-running soak test on a pre-March-2026
build. On current builds (25.2.4+, which include the branch-profile fix referenced below) that same
workload no longer produces the failure, across runs spanning four orders of magnitude in size, so I
cannot offer it as a reproducer for the type-profile half.
- The defect is reproducible analytically from the code: construct any interleaving where a
counter++ at :751 is lost (read-modify-write overlap between two threads) while both threads'
counts[i]++ at :745 survive, or where two threads incrementing different slots each lose a
counter update. The invariant Σ counts[i] <= counter, which toTypeProfile() depends on, is not
enforced anywhere, and once it is violated by one the profile is permanently over unity for as long
as the deficit persists.
- A targeted test would be a unit test on
TypeProfile that hammers incrementTypeProfile from
several threads and asserts Σ counts <= counter (or asserts on toTypeProfile()'s sum directly)
— it should fail today.
Additional Context
The BranchProfile sibling in the same file had the same defect class and was fixed in March:
GR-73833, PR #13127, commit c4c30930f84 (with 2ca0eb47317 doing the structural part: two
independent counters so the ratio is <= 1 by construction). TypeProfile was not touched by that
change and still has the original shape.
The comment at MethodProfile.java:762 says:
// taken from HotSpotMethodData.java#createTypeProfile - sync any bug fixes there
That is worth acting on here, because HotSpot's version does not have this problem for a structural
reason rather than a lucky one. HotSpotMethodData.getRawTypeProfile
(jdk/vm/ci/hotspot/HotSpotMethodData.java:459-492) reads the MDO row once into an immutable
RawItemProfile record (:446) and accumulates totalCount from exactly the counts it just read
(:479, :485) plus the row's counter value (:491). createTypeProfile (:495-513) then divides
by that single snapshot totalCount, so Σ counts <= totalCount holds by construction even though
the underlying MDO is being mutated concurrently. The port kept the arithmetic but not that property:
it reads the counts and the denominator from live fields, and re-reads the denominator per iteration.
Suggested fix shape, in the same spirit:
- Snapshot
counts into a local array and counter into a local, once, before the loop.
- Derive a single denominator, e.g.
long total = Math.max(localCounter, sum(localCounts)).
- Divide every entry by that one stable
total.
That makes Σ p <= 1 hold by construction, costs one small allocation on a path that already
allocates the ProfiledType[], and keeps the ratios meaningful (an under-read counter just gets
corrected upward by the counts that were actually observed). getProbability at :713 should use the
same derived total.
Downstream tracking issue for the original observation: https://github.com/elide-dev/bali/issues/78
Describe the Issue
MethodProfile.TypeProfile.toTypeProfile()can hand the compiler aJavaTypeProfilewhose per-typeprobabilities sum to slightly more than 1.0. JVMCI rejects that in
AbstractJavaProfile's constructor(
jdk/vm/ci/meta/AbstractJavaProfile.java:51,assert totalProbablility() >= 0 && totalProbablility() <= 1.0001), and the rejection reaches the user asThe compilation is aborted and the method stays interpreted. Enough of these in a row trip the
Systemic Graal compilation failure detectedwarning, which is how I noticed it.This is a lost-update race, not floating-point accumulation. File and line references below are
against master at
c6d3d9795c9, filesubstratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/profile/MethodProfile.java.Mechanism
incrementTypeProfile(MethodProfile.java:730-752) writes two plain, non-atomic fields, in this order:counts[i]++—MethodProfile.java:745— only on the path where the observed type matched sloticounter++—MethodProfile.java:751— on every call ("Always update the total count, even ifrecording the type failed")
Slot claiming is done with a CAS, but neither counter update is atomic.
toTypeProfile()(MethodProfile.java:754-780) then computes, per type:Two things go wrong:
The denominator is re-read from the live field on every iteration.
counteris still beingincremented by profiling threads while the loop runs, so the probabilities that end up in one
JavaTypeProfileare not even normalized against a common total.counteris the more contended of the two fields, so it loses proportionally more updates.Every profiled execution of the bci writes
counter; a givencounts[i]is written only by theexecutions that observed type
i. On a polymorphic site undernthreads,countertakesn-waycontention while each
counts[i]takes only its own share, so lost updates are biased against thedenominator.
Σ counts[i] > counteris therefore reachable, and thenΣ p > 1.Nothing downstream clamps it:
notRecordedTypeProbabilityis floored at 0(
MethodProfile.java:775) but the over-unityptypesarray is passed to theJavaTypeProfileconstructor as-is.
getProbability(MethodProfile.java:713) divides by the same livecounterand can likewise returna probability above 1.
The observation
The recorded probability sum in the failure I hit was
1.0007639419404126. That is bit-exactly1310.0 / 1309.0— the only ratio of small integers in that neighbourhood that reproduces the value.So the counts summed to 1310 while the denominator read 1309: a single lost increment on
counter,in a profile that had just matured past
JITProfileMatureInvocationThreshold(default 1000,substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/profile/InterpreterProfilingOptions.java:34).One lost update, not an accumulation of rounding error.
Using the latest version of GraalVM can resolve many issues.
(
c6d3d9795c9) and is unchanged in the relevant respect.GraalVM Version
Observed on Oracle GraalVM
25.2.4+7.1(build 25.0.4+7-LTS-jvmci-25.2-b20).Code inspected on oracle/graal master
c6d3d9795c9.Operating System and Version
linux-amd64 (Linux 6.6 kernel, WSL2).
Troubleshooting Confirmation
user-visible troubleshooting steps do not apply; the diagnosis is from the profile code itself.
Run Command
Native image built with
-H:+RuntimeClassLoading -H:+GraalJITCompileAtRuntime, running amulti-threaded workload (JDK
HttpServerplusHttpClientin one process) long enough for theaffected call sites to mature past the profiling threshold.
Expected Behavior
toTypeProfile()always produces aJavaTypeProfilewhose per-type probabilities sum to at most 1.0,regardless of concurrent profiling activity, so JVMCI's invariant holds and the compilation proceeds.
Actual Behavior
The sum can exceed 1.0. With assertions enabled the
AbstractJavaProfilecheck fires,BytecodeParserwraps it as
BytecodeParserError, the compilation is discarded and the method stays interpreted.In a product build the assertion is not active, so the malformed profile is not rejected at all — the
compiler consumes type probabilities that sum above 1. That is the more concerning half of this, since
it is silent.
Steps to Reproduce
I want to be straightforward about this: I do not have a reliable runnable reproducer, and I do not
think a short one exists. The window is a lost update on a non-atomic
longfield, and the profilehas to be read out during the race.
build. On current builds (25.2.4+, which include the branch-profile fix referenced below) that same
workload no longer produces the failure, across runs spanning four orders of magnitude in size, so I
cannot offer it as a reproducer for the type-profile half.
counter++at:751is lost (read-modify-write overlap between two threads) while both threads'counts[i]++at:745survive, or where two threads incrementing different slots each lose acounterupdate. The invariantΣ counts[i] <= counter, whichtoTypeProfile()depends on, is notenforced anywhere, and once it is violated by one the profile is permanently over unity for as long
as the deficit persists.
TypeProfilethat hammersincrementTypeProfilefromseveral threads and asserts
Σ counts <= counter(or asserts ontoTypeProfile()'s sum directly)— it should fail today.
Additional Context
The
BranchProfilesibling in the same file had the same defect class and was fixed in March:GR-73833, PR #13127, commit
c4c30930f84(with2ca0eb47317doing the structural part: twoindependent counters so the ratio is
<= 1by construction).TypeProfilewas not touched by thatchange and still has the original shape.
The comment at
MethodProfile.java:762says:// taken from HotSpotMethodData.java#createTypeProfile - sync any bug fixes thereThat is worth acting on here, because HotSpot's version does not have this problem for a structural
reason rather than a lucky one.
HotSpotMethodData.getRawTypeProfile(
jdk/vm/ci/hotspot/HotSpotMethodData.java:459-492) reads the MDO row once into an immutableRawItemProfilerecord (:446) and accumulatestotalCountfrom exactly the counts it just read(
:479,:485) plus the row's counter value (:491).createTypeProfile(:495-513) then dividesby that single snapshot
totalCount, soΣ counts <= totalCountholds by construction even thoughthe underlying MDO is being mutated concurrently. The port kept the arithmetic but not that property:
it reads the counts and the denominator from live fields, and re-reads the denominator per iteration.
Suggested fix shape, in the same spirit:
countsinto a local array andcounterinto a local, once, before the loop.long total = Math.max(localCounter, sum(localCounts)).total.That makes
Σ p <= 1hold by construction, costs one small allocation on a path that alreadyallocates the
ProfiledType[], and keeps the ratios meaningful (an under-readcounterjust getscorrected upward by the counts that were actually observed).
getProbabilityat:713should use thesame derived total.
Downstream tracking issue for the original observation: https://github.com/elide-dev/bali/issues/78