diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 9090fbc6be385..f0ff530e1bd63 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -26,6 +26,7 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -133,6 +134,7 @@ public class CamelMonitor extends CamelCommand { // Route sort columns private static final String[] ROUTE_SORT_COLUMNS = { "name", "group", "from", "status", "total", "failed" }; + private static final String[] ROUTE_TOP_SORT_COLUMNS = { "mean", "max", "min", "last", "delta" }; // Consumer sort columns (order matches table column order) private static final String[] CONSUMER_SORT_COLUMNS = { "id", "status", "type", "inflight", "total", "uri" }; @@ -214,6 +216,10 @@ public class CamelMonitor extends CamelCommand { private String routeSort = "name"; private int routeSortIndex = 0; private boolean routeSortReversed; + private boolean routeTopMode; + private String routeTopSort = "mean"; + private int routeTopSortIndex = 0; + private boolean routeTopSortReversed; // Consumer sort state (default: id = index 0) private String consumerSort = "id"; @@ -312,8 +318,10 @@ public class CamelMonitor extends CamelCommand { private int chartMode = CHART_ALL; private boolean showDiagram; private boolean diagramTextMode; + private boolean diagramAllRoutes; private boolean diagramMetrics = true; private List diagramCounterPositions = Collections.emptyList(); + private Set diagramRouteTitleRows = Collections.emptySet(); private List diagramLines = Collections.emptyList(); private int diagramScroll; private int diagramScrollX; @@ -454,7 +462,15 @@ private boolean handleEvent(Event event, TuiRunner runner) { return handleTabKey(TAB_CIRCUIT_BREAKER); } - // Tab cycling + // Tab cycling (check Shift+Tab before Tab since Tab binding also matches Shift+Tab) + if (ke.isFocusPrevious()) { + int prev = (tabsState.selected() - 1 + NUM_TABS) % NUM_TABS; + if (prev != TAB_OVERVIEW) { + selectCurrentIntegration(); + } + tabsState.select(prev); + return true; + } if (ke.isFocusNext()) { int next = (tabsState.selected() + 1) % NUM_TABS; if (next != TAB_OVERVIEW) { @@ -692,13 +708,31 @@ private boolean handleEvent(Event event, TuiRunner runner) { // Routes tab: sort and diagram if (tab == TAB_ROUTES && ke.isChar('s')) { - routeSortIndex = (routeSortIndex + 1) % ROUTE_SORT_COLUMNS.length; - routeSort = ROUTE_SORT_COLUMNS[routeSortIndex]; - routeSortReversed = false; + if (routeTopMode) { + routeTopSortIndex = (routeTopSortIndex + 1) % ROUTE_TOP_SORT_COLUMNS.length; + routeTopSort = ROUTE_TOP_SORT_COLUMNS[routeTopSortIndex]; + routeTopSortReversed = false; + } else { + routeSortIndex = (routeSortIndex + 1) % ROUTE_SORT_COLUMNS.length; + routeSort = ROUTE_SORT_COLUMNS[routeSortIndex]; + routeSortReversed = false; + } return true; } if (tab == TAB_ROUTES && ke.isChar('S')) { - routeSortReversed = !routeSortReversed; + if (routeTopMode) { + routeTopSortReversed = !routeTopSortReversed; + } else { + routeSortReversed = !routeSortReversed; + } + return true; + } + if (tab == TAB_ROUTES && !showSource && !showDiagram && ke.isCharIgnoreCase('t')) { + routeTopMode = !routeTopMode; + return true; + } + if (tab == TAB_ROUTES && !showSource && !showDiagram && ke.isCharIgnoreCase('a')) { + diagramAllRoutes = !diagramAllRoutes; return true; } if (tab == TAB_ROUTES && ke.isChar('d')) { @@ -1191,7 +1225,7 @@ private void renderTabs(Frame frame, Rect area) { Line[] labels = { Line.from(" 1 Overview "), Line.from(" 2 Log "), - Line.from(" 3 Route "), + Line.from(routeTopMode ? " 3 Top " : " 3 Route "), Line.from(" 4 Consumer "), Line.from(" 5 Endpoint "), Line.from(" 6 HTTP "), @@ -1683,12 +1717,16 @@ private void renderRoutes(Frame frame, Rect area) { // Fullscreen diagram mode if (showDiagram && (diagramTextMode ? !diagramLines.isEmpty() : diagramFullImageData != null)) { - // Split: route info header (4 rows) + diagram (fill) - List fullChunks = Layout.vertical() - .constraints(Constraint.length(4), Constraint.fill()) - .split(area); - renderRouteHeader(frame, fullChunks.get(0), info); - renderDiagram(frame, fullChunks.get(1)); + if (diagramAllRoutes) { + renderDiagram(frame, area); + } else { + // Split: route info header (4 rows) + diagram (fill) + List fullChunks = Layout.vertical() + .constraints(Constraint.length(4), Constraint.fill()) + .split(area); + renderRouteHeader(frame, fullChunks.get(0), info); + renderDiagram(frame, fullChunks.get(1)); + } return; } @@ -1702,69 +1740,130 @@ private void renderRoutes(Frame frame, Rect area) { .split(area); // Routes table - List routeRows = new ArrayList<>(); - for (RouteInfo route : sortedRoutes) { - Style stateStyle = "Started".equals(route.state) - ? Style.EMPTY.fg(Color.GREEN) - : Style.EMPTY.fg(Color.LIGHT_RED); - - Style failStyle = route.failed > 0 - ? Style.EMPTY.fg(Color.LIGHT_RED).bold() - : Style.EMPTY; - - String sinceLastRoute = formatSinceLastRoute(route); - - routeRows.add(Row.from( - Cell.from(Span.styled(route.routeId != null ? route.routeId : "", Style.EMPTY.fg(Color.CYAN))), - Cell.from(Span.styled(route.group != null ? route.group : "", Style.EMPTY.dim())), - Cell.from(route.from != null ? route.from : ""), - Cell.from(Span.styled(route.state != null ? route.state : "", stateStyle)), - Cell.from(route.uptime != null ? route.uptime : ""), - rightCell(route.coverage != null ? route.coverage : "", 6), - rightCell(route.throughput != null ? route.throughput : "", 8), - rightCell(String.valueOf(route.total), 8), - rightCell(String.valueOf(route.failed), 6, failStyle), - rightCell(String.valueOf(route.inflight), 8), - rightCell(route.total > 0 - ? route.minTime + "/" + route.maxTime + "/" + route.meanTime - : "", 14), - Cell.from(sinceLastRoute))); + Table routeTable; + if (routeTopMode) { + sortedRoutes.sort(this::sortRouteTop); + + List routeRows = new ArrayList<>(); + for (RouteInfo route : sortedRoutes) { + Style failStyle = route.failed > 0 + ? Style.EMPTY.fg(Color.LIGHT_RED).bold() + : Style.EMPTY; + + routeRows.add(Row.from( + Cell.from(Span.styled(route.routeId != null ? route.routeId : "", Style.EMPTY.fg(Color.CYAN))), + Cell.from(route.from != null ? route.from : ""), + rightCell(route.total > 0 ? String.valueOf(route.meanTime) : "", 6, topTimeStyle(route.meanTime)), + rightCell(route.total > 0 ? String.valueOf(route.maxTime) : "", 6, topTimeStyle(route.maxTime)), + rightCell(route.total > 0 ? String.valueOf(route.minTime) : "", 6), + rightCell(route.total > 0 ? String.valueOf(route.lastTime) : "", 6), + rightCell(route.deltaTime != 0 ? String.valueOf(route.deltaTime) : "", 6, + topDeltaStyle(route.deltaTime)), + rightCell(String.valueOf(route.total), 8), + rightCell(String.valueOf(route.failed), 6, failStyle), + rightCell(String.valueOf(route.inflight), 8), + rightCell(route.throughput != null ? route.throughput : "", 8), + rightCell(formatLoad(route.load01, route.load05, route.load15), 12))); + } + + routeTable = Table.builder() + .rows(routeRows) + .header(Row.from( + Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())), + Cell.from(Span.styled("FROM", Style.EMPTY.bold())), + rightCell(routeTopSortLabel("MEAN", "mean"), 6, routeTopSortStyle("mean")), + rightCell(routeTopSortLabel("MAX", "max"), 6, routeTopSortStyle("max")), + rightCell(routeTopSortLabel("MIN", "min"), 6, routeTopSortStyle("min")), + rightCell(routeTopSortLabel("LAST", "last"), 6, routeTopSortStyle("last")), + rightCell(routeTopSortLabel("DELTA", "delta"), 6, routeTopSortStyle("delta")), + rightCell("TOTAL", 8, Style.EMPTY.bold()), + rightCell("FAIL", 6, Style.EMPTY.bold()), + rightCell("INFLIGHT", 8, Style.EMPTY.bold()), + rightCell("MSG/S", 8, Style.EMPTY.bold()), + rightCell("LOAD", 12, Style.EMPTY.bold()))) + .widths( + Constraint.length(12), + Constraint.fill(), + Constraint.length(6), + Constraint.length(6), + Constraint.length(6), + Constraint.length(6), + Constraint.length(6), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8), + Constraint.length(8), + Constraint.length(12)) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" Top Routes sort:" + routeTopSort + " ").build()) + .build(); + } else { + List routeRows = new ArrayList<>(); + for (RouteInfo route : sortedRoutes) { + Style stateStyle = "Started".equals(route.state) + ? Style.EMPTY.fg(Color.GREEN) + : Style.EMPTY.fg(Color.LIGHT_RED); + + Style failStyle = route.failed > 0 + ? Style.EMPTY.fg(Color.LIGHT_RED).bold() + : Style.EMPTY; + + String sinceLastRoute = formatSinceLastRoute(route); + + routeRows.add(Row.from( + Cell.from(Span.styled(route.routeId != null ? route.routeId : "", Style.EMPTY.fg(Color.CYAN))), + Cell.from(Span.styled(route.group != null ? route.group : "", Style.EMPTY.dim())), + Cell.from(route.from != null ? route.from : ""), + Cell.from(Span.styled(route.state != null ? route.state : "", stateStyle)), + Cell.from(route.uptime != null ? route.uptime : ""), + rightCell(route.coverage != null ? route.coverage : "", 6), + rightCell(route.throughput != null ? route.throughput : "", 8), + rightCell(String.valueOf(route.total), 8), + rightCell(String.valueOf(route.failed), 6, failStyle), + rightCell(String.valueOf(route.inflight), 8), + rightCell(route.total > 0 + ? route.minTime + "/" + route.maxTime + "/" + route.meanTime + : "", 14), + Cell.from(sinceLastRoute))); + } + + routeTable = Table.builder() + .rows(routeRows) + .header(Row.from( + Cell.from(Span.styled(routeSortLabel("ROUTE", "name"), routeSortStyle("name"))), + Cell.from(Span.styled(routeSortLabel("GROUP", "group"), routeSortStyle("group"))), + Cell.from(Span.styled(routeSortLabel("FROM", "from"), routeSortStyle("from"))), + Cell.from(Span.styled(routeSortLabel("STATUS", "status"), routeSortStyle("status"))), + Cell.from(Span.styled("AGE", Style.EMPTY.bold())), + rightCell("COVER", 6, Style.EMPTY.bold()), + rightCell("MSG/S", 8, Style.EMPTY.bold()), + rightCell(routeSortLabel("TOTAL", "total"), 8, routeSortStyle("total")), + rightCell(routeSortLabel("FAIL", "failed"), 6, routeSortStyle("failed")), + rightCell("INFLIGHT", 8, Style.EMPTY.bold()), + rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()), + Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())))) + .widths( + Constraint.length(12), + Constraint.length(14), + Constraint.fill(), + Constraint.length(10), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8), + Constraint.length(14), + Constraint.length(12)) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" Routes sort:" + routeSort + " ").build()) + .build(); } - Table routeTable = Table.builder() - .rows(routeRows) - .header(Row.from( - Cell.from(Span.styled(routeSortLabel("ROUTE", "name"), routeSortStyle("name"))), - Cell.from(Span.styled(routeSortLabel("GROUP", "group"), routeSortStyle("group"))), - Cell.from(Span.styled(routeSortLabel("FROM", "from"), routeSortStyle("from"))), - Cell.from(Span.styled(routeSortLabel("STATUS", "status"), routeSortStyle("status"))), - Cell.from(Span.styled("AGE", Style.EMPTY.bold())), - rightCell("COVER", 6, Style.EMPTY.bold()), - rightCell("MSG/S", 8, Style.EMPTY.bold()), - rightCell(routeSortLabel("TOTAL", "total"), 8, routeSortStyle("total")), - rightCell(routeSortLabel("FAIL", "failed"), 6, routeSortStyle("failed")), - rightCell("INFLIGHT", 8, Style.EMPTY.bold()), - rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()), - Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())))) - .widths( - Constraint.length(12), - Constraint.length(14), - Constraint.fill(), - Constraint.length(10), - Constraint.length(8), - Constraint.length(6), - Constraint.length(8), - Constraint.length(8), - Constraint.length(6), - Constraint.length(8), - Constraint.length(14), - Constraint.length(12)) - .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) - .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED) - .title(" Routes sort:" + routeSort + " ").build()) - .build(); - frame.renderStatefulWidget(routeTable, chunks.get(0), routeTableState); // Bottom panel: diagram or processors @@ -1853,6 +1952,63 @@ private int sortRoute(RouteInfo a, RouteInfo b) { return routeSortReversed ? -result : result; } + private int sortRouteTop(RouteInfo a, RouteInfo b) { + int result = switch (routeTopSort) { + case "mean" -> Long.compare(b.meanTime, a.meanTime); + case "max" -> Long.compare(b.maxTime, a.maxTime); + case "min" -> Long.compare(b.minTime, a.minTime); + case "last" -> Long.compare(b.lastTime, a.lastTime); + case "delta" -> Long.compare(b.deltaTime, a.deltaTime); + default -> 0; + }; + return routeTopSortReversed ? -result : result; + } + + private int sortProcessorTop(ProcessorInfo a, ProcessorInfo b) { + int result = switch (routeTopSort) { + case "mean" -> Long.compare(b.meanTime, a.meanTime); + case "max" -> Long.compare(b.maxTime, a.maxTime); + case "min" -> Long.compare(b.minTime, a.minTime); + case "last" -> Long.compare(b.lastTime, a.lastTime); + case "delta" -> Long.compare(b.deltaTime, a.deltaTime); + default -> 0; + }; + return routeTopSortReversed ? -result : result; + } + + private String routeTopSortLabel(String label, String column) { + return sortLabel(label, column, routeTopSort, routeTopSortReversed); + } + + private Style routeTopSortStyle(String column) { + return sortStyle(column, routeTopSort); + } + + private static Style topTimeStyle(long ms) { + if (ms >= 1000) { + return Style.EMPTY.fg(Color.LIGHT_RED).bold(); + } else if (ms >= 100) { + return Style.EMPTY.fg(Color.YELLOW); + } + return Style.EMPTY; + } + + private static Style topDeltaStyle(long delta) { + if (delta > 0) { + return Style.EMPTY.fg(Color.LIGHT_RED); + } else if (delta < 0) { + return Style.EMPTY.fg(Color.GREEN); + } + return Style.EMPTY; + } + + private static String formatLoad(String l1, String l5, String l15) { + String s1 = l1 != null && !"0.00".equals(l1) ? l1 : "0"; + String s5 = l5 != null && !"0.00".equals(l5) ? l5 : "0"; + String s15 = l15 != null && !"0.00".equals(l15) ? l15 : "0"; + return s1 + "/" + s5 + "/" + s15; + } + private String traceSortLabel(String label, String column) { return sortLabel(label, column, traceSort, traceSortReversed); } @@ -2076,71 +2232,166 @@ private static Style sortStyle(String column, String currentSort) { } private void renderProcessors(Frame frame, Rect area, RouteInfo route) { - List rows = new ArrayList<>(); + Table table; + + if (routeTopMode) { + List rows = new ArrayList<>(); + + List sorted = new ArrayList<>(route.processors); + sorted.sort(this::sortProcessorTop); + + long maxValue = sorted.stream().mapToLong(this::procChartValue).max().orElse(1); + if (maxValue <= 0) { + maxValue = 1; + } - // Synthetic top row representing the route itself - Style routeStyle = route.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.CYAN); - rows.add(Row.from( - Cell.from(" route"), - Cell.from(Span.styled(route.from != null ? route.from : route.routeId, routeStyle)), - Cell.from(""), Cell.from(""), Cell.from(""), Cell.from(""), - rightCell(String.valueOf(route.total), 8), - rightCell(String.valueOf(route.failed), 6, - route.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY), - rightCell(String.valueOf(route.inflight), 8), - rightCell(route.total > 0 - ? route.minTime + "/" + route.maxTime + "/" + route.meanTime - : "", 14), - Cell.from(""))); - - for (ProcessorInfo proc : route.processors) { - String indent = " ".repeat(proc.level); - Style nameStyle = proc.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.CYAN); + for (ProcessorInfo proc : sorted) { + Style nameStyle = proc.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.CYAN); + long chartVal = procChartValue(proc); + String bar; + if (chartVal > 0) { + bar = buildBar(chartVal, maxValue, 20); + } else if (proc.total > 0) { + bar = "█"; + } else { + bar = ""; + } + Style barStyle = topTimeStyle(chartVal); + if (barStyle == Style.EMPTY) { + barStyle = Style.EMPTY.fg(Color.CYAN); + } + + rows.add(Row.from( + Cell.from(" " + (proc.processor != null ? proc.processor : "")), + Cell.from(Span.styled(proc.id != null ? proc.id : "", nameStyle)), + Cell.from(Span.styled(bar, barStyle)), + rightCell(proc.total > 0 ? String.valueOf(proc.meanTime) : "", 6, topTimeStyle(proc.meanTime)), + rightCell(proc.total > 0 ? String.valueOf(proc.maxTime) : "", 6, topTimeStyle(proc.maxTime)), + rightCell(proc.total > 0 ? String.valueOf(proc.minTime) : "", 6), + rightCell(proc.total > 0 ? String.valueOf(proc.lastTime) : "", 6), + rightCell(proc.deltaTime != 0 ? String.valueOf(proc.deltaTime) : "", 6, topDeltaStyle(proc.deltaTime)), + rightCell(String.valueOf(proc.total), 8), + rightCell(String.valueOf(proc.failed), 6, + proc.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY), + rightCell(String.valueOf(proc.inflight), 8))); + } + + table = Table.builder() + .rows(rows) + .header(Row.from( + Cell.from(Span.styled(" TYPE", Style.EMPTY.bold())), + Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())), + Cell.from(""), + rightCell(routeTopSortLabel("MEAN", "mean"), 6, routeTopSortStyle("mean")), + rightCell(routeTopSortLabel("MAX", "max"), 6, routeTopSortStyle("max")), + rightCell(routeTopSortLabel("MIN", "min"), 6, routeTopSortStyle("min")), + rightCell(routeTopSortLabel("LAST", "last"), 6, routeTopSortStyle("last")), + rightCell(routeTopSortLabel("DELTA", "delta"), 6, routeTopSortStyle("delta")), + rightCell("TOTAL", 8, Style.EMPTY.bold()), + rightCell("FAIL", 6, Style.EMPTY.bold()), + rightCell("INFLIGHT", 8, Style.EMPTY.bold()))) + .widths( + Constraint.length(20), + Constraint.length(14), + Constraint.fill(), + Constraint.length(6), + Constraint.length(6), + Constraint.length(6), + Constraint.length(6), + Constraint.length(6), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8)) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" Top Processors [" + route.routeId + "] sort:" + routeTopSort + " ").build()) + .build(); + } else { + List rows = new ArrayList<>(); + // Synthetic top row representing the route itself + Style routeStyle = route.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.CYAN); rows.add(Row.from( - Cell.from(" " + (proc.processor != null ? proc.processor : "")), - Cell.from(Span.styled(indent + (proc.id != null ? proc.id : ""), nameStyle)), + Cell.from(" route"), + Cell.from(Span.styled(route.from != null ? route.from : route.routeId, routeStyle)), Cell.from(""), Cell.from(""), Cell.from(""), Cell.from(""), - rightCell(String.valueOf(proc.total), 8), - rightCell(String.valueOf(proc.failed), 6, - proc.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY), - rightCell(String.valueOf(proc.inflight), 8), - rightCell(proc.total > 0 - ? proc.minTime + "/" + proc.maxTime + "/" + proc.meanTime + rightCell(String.valueOf(route.total), 8), + rightCell(String.valueOf(route.failed), 6, + route.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY), + rightCell(String.valueOf(route.inflight), 8), + rightCell(route.total > 0 + ? route.minTime + "/" + route.maxTime + "/" + route.meanTime : "", 14), Cell.from(""))); - } - Table table = Table.builder() - .rows(rows) - .header(Row.from( - Cell.from(Span.styled(" TYPE", Style.EMPTY.bold())), - Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())), + for (ProcessorInfo proc : route.processors) { + String indent = " ".repeat(proc.level); + Style nameStyle = proc.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.CYAN); + + rows.add(Row.from( + Cell.from(" " + (proc.processor != null ? proc.processor : "")), + Cell.from(Span.styled(indent + (proc.id != null ? proc.id : ""), nameStyle)), Cell.from(""), Cell.from(""), Cell.from(""), Cell.from(""), - rightCell("TOTAL", 8, Style.EMPTY.bold()), - rightCell("FAIL", 6, Style.EMPTY.bold()), - rightCell("INFLIGHT", 8, Style.EMPTY.bold()), - rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()), - Cell.from(""))) - .widths( - Constraint.length(20), - Constraint.fill(), - Constraint.length(10), - Constraint.length(8), - Constraint.length(6), - Constraint.length(8), - Constraint.length(8), - Constraint.length(6), - Constraint.length(8), - Constraint.length(14), - Constraint.length(12)) - .block(Block.builder().borderType(BorderType.ROUNDED) - .title(" Processors [" + route.routeId + "] ").build()) - .build(); + rightCell(String.valueOf(proc.total), 8), + rightCell(String.valueOf(proc.failed), 6, + proc.failed > 0 ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY), + rightCell(String.valueOf(proc.inflight), 8), + rightCell(proc.total > 0 + ? proc.minTime + "/" + proc.maxTime + "/" + proc.meanTime + : "", 14), + Cell.from(""))); + } + + table = Table.builder() + .rows(rows) + .header(Row.from( + Cell.from(Span.styled(" TYPE", Style.EMPTY.bold())), + Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())), + Cell.from(""), Cell.from(""), Cell.from(""), Cell.from(""), + rightCell("TOTAL", 8, Style.EMPTY.bold()), + rightCell("FAIL", 6, Style.EMPTY.bold()), + rightCell("INFLIGHT", 8, Style.EMPTY.bold()), + rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()), + Cell.from(""))) + .widths( + Constraint.length(20), + Constraint.fill(), + Constraint.length(10), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8), + Constraint.length(8), + Constraint.length(6), + Constraint.length(8), + Constraint.length(14), + Constraint.length(12)) + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(" Processors [" + route.routeId + "] ").build()) + .build(); + } frame.renderStatefulWidget(table, area, processorTableState); } + private static String buildBar(long value, long maxValue, int maxWidth) { + if (value <= 0 || maxValue <= 0) { + return ""; + } + int len = (int) Math.round((double) value / maxValue * maxWidth); + len = Math.max(len > 0 ? 1 : 0, Math.min(len, maxWidth)); + return "█".repeat(len); + } + + private long procChartValue(ProcessorInfo proc) { + return switch (routeTopSort) { + case "mean" -> proc.meanTime; + case "max" -> proc.maxTime; + case "min" -> proc.minTime; + case "last" -> proc.lastTime; + case "delta" -> Math.abs(proc.deltaTime); + default -> proc.meanTime; + }; + } + private void renderRouteHeader(Frame frame, Rect area, IntegrationInfo info) { RouteInfo route = null; if (diagramRouteId != null) { @@ -2356,6 +2607,10 @@ private void renderImageDiagram(Frame frame, Rect area, Block block) { } private Line styleDiagramLine(String text, int row, int scrollX) { + if (diagramRouteTitleRows.contains(row)) { + return Line.from(Span.styled(text, Style.EMPTY.fg(Color.WHITE).bold())); + } + // Build counter color ranges for this row List counterRanges = new ArrayList<>(); for (RouteDiagramAsciiRenderer.CounterPos cp : diagramCounterPositions) { @@ -2486,11 +2741,11 @@ private void loadDiagramForSelectedRoute() { String pid = selectedPid; boolean textMode = diagramTextMode; boolean showMetrics = diagramMetrics; - String routeId = selectedRoute.routeId; + String routeId = diagramAllRoutes ? null : selectedRoute.routeId; boolean initialLoad = !showDiagram; if (initialLoad) { - diagramRouteId = routeId; + diagramRouteId = routeId != null ? routeId : "all"; diagramLines = List.of("(Loading diagram...)"); diagramImageData = null; diagramFullImageData = null; @@ -2759,39 +3014,49 @@ private void loadDiagramInBackground(String pid, boolean textMode, String routeI RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine( RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE, RouteDiagramLayoutEngine.NodeLabelMode.CODE); - List layoutRoutes = new ArrayList<>(); - int currentY = RouteDiagramLayoutEngine.PADDING; - for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) { - RouteDiagramLayoutEngine.LayoutRoute lr = engine.layoutRoute(r, currentY); - layoutRoutes.add(lr); - currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP; - } - RouteDiagramAsciiRenderer asciiRenderer = new RouteDiagramAsciiRenderer( - RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH * RouteDiagramLayoutEngine.SCALE, true, metrics); - String ascii = asciiRenderer.renderDiagram(layoutRoutes, currentY); - List origPositions = asciiRenderer.getCounterPositions(); - // Build result lines, remapping counter positions to account for removed empty lines - String[] rawLines = ascii.split("\n", -1); List result = new ArrayList<>(); - int[] rowMapping = new int[rawLines.length]; - int newRow = 0; - for (int i = 0; i < rawLines.length; i++) { - if (!rawLines[i].isEmpty()) { - rowMapping[i] = newRow++; - result.add(rawLines[i]); - } else { - rowMapping[i] = -1; - } - } List positions = new ArrayList<>(); - for (RouteDiagramAsciiRenderer.CounterPos cp : origPositions) { - if (cp.row() >= 0 && cp.row() < rowMapping.length && rowMapping[cp.row()] >= 0) { - positions.add(new RouteDiagramAsciiRenderer.CounterPos( - rowMapping[cp.row()], cp.col(), cp.length(), cp.type())); + Set titleRows = new HashSet<>(); + + for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) { + // Add separator between routes + if (!result.isEmpty()) { + result.add(""); + result.add(""); + } + + int titleRow = result.size(); + + RouteDiagramLayoutEngine.LayoutRoute lr = engine.layoutRoute(r, RouteDiagramLayoutEngine.PADDING); + RouteDiagramAsciiRenderer asciiRenderer = new RouteDiagramAsciiRenderer( + RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH * RouteDiagramLayoutEngine.SCALE, true, metrics); + String ascii = asciiRenderer.renderDiagram(List.of(lr), lr.maxY + RouteDiagramLayoutEngine.V_GAP); + List origPositions = asciiRenderer.getCounterPositions(); + + // Strip empty lines and remap counter positions + String[] rawLines = ascii.split("\n", -1); + int[] rowMapping = new int[rawLines.length]; + int baseRow = result.size(); + int newRow = baseRow; + for (int i = 0; i < rawLines.length; i++) { + if (!rawLines[i].isEmpty()) { + rowMapping[i] = newRow++; + result.add(rawLines[i]); + } else { + rowMapping[i] = -1; + } + } + for (RouteDiagramAsciiRenderer.CounterPos cp : origPositions) { + if (cp.row() >= 0 && cp.row() < rowMapping.length && rowMapping[cp.row()] >= 0) { + positions.add(new RouteDiagramAsciiRenderer.CounterPos( + rowMapping[cp.row()], cp.col(), cp.length(), cp.type())); + } } + titleRows.add(titleRow); } - applyDiagramResult(routeId, result, null, null, null, positions); + + applyDiagramResult(routeId, result, null, null, null, positions, titleRows); } else { TerminalImageCapabilities caps = TerminalImageCapabilities.detect(); if (caps.supportsNativeImages()) { @@ -2822,20 +3087,27 @@ private void loadDiagramInBackground(String pid, boolean textMode, String routeI private void applyDiagramResult( String routeId, List lines, ImageData imageData, ImageData fullImageData, ImageProtocol protocol) { - applyDiagramResult(routeId, lines, imageData, fullImageData, protocol, Collections.emptyList()); + applyDiagramResult(routeId, lines, imageData, fullImageData, protocol, Collections.emptyList(), Collections.emptySet()); } private void applyDiagramResult( String routeId, List lines, ImageData imageData, ImageData fullImageData, ImageProtocol protocol, List positions) { + applyDiagramResult(routeId, lines, imageData, fullImageData, protocol, positions, Collections.emptySet()); + } + + private void applyDiagramResult( + String routeId, List lines, ImageData imageData, ImageData fullImageData, ImageProtocol protocol, + List positions, Set titleRows) { if (runner == null) { return; } runner.runOnRenderThread(() -> { boolean wasShowing = showDiagram; - diagramRouteId = routeId; + diagramRouteId = routeId != null ? routeId : "all"; diagramLines = lines; diagramCounterPositions = positions; + diagramRouteTitleRows = titleRows; diagramImageData = imageData; diagramFullImageData = fullImageData; diagramProtocol = protocol; @@ -4848,23 +5120,27 @@ private void renderFooter(Frame frame, Rect area) { hint(spans, "Esc", "back"); hint(spans, "\u2191\u2193", "navigate"); hint(spans, "s", "sort"); - hint(spans, "c", "source"); - hint(spans, "d", "diagram"); - hint(spans, "D", "text diagram"); - String routeState = selectedRouteState(); - boolean supSus = selectedRouteSupportsSuspension(); - if ("Started".equals(routeState)) { - hint(spans, "p", "stop"); - if (supSus) { - hint(spans, "P", "suspend"); - } - } else if ("Suspended".equals(routeState)) { - hint(spans, "p", "start"); - if (supSus) { - hint(spans, "P", "resume"); + hint(spans, "t", routeTopMode ? "top [on]" : "top [off]"); + if (!routeTopMode) { + hint(spans, "c", "source"); + hint(spans, "d", "diagram"); + hint(spans, "D", "text diagram"); + hint(spans, "a", diagramAllRoutes ? "all [on]" : "all [off]"); + String routeState = selectedRouteState(); + boolean supSus = selectedRouteSupportsSuspension(); + if ("Started".equals(routeState)) { + hint(spans, "p", "stop"); + if (supSus) { + hint(spans, "P", "suspend"); + } + } else if ("Suspended".equals(routeState)) { + hint(spans, "p", "start"); + if (supSus) { + hint(spans, "P", "resume"); + } + } else if (routeState != null) { + hint(spans, "p", "start"); } - } else if (routeState != null) { - hint(spans, "p", "start"); } hint(spans, "1-9", "tabs"); } else if (tab == TAB_CONSUMERS) { @@ -5814,6 +6090,11 @@ private IntegrationInfo parseIntegration(ProcessHandle ph, JsonObject root) { ri.meanTime = Math.max(0, objToLong(rs.get("meanProcessingTime"))); ri.minTime = Math.max(0, objToLong(rs.get("minProcessingTime"))); ri.maxTime = Math.max(0, objToLong(rs.get("maxProcessingTime"))); + ri.lastTime = Math.max(0, objToLong(rs.get("lastProcessingTime"))); + ri.deltaTime = objToLong(rs.get("deltaProcessingTime")); + ri.load01 = objToString(rs.get("load01")); + ri.load05 = objToString(rs.get("load05")); + ri.load15 = objToString(rs.get("load15")); long tsStarted = objToLong(rs.get("lastCreatedExchangeTimestamp")); if (tsStarted > 0) { ri.sinceLastStarted = TimeUtils.printSince(tsStarted); @@ -5846,6 +6127,7 @@ private IntegrationInfo parseIntegration(ProcessHandle ph, JsonObject root) { pi.minTime = Math.max(0, objToLong(ps.get("minProcessingTime"))); pi.maxTime = Math.max(0, objToLong(ps.get("maxProcessingTime"))); pi.lastTime = objToLong(ps.get("lastProcessingTime")); + pi.deltaTime = objToLong(ps.get("deltaProcessingTime")); pi.inflight = objToLong(ps.get("exchangesInflight")); long tsStarted = objToLong(ps.get("lastCreatedExchangeTimestamp")); if (tsStarted > 0) { @@ -6276,6 +6558,11 @@ static class RouteInfo { long meanTime; long minTime; long maxTime; + long lastTime; + long deltaTime; + String load01; + String load05; + String load15; String sinceLastStarted; String sinceLastCompleted; String sinceLastFailed; @@ -6292,6 +6579,7 @@ static class ProcessorInfo { long minTime; long maxTime; long lastTime; + long deltaTime; long inflight; String sinceLastStarted; String sinceLastCompleted;