This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-23865 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 0dabf2bbc696845a2d0f86c1309998b004b474d0 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Jul 1 11:34:39 2026 +0200 CAMEL-23865: TUI sparkline scaling for sub-1.0 throughput rates Use THROUGHPUT_SCALE (x100) in MetricsCollector so fractional msg/s rates are preserved as longs. Apply niceMax (1-2-5 step sequence) for Y-axis scaling. Use Locale.US for consistent dot decimal formatting. Endpoint charts use showYAxis(false) until tamboui supports yAxisFormatter. Co-Authored-By: Claude Opus 4.7 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/EndpointsTab.java | 59 +++++++++++++++++++--- .../jbang/core/commands/tui/MetricsCollector.java | 25 +++++++-- .../dsl/jbang/core/commands/tui/OverviewTab.java | 59 +++++++++++++++++----- 3 files changed, 120 insertions(+), 23 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java index e50c0b0acb79..7e9511e6bd00 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java @@ -444,17 +444,19 @@ class EndpointsTab implements MonitorTab { Line chartTitle = Line.from( Span.styled("▬", Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))), - Span.raw(String.format(" in:%-4d ", curIn)), + Span.raw(String.format(" in:%-4s ", formatThroughput(curIn))), Span.styled("▬", Style.EMPTY.fg(Color.CYAN)), - Span.raw(String.format(" out:%-4d msg/s", curOut))); + Span.raw(String.format(" out:%-4s msg/s", formatThroughput(curOut)))); Rect rightArea = hSplit.get(1); + long maxEp = niceMax(Math.max(maxOf(inArr), maxOf(outArr))); frame.renderWidget(DualSparkline.builder() .topData(inArr) .bottomData(outArr) + .max(maxEp) .topStyle(Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))) .bottomStyle(Style.EMPTY.fg(Color.CYAN)) - .showYAxis(true) + .showYAxis(false) .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 4) + "s", "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 4) + "s", "now") .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) @@ -555,16 +557,20 @@ class EndpointsTab implements MonitorTab { Span.styled(uriLabel, Style.EMPTY.fg(Color.YELLOW)), Span.raw("] "), Span.styled("▬", Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))), - Span.raw(String.format(" in:%-4d ", curIn)), + Span.raw(String.format(" in:%-4s ", formatThroughput(curIn))), Span.styled("▬", Style.EMPTY.fg(Color.CYAN)), - Span.raw(String.format(" out:%-4d msg/s", curOut))); + Span.raw(String.format(" out:%-4s msg/s", formatThroughput(curOut)))); + // TODO: use .showYAxis(true).yAxisFormatter(EndpointsTab::formatThroughput) when tamboui 0.5.0 is released + // see https://github.com/tamboui/tamboui/pull/396 + long maxEpSingle = niceMax(Math.max(maxOf(inArr), maxOf(outArr))); frame.renderWidget(DualSparkline.builder() .topData(inArr) .bottomData(outArr) + .max(maxEpSingle) .topStyle(Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))) .bottomStyle(Style.EMPTY.fg(Color.CYAN)) - .showYAxis(true) + .showYAxis(false) .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 4) + "s", "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 4) + "s", "now") .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) @@ -770,4 +776,45 @@ class EndpointsTab implements MonitorTab { result.put("selectedIndex", sel != null ? sel : -1); return result; } + + private static long maxOf(long[] arr) { + long max = 0; + for (long v : arr) { + if (v > max) { + max = v; + } + } + return max; + } + + private static long niceMax(long rawMax) { + if (rawMax <= 0) { + return MetricsCollector.THROUGHPUT_SCALE; + } + long scale = MetricsCollector.THROUGHPUT_SCALE; + int[] steps = { 1, 2, 5 }; + long multiplier = scale; + while (true) { + for (int s : steps) { + long candidate = s * multiplier; + if (candidate >= rawMax) { + return candidate; + } + } + multiplier *= 10; + } + } + + private static String formatThroughput(long scaledValue) { + double v = scaledValue / (double) MetricsCollector.THROUGHPUT_SCALE; + if (v >= 10) { + return String.valueOf(Math.round(v)); + } else if (v >= 1) { + return String.format(Locale.US, "%.1f", v); + } else if (scaledValue > 0) { + return String.format(Locale.US, "%.2f", v); + } else { + return "0"; + } + } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java index 974af492c85f..e4c0ffe530ee 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java @@ -33,6 +33,8 @@ class MetricsCollector { static final int MAX_ENDPOINT_CHART_POINTS = 300; static final int MAX_HEAP_HISTORY_POINTS = 120; static final long HEAP_SAMPLE_INTERVAL_MS = 5000; + // Throughput values are stored scaled by this factor so sub-1.0 msg/s rates are preserved as longs + static final long THROUGHPUT_SCALE = 100; // Throughput history per PID (one point per second) private final Map<String, LinkedList<Long>> throughputHistory = new ConcurrentHashMap<>(); @@ -164,15 +166,28 @@ class MetricsCollector { samples.remove(0); } + // Use the EWMA throughput from the status JSON (already smoothed in camel-core) + // and store scaled by 100 so sub-1.0 rates (e.g. 0.20 msg/s) are preserved as integers + long tp = 0; + if (info.throughput != null) { + try { + tp = Math.round(Double.parseDouble(info.throughput) * THROUGHPUT_SCALE); + } catch (NumberFormatException e) { + // ignore + } + } + + // Failed throughput still computed from delta (no EWMA source for failed-only) + long fp = 0; if (samples.size() >= 2) { long[] oldest = samples.get(0); long[] newest = samples.get(samples.size() - 1); - long deltaTotal = newest[1] - oldest[1]; long deltaFailed = newest[2] - oldest[2]; long deltaTimeMs = newest[0] - oldest[0]; - long tp = deltaTimeMs > 0 ? (deltaTotal * 1000) / deltaTimeMs : 0; - long fp = deltaTimeMs > 0 ? (deltaFailed * 1000) / deltaTimeMs : 0; + fp = deltaTimeMs > 0 ? (deltaFailed * 1000 * THROUGHPUT_SCALE) / deltaTimeMs : 0; + } + { Long lastTime = previousExchangesTime.get(pid); if (lastTime == null || now - lastTime >= 1000) { previousExchangesTime.put(pid, now); @@ -278,8 +293,8 @@ class MetricsCollector { long[] oldest = samples.get(0); long[] newest = samples.get(samples.size() - 1); long deltaMs = newest[0] - oldest[0]; - long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 / deltaMs : 0; - long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 / deltaMs : 0; + long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 * THROUGHPUT_SCALE / deltaMs : 0; + long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 * THROUGHPUT_SCALE / deltaMs : 0; Long lastTime = prevTimeMap.get(pid); if (lastTime == null || now - lastTime >= 1000) { prevTimeMap.put(pid, now); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java index 09b95d7995cf..23e0e062abc5 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java @@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -222,7 +223,7 @@ class OverviewTab implements MonitorTab { List<Constraint> constraints = new ArrayList<>(); constraints.add(Constraint.fill()); if (hasSparkline) { - constraints.add(Constraint.length(14)); + constraints.add(Constraint.length(17)); } else if (showInfoPanel) { int panelH = countInfraLines(infraSel) + 2; constraints.add(Constraint.length(Math.min(panelH, area.height() / 2))); @@ -404,7 +405,7 @@ class OverviewTab implements MonitorTab { .split(chartInner); List<Rect> hChunks = Layout.horizontal() - .constraints(Constraint.length(4), Constraint.fill()) + .constraints(Constraint.length(5), Constraint.fill()) .split(vChunks.get(0)); Rect barChartArea = hChunks.get(1); @@ -438,10 +439,16 @@ class OverviewTab implements MonitorTab { for (long v : mergedTotal) { maxTp = Math.max(maxTp, v); } + maxTp = niceMax(maxTp); long curTp = mergedTotal[renderPoints - 1]; long curFailed = mergedFailed[renderPoints - 1]; long curOk = Math.max(0, curTp - curFailed); + // Format throughput values unscaled for display + String curTpFmt = formatThroughput(curTp); + String curOkFmt = formatThroughput(curOk); + String curFailFmt = formatThroughput(curFailed); + Line titleLine; if (chartMode == CHART_SINGLE && ctx.selectedPid != null) { IntegrationInfo chartSel = ctx.findSelectedIntegration(); @@ -450,18 +457,18 @@ class OverviewTab implements MonitorTab { titleLine = Line.from( Span.raw(" ["), Span.styled(chartName, Style.EMPTY.fg(Color.YELLOW)), - Span.raw(String.format("] Throughput: %d msg/s ", curTp)), + Span.raw(String.format("] Throughput: %s msg/s ", curTpFmt)), Span.styled("■", Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))), - Span.raw(String.format(" ok:%d ", curOk)), + Span.raw(String.format(" ok:%s ", curOkFmt)), Span.styled("■", Style.EMPTY.fg(Color.RED)), - Span.raw(String.format(" fail:%d ", curFailed))); + Span.raw(String.format(" fail:%s ", curFailFmt))); } else { titleLine = Line.from( - Span.raw(String.format(" [All] Throughput: %d msg/s ", curTp)), + Span.raw(String.format(" [All] Throughput: %s msg/s ", curTpFmt)), Span.styled("■", Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))), - Span.raw(String.format(" ok:%d ", curOk)), + Span.raw(String.format(" ok:%s ", curOkFmt)), Span.styled("■", Style.EMPTY.fg(Color.RED)), - Span.raw(String.format(" fail:%d ", curFailed))); + Span.raw(String.format(" fail:%s ", curFailFmt))); } Block chartBlock = Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) @@ -480,7 +487,7 @@ class OverviewTab implements MonitorTab { BarChart barChart = BarChart.builder() .data(groups) - .max(maxTp > 0 ? maxTp + 2 : 2) + .max(maxTp) .barWidth(1) .barGap(0) .groupGap(0) @@ -493,11 +500,11 @@ class OverviewTab implements MonitorTab { Style dimStyle = Style.EMPTY.dim(); for (int row = 0; row < barRows; row++) { if (row == 0) { - yLines.add(Line.from(Span.styled(String.format("%3d", maxTp), dimStyle))); + yLines.add(Line.from(Span.styled(String.format("%4s", formatThroughput(maxTp)), dimStyle))); } else if (barRows > 4 && row == barRows / 2) { - yLines.add(Line.from(Span.styled(String.format("%3d", maxTp / 2), dimStyle))); + yLines.add(Line.from(Span.styled(String.format("%4s", formatThroughput(maxTp / 2)), dimStyle))); } else if (row == barRows - 1) { - yLines.add(Line.from(Span.styled(" 0", dimStyle))); + yLines.add(Line.from(Span.styled(" 0", dimStyle))); } else { yLines.add(Line.from("")); } @@ -637,6 +644,34 @@ class OverviewTab implements MonitorTab { frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), inner); } + /** + * Snap to a nice Y-axis ceiling using a 1-2-5 sequence (scaled by THROUGHPUT_SCALE). For example with scale=100: + * 0.20 → 1, 1.5 → 2, 3 → 5, 7 → 10, 15 → 20, 35 → 50, 80 → 100, etc. + */ + private static long niceMax(long rawMax) { + long s = MetricsCollector.THROUGHPUT_SCALE; + long[] steps = { s, 2 * s, 5 * s, 10 * s, 20 * s, 50 * s, 100 * s, 200 * s, 500 * s, 1000 * s }; + for (long step : steps) { + if (rawMax <= step) { + return step; + } + } + return rawMax; + } + + private static String formatThroughput(long scaledValue) { + double v = scaledValue / (double) MetricsCollector.THROUGHPUT_SCALE; + if (v >= 10) { + return String.valueOf(Math.round(v)); + } else if (v >= 1) { + return String.format(Locale.US, "%.1f", v); + } else if (scaledValue > 0) { + return String.format(Locale.US, "%.2f", v); + } else { + return "0"; + } + } + private static int countInfraLines(InfraInfo infra) { int count = 2; // "Service: ..." + blank line for (Map.Entry<String, Object> e : infra.properties.entrySet()) {
