Skip to content

Commit

Permalink
Rework how the compiler plugin is loaded
Browse files Browse the repository at this point in the history
The previous implementation had a performance regression due to the inclusion of `tools.jar`
on the worker classpath. Some classes of the Java compiler were loaded multiple times. To
avoid this, we need to separate the compiler plugin from Gradle itself, so that we can load
it in isolation in the same classloader as the loader which has `tools.jar`.

Therefore, the compiler plugin is restricted to plain Java APIs, and the "communication"
with Gradle, for example the intelligence of relativizing paths or writing the generated
mapping file, is done passing lambdas to the compiler.

Last but not least, this also means that the construction of the incremental compile task
has to be done via reflection (otherwise we would load the task in the wrong classloader).
  • Loading branch information
melix committed Apr 24, 2020
1 parent 18a6bf9 commit 112af85
Show file tree
Hide file tree
Showing 22 changed files with 476 additions and 237 deletions.
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ include("platformNative")
include("platformJvm")
include("languageJvm")
include("languageJava")
include("javaCompilerPlugin")
include("languageGroovy")
include("languageNative")
include("toolingNative")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public ClassPath findClassPath(String name) {
if (name.equals("JAVA-COMPILER")) {
return addJavaCompilerModules(ClassPath.EMPTY);
}
if (name.equals("JAVA-COMPILER-PLUGIN")) {
return addJavaCompilerModules(moduleRegistry.getModule("gradle-java-compiler-plugin").getImplementationClasspath());
}
if (name.equals("ANT")) {
ClassPath classpath = ClassPath.EMPTY;
classpath = classpath.plus(moduleRegistry.getExternalModule("ant").getClasspath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.gradle.process.internal.worker.child;

import com.google.common.collect.Lists;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.JavaVersion;
Expand Down Expand Up @@ -61,6 +62,7 @@
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
Expand Down Expand Up @@ -107,6 +109,39 @@ public class WorkerProcessClassPathProvider implements ClassPathProvider, Closea
"asm"
};

// This list is ordered by the number of classes we load from each jar descending
private static final String[] WORKER_OPTIMIZED_LOADING_ORDER = new String[] {
"gradle-base-services",
"guava",
"gradle-messaging",
"gradle-model-core",
"gradle-logging",
"gradle-core-api",
"gradle-workers",
"native-platform",
"gradle-core",
"gradle-native",
"gradle-file-collections",
"gradle-language-java",
"gradle-worker-processes",
"gradle-process-services",
"slf4j-api",
"gradle-language-jvm",
"gradle-persistent-cache",
"gradle-files",
"gradle-hashing",
"gradle-snapshots",
"gradle-worker",
"groovy-all",
"kryo",
"gradle-platform-base",
"gradle-cli",
"jul-to-slf4j",
"javax.inject",
"gradle-jvm-services",
"asm"
};

public WorkerProcessClassPathProvider(CacheRepository cacheRepository, ModuleRegistry moduleRegistry) {
this.cacheRepository = cacheRepository;
this.moduleRegistry = moduleRegistry;
Expand Down Expand Up @@ -146,12 +181,34 @@ public ClassPath findClassPath(String name) {
for (String externalModule : RUNTIME_EXTERNAL_MODULES) {
classpath = classpath.plus(moduleRegistry.getExternalModule(externalModule).getImplementationClasspath());
}
classpath = optimizeForClassloading(classpath);
return classpath;
}

return null;
}

private static ClassPath optimizeForClassloading(ClassPath classpath) {
ClassPath optimizedForLoading = ClassPath.EMPTY;
List<File> optimizedFiles = Lists.newArrayListWithCapacity(WORKER_OPTIMIZED_LOADING_ORDER.length);
List<File> remainder = Lists.newArrayList(classpath.getAsFiles());
for (String module : WORKER_OPTIMIZED_LOADING_ORDER) {
Iterator<File> asFiles = remainder.iterator();
while (asFiles.hasNext()) {
File file = asFiles.next();
if (file.getName().startsWith(module)) {
optimizedFiles.add(file);
asFiles.remove();
}
}
if (remainder.isEmpty()) {
break;
}
}
classpath = optimizedForLoading.plus(optimizedFiles).plus(remainder);
return classpath;
}

@Override
public void close() {
// This isn't quite right. Should close the worker classpath cache once we're finished with the worker processes. This may be before the end of this build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import static org.junit.Assert.assertThat

abstract class DistributionIntegrationSpec extends AbstractIntegrationSpec {

protected static final THIRD_PARTY_LIB_COUNT = 180
protected static final THIRD_PARTY_LIB_COUNT = 181

@Rule public final PreconditionVerifier preconditionVerifier = new PreconditionVerifier()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ enum JavaTestProject {
.withSourceFiles(100)
.withSubProjects(500)
.withDaemonMemory('1536m')
.withCompilerMemory('256m')
.withCompilerMemory('512m')
.assembleChangeFile()
.testChangeFile(450, 2250, 45000).create()),
LARGE_MONOLITHIC_GROOVY_PROJECT(new TestProjectGeneratorConfigurationBuilder("largeMonolithicGroovyProject", Language.GROOVY)
Expand Down
26 changes: 26 additions & 0 deletions subprojects/java-compiler-plugin/java-compiler-plugin.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import org.gradle.gradlebuild.unittestandcompile.ModuleType

plugins {
`java-library`
gradlebuild.classycle
}

gradlebuildJava {
moduleType = ModuleType.INTERNAL
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.internal.compiler.java;

import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.code.Symbol;

import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.TreeSet;
import java.util.function.Function;

public class ClassNameCollector implements TaskListener {
private final Map<File, Optional<String>> relativePaths = new HashMap<>();
private final Map<String, Collection<String>> mapping = new HashMap<>();
private final Function<File, Optional<String>> relativize;

public ClassNameCollector(Function<File, Optional<String>> relativize) {
this.relativize = relativize;
}

public Map<String, Collection<String>> getMapping() {
return mapping;
}

@Override
public void started(TaskEvent e) {

}

@Override
public void finished(TaskEvent e) {
JavaFileObject sourceFile = e.getSourceFile();
if (isSourceFile(sourceFile)) {
File asSourceFile = new File(sourceFile.getName());
if (isClassGenerationPhase(e)) {
processSourceFile(e, asSourceFile);
} else if (isPackageInfoFile(e, asSourceFile)) {
processPackageInfo(asSourceFile);
}
}
}

private static boolean isSourceFile(JavaFileObject sourceFile) {
return sourceFile != null && sourceFile.getKind() == JavaFileObject.Kind.SOURCE;
}

private void processSourceFile(TaskEvent e, File sourceFile) {
Optional<String> relativePath = findRelativePath(sourceFile);
if (relativePath.isPresent()) {
String key = relativePath.get();
TypeElement typeElement = e.getTypeElement();
Name name = typeElement.getQualifiedName();
if (typeElement instanceof Symbol.TypeSymbol) {
Symbol.TypeSymbol symbol = (Symbol.TypeSymbol) typeElement;
name = symbol.flatName();
}
String symbol = normalizeName(name);
registerMapping(key, symbol);
}
}

private void processPackageInfo(File sourceFile) {
Optional<String> relativePath = findRelativePath(sourceFile);
if (relativePath.isPresent()) {
String key = relativePath.get();
String pkgInfo = key.substring(0, key.lastIndexOf(".java")).replace('/', '.');
registerMapping(key, pkgInfo);
}
}

private Optional<String> findRelativePath(File asSourceFile) {
return relativePaths.computeIfAbsent(asSourceFile, relativize);
}

private static String normalizeName(Name name) {
String symbol = name.toString();
if (symbol.endsWith("module-info")) {
symbol = "module-info";
}
return symbol;
}

private static boolean isPackageInfoFile(TaskEvent e, File asSourceFile) {
return e.getKind() == TaskEvent.Kind.ANALYZE && "package-info.java".equals(asSourceFile.getName());
}

private static boolean isClassGenerationPhase(TaskEvent e) {
return e.getKind() == TaskEvent.Kind.GENERATE;
}

public void registerMapping(String key, String symbol) {
Collection<String> symbols = mapping.get(key);
if (symbols == null) {
symbols = new TreeSet<String>();
mapping.put(key, symbols);
}
symbols.add(symbol);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.internal.compiler.java;

import com.sun.source.util.JavacTask;

import javax.annotation.processing.Processor;
import javax.tools.JavaCompiler;
import java.io.File;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

/**
* This is a Java compiler plugin, which must be loaded in the same classloader
* as the one which loads the JDK compiler itself. For this reason this task lives
* in its own subproject and uses as little dependencies as possible (in particular
* it only depends on JDK types).
*
* This class is therefore loaded (and tested) via reflection in org.gradle.api.internal.tasks.compile.JdkTools.
*/
@SuppressWarnings("unused")
public class IncrementalCompileTask implements JavaCompiler.CompilationTask {

private final Function<File, Optional<String>> relativize;
private final Consumer<Map<String, Collection<String>>> onComplete;
private final JavaCompiler.CompilationTask delegate;

public IncrementalCompileTask(JavaCompiler.CompilationTask delegate,
Function<File, Optional<String>> relativize,
Consumer<Map<String, Collection<String>>> onComplete) {
this.relativize = relativize;
this.onComplete = onComplete;
this.delegate = delegate;
}

@Override
public void addModules(Iterable<String> moduleNames) {
delegate.addModules(moduleNames);
}

@Override
public void setProcessors(Iterable<? extends Processor> processors) {
delegate.setProcessors(processors);
}

@Override
public void setLocale(Locale locale) {
delegate.setLocale(locale);
}

@Override
public Boolean call() {
if (delegate instanceof JavacTask) {
ClassNameCollector collector = new ClassNameCollector(relativize);
((JavacTask) delegate).addTaskListener(collector);
try {
return delegate.call();
} finally {
onComplete.accept(collector.getMapping());
}
} else {
throw new UnsupportedOperationException("Unexpected Java compile task : " + delegate.getClass().getName());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import org.gradle.workers.internal.WorkerFactory;

import javax.inject.Inject;
import java.io.File;
import java.util.List;

public class GroovyCompilerFactory implements CompilerFactory<GroovyJavaJointCompileSpec> {
private final WorkerDaemonFactory workerDaemonFactory;
Expand Down Expand Up @@ -75,11 +77,13 @@ public Compiler<GroovyJavaJointCompileSpec> newCompiler(GroovyJavaJointCompileSp
public static class DaemonSideCompiler implements Compiler<GroovyJavaJointCompileSpec> {
private final ExecHandleFactory execHandleFactory;
private final ProjectLayout projectLayout;
private final List<File> javaCompilerPlugins;

@Inject
public DaemonSideCompiler(ExecHandleFactory execHandleFactory, ProjectLayout projectLayout) {
public DaemonSideCompiler(ExecHandleFactory execHandleFactory, ProjectLayout projectLayout, List<File> javaCompilerPlugins) {
this.execHandleFactory = execHandleFactory;
this.projectLayout = projectLayout;
this.javaCompilerPlugins = javaCompilerPlugins;
}

@Override
Expand All @@ -88,10 +92,11 @@ public WorkResult execute(GroovyJavaJointCompileSpec spec) {
if (CommandLineJavaCompileSpec.class.isAssignableFrom(spec.getClass())) {
javaCompiler = new CommandLineJavaCompiler(execHandleFactory);
} else {
javaCompiler = new JdkJavaCompiler(new JavaHomeBasedJavaCompilerFactory());
javaCompiler = new JdkJavaCompiler(new JavaHomeBasedJavaCompilerFactory(javaCompilerPlugins));
}
Compiler<GroovyJavaJointCompileSpec> groovyCompiler = new ApiGroovyCompiler(javaCompiler, projectLayout);
return groovyCompiler.execute(spec);
}

}
}
Loading

0 comments on commit 112af85

Please sign in to comment.