Skip to content

Commit

Permalink
CLI: add an option to generate a simple build report
Browse files Browse the repository at this point in the history
- resolves quarkusio#29174

Co-authored-by: Guillaume Smet <[email protected]>
  • Loading branch information
mkouba and gsmet committed Aug 15, 2023
1 parent b4f8555 commit 2aef8e9
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ public class DebugConfig {
*/
@ConfigItem
Optional<String> generatedSourcesDir;

/**
* If set to true then dump the build metrics to a JSON file in the build directory.
*/
@ConfigItem(defaultValue = "false")
boolean dumpBuildMetrics;
}
17 changes: 17 additions & 0 deletions devtools/cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-netty</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
<!--
Somehow, we need this as otherwise you end up with:
java.lang.ClassNotFoundException: io.netty.handler.codec.http2.DefaultHttp2FrameWriter
Expand Down Expand Up @@ -126,6 +130,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute-deployment</artifactId>
<type>pom</type>
<scope>test</scope>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
Expand Down
12 changes: 11 additions & 1 deletion devtools/cli/src/main/java/io/quarkus/cli/Build.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,24 @@ public Integer call() {
output.throwIfUnmatchedArguments(spec.commandLine());

BuildSystemRunner runner = getRunner();
if (buildOptions.generateReport) {
params.add("-Dquarkus.debug.dump-build-metrics=true");
}
BuildSystemRunner.BuildCommandArgs commandArgs = runner.prepareBuild(buildOptions, runMode, params);

if (runMode.isDryRun()) {
dryRunBuild(spec.commandLine().getHelp(), runner.getBuildTool(), commandArgs);
return CommandLine.ExitCode.OK;
}

return runner.run(commandArgs);
int exitCode = runner.run(commandArgs);
if (exitCode == CommandLine.ExitCode.OK && buildOptions.generateReport) {
output.printText(new String[] {
"\nBuild report available: " + new BuildReport(runner).generate().toPath().toAbsolutePath().toString()
+ "\n"
});
}
return exitCode;
} catch (Exception e) {
return output.handleCommandException(e,
"Unable to build project: " + e.getMessage());
Expand Down
82 changes: 82 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/BuildReport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.quarkus.cli;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.cli.build.BuildSystemRunner;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;

@CheckedTemplate(basePath = "")
public class BuildReport {

static native TemplateInstance buildReport(String buildTarget, long duration, Set<String> threads,
List<BuildStepRecord> records);

private final BuildSystemRunner runner;

public BuildReport(BuildSystemRunner runner) {
this.runner = runner;
}

File generate() throws IOException {
File metricsJsonFile = runner.getProjectRoot().resolve(runner.getBuildTool().getBuildDirectory())
.resolve("build-metrics.json").toFile();
if (!metricsJsonFile.canRead()) {
throw new IllegalStateException("Build metrics file cannot be read: " + metricsJsonFile);
}

ObjectMapper objectMapper = new ObjectMapper();
JsonNode root = objectMapper.readTree(metricsJsonFile);

String buildTarget = root.get("buildTarget").asText();
long duration = root.get("duration").asLong();
Set<String> threads = new HashSet<>();
List<BuildStepRecord> records = new ArrayList<>();

for (JsonNode record : root.get("records")) {
String thread = record.get("thread").asText();
threads.add(thread);
records.add(new BuildStepRecord(record.get("stepId").asText(), record.get("started").asText(),
record.get("duration").asLong(), thread));
}

File output = runner.getProjectRoot().resolve(runner.getBuildTool().getBuildDirectory())
.resolve("build-report.html").toFile();
if (output.exists() && !output.canWrite()) {
throw new IllegalStateException("Build report file cannot be written to: " + output);
}
try {
Files.writeString(output.toPath(),
BuildReport.buildReport(buildTarget, duration, threads, records).render());
return output;
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}

public static class BuildStepRecord {

public final String stepId;
public final String started;
public final long duration;
public final String thread;

public BuildStepRecord(String stepId, String started, long duration, String thread) {
this.stepId = stepId;
this.started = started;
this.duration = duration;
this.thread = thread;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ public class BuildOptions {
"--no-tests" }, description = "Run tests.", negatable = true, defaultValue = "false")
public boolean skipTests = false;

@CommandLine.Option(order = 7, names = {
"--report" }, description = "Generate build report.", negatable = true, defaultValue = "false")
public boolean generateReport = false;

public boolean skipTests() {
return skipTests;
}

@Override
public String toString() {
return "BuildOptions [buildNative=" + buildNative + ", clean=" + clean + ", offline=" + offline + ", skipTests="
+ skipTests + "]";
+ skipTests + ", generateReport=" + generateReport + "]";
}
}
120 changes: 120 additions & 0 deletions devtools/cli/src/main/resources/templates/buildReport.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>Quarkus Build Report - {buildTarget}</title>
<style>
body {
font-family: sans;
padding: 2rem;
}

div.container {
width: 100%;
margin-right: auto;
margin-left: auto;
}

table {
width: 90%;
margin-bottom: 1rem;
color: #212529;
vertical-align: top;
border-color: #dee2e6;
caption-side: bottom;
border-collapse: collapse;
--bs-table-bg: transparent;
--bs-table-accent-bg: transparent;
--bs-table-striped-color: #212529;
--bs-table-striped-bg: rgba(0, 0, 0, 0.05);
--bs-table-active-color: #212529;
--bs-table-active-bg: rgba(0, 0, 0, 0.1);
--bs-table-hover-color: #212529;
--bs-table-hover-bg: rgba(0, 0, 0, 0.075);
}

table> :not(caption)>*>* {
padding: .5rem .5rem;
background-color: var(--bs-table-bg);
border-bottom-width: 1px;
box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);
}

table> :not(:first-child) {
border-top: 2px solid black;
}

tbody>tr:nth-of-type(2n+1)>* {
--bs-table-accent-bg: var(--bs-table-striped-bg);
color: var(--bs-table-striped-color);
}

h1,
h2 {
margin-top: 0;
margin-bottom: .5rem;
font-weight: 500;
line-height: 1.2;
}

p.lead {
font-size: 1.25rem;
font-weight: 300;
}

th {
text-align: left;
}

tr {
border-bottom: 1px solid #dee2e6;
}
</style>
</head>

<body>
<div class="container">

<h1>Quarkus Build Report - {buildTarget}</h1>

<p class="lead">
Executed <strong>{records.size}</strong> build steps on <strong>{threads.size}</strong> threads in
<strong>{duration}</strong> ms.
</p>

<h2>Build Steps</h2>

<table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Build Step</th>
<th scope="col">Started</th>
<th scope="col">Duration</th>
<th scope="col">Thread</th>
</tr>
</thead>
<tbody>
{#for record in records}
<tr>
<td>{record_count}</td>
<td>
{record.stepId}
</td>
<td>
{record.started}
</td>
<td>
{#if record.duration < 1} &lt; 1ms {#else} {record.duration} ms {/if} </td>
<td>
{record.thread}
</td>
{/for}
</tbody>
</table>
</div>

</body>

</html>

0 comments on commit 2aef8e9

Please sign in to comment.