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 2, 2020
1 parent 6cf7aea commit 33424b6
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 12 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,24 @@ 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+.
* Finally, care must be taken to use the same exact JVM version when building and running the application.
*/
@ConfigItem
public boolean createAppcds;

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

public boolean isFastJar() {
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,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;
}
}
}
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
Expand Up @@ -69,7 +69,7 @@
import io.quarkus.runtime.annotations.QuarkusMain;
import io.quarkus.runtime.configuration.ProfileManager;

class MainClassBuildStep {
public class MainClassBuildStep {

static final String MAIN_CLASS = "io.quarkus.runner.GeneratedMain";
static final String STARTUP_CONTEXT = "STARTUP_CONTEXT";
Expand All @@ -80,6 +80,8 @@ class MainClassBuildStep {
private static final FieldDescriptor STARTUP_CONTEXT_FIELD = FieldDescriptor.of(Application.APP_CLASS_NAME, STARTUP_CONTEXT,
StartupContext.class);

public static final String FAILED_TO_START_MESSAGE = "Failed to start application";

@BuildStep
void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
List<ObjectSubstitutionBuildItem> substitutions,
Expand Down Expand Up @@ -231,7 +233,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
tryBlock.load(LaunchMode.DEVELOPMENT.equals(launchMode.getLaunchMode())));
cb = tryBlock.addCatch(Throwable.class);
cb.invokeVirtualMethod(ofMethod(Logger.class, "error", void.class, Object.class, Throwable.class),
cb.readStaticField(logField.getFieldDescriptor()), cb.load("Failed to start application"),
cb.readStaticField(logField.getFieldDescriptor()), cb.load(FAILED_TO_START_MESSAGE),
cb.getCaughtException());

// an exception was thrown before logging was actually setup, we simply dump everything to the console
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 33424b6

Please sign in to comment.