diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/devui/BuildMetricsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/devui/BuildMetricsTest.java index cf8adc6920ba7..7065f94e77e75 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/devui/BuildMetricsTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/devui/BuildMetricsTest.java @@ -20,14 +20,13 @@ public BuildMetricsTest() { @Test public void testGetBuildStepsMetrics() throws Exception { - JsonNode buildStepsMetricsResponse = super.executeJsonRPCMethod("getBuildStepsMetrics"); + JsonNode buildStepsMetricsResponse = super.executeJsonRPCMethod("getBuildMetrics"); Assertions.assertNotNull(buildStepsMetricsResponse); - int duration = buildStepsMetricsResponse.get("duration").asInt(); Assertions.assertTrue(duration > 0); - boolean dependencyGraphsIncluded = buildStepsMetricsResponse.get("dependencyGraphs").isObject(); - Assertions.assertTrue(dependencyGraphsIncluded); + boolean recordsIncluded = buildStepsMetricsResponse.get("records").isArray(); + Assertions.assertTrue(recordsIncluded); } } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-step-graph.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-step-graph.js new file mode 100644 index 0000000000000..8ad69858f757e --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-step-graph.js @@ -0,0 +1,200 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import 'echarts-force-graph'; +import '@vaadin/button'; +import '@vaadin/checkbox'; +import '@vaadin/checkbox-group'; +import '@vaadin/progress-bar'; + +/** + * This component shows the Build Step Graph + */ +export class QwcBuildStepGraph extends LitElement { + + static styles = css` + .top-bar { + display: flex; + align-items: baseline; + gap: 20px; + padding-left: 20px; + justify-content: space-between; + padding-right: 20px; + } + + .top-bar h4 { + color: var(--lumo-contrast-60pct); + } + `; + + static properties = { + stepId: {type: String}, + extensionName: {type: String}, // TODO: Add 'pane' concept in router to register internal extension pages. + _edgeLength: {type: Number, state: true}, + _dependencyGraph: {state: true}, + _categories: {state: false}, + _colors: {state: false}, + _nodes: {state: true}, + _links: {state: false}, + _showSimpleDescription: {state: false} + }; + + constructor() { + super(); + this.stepId = null; + this._dependencyGraph = null; + this._categories = ['root' , 'direct dependencies', 'direct dependents']; + this._categoriesEnum = ['root' , 'directDependency' , 'directDependent']; + this._colors = ['#ee6666', '#5470c6' , '#fac858']; + this._edgeLength = 120; + this._nodes = null; + this._links = null; + this._showSimpleDescription = []; + } + + connectedCallback() { + super.connectedCallback(); + this.jsonRpc = new JsonRpc(this.extensionName); + this._fetchDependencyGraph(); + } + + _fetchDependencyGraph(){ + if(this.stepId){ + this.jsonRpc.getDependencyGraph({buildStepId: this.stepId}).then(jsonRpcResponse => { + this._dependencyGraph = jsonRpcResponse.result; + this._createNodes(); + }); + } + } + + _createNodes(){ + if(this._dependencyGraph){ + + let dependencyGraphNodes = this._dependencyGraph.nodes; + let dependencyGraphLinks = this._dependencyGraph.links; + + this._links = [] + this._nodes = [] + for (var l = 0; l < dependencyGraphLinks.length; l++) { + let link = new Object(); + link.source = dependencyGraphNodes.findIndex(item => item.stepId === dependencyGraphLinks[l].source); + link.target = dependencyGraphNodes.findIndex(item => item.stepId === dependencyGraphLinks[l].target); + let catindex = this._categoriesEnum.indexOf(dependencyGraphLinks[l].type); + + this._addToNodes(dependencyGraphNodes[link.source],catindex); + this._addToNodes(dependencyGraphNodes[link.target],catindex); + this._links.push(link); + } + } + + } + + _addToNodes(dependencyGraphNode, catindex){ + let newNode = this._createNode(dependencyGraphNode); + let index = this._nodes.findIndex(item => item.name === newNode.name); + if (index < 0 ) { + if(dependencyGraphNode.stepId === this.stepId){ + newNode.category = 0; // Root + }else { + newNode.category = catindex; + } + this._nodes.push(newNode); + } + } + + _createNode(node){ + let nodeObject = new Object(); + if(this._showSimpleDescription.length>0){ + nodeObject.name = node.simpleName; + }else{ + nodeObject.name = node.stepId; + } + + nodeObject.value = 1; + nodeObject.id = node.stepId; + nodeObject.description = node.simpleName; + return nodeObject; + } + + render() { + if(this.stepId && this._dependencyGraph){ + return html`${this._renderTopBar()} + + `; + } else if(this.stepId) { + return html` +
+
Loading Dependency Graph...
+ +
+ `; + } else { + return html`No build step provided`; + } + } + + _renderTopBar(){ + return html` +
+ + + Back + +

${this.stepId}

+
+ ${this._renderCheckbox()} + + + + + + + +
+
`; + } + + _renderCheckbox(){ + return html` + + `; + } + + _backAction(){ + const back = new CustomEvent("build-steps-graph-back", { + detail: {}, + bubbles: true, + cancelable: true, + composed: false, + }); + this.dispatchEvent(back); + } + + _zoomIn(){ + if(this._edgeLength>10){ + this._edgeLength = this._edgeLength - 10; + }else{ + this._edgeLength = 10; + } + } + + _zoomOut(){ + this._edgeLength = this._edgeLength + 10; + } + + _echartClicked(e){ + this.stepId = e.detail.id; + this._fetchDependencyGraph(); + } +} +customElements.define('qwc-build-step-graph', QwcBuildStepGraph); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js index bb1b358090add..77c6faa9c6e04 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js @@ -9,6 +9,9 @@ import '@vaadin/text-field'; import '@vaadin/vertical-layout'; import '@vaadin/horizontal-layout'; import '@vaadin/progress-bar'; +import './qwc-build-step-graph.js'; + + /** * This component shows the Build Steps */ @@ -35,27 +38,44 @@ export class QwcBuildSteps extends QwcHotReloadElement { .datatable { width: 100%; - }`; + } + + .graph-icon { + font-size: small; + color: var(--lumo-contrast-50pct); + cursor: pointer; + } + + .graph { + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; + } + `; static properties = { - _buildStepsMetrics: { state: true }, + _buildMetrics: { state: true }, + _selectedBuildStep: {state: true}, _filtered: {state: true, type: Array} }; constructor() { super(); + this._buildMetrics = null; + this._selectedBuildStep = null; this.hotReload(); } hotReload(){ - this.jsonRpc.getBuildStepsMetrics().then(e => { - this._buildStepsMetrics = e.result; - this._filtered = this._buildStepsMetrics.records; + this.jsonRpc.getBuildMetrics().then(e => { + this._buildMetrics = e.result; + this._filtered = this._buildMetrics.records; }); } render() { - if (this._buildStepsMetrics && this._filtered) { + if (this._buildMetrics && this._filtered) { return this._render(); }else { return html` @@ -77,18 +97,27 @@ export class QwcBuildSteps extends QwcHotReloadElement { _filter(e) { const searchTerm = (e.detail.value || '').trim(); if (searchTerm === '') { - this._filtered = this._buildStepsMetrics.records; + this._filtered = this._buildMetrics.records; return; } - this._filtered = this._buildStepsMetrics.records.filter((record) => { + this._filtered = this._buildMetrics.records.filter((record) => { return this._match(record.stepId, searchTerm); }); } _render() { + if(this._selectedBuildStep){ + return this._renderBuildStepGraph(); + }else{ + return this._renderBuildStepList(); + } + } + + _renderBuildStepList(){ + return html`
-
Executed ${this._buildStepsMetrics.records.length} build steps on ${Object.keys(this._buildStepsMetrics.threadSlotRecords).length} threads in ${this._buildStepsMetrics.duration} ms.
+
Executed ${this._buildMetrics.records.length} build steps on ${this._buildMetrics.numberOfThreads} threads in ${this._buildMetrics.duration} ms.
+ + +
`; } + _renderBuildStepGraph(){ + + return html``; + + } + _stepIdRenderer(record) { return html`${record.stepId}`; } + + _graphIconRenderer(buildStep){ + return html` this._showGraph(buildStep)}>`; + } + + _showGraph(buildStep){ + this._selectedBuildStep = buildStep; + } + + _showBuildStepsList(){ + this._selectedBuildStep = null; + } + } customElements.define('qwc-build-steps', QwcBuildSteps); \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsJsonRPCService.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsJsonRPCService.java index 1698e90cf9fab..b8d410bc66543 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsJsonRPCService.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/build/BuildMetricsJsonRPCService.java @@ -4,14 +4,45 @@ import jakarta.enterprise.context.ApplicationScoped; -import io.smallrye.mutiny.Uni; -import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; @ApplicationScoped public class BuildMetricsJsonRPCService { - public Uni> getBuildStepsMetrics() { - return Uni.createFrom().item(() -> BuildMetricsDevUIController.get().getBuildStepsMetrics()) - .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + public BuildMetrics getBuildMetrics() { + BuildMetrics buildMetrics = new BuildMetrics(); + Map buildStepMetrics = buildStepMetrics(); + + JsonArray records = (JsonArray) buildStepMetrics.get("records"); + Map threadSlotRecords = (Map) buildStepMetrics.get("threadSlotRecords"); + Long duration = (Long) buildStepMetrics.get("duration"); + + buildMetrics.numberOfThreads = threadSlotRecords.size(); + buildMetrics.duration = duration; + buildMetrics.records = records; + + return buildMetrics; + } + + public JsonObject getDependencyGraph(String buildStepId) { + Map buildStepMetrics = buildStepMetrics(); + Map dependencyGraphs = (Map) buildStepMetrics.get("dependencyGraphs"); + + if (dependencyGraphs.containsKey(buildStepId)) { + return dependencyGraphs.get(buildStepId); + } + return null; + } + + private Map buildStepMetrics() { + BuildMetricsDevUIController controller = BuildMetricsDevUIController.get(); + return controller.getBuildStepsMetrics(); + } + + static class BuildMetrics { + public int numberOfThreads; + public Long duration; + public JsonArray records; } }