Skip to content

Commit

Permalink
Introduce the ability to generate AppCDS
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Jun 1, 2020
1 parent 03d826f commit e295147
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 10 deletions.
4 changes: 4 additions & 0 deletions core/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devtools-utilities</artifactId>
</dependency>
<dependency>
<!-- We don't want the annotation processor to run
as it leaves junk files in the project-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.microprofile.config.spi.ConfigBuilder;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
Expand All @@ -37,6 +35,7 @@
import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.builditem.DeploymentResultBuildItem;
import io.quarkus.deployment.util.JavaVersionUtil;
import io.quarkus.runtime.LaunchMode;

public class QuarkusAugmentor {
Expand Down Expand Up @@ -78,9 +77,7 @@ public class QuarkusAugmentor {
}

public BuildResult run() throws Exception {
Pattern pattern = Pattern.compile("(?:1\\.)?(\\d+)(?:\\..*)?");
Matcher matcher = pattern.matcher(System.getProperty("java.version", ""));
if (matcher.matches() && Integer.parseInt(matcher.group(1)) < 11) {
if (!JavaVersionUtil.isJava11OrHigher()) {
log.warn("Using Java versions older than 11 to build"
+ " Quarkus applications is deprecated and will be disallowed in a future release!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,23 @@ public class PackageConfig {
*/
@ConfigItem
public Optional<String> outputName;

/**
* Whether to automate the creation of AppCDS. This has not effect when a native binary is needed and will be ignored in
* that case.
* Furthermore, this options only works for Java 11+.
*/
@ConfigItem
public boolean enableAppcds;

public boolean isAnyJarType() {
return (type.equalsIgnoreCase(PackageConfig.LEGACY) ||
type.equalsIgnoreCase(PackageConfig.JAR) ||
type.equalsIgnoreCase(PackageConfig.FAST_JAR) ||
type.equalsIgnoreCase(PackageConfig.UBER_JAR));
}

public boolean isFastFar() {
return type.equalsIgnoreCase(PackageConfig.FAST_JAR);
}
}
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
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.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.isFastFar());
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
*/
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)) {
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.enableAppcds || !packageConfig.isAnyJarType()) {
return false;
}

if (!JavaVersionUtil.isJava11OrHigher()) {
log.warn("AppCDS can only be used with Java 11+.");
return false;
}
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1088,10 +1088,7 @@ static class JarRequired implements BooleanSupplier {

@Override
public boolean getAsBoolean() {
return packageConfig.type.equalsIgnoreCase(PackageConfig.LEGACY) ||
packageConfig.type.equalsIgnoreCase(PackageConfig.JAR) ||
packageConfig.type.equalsIgnoreCase(PackageConfig.FAST_JAR) ||
packageConfig.type.equalsIgnoreCase(PackageConfig.UBER_JAR);
return packageConfig.isAnyJarType();
}
}

Expand Down
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class Timing {

private static final String UNSET_VALUE = "<<unset>>";

public static final String INSTALLED_FEATURES = "Installed features";

public static void staticInitStarted() {
if (bootStartTime < 0) {
bootStartTime = System.nanoTime();
Expand Down Expand Up @@ -89,7 +91,7 @@ public static void printStartupTime(String name, String version, String quarkusV
secondsRepresentation, httpServerInfo);
}
logger.infof("Profile %s activated. %s", profile, liveCoding ? "Live Coding activated." : "");
logger.infof("Installed features: [%s]", features);
logger.infof(INSTALLED_FEATURES + ": [%s]", features);
bootStartTime = -1;
}

Expand Down

0 comments on commit e295147

Please sign in to comment.