From 25d8a126c4063377815af1aac4286feb690cf2cf Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 22 Jun 2022 16:29:13 +0200 Subject: [PATCH] Dev UI build steps view improvements - add build step dependency graph (direct dependencies and dependents only) - add build step concurrent execution chart --- build-parent/pom.xml | 6 + .../java/io/quarkus/builder/BuildContext.java | 2 +- .../builder/{metrics => }/BuildMetrics.java | 61 +++-- .../java/io/quarkus/builder/BuildResult.java | 1 - .../java/io/quarkus/builder/Execution.java | 5 +- .../quarkus/builder/{metrics => }/Json.java | 2 +- .../quarkus/deployment/ExtensionLoader.java | 80 +++--- extensions/vertx-http/deployment/pom.xml | 18 ++ .../BuildMetricsDevConsoleProcessor.java | 239 +++++++++++++++++- .../build-step-dependency-graph.html | 157 ++++++++++++ .../build-steps.html | 195 +++++++++++++- .../main/resources/dev-templates/main.html | 2 +- 12 files changed, 687 insertions(+), 81 deletions(-) rename core/builder/src/main/java/io/quarkus/builder/{metrics => }/BuildMetrics.java (58%) rename core/builder/src/main/java/io/quarkus/builder/{metrics => }/Json.java (99%) create mode 100644 extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-step-dependency-graph.html diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 5d7707ae0e12d..1edb44a7de254 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -138,6 +138,7 @@ 8.9.1 6.6.0 + 3.7.1 0.14.6 @@ -321,6 +322,11 @@ d3js ${webjar.d3js.version} + + org.webjars + chartjs + ${webjar.chartjs.version} + com.github.davidmoten diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildContext.java b/core/builder/src/main/java/io/quarkus/builder/BuildContext.java index ffaf9aed77937..1e3b289b267a3 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildContext.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildContext.java @@ -290,7 +290,7 @@ void run() { } } finally { long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); - execution.getMetrics().buildStepFinished(buildStep.getId(), currentThread.getName(), started, duration); + execution.getMetrics().buildStepFinished(stepInfo, currentThread.getName(), started, duration); log.tracef("Finished step \"%s\" in %s ms", buildStep, duration); execution.removeBuildContext(stepInfo, this); } diff --git a/core/builder/src/main/java/io/quarkus/builder/metrics/BuildMetrics.java b/core/builder/src/main/java/io/quarkus/builder/BuildMetrics.java similarity index 58% rename from core/builder/src/main/java/io/quarkus/builder/metrics/BuildMetrics.java rename to core/builder/src/main/java/io/quarkus/builder/BuildMetrics.java index 1e4625b452854..39071ad082bbc 100644 --- a/core/builder/src/main/java/io/quarkus/builder/metrics/BuildMetrics.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildMetrics.java @@ -1,4 +1,4 @@ -package io.quarkus.builder.metrics; +package io.quarkus.builder; import java.io.BufferedWriter; import java.io.FileWriter; @@ -19,20 +19,22 @@ import org.jboss.logging.Logger; -import io.quarkus.builder.metrics.Json.JsonArrayBuilder; -import io.quarkus.builder.metrics.Json.JsonObjectBuilder; +import io.quarkus.builder.Json.JsonArrayBuilder; +import io.quarkus.builder.Json.JsonObjectBuilder; public class BuildMetrics { static final Logger LOG = Logger.getLogger(BuildMetrics.class.getName()); private volatile LocalDateTime started; + private volatile long duration; private final String buildTargetName; private final ConcurrentMap records = new ConcurrentHashMap<>(); - private final AtomicInteger duplicates = new AtomicInteger(); + private final AtomicInteger idGenerator; public BuildMetrics(String buildTargetName) { this.buildTargetName = buildTargetName; + this.idGenerator = new AtomicInteger(); } public Collection getRecords() { @@ -43,13 +45,13 @@ public void buildStarted() { this.started = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS); } - public void buildStepFinished(String stepId, String thread, LocalTime started, long duration) { - BuildStepRecord prev = records.putIfAbsent(stepId, new BuildStepRecord(stepId, thread, started, duration)); - if (prev != null) { - String newName = stepId + "_d#" + duplicates.incrementAndGet(); - LOG.debugf("A build step with the same identifier already exists - added a generated suffix: %s", newName); - buildStepFinished(newName, thread, started, duration); - } + public void buildFinished(long duration) { + this.duration = 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)); } public void dumpTo(Path file) throws IOException { @@ -66,16 +68,28 @@ public int compare(BuildStepRecord o1, BuildStepRecord o2) { 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("steps", steps); + json.put("records", steps); for (BuildStepRecord rec : sorted) { - JsonObjectBuilder recJson = Json.object(); - recJson.put("stepId", rec.stepId); - recJson.put("thread", rec.thread); - recJson.put("started", rec.started.format(formatter)); - recJson.put("duration", rec.duration); - steps.add(recJson); + 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); + steps.add(recObject); } try (BufferedWriter writer = new BufferedWriter(new FileWriter(file.toFile(), StandardCharsets.UTF_8))) { json.appendTo(writer); @@ -85,9 +99,11 @@ public int compare(BuildStepRecord o1, BuildStepRecord o2) { public static class BuildStepRecord { /** - * The identifier of the build step. + * A unique record id. */ - public final String stepId; + public final int id; + + public final StepInfo stepInfo; /** * The name of the thread this build step was executed on. @@ -104,8 +120,9 @@ public static class BuildStepRecord { */ public final long duration; - BuildStepRecord(String stepId, String thread, LocalTime started, long duration) { - this.stepId = stepId; + BuildStepRecord(int id, StepInfo stepInfo, String thread, LocalTime started, long duration) { + this.id = id; + this.stepInfo = stepInfo; this.thread = thread; this.started = started; this.duration = duration; diff --git a/core/builder/src/main/java/io/quarkus/builder/BuildResult.java b/core/builder/src/main/java/io/quarkus/builder/BuildResult.java index 99930e723002a..63947904785ea 100644 --- a/core/builder/src/main/java/io/quarkus/builder/BuildResult.java +++ b/core/builder/src/main/java/io/quarkus/builder/BuildResult.java @@ -11,7 +11,6 @@ import io.quarkus.builder.item.BuildItem; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.builder.item.SimpleBuildItem; -import io.quarkus.builder.metrics.BuildMetrics; /** * The final result of a successful deployment operation. diff --git a/core/builder/src/main/java/io/quarkus/builder/Execution.java b/core/builder/src/main/java/io/quarkus/builder/Execution.java index b46e91de5dd2d..aaa7c96fadc03 100644 --- a/core/builder/src/main/java/io/quarkus/builder/Execution.java +++ b/core/builder/src/main/java/io/quarkus/builder/Execution.java @@ -20,7 +20,6 @@ import io.quarkus.builder.diag.Diagnostic; import io.quarkus.builder.item.BuildItem; -import io.quarkus.builder.metrics.BuildMetrics; /** */ @@ -133,8 +132,10 @@ BuildResult run() throws BuildException { if (lastStepCount.get() > 0) throw new BuildException("Extra steps left over", Collections.emptyList()); + long duration = max(0, System.nanoTime() - start); + metrics.buildFinished(TimeUnit.NANOSECONDS.toMillis(duration)); return new BuildResult(singles, multis, finalIds, Collections.unmodifiableList(diagnostics), - max(0, System.nanoTime() - start), metrics); + duration, metrics); } EnhancedQueueExecutor getExecutor() { diff --git a/core/builder/src/main/java/io/quarkus/builder/metrics/Json.java b/core/builder/src/main/java/io/quarkus/builder/Json.java similarity index 99% rename from core/builder/src/main/java/io/quarkus/builder/metrics/Json.java rename to core/builder/src/main/java/io/quarkus/builder/Json.java index 30f4c75cd613d..bde06cbaff03f 100644 --- a/core/builder/src/main/java/io/quarkus/builder/metrics/Json.java +++ b/core/builder/src/main/java/io/quarkus/builder/Json.java @@ -1,4 +1,4 @@ -package io.quarkus.builder.metrics; +package io.quarkus.builder; import java.io.IOException; import java.util.ArrayList; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java index 558b01b82e5b1..3d2c40e816ab8 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ExtensionLoader.java @@ -192,8 +192,17 @@ public static Consumer loadStepsFrom(ClassLoader classLoader, Consumer result = Functions.discardingConsumer(); // BooleanSupplier factory - result = result.andThen(bcb -> bcb.addBuildStep(bc -> { - bc.produce(bsf); + result = result.andThen(bcb -> bcb.addBuildStep(new io.quarkus.builder.BuildStep() { + + @Override + public void execute(BuildContext context) { + context.produce(bsf); + } + + @Override + public String getId() { + return ExtensionLoader.class.getName() + "#booleanSupplierFactory"; + } }).produces(BooleanSupplierFactoryBuildItem.class).build()); // the proxy objects used for run time config in the recorders @@ -226,40 +235,49 @@ public static Consumer loadStepsFrom(ClassLoader classLoader, throw new IllegalStateException("No config found for " + entry.getKey()); } - result = result.andThen(bcb -> bcb.addBuildStep(bc -> { - bc.produce(new ConfigurationBuildItem(readResult)); - bc.produce(new RunTimeConfigurationProxyBuildItem(proxies)); + result = result.andThen(bcb -> bcb.addBuildStep(new io.quarkus.builder.BuildStep() { - ObjectLoader rootLoader = new ObjectLoader() { - public ResultHandle load(final BytecodeCreator body, final Object obj, final boolean staticInit) { - return body.readStaticField(rootFields.get(obj)); - } + @Override + public void execute(BuildContext bc) { + bc.produce(new ConfigurationBuildItem(readResult)); + bc.produce(new RunTimeConfigurationProxyBuildItem(proxies)); - public boolean canHandleObject(final Object obj, final boolean staticInit) { - return rootFields.containsKey(obj); - } - }; + ObjectLoader rootLoader = new ObjectLoader() { + public ResultHandle load(final BytecodeCreator body, final Object obj, final boolean staticInit) { + return body.readStaticField(rootFields.get(obj)); + } - ObjectLoader mappingLoader = new ObjectLoader() { - @Override - public ResultHandle load(final BytecodeCreator body, final Object obj, final boolean staticInit) { - ConfigClassWithPrefix mapping = mappingClasses.get(obj); - MethodDescriptor getConfig = MethodDescriptor.ofMethod(ConfigProvider.class, "getConfig", Config.class); - ResultHandle config = body.invokeStaticMethod(getConfig); - MethodDescriptor getMapping = MethodDescriptor.ofMethod(SmallRyeConfig.class, "getConfigMapping", - Object.class, Class.class, String.class); - return body.invokeVirtualMethod(getMapping, config, body.loadClass(mapping.getKlass()), - body.load(mapping.getPrefix())); - } + public boolean canHandleObject(final Object obj, final boolean staticInit) { + return rootFields.containsKey(obj); + } + }; - @Override - public boolean canHandleObject(final Object obj, final boolean staticInit) { - return mappingClasses.containsKey(obj); - } - }; + ObjectLoader mappingLoader = new ObjectLoader() { + @Override + public ResultHandle load(final BytecodeCreator body, final Object obj, final boolean staticInit) { + ConfigClassWithPrefix mapping = mappingClasses.get(obj); + MethodDescriptor getConfig = MethodDescriptor.ofMethod(ConfigProvider.class, "getConfig", Config.class); + ResultHandle config = body.invokeStaticMethod(getConfig); + MethodDescriptor getMapping = MethodDescriptor.ofMethod(SmallRyeConfig.class, "getConfigMapping", + Object.class, Class.class, String.class); + return body.invokeVirtualMethod(getMapping, config, body.loadClass(mapping.getKlass()), + body.load(mapping.getPrefix())); + } - bc.produce(new BytecodeRecorderObjectLoaderBuildItem(rootLoader)); - bc.produce(new BytecodeRecorderObjectLoaderBuildItem(mappingLoader)); + @Override + public boolean canHandleObject(final Object obj, final boolean staticInit) { + return mappingClasses.containsKey(obj); + } + }; + + bc.produce(new BytecodeRecorderObjectLoaderBuildItem(rootLoader)); + bc.produce(new BytecodeRecorderObjectLoaderBuildItem(mappingLoader)); + } + + @Override + public String getId() { + return ExtensionLoader.class.getName() + "#config"; + } }).produces(ConfigurationBuildItem.class) .produces(RunTimeConfigurationProxyBuildItem.class) diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index 9a533cbe04e0f..72d2e5a02637d 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -72,6 +72,11 @@ d3js provided + + org.webjars + chartjs + provided + io.quarkus @@ -316,6 +321,19 @@ + + + org.webjars + chartjs + ${webjar.chartjs.version} + jar + true + ${project.build.directory}/classes/dev-static/js/ + **/chart.min.js + + + + diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/BuildMetricsDevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/BuildMetricsDevConsoleProcessor.java index b6679b2c16a8e..174df44dbee5b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/BuildMetricsDevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/BuildMetricsDevConsoleProcessor.java @@ -1,11 +1,20 @@ package io.quarkus.vertx.http.deployment.devmode; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; @@ -16,6 +25,9 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.vertx.http.deployment.devmode.BuildMetricsDevConsoleProcessor.DependecyGraph.Link; +import io.quarkus.vertx.http.deployment.devmode.BuildMetricsDevConsoleProcessor.DependecyGraph.Node; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; public class BuildMetricsDevConsoleProcessor { @@ -24,35 +36,246 @@ public class BuildMetricsDevConsoleProcessor { @BuildStep(onlyIf = IsDevelopment.class) DevConsoleTemplateInfoBuildItem collectMetrics(BuildSystemTargetBuildItem buildSystemTarget) { - - // We need to read the data lazily because the build is not finished yet at the time this build item is produced return new DevConsoleTemplateInfoBuildItem("buildMetrics", + // We need to read the data lazily because the build is not finished yet at the time this build item is produced + // This also means that no parsing is done until the build steps view is actually used new LazyValue>(new Supplier>() { @Override public Map get() { Map metrics = new HashMap<>(); + Map stepIdToRecord = new HashMap<>(); + Map recordIdToRecord = new HashMap<>(); + Map> threadToRecords = new HashMap<>(); + long buildDuration = 0; + LocalTime buildStarted = null; Path metricsJsonFile = buildSystemTarget.getOutputDirectory().resolve("build-metrics.json"); if (Files.isReadable(metricsJsonFile)) { try { JsonObject data = new JsonObject(Files.readString(metricsJsonFile)); - metrics.put("steps", data.getValue("steps")); + buildDuration = data.getLong("duration"); + buildStarted = LocalDateTime + .parse(data.getString("started"), DateTimeFormatter.ISO_LOCAL_DATE_TIME).toLocalTime(); + JsonArray records = data.getJsonArray("records"); - Set threads = new HashSet<>(); - for (Object step : data.getJsonArray("steps")) { - threads.add(((JsonObject) step).getString("thread")); + for (Object record : records) { + JsonObject recordObj = (JsonObject) record; + recordObj.put("encodedStepId", URLEncoder.encode(recordObj.getString("stepId"), + StandardCharsets.UTF_8.toString())); + String thread = recordObj.getString("thread"); + stepIdToRecord.put(recordObj.getString("stepId"), recordObj); + recordIdToRecord.put(recordObj.getInteger("id"), recordObj); + List steps = threadToRecords.get(thread); + if (steps == null) { + steps = new ArrayList<>(); + threadToRecords.put(thread, steps); + } + steps.add(recordObj); } - metrics.put("threads", threads); - + metrics.put("records", records); + metrics.put("duration", buildDuration); } catch (IOException e) { LOG.error(e); } } + + // Build dependency graphs + Map dependencyGraphs = new HashMap<>(); + for (Map.Entry e : stepIdToRecord.entrySet()) { + dependencyGraphs.put(e.getKey(), + buildDependencyGraph(e.getValue(), stepIdToRecord, recordIdToRecord)); + } + metrics.put("dependencyGraphs", dependencyGraphs); + + // Time slots + long slotDuration = Math.max(10, buildDuration / 100); + List slots = new ArrayList<>(); + long currentSlot = slotDuration; + while (currentSlot < buildDuration) { + slots.add(currentSlot); + currentSlot += slotDuration; + } + if (currentSlot != buildDuration) { + slots.add(buildDuration); + } + metrics.put("slots", slots); + + Map>> threadToSlotRecords = new HashMap<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + + for (Map.Entry> entry : threadToRecords.entrySet()) { + String thread = entry.getKey(); + List records = entry.getValue(); + List> threadSlots = new ArrayList<>(); + + for (Long slot : slots) { + List slotRecords = new ArrayList<>(); + for (JsonObject record : records) { + LocalTime started = LocalTime.parse(record.getString("started"), formatter); + long startAt = Duration.between(buildStarted, started).toMillis(); + if (startAt < slot && (slot - slotDuration) < (startAt + record.getLong("duration"))) { + slotRecords.add(record.getString("stepId")); + } + } + threadSlots.add(slotRecords); + } + threadToSlotRecords.put(thread, threadSlots); + } + metrics.put("threadSlotRecords", threadToSlotRecords); + return metrics; } })); } + DependecyGraph buildDependencyGraph(JsonObject step, Map stepIdToRecord, + Map recordIdToRecord) { + Set nodes = new HashSet<>(); + Set links = new HashSet<>(); + + addNodesDependents(step, nodes, links, step, stepIdToRecord, recordIdToRecord); + addNodeDependencies(step, nodes, links, step, stepIdToRecord, recordIdToRecord); + return new DependecyGraph(nodes, links); + } + + void addNodesDependents(JsonObject root, Set nodes, Set links, JsonObject record, + Map stepIdToRecord, Map recordIdToRecord) { + String stepId = record.getString("stepId"); + nodes.add(new Node(stepId, record.getString("encodedStepId"))); + for (Object dependentRecordId : record.getJsonArray("dependents")) { + int recordId = (int) dependentRecordId; + if (recordId != record.getInteger("id")) { + JsonObject dependentRecord = recordIdToRecord.get(recordId); + String dependentStepId = dependentRecord.getString("stepId"); + links.add(Link.dependent(root.equals(record), dependentStepId, stepId)); + nodes.add(new Node(dependentStepId, dependentRecord.getString("encodedStepId"))); + // NOTE: we do not fetch transient dependencies yet because the UI is not ready to show so many nodes + // if (added) { + // addNodesDependents(root, nodes, links, dependentRecord, stepIdToRecord, recordIdToRecord); + // } + } + } + } + + void addNodeDependencies(JsonObject root, Set nodes, Set links, JsonObject record, + Map stepIdToRecord, Map recordIdToRecord) { + for (Map.Entry entry : stepIdToRecord.entrySet()) { + for (Object dependentRecordId : entry.getValue().getJsonArray("dependents")) { + int recordId = (int) dependentRecordId; + if (record.getInteger("id") == recordId) { + links.add(Link.dependency(root.equals(record), + record.getString("stepId"), entry.getValue().getString("stepId"))); + nodes.add(new Node(entry.getValue().getString("stepId"), entry.getValue().getString("encodedStepId"))); + // NOTE: we do not fetch transient dependencies yet because the UI is not ready to show so many nodes + // if (added) { + // addNodeDependencies(root, nodes, links, entry.getValue(), stepIdToRecord, recordIdToRecord); + // } + } + } + } + } + + public static class DependecyGraph { + + public final Set nodes; + public final Set links; + + public DependecyGraph(Set nodes, Set links) { + this.nodes = nodes; + this.links = links; + } + + public static class Node { + + public final String stepId; + public final String simpleName; + public final String encodedStepId; + + public Node(String stepId, String encodedStepId) { + this.stepId = stepId; + this.encodedStepId = encodedStepId; + int lastDot = stepId.lastIndexOf('.'); + String simple = lastDot > 0 ? stepId.substring(lastDot + 1) : stepId; + int hash = simple.indexOf('#'); + if (hash > 0) { + StringBuilder sb = new StringBuilder(); + char[] chars = simple.substring(0, hash).toCharArray(); + for (char c : chars) { + if (Character.isUpperCase(c)) { + sb.append(c); + } + } + simple = sb + simple.substring(hash); + } + this.simpleName = simple; + } + + @Override + public int hashCode() { + return Objects.hash(stepId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Node other = (Node) obj; + return Objects.equals(stepId, other.stepId); + } + + } + + public static class Link { + + static Link dependent(boolean direct, String source, String target) { + return new Link(source, target, direct ? "directDependent" : "dependency"); + } + + static Link dependency(boolean direct, String source, String target) { + return new Link(source, target, direct ? "directDependency" : "dependency"); + } + + public final String source; + public final String target; + public final String type; + + public Link(String source, String target, String type) { + this.source = source; + this.target = target; + this.type = type; + } + + @Override + public int hashCode() { + return Objects.hash(source, target); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Link other = (Link) obj; + return Objects.equals(source, other.source) && Objects.equals(target, other.target); + } + + } + + } + } diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-step-dependency-graph.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-step-dependency-graph.html new file mode 100644 index 0000000000000..5d1021effe55b --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-step-dependency-graph.html @@ -0,0 +1,157 @@ +{#include main fluid=true} + + {#script} + // Build step dependency graph built with d3.js + // Based on https://observablehq.com/@d3/mobile-patent-suits + + const stepId = "{currentRequest.getParam('stepId')}"; + + const nodes = [ + {#each info:buildMetrics.get.dependencyGraphs.get(currentRequest.getParam('stepId')).nodes} + { id:"{it.stepId}", root:{#if it.stepId == currentRequest.getParam('stepId')}true{#else}false{/if}, simpleName:"{it.simpleName}", encodedId:"{it.encodedStepId}" }, + {/each} + ]; + const links = [ + {#each info:buildMetrics.get.dependencyGraphs.get(currentRequest.getParam('stepId')).links} + { source:"{it.source}", target:"{it.target}", type:"{it.type}" }, + {/each} + ]; + + {| + const types = ['directDependency','directDependent','dependency']; + const height = 600; + const width = 1200; + const color = d3.scaleOrdinal(types, d3.schemeCategory10); + + // Legend colors + const legendDirectDependency = document.querySelector(".legend-direct-dependency"); + legendDirectDependency.style.color = color('directDependency'); + const legendDirectDependent = document.querySelector(".legend-direct-dependent"); + legendDirectDependent.style.color = color('directDependent'); + // const legendDependency = document.querySelector(".legend-dependency"); + // legendDependency.style.color = color('dependency'); + + function linkArc(d) { + const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y); + return ` + M${d.source.x},${d.source.y} + A${r},${r} 0 0,1 ${d.target.x},${d.target.y} + `; + } + + const simulation = d3.forceSimulation(nodes) + .force("link", d3.forceLink(links).id(d => d.id).distance(function(d) { + return d.source.id === stepId || d.type === 'directDependency' ? 125 : 255; + })) + .force("charge", d3.forceManyBody().strength(-400)) + .force("x", d3.forceX()) + .force("y", d3.forceY()); + + function dragstart(event, d){ + // this line is needed, otherwise the simulation stops after few seconds + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }; + + function dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + function dragended(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + const svg = d3.select("#buildStepDepGraph_area") + .attr("viewBox", [-width / 3, -height / 3, width, height]) + .style("font", "11px sans-serif"); + + svg.append("defs").selectAll("marker") + .data(types) + .join("marker") + .attr("id", d => `arrow-${d}`) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 15) + .attr("refY", -0.5) + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("path") + .attr("fill", color) + .attr("d", "M0,-5L10,0L0,5"); + + const link = svg.append("g") + .attr("fill", "none") + .attr("stroke-width", 1.5) + .selectAll("path") + .data(links) + .join("path") + .attr("stroke", d => color(d.type)) + .attr("marker-end", d => `url(${new URL(`#arrow-${d.type}`, location)})`); + + const node = svg.append("g") + .attr("fill", "currentColor") + .attr("stroke-linecap", "round") + .attr("stroke-linejoin", "round") + .selectAll("g") + .data(nodes) + .join("g") + .call(d3.drag().on("drag", dragged).on("end", dragended).on("start", dragstart)); + + node.append("circle") + .attr("stroke", "white") + .attr("stroke-width", 1) + .attr("r", 5) + .style("fill", d => d.root ? "red" : "black"); + + const anchor = node.append("a") + .attr("xlink:href", d => "build-step-dependency-graph?stepId=" + d.encodedId); + + anchor.append("svg:text") + .attr("x", 8) + .attr("y", "0.31em") + .style("fill", "#1f77b4") + .text(d => d.simpleName); + + anchor.append("svg:title") + .text(d => d.id); + + simulation.on("tick", () => { + link.attr("d", linkArc); + node.attr("transform", d => `translate(${d.x},${d.y})`); + }); + + |} + + {#breadcrumbs} Build Steps{/breadcrumbs} + {#title}Build Step Dependency Graph{/title} + {#body} + +
+
+ {currentRequest.getParam('stepId')} +
+
+
+
+
    +
  • root
  • +
  • direct dependencies
  • +
  • direct dependents
  • + + +
+
+
+ +
+
+ + {/body} + + {#scriptref} + + {/scriptref} +{/include} diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-steps.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-steps.html index d705d699d9661..565fcfdad1f6f 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-steps.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/build-steps.html @@ -1,14 +1,33 @@ +{#let metrics=info:buildMetrics.get} + {#include main fluid=true} +{#style} +#chartjs-tooltip { + color: white; + border-radius: 0em 1em 1em 1em; + padding: 1em; +} +.tooltip-steps { + padding-left: 0.5em; + margin-bottom: 0; + font-size: 1.2em; +} +.tooltip-title { + font-size: 1.5em; + font-weight: bold; +} +{/style} {#title}Build Steps{/title} {#body} - -{#let metrics=info:buildMetrics.get} -

-Executed {metrics.steps.size} build steps on {metrics.threads.size} threads. +Executed {metrics.records.size} build steps on {metrics.threadSlotRecords.keys.size} threads in {metrics.duration} ms.

- +

+Build Steps Concurrent Execution Chart +

+ +
@@ -16,29 +35,177 @@ + - {#for step in metrics.steps} + {#for record in metrics.records} - + + {/for}
#Started Duration ThreadActions
{step_count}{record_count} - {step.stepId} + {record.stepId} - {step.started} + {record.started} - {step.duration} ms + {record.duration} ms - {step.thread} + {record.thread} + + {#if !metrics.dependencyGraphs.get(record.stepId).links.empty} + + {/if}
+ +

Build Steps Concurrent Execution Chart

+
+ +
-{/let} +{#script} + const labels = [ {#each metrics.slots}{it}{#if it_hasNext},{/if}{/each} ]; + const nextColor = function(number) { + const hue = number * 137.508; // golden angle approximation + return "hsl(" + hue +",80%,65%)"; + }; -{/body} -{/include} \ No newline at end of file + const threadBuildSteps = { + {#for entry in metrics.threadSlotRecords.entrySet} + "{entry.key}" : [ + {#for data in entry.value} + [ {#for stepId in data} '{stepId}',{/for} ], + {/for} + ], + {/for} + } + + const data = { + labels: labels, + datasets: [ + {#for entry in metrics.threadSlotRecords.entrySet} + { + label: '{entry.key}', + data: [{#each entry.value}{#if it.empty}0{#else}1{/if},{/each}], + backgroundColor: nextColor({entry_index}), + }, + {/for} + ] + }; + + const externalTooltip = (context) => { + const { chart, tooltip } = context; + let tooltipEl = document.getElementById('chartjs-tooltip'); + + // Create element on first render + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.id = 'chartjs-tooltip'; + chart.canvas.parentNode.appendChild(tooltipEl); + } + + // Hide if no tooltip + if (tooltip.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + // Set caret Position + tooltipEl.classList.remove('above', 'below', 'no-transform'); + if (tooltip.yAlign) { + tooltipEl.classList.add(tooltip.yAlign); + } else { + tooltipEl.classList.add('no-transform'); + } + + let innerHtml = ''; + // We expect a single tooltip item + const tooltipItem = context.tooltip.dataPoints[0]; + const thread = tooltipItem.dataset.label; + const buildStepIds = threadBuildSteps[thread][tooltipItem.dataIndex]; + + innerHtml += '
' + thread + '
'; + innerHtml += '
    '; + buildStepIds.forEach(function(stepId, i) { + innerHtml += '
  • '; + const lastDot = stepId.lastIndexOf('.'); + if (lastDot > 0) { + innerHtml += stepId.substring(lastDot + 1); + } else { + innerHtml += stepId; + } + innerHtml += '
  • '; + }); + innerHtml += '
'; + + let ulRoot = tooltipEl; + ulRoot.innerHTML = innerHtml; + + const position = context.chart.canvas.getBoundingClientRect(); + const bodyFont = Chart.helpers.toFont(tooltip.options.bodyFont); + + // Display, position, and font + tooltipEl.style.opacity = 1; + tooltipEl.style.position = 'absolute'; + + const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas; + tooltipEl.style.left = positionX + tooltip.caretX + 'px'; + tooltipEl.style.top = (positionY + tooltip.caretY + 7) + 'px'; + + //tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 'px'; + //tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY + 'px'; + tooltipEl.style.font = bodyFont.string; + tooltipEl.style.padding = tooltip.padding + 'px ' + tooltip.padding + 'px'; + tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)'; + tooltipEl.style.pointerEvents = 'none'; + }; + + const config = { + type: 'bar', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Build Step Concurrent Execution', + }, + tooltip: { + enabled: false, + external: externalTooltip, + } + }, + responsive: true, + scales: { + x: { + stacked: true, + title: { + display: true, + text: "{metrics.slots.size} time slots ({metrics.slots.get(0)} ms)", + }, + }, + y: { + stacked: true, + title: { + display: true, + text: "Number of build threads used in a time slot", + }, + } + } + } + }; + + const ctx = document.getElementById('buildStepsChart').getContext('2d'); + const buildStepsChart = new Chart(ctx, config); + +{#scriptref} + +{/include} + +{/let} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html index de55f148c101e..69e3dd936786e 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html @@ -38,7 +38,7 @@ {#if currentExtensionName == 'Eclipse Vert.x - HTTP'} - {#insert title/} + {#insert breadcrumbs/} {#insert title/} {#else} {currentExtensionName}{#insert breadcrumbs/} {#insert title/} {/if}