Skip to content
Open
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
8 changes: 6 additions & 2 deletions TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,14 @@ This can be achieved by adding the following system property: `-Dquarkus.debug.p

There is also a nice visualization of build steps available in the Dev UI located here: <http://localhost:8080/q/dev/build-steps>.

If you want to have the same visualization of build steps processing when building your application, you can use the `quarkus.debug.dump-build-metrics=true` property.
For example using `mvn package -Dquarkus.debug.dump-build-metrics=true`, will generate a `build-metrics.json` in your `target` repository that you can process via the quarkus-build-report application available here <https://github.com/mkouba/quarkus-build-report>.
If you want to have a similar visualization of build steps processing when building your application, you can use the `quarkus.builder.metrics.enabled` property.
For example using `mvn package -Dquarkus.builder.metrics.enabled=true`, will generate a `build-metrics.json` in your `target` repository.
The generated file can be processed with the `quarkus-build-report` application available here <https://github.com/mkouba/quarkus-build-report>.
This application will generate a `report.html` that you can open in your browser.

There is also the `quarkus.builder.metrics.extended-capture` config property.
If set to `true` then the collection of metrics is enhanced but the size of the generated JSON file may grow significantly.

## What about Windows?

If you are on Windows, you can still get useful performance insights using JFR - Java Flight Recorder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ private void doProduce(ItemId id, BuildItem value) {
throw Messages.msg.cannotMulti(id);
}
}
execution.getMetrics().buildItemProduced(value);
execution.getMetrics().buildItemProduced(stepInfo, value);
}

void depFinished() {
Expand Down
203 changes: 138 additions & 65 deletions core/builder/src/main/java/io/quarkus/builder/BuildMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;

Expand All @@ -26,18 +29,43 @@

public class BuildMetrics {

public static final String BUILDER_METRICS_ENABLED = "quarkus.builder.metrics.enabled";
public static final String BUILDER_METRICS_EXTENDED_CAPTURE = "quarkus.builder.metrics.extended-capture";

static final Logger LOG = Logger.getLogger(BuildMetrics.class.getName());

private volatile LocalDateTime started;
private volatile long duration;
private final String buildTargetName;
private final ConcurrentMap<String, BuildStepRecord> records = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Integer> buildItems = new ConcurrentHashMap<>();
// build step id -> record
private final ConcurrentMap<String, BuildStepRecord> records;
// build item class -> count
private final ConcurrentMap<String, Long> buildItems;
// build step id -> produced build items
private final ConcurrentMap<String, List<String>> buildItemsExtended;
private final AtomicInteger idGenerator;

public BuildMetrics(String buildTargetName) {
boolean enabled = Boolean.getBoolean(BUILDER_METRICS_ENABLED)
// This system property is deprecated and will be removed
|| Boolean.getBoolean("quarkus.debug.dump-build-metrics");
this.buildTargetName = buildTargetName;
this.idGenerator = new AtomicInteger();
if (enabled) {
this.idGenerator = new AtomicInteger();
this.records = new ConcurrentHashMap<>();
if (Boolean.getBoolean(BUILDER_METRICS_EXTENDED_CAPTURE)) {
this.buildItemsExtended = new ConcurrentHashMap<>();
this.buildItems = null;
} else {
this.buildItemsExtended = null;
this.buildItems = new ConcurrentHashMap<>();
}
} else {
this.idGenerator = null;
this.records = null;
this.buildItemsExtended = null;
this.buildItems = null;
}
}

public Collection<BuildStepRecord> getRecords() {
Expand All @@ -53,82 +81,127 @@ public void buildFinished(long duration) {
}

public void buildStepFinished(StepInfo stepInfo, String thread, LocalTime started, long duration) {
records.put(stepInfo.getBuildStep().getId(),
new BuildStepRecord(idGenerator.incrementAndGet(), stepInfo, thread, started, duration));
if (enabled()) {
records.put(stepInfo.getBuildStep().getId(),
new BuildStepRecord(idGenerator.incrementAndGet(), stepInfo, thread, started, duration));
}
}

public void buildItemProduced(BuildItem buildItem) {
buildItems.compute(buildItem.getClass().getName(), this::itemProduced);
public void buildItemProduced(StepInfo stepInfo, BuildItem buildItem) {
if (enabled()) {
if (buildItems != null) {
buildItems.compute(buildItem.getClass().getName(), this::itemProduced);
} else {
buildItemsExtended.compute(stepInfo.getBuildStep().getId(), (key, list) -> {
String buildItemClass = buildItem.getClass().getName();
if (list == null) {
list = new ArrayList<>();
}
list.add(buildItemClass);
return list;
});
}
}
}

private Integer itemProduced(String key, Integer val) {
if (val == null) {
return 1;
}
return val + 1;
private Long itemProduced(String key, Long val) {
return val == null ? 1 : val + 1;
}

public void dumpTo(Path file) throws IOException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
if (enabled()) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

List<BuildStepRecord> sortedSteps = new ArrayList<>(records.values());
sortedSteps.sort(new Comparator<BuildStepRecord>() {
@Override
public int compare(BuildStepRecord o1, BuildStepRecord o2) {
return Long.compare(o2.duration, o1.duration);
}
});

JsonObjectBuilder json = Json.object();
json.put("buildTarget", buildTargetName);
json.put("started", started.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
json.put("duration", duration);

JsonArrayBuilder steps = Json.array();
json.put("records", steps);
for (BuildStepRecord rec : sortedSteps) {
JsonObjectBuilder recObject = Json.object();
recObject.put("id", rec.id);
recObject.put("stepId", rec.stepInfo.getBuildStep().getId());
recObject.put("thread", rec.thread);
recObject.put("started", rec.started.format(formatter));
recObject.put("duration", rec.duration);
JsonArrayBuilder dependentsArray = Json.array();
for (StepInfo dependent : rec.stepInfo.getDependents()) {
BuildStepRecord dependentRecord = records.get(dependent.getBuildStep().getId());
if (dependentRecord != null) {
dependentsArray.add(dependentRecord.id);
} else {
LOG.warnf("Dependent record not found for stepId: %s", dependent.getBuildStep().getId());
}
}
recObject.put("dependents", dependentsArray);
if (buildItemsExtended != null) {
JsonArrayBuilder producedItems = Json.array();
List<String> items = buildItemsExtended.get(rec.stepInfo.getBuildStep().getId());
if (items != null) {
// build item class -> count
Map<String, Long> counts = items
.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
List<Entry<String, Long>> sortedItems = new ArrayList<>(counts.entrySet());
sortedItems.sort(this::compareBuildItems);
for (Entry<String, Long> e : sortedItems) {
producedItems.add(Json.object()
.put("item", e.getKey())
.put("count", e.getValue()));
}
recObject.put("producedItems", producedItems);
}
}
steps.add(recObject);
}

List<BuildStepRecord> sortedSteps = new ArrayList<>(records.values());
sortedSteps.sort(new Comparator<BuildStepRecord>() {
@Override
public int compare(BuildStepRecord o1, BuildStepRecord o2) {
return Long.compare(o2.duration, o1.duration);
List<Entry<String, Long>> sortedItems;
if (buildItemsExtended != null) {
// build item class -> count
Map<String, Long> counts = buildItemsExtended.values()
.stream()
.flatMap(List::stream)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
sortedItems = new ArrayList<>(counts.entrySet());
} else {
sortedItems = new ArrayList<>(buildItems.size());
buildItems.entrySet().forEach(sortedItems::add);
}
});

JsonObjectBuilder json = Json.object();
json.put("buildTarget", buildTargetName);
json.put("started", started.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
json.put("duration", duration);

JsonArrayBuilder steps = Json.array();
json.put("records", steps);
for (BuildStepRecord rec : sortedSteps) {
JsonObjectBuilder recObject = Json.object();
recObject.put("id", rec.id);
recObject.put("stepId", rec.stepInfo.getBuildStep().getId());
recObject.put("thread", rec.thread);
recObject.put("started", rec.started.format(formatter));
recObject.put("duration", rec.duration);
JsonArrayBuilder dependentsArray = Json.array();
for (StepInfo dependent : rec.stepInfo.getDependents()) {
BuildStepRecord dependentRecord = records.get(dependent.getBuildStep().getId());
if (dependentRecord != null) {
dependentsArray.add(dependentRecord.id);
} else {
LOG.warnf("Dependent record not found for stepId: %s", dependent.getBuildStep().getId());
}
sortedItems.sort(this::compareBuildItems);
JsonArrayBuilder items = Json.array();
json.put("items", items);
long itemsCount = 0;
for (Entry<String, Long> e : sortedItems) {
JsonObjectBuilder itemObject = Json.object();
itemObject.put("class", e.getKey());
itemObject.put("count", e.getValue());
items.add(itemObject);
itemsCount += e.getValue();
}
recObject.put("dependents", dependentsArray);
steps.add(recObject);
}
json.put("itemsCount", itemsCount);

List<Entry<String, Integer>> sortedItems = new ArrayList<>(buildItems.size());
buildItems.entrySet().forEach(sortedItems::add);
sortedItems.sort(new Comparator<Entry<String, Integer>>() {
@Override
public int compare(Entry<String, Integer> o1, Entry<String, Integer> o2) {
return Integer.compare(o2.getValue(), o1.getValue());
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file.toFile(), StandardCharsets.UTF_8))) {
json.appendTo(writer);
}
});
JsonArrayBuilder items = Json.array();
json.put("items", items);
Integer itemsCount = 0;
for (Entry<String, Integer> e : sortedItems) {
JsonObjectBuilder itemObject = Json.object();
itemObject.put("class", e.getKey());
itemObject.put("count", e.getValue());
items.add(itemObject);
itemsCount += e.getValue();
}
json.put("itemsCount", itemsCount);
}

try (BufferedWriter writer = new BufferedWriter(new FileWriter(file.toFile(), StandardCharsets.UTF_8))) {
json.appendTo(writer);
}
private boolean enabled() {
return records != null;
}

private int compareBuildItems(Entry<String, Long> o1, Entry<String, Long> o2) {
return Long.compare(o2.getValue(), o1.getValue());
}

public static class BuildStepRecord {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ public interface DebugConfig {

/**
* If set to true then dump the build metrics to a JSON file in the build directory.
*
* @deprecated Use {@link io.quarkus.runtime.BuilderConfig#Metrics()} instead.
*/
@Deprecated(forRemoval = true, since = "3.31")
@WithDefault("false")
boolean dumpBuildMetrics();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.quarkus.builder.BuildChain;
import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildExecutionBuilder;
import io.quarkus.builder.BuildMetrics;
import io.quarkus.builder.BuildResult;
import io.quarkus.builder.item.BuildItem;
import io.quarkus.deployment.builditem.AdditionalApplicationArchiveBuildItem;
Expand Down Expand Up @@ -132,6 +133,14 @@ public BuildResult run() throws Exception {
if (launchMode.isDevOrTest()) {
chainBuilder.addFinal(RuntimeApplicationShutdownBuildItem.class);
}
if (System.getProperty(BuildMetrics.BUILDER_METRICS_ENABLED) == null
&& launchMode.isDev()
&& !launchMode.isRemoteDev()) {
// If quarkus.builder.metrics.enabled is not set then
// collect build metrics in dev mode but not in remote-dev
// (as it could cause issues with container permissions)
System.setProperty("quarkus.builder.metrics.enabled", "true");
}

final ArchiveRootBuildItem.Builder rootBuilder = ArchiveRootBuildItem.builder();
if (root != null) {
Expand Down Expand Up @@ -162,18 +171,16 @@ public BuildResult run() throws Exception {
+ "ms";
if (launchMode.isProduction()) {
log.info(message);
if (Boolean.parseBoolean(System.getProperty("quarkus.debug.dump-build-metrics"))) {
buildResult.getMetrics().dumpTo(targetDir.resolve("build-metrics.json"));
}
} else {
//test and dev mode already report the total startup time, no need to add noise to the logs
log.debug(message);
}

// Dump the metrics in the dev mode but not remote-dev (as it could cause issues with container permissions)
if (launchMode.isDev() && !launchMode.isRemoteDev()) {
buildResult.getMetrics().dumpTo(targetDir.resolve("build-metrics.json"));
}
// If enabled then dump build metrics to a JSON file in the build directory
if (targetDir != null) {
buildResult.getMetrics().dumpTo(targetDir.resolve("build-metrics.json"));
}

return buildResult;
} finally {
try {
Expand Down
23 changes: 23 additions & 0 deletions core/runtime/src/main/java/io/quarkus/runtime/BuilderConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

/**
* Builder.
Expand All @@ -26,4 +27,26 @@ public interface BuilderConfig {
* Whether to log the cause of a conflict.
*/
Optional<Boolean> logConflictCause();

/**
* Build metrics configuration.
*/
Metrics Metrics();

interface Metrics {

/**
* If set to true then dump the build metrics to a JSON file in the build directory.
*/
@WithDefault("false")
boolean enabled();

/**
* If set to true then the collection of metrics is enhanced but the size of the generated JSON file may grow
* significantly.
*/
@WithDefault("false")
boolean extendedCapture();

}
}
Loading
Loading