-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce the ability to generate AppCDS
- Loading branch information
Showing
9 changed files
with
284 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSBuildItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package io.quarkus.deployment.pkg.builditem; | ||
|
||
import java.nio.file.Path; | ||
|
||
import io.quarkus.builder.item.SimpleBuildItem; | ||
|
||
public final class AppCDSBuildItem extends SimpleBuildItem { | ||
|
||
private final Path appCDS; | ||
|
||
public AppCDSBuildItem(Path appCDS) { | ||
this.appCDS = appCDS; | ||
} | ||
|
||
public Path getAppCDS() { | ||
return appCDS; | ||
} | ||
} |
211 changes: 211 additions & 0 deletions
211
core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
package io.quarkus.deployment.pkg.steps; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.function.BooleanSupplier; | ||
|
||
import org.jboss.logging.Logger; | ||
|
||
import io.quarkus.bootstrap.runner.Timing; | ||
import io.quarkus.bootstrap.util.IoUtils; | ||
import io.quarkus.deployment.annotations.BuildProducer; | ||
import io.quarkus.deployment.annotations.BuildStep; | ||
import io.quarkus.deployment.pkg.PackageConfig; | ||
import io.quarkus.deployment.pkg.builditem.AppCDSBuildItem; | ||
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; | ||
import io.quarkus.deployment.pkg.builditem.JarBuildItem; | ||
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; | ||
import io.quarkus.deployment.steps.MainClassBuildStep; | ||
import io.quarkus.deployment.util.JavaVersionUtil; | ||
import io.quarkus.utilities.JavaBinFinder; | ||
|
||
public class AppCDSBuildStep { | ||
|
||
private static final Logger log = Logger.getLogger(AppCDSBuildStep.class); | ||
|
||
@BuildStep(onlyIf = AppCDSRequired.class) | ||
public void build(OutputTargetBuildItem outputTarget, JarBuildItem jarResult, PackageConfig packageConfig, | ||
BuildProducer<AppCDSBuildItem> appCDS, | ||
BuildProducer<ArtifactResultBuildItem> artifactResult) throws Exception { | ||
Path appCDSDir = outputTarget.getOutputDirectory().resolve("appcds"); | ||
IoUtils.recursiveDelete(appCDSDir); | ||
Files.createDirectories(appCDSDir); | ||
|
||
String javaBin = JavaBinFinder.findBin(); | ||
|
||
Path classesLstPath = createClassesLst(outputTarget, jarResult, javaBin, appCDSDir); | ||
if (classesLstPath == null) { | ||
log.warn("Unable to create AppCDS because classes.lst was not created."); | ||
return; | ||
} | ||
|
||
log.debug("'classes.lst' successfully created."); | ||
|
||
Path appCDSPath = createAppCDS(jarResult, javaBin, classesLstPath, packageConfig.isFastJar()); | ||
if (appCDSPath == null) { | ||
log.warn("Unable to create AppCDS."); | ||
return; | ||
} | ||
|
||
log.infof( | ||
"AppCDS successfully created at: '%s'.\nTo ensure they are loaded properly, " + | ||
"run the application jar from its directory and also add the '-XX:SharedArchiveFile=app-cds.jsa' " + | ||
"JVM flag.\nMoreover, make sure to use the same java version to run the application as was used to build it.", | ||
appCDSPath.toAbsolutePath().toString()); | ||
|
||
appCDS.produce(new AppCDSBuildItem(appCDSPath)); | ||
artifactResult.produce(new ArtifactResultBuildItem(appCDSPath, "appCDS", Collections.emptyMap())); | ||
} | ||
|
||
/** | ||
* @return The path of the created classes.lst file or null of the file was not created | ||
*/ | ||
private Path createClassesLst(OutputTargetBuildItem outputTarget, JarBuildItem jarResult, String javaBin, Path appCDSDir) { | ||
Path classesLstPath = appCDSDir.resolve("classes.lst"); | ||
|
||
List<String> command = new ArrayList<>(4); | ||
command.add(javaBin); | ||
command.add("-XX:DumpLoadedClassList=" + classesLstPath.toAbsolutePath().toString()); | ||
command.add("-jar"); | ||
command.add(jarResult.getPath().toAbsolutePath().toString()); | ||
|
||
if (log.isDebugEnabled()) { | ||
log.debugf("Launching command: '%s' to create class list", String.join(" ", command)); | ||
} | ||
try { | ||
Process process = new ProcessBuilder(command) | ||
.redirectErrorStream(true) | ||
.directory(outputTarget.getOutputDirectory().toFile()) | ||
.start(); | ||
waitForApplicationToStart(process); | ||
} catch (IOException e) { | ||
log.debug("Failed to launch process used to create 'classes.lst'.", e); | ||
return null; | ||
} | ||
|
||
if (!classesLstPath.toFile().exists()) { // shouldn't happen, but let's avoid any surprises | ||
return null; | ||
} | ||
|
||
return classesLstPath; | ||
} | ||
|
||
/** | ||
* Waits for the application terminate by looking for the proper logs, or just giving up after some time | ||
*/ | ||
private void waitForApplicationToStart(Process process) throws IOException { | ||
if (!process.isAlive()) { | ||
log.debug("The process that was supposed to create 'classes.lst' exited."); | ||
return; | ||
} | ||
// don't wait for more than 10 seconds | ||
// TODO: make it configurable? | ||
final int maxWaits = 200; | ||
int waits = 0; | ||
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); | ||
while (true) { | ||
if (!process.isAlive()) { | ||
log.debug("The process that was supposed to create classes.lst exited"); | ||
break; | ||
} | ||
String line = in.readLine(); | ||
if (line != null) { | ||
if (line.contains(Timing.INSTALLED_FEATURES) || line.contains(MainClassBuildStep.FAILED_TO_START_MESSAGE)) { | ||
process.destroy(); | ||
break; | ||
} | ||
} | ||
try { | ||
Thread.sleep(50); | ||
} catch (InterruptedException ignored) { | ||
} | ||
|
||
// we don't want to wait forever | ||
if (++waits >= maxWaits) { | ||
break; | ||
} | ||
} | ||
try { | ||
in.close(); | ||
} catch (IOException ignored) { | ||
} | ||
} | ||
|
||
/** | ||
* @return The path of the created app-cds.jsa file or null of the file was not created | ||
*/ | ||
private Path createAppCDS(JarBuildItem jarResult, String javaBin, | ||
Path classesLstPath, boolean isFastFar) { | ||
|
||
Path workingDirectory = isFastFar ? jarResult.getPath().getParent().getParent() : jarResult.getPath().getParent(); | ||
Path appCDSPath = workingDirectory.resolve("app-cds.jsa"); | ||
|
||
List<String> command = new ArrayList<>(6); | ||
command.add(javaBin); | ||
command.add("-Xshare:dump"); | ||
command.add("-XX:SharedClassListFile=" + classesLstPath.toAbsolutePath().toString()); | ||
// We use the relative paths because at runtime 'java -XX:SharedArchiveFile=... -jar ...' expects the AppCDS and jar files | ||
// to match exactly what was used at build time. | ||
// For that reason we also run the creation process from inside the output directory, | ||
// The end result is that users can simply use 'java -XX:SharedArchiveFile=app-cds.jsa -jar app.jar' | ||
command.add("-XX:SharedArchiveFile=" + appCDSPath.getFileName().toString()); | ||
command.add("--class-path"); | ||
command.add(jarResult.getPath().getFileName().toString()); | ||
|
||
if (log.isDebugEnabled()) { | ||
log.debugf("Launching command: '%s' to create final AppCDS", String.join(" ", command)); | ||
} | ||
|
||
int exitCode; | ||
try { | ||
exitCode = new ProcessBuilder(command) | ||
.redirectErrorStream(true) | ||
.directory(workingDirectory.toFile()) | ||
.start() | ||
.waitFor(); | ||
} catch (Exception e) { | ||
log.debug("Failed to launch process used to create AppCDS.", e); | ||
return null; | ||
} | ||
|
||
if (exitCode != 0) { | ||
log.debugf("The process that was supposed to create AppCDS exited with error code: %d", exitCode); | ||
return null; | ||
} | ||
|
||
if (!appCDSPath.toFile().exists()) { // shouldn't happen, but let's avoid any surprises | ||
return null; | ||
} | ||
|
||
return appCDSPath; | ||
} | ||
|
||
static class AppCDSRequired implements BooleanSupplier { | ||
|
||
private final PackageConfig packageConfig; | ||
|
||
AppCDSRequired(PackageConfig packageConfig) { | ||
this.packageConfig = packageConfig; | ||
} | ||
|
||
@Override | ||
public boolean getAsBoolean() { | ||
if (!packageConfig.createAppcds || !packageConfig.isAnyJarType()) { | ||
return false; | ||
} | ||
|
||
if (!JavaVersionUtil.isJava11OrHigher()) { | ||
log.warn("AppCDS can only be used with Java 11+."); | ||
return false; | ||
} | ||
return true; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
core/deployment/src/main/java/io/quarkus/deployment/util/JavaVersionUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package io.quarkus.deployment.util; | ||
|
||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
public class JavaVersionUtil { | ||
|
||
private static final Pattern PATTERN = Pattern.compile("(?:1\\.)?(\\d+)(?:\\..*)?"); | ||
|
||
private static boolean IS_JAVA_11_OR_NEWER; | ||
|
||
static { | ||
Matcher matcher = PATTERN.matcher(System.getProperty("java.version", "")); | ||
IS_JAVA_11_OR_NEWER = !matcher.matches() || Integer.parseInt(matcher.group(1)) >= 11; | ||
|
||
} | ||
|
||
public static boolean isJava11OrHigher() { | ||
return IS_JAVA_11_OR_NEWER; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters