Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[#26337] Implement class level compile avoidance based on ABI
  • Loading branch information
beikov committed Sep 3, 2025
commit b05f9156c5f8ffffaabc316500846c50bfd765cb
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ class JavaClassChangeIncrementalCompilationIntegrationTest extends BaseJavaClass
outputs.recompiledClasses('MyClass', 'MyClass2', 'TopLevel$Inner', 'TopLevel')
}

// Expected since any change inside same compile task will cause recompilation of any dependent class
@Issue("https://github.com/gradle/gradle/issues/26337")
def "non-abi change to constant origin class causes partial recompilation"() {
source "class A { final static int x = 1; int method() { return 1; } }",
"class B { int method() { return A.x; } }",
Expand All @@ -405,6 +405,36 @@ class JavaClassChangeIncrementalCompilationIntegrationTest extends BaseJavaClass
source "class A { final static int x = 1; int method() { return 2; } }"
run language.compileTaskName

then:
outputs.recompiledClasses('A')
}

@Issue("https://github.com/gradle/gradle/issues/26337")
def "abi change to method origin class causes partial recompilation"() {
source "class A { final static int x = 1; int method() { return 1; } }",
"class B { int method() { return A.x; } }",
"class C {}"
outputs.snapshot { run language.compileTaskName }

when:
source "class A { final static int x = 1; long method() { return 2L; } }"
run language.compileTaskName

then:
outputs.recompiledClasses('A', 'B')
}

@Issue("https://github.com/gradle/gradle/issues/26337")
def "abi change to constant origin class causes partial recompilation"() {
source "class A { final static int x = 1; int method() { return 1; } }",
"class B { int method() { return A.x; } }",
"class C {}"
outputs.snapshot { run language.compileTaskName }

when:
source "class A { final static int x = 2; int method() { return 1; } }"
run language.compileTaskName

then:
outputs.recompiledClasses('A', 'B')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@
package org.gradle.api.internal.tasks.compile.incremental;

import com.google.common.collect.Iterables;
import org.gradle.api.internal.tasks.compile.ApiCompilerResult;
import org.gradle.api.internal.tasks.compile.CleaningJavaCompiler;
import org.gradle.api.internal.tasks.compile.JavaCompileSpec;
import org.gradle.api.internal.tasks.compile.incremental.compilerapi.constants.ConstantToDependentsMapping;
import org.gradle.api.internal.tasks.compile.incremental.compilerapi.constants.ConstantsAnalysisResult;
import org.gradle.api.internal.tasks.compile.incremental.compilerapi.deps.DependentsSet;
import org.gradle.api.internal.tasks.compile.incremental.compilerapi.deps.GeneratedResource;
import org.gradle.api.internal.tasks.compile.incremental.processing.AnnotationProcessingResult;
import org.gradle.api.internal.tasks.compile.incremental.recomp.CurrentCompilation;
import org.gradle.api.internal.tasks.compile.incremental.recomp.CurrentCompilationAccess;
import org.gradle.api.internal.tasks.compile.incremental.recomp.DefaultIncrementalCompileResult;
import org.gradle.api.internal.tasks.compile.incremental.recomp.PreviousCompilation;
import org.gradle.api.internal.tasks.compile.incremental.recomp.PreviousCompilationAccess;
import org.gradle.api.internal.tasks.compile.incremental.recomp.PreviousCompilationData;
Expand All @@ -36,7 +43,12 @@

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
* A compiler that selects classes for compilation. It also handles restore of output state in case of a compile failure.
Expand Down Expand Up @@ -99,7 +111,9 @@ public WorkResult execute(T spec) {
}
try {
WorkResult result = cleaningCompiler.getCompiler().execute(spec);
Set<String> alreadyCompiledClasses = new HashSet<>(spec.getClassesToCompile());
result = recompilationSpecProvider.decorateResult(recompilationSpec, previousCompilationData, result);
result = recompileDependents(result, spec, alreadyCompiledClasses, previousCompilationData, previousCompilation, clock);
return result.or(workResult);
} finally {
Collection<String> classesToCompile = recompilationSpec.getClassesToCompile();
Expand All @@ -108,4 +122,113 @@ public WorkResult execute(T spec) {
}
});
}

private WorkResult recompileDependents(
WorkResult result,
T spec,
Set<String> alreadyCompiledClasses,
PreviousCompilationData previousCompilationData,
PreviousCompilation previousCompilation,
Timer clock
) {
CurrentCompilation currentCompilation = new CurrentCompilation(spec, classpathSnapshotter);
RecompilationSpec newRecompilationSpec = recompilationSpecProvider.provideAbiDependentRecompilationSpec(spec, currentCompilation, previousCompilation, alreadyCompiledClasses);

if (newRecompilationSpec.isFullRebuildNeeded()) {
LOG.info("Full recompilation is required because {}. Analysis took {}.", newRecompilationSpec.getFullRebuildCause(), clock.getElapsed());
return rebuildAllCompiler.execute(spec);
}

recompilationSpecProvider.initCompilationSpecAndTransaction(spec, newRecompilationSpec);
if (Iterables.isEmpty(spec.getSourceFiles()) && spec.getClassesToProcess().isEmpty()) {
// No dependents need recompilation
return result;
}
WorkResult dependentCompileResult = cleaningCompiler.getCompiler().execute(spec);
alreadyCompiledClasses.addAll(spec.getClassesToCompile());
dependentCompileResult = recompileDependents(dependentCompileResult, spec, alreadyCompiledClasses, previousCompilationData, previousCompilation, clock);
dependentCompileResult = recompilationSpecProvider.decorateResult(newRecompilationSpec, previousCompilationData, dependentCompileResult);
return combine(result, dependentCompileResult).or(result);
}

private static WorkResult combine(WorkResult result, WorkResult dependentCompileResult) {
if (result instanceof ApiCompilerResult && dependentCompileResult instanceof ApiCompilerResult) {
ApiCompilerResult apiCompilerResult = (ApiCompilerResult) result;
ApiCompilerResult dependentApiCompilerResult = (ApiCompilerResult) dependentCompileResult;
ApiCompilerResult combined = new ApiCompilerResult();

addAnnotationProcessingResult(combined.getAnnotationProcessingResult(), apiCompilerResult.getAnnotationProcessingResult());
addAnnotationProcessingResult(combined.getAnnotationProcessingResult(), dependentApiCompilerResult.getAnnotationProcessingResult());
addConstantsAnalysisResult(combined.getConstantsAnalysisResult(), apiCompilerResult.getConstantsAnalysisResult());
addConstantsAnalysisResult(combined.getConstantsAnalysisResult(), dependentApiCompilerResult.getConstantsAnalysisResult());
addSourceClassesMapping(combined.getSourceClassesMapping(), apiCompilerResult.getSourceClassesMapping());
addSourceClassesMapping(combined.getSourceClassesMapping(), dependentApiCompilerResult.getSourceClassesMapping());

combined.getBackupClassFiles().putAll(apiCompilerResult.getBackupClassFiles());
combined.getBackupClassFiles().putAll(dependentApiCompilerResult.getBackupClassFiles());
return combined;
}
if (result instanceof DefaultIncrementalCompileResult && dependentCompileResult instanceof DefaultIncrementalCompileResult) {
DefaultIncrementalCompileResult defaultIncrementalCompileResult = (DefaultIncrementalCompileResult) result;
DefaultIncrementalCompileResult dependentDefaultIncrementalCompileResult = (DefaultIncrementalCompileResult) dependentCompileResult;
RecompilationSpec recompilationSpec = new RecompilationSpec();
recompilationSpec.addClassesToCompile(defaultIncrementalCompileResult.getRecompilationSpec().getClassesToCompile());
recompilationSpec.addClassesToCompile(dependentDefaultIncrementalCompileResult.getRecompilationSpec().getClassesToCompile());
recompilationSpec.addSourcePaths(defaultIncrementalCompileResult.getRecompilationSpec().getSourcePaths());
recompilationSpec.addSourcePaths(dependentDefaultIncrementalCompileResult.getRecompilationSpec().getSourcePaths());
for (String classesToProcess : defaultIncrementalCompileResult.getRecompilationSpec().getClassesToProcess()) {
recompilationSpec.addClassToReprocess(classesToProcess);
}
for (String classesToProcess : dependentDefaultIncrementalCompileResult.getRecompilationSpec().getClassesToProcess()) {
recompilationSpec.addClassToReprocess(classesToProcess);
}
recompilationSpec.addResourcesToGenerate(defaultIncrementalCompileResult.getRecompilationSpec().getResourcesToGenerate());
recompilationSpec.addResourcesToGenerate(dependentDefaultIncrementalCompileResult.getRecompilationSpec().getResourcesToGenerate());

return new DefaultIncrementalCompileResult(
defaultIncrementalCompileResult.getPreviousCompilationData(),
recompilationSpec,
combine(defaultIncrementalCompileResult.getCompilerResult(), dependentDefaultIncrementalCompileResult.getCompilerResult())
);
}
return result;
}

private static void addAnnotationProcessingResult(AnnotationProcessingResult result, AnnotationProcessingResult origin) {
for (Map.Entry<String, Set<String>> entry : origin.getGeneratedTypesWithIsolatedOrigin().entrySet()) {
result.addGeneratedType(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, Set<GeneratedResource>> entry : origin.getGeneratedResourcesWithIsolatedOrigin().entrySet()) {
for (GeneratedResource generatedResource : entry.getValue()) {
result.addGeneratedResource(generatedResource, Collections.singleton(entry.getKey()));
}
}
result.getAggregatedTypes().addAll(origin.getAggregatedTypes());
result.getGeneratedAggregatingTypes().addAll(origin.getGeneratedAggregatingTypes());
result.getGeneratedAggregatingResources().addAll(origin.getGeneratedAggregatingResources());
result.getAnnotationProcessorResults().addAll(origin.getAnnotationProcessorResults());
if (result.getFullRebuildCause() == null) {
result.setFullRebuildCause(origin.getFullRebuildCause());
}
}

private static void addConstantsAnalysisResult(ConstantsAnalysisResult result, ConstantsAnalysisResult origin) {
Optional<ConstantToDependentsMapping> constantToDependentsMapping = origin.getConstantToDependentsMapping();
if (constantToDependentsMapping.isPresent()) {
for (Map.Entry<String, DependentsSet> entry : constantToDependentsMapping.get().getConstantDependents().entrySet()) {
for (String privateDependentClass : entry.getValue().getPrivateDependentClasses()) {
result.addPrivateDependent(entry.getKey(), privateDependentClass);
}
for (String accessibleDependentClass : entry.getValue().getAccessibleDependentClasses()) {
result.addPublicDependent(entry.getKey(), accessibleDependentClass);
}
}
}
}

private static void addSourceClassesMapping(Map<String, Set<String>> result, Map<String, Set<String>> origin) {
for (Map.Entry<String, Set<String>> entry : origin.entrySet()) {
result.computeIfAbsent(entry.getKey(), k -> new HashSet<>()).addAll(entry.getValue());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import it.unimi.dsi.fastutil.ints.IntSet;
import org.gradle.api.internal.cache.StringInterner;
import org.gradle.api.internal.initialization.transform.utils.ClassAnalysisUtils;
import org.gradle.api.internal.tasks.compile.incremental.deps.ClassAbi;
import org.gradle.api.internal.tasks.compile.incremental.deps.ClassAnalysis;
import org.gradle.api.internal.tasks.compile.incremental.deps.FieldAbi;
import org.gradle.api.internal.tasks.compile.incremental.deps.MethodAbi;
import org.gradle.model.internal.asm.AsmConstants;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
Expand All @@ -37,7 +40,11 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

Expand Down Expand Up @@ -75,12 +82,21 @@ public class ClassDependenciesVisitor extends ClassVisitor {
private String moduleName;
private final RetentionPolicyVisitor retentionPolicyVisitor;

private int access;
private String signature;
private String superName;
private String[] interfaces;
private final Map<String, FieldAbi> fieldAbis;
private final Map<String, MethodAbi> methodAbis;

private ClassDependenciesVisitor(Predicate<String> typeFilter, ClassReader reader, StringInterner interner) {
super(API);
this.constants = new IntOpenHashSet(2);
this.privateTypes = new HashSet<>();
this.accessibleTypes = new HashSet<>();
this.retentionPolicyVisitor = new RetentionPolicyVisitor();
this.fieldAbis = new HashMap<>();
this.methodAbis = new HashMap<>();
this.typeFilter = typeFilter;
this.interner = interner;
collectRemainingClassDependencies(reader);
Expand All @@ -93,7 +109,7 @@ public static ClassAnalysis analyze(String className, ClassReader reader, String
// Remove the "API accessible" types from the "privately used types"
visitor.privateTypes.removeAll(visitor.accessibleTypes);
String name = visitor.moduleName != null ? visitor.moduleName : className;
return new ClassAnalysis(interner.intern(name), visitor.getPrivateClassDependencies(), visitor.getAccessibleClassDependencies(), visitor.getDependencyToAllReason(), visitor.getConstants());
return new ClassAnalysis(interner.intern(name), visitor.getPrivateClassDependencies(), visitor.getAccessibleClassDependencies(), visitor.getDependencyToAllReason(), visitor.getConstants(), visitor.getClassAbi());
}

@Override
Expand All @@ -111,6 +127,11 @@ public void visit(int version, int access, String name, String signature, String
Type interfaceType = Type.getObjectType(s);
maybeAddDependentType(types, interfaceType);
}

this.access = access;
this.signature = signature;
this.superName = superName;
this.interfaces = interfaces;
}

@Override
Expand Down Expand Up @@ -167,13 +188,18 @@ public IntSet getConstants() {
return constants;
}

public ClassAbi getClassAbi() {
return new ClassAbi(access, signature, superName, interfaces == null ? Collections.emptyList() : Arrays.asList(interfaces), fieldAbis, methodAbis);
}

private boolean isAnnotationType(String[] interfaces) {
return interfaces.length == 1 && interfaces[0].equals("java/lang/annotation/Annotation");
}

@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
Set<String> types = isAccessible(access) ? accessibleTypes : privateTypes;
boolean isAccessible = isAccessible(access);
Set<String> types = isAccessible ? accessibleTypes : privateTypes;
maybeAddClassTypesFromSignature(signature, types);
maybeAddDependentType(types, Type.getType(desc));
if (isAccessibleConstant(access, value)) {
Expand All @@ -182,14 +208,21 @@ public FieldVisitor visitField(int access, String name, String desc, String sign
// two values are switched
constants.add((name + '|' + value).hashCode()); //non-private const
}
if (isAccessible) {
fieldAbis.put(name, new FieldAbi(access, desc, signature, value));
}
return new FieldVisitor(types);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
Set<String> types = isAccessible(access) ? accessibleTypes : privateTypes;
boolean isAccessible = isAccessible(access);
Set<String> types = isAccessible ? accessibleTypes : privateTypes;
maybeAddClassTypesFromSignature(signature, types);
addTypesFromMethodDescriptor(types, desc);
if (isAccessible) {
methodAbis.put(name, new MethodAbi(access, desc, signature, exceptions == null ? Collections.emptyList() : Arrays.asList(exceptions)));
}
return new MethodVisitor(types);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void visitFile(FileVisitDetails fileDetails) {

private ClassAnalysis maybeStripToAbi(ClassAnalysis analysis) {
if (abiOnly) {
return new ClassAnalysis(analysis.getClassName(), ImmutableSet.of(), analysis.getAccessibleClassDependencies(), analysis.getDependencyToAllReason(), analysis.getConstants());
return new ClassAnalysis(analysis.getClassName(), ImmutableSet.of(), analysis.getAccessibleClassDependencies(), analysis.getDependencyToAllReason(), analysis.getConstants(), analysis.getClassAbi());
} else {
return analysis;
}
Expand Down
Loading