This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 5df38cd33451 CAMEL-23865: Re-apply TUI chart throughput scaling lost
during refactoring
5df38cd33451 is described below
commit 5df38cd334518c87f1447142a48a5091778c6f9c
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 8 07:06:33 2026 +0200
CAMEL-23865: Re-apply TUI chart throughput scaling lost during refactoring
Re-applies the TUI chart-side throughput scaling lost when commit
065fd8314d9
(mouse support refactoring) rewrote MetricsCollector, OverviewTab, and
EndpointsTab. The backend EWMA calculation survived but the TUI rendering
regressed to integer division, causing sub-1.0 msg/s workloads to show an
empty chart. Fixes: scale throughput by 100x to preserve fractional rates,
add formatThroughput()/niceMax() helpers, clamp failed to total in chart
title, and add render tests to prevent future regressions.
Closes #24502
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 8 +-
.../jbang/core/commands/tui/MetricsCollector.java | 102 +++++++++--
.../dsl/jbang/core/commands/tui/OverviewTab.java | 43 +++--
.../tui/MetricsCollectorThroughputTest.java | 114 +++++++++++++
.../tui/OverviewTabThroughputRenderTest.java | 186 +++++++++++++++++++++
5 files changed, 412 insertions(+), 41 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 53680458b40b..605b1473c2b9 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
@@ -524,9 +524,9 @@ class EndpointsTab extends AbstractTableTab {
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 ",
MetricsCollector.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",
MetricsCollector.formatThroughput(curOut))));
Rect rightArea = hParts.get(1);
frame.renderWidget(DualSparkline.builder()
@@ -637,9 +637,9 @@ class EndpointsTab extends AbstractTableTab {
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 ",
MetricsCollector.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",
MetricsCollector.formatThroughput(curOut))));
frame.renderWidget(DualSparkline.builder()
.topData(inArr)
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 9670b766d22f..fe752fd47f42 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
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.LinkedList;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -33,6 +34,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<>();
@@ -152,33 +155,43 @@ class MetricsCollector {
// --- Update methods ---
void updateThroughputHistory(IntegrationInfo info) {
- long currentTotal = info.exchangesTotal;
long currentFailed = info.failed;
long now = System.currentTimeMillis();
String pid = info.pid;
LinkedList<long[]> samples = throughputSamples.computeIfAbsent(pid, k
-> new LinkedList<>());
- samples.add(new long[] { now, currentTotal, currentFailed });
+ samples.add(new long[] { now, info.exchangesTotal, currentFailed });
while (!samples.isEmpty() && now - samples.get(0)[0] > 2000) {
samples.remove(0);
}
+ // Use the EWMA throughput from the status JSON (already smoothed in
camel-core)
+ // and store scaled by THROUGHPUT_SCALE so sub-1.0 rates (e.g. 0.20
msg/s) are preserved as longs
+ 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);
- addToHistory(throughputHistory, pid, tp, MAX_SPARKLINE_POINTS);
- addToHistory(failedHistory, pid, fp, MAX_SPARKLINE_POINTS);
- }
+ Long lastTime = previousExchangesTime.get(pid);
+ if (lastTime == null || now - lastTime >= 1000) {
+ previousExchangesTime.put(pid, now);
+ addToHistory(throughputHistory, pid, tp, MAX_SPARKLINE_POINTS);
+ addToHistory(failedHistory, pid, fp, MAX_SPARKLINE_POINTS);
}
}
@@ -253,6 +266,15 @@ class MetricsCollector {
String pid, long now, long inTotal, long outTotal,
Map<String, LinkedList<long[]>> samplesMap, Map<String, Long>
prevTimeMap,
Map<String, LinkedList<Long>> inHistMap, Map<String,
LinkedList<Long>> outHistMap) {
+ recordEndpointSample(pid, now, inTotal, outTotal,
+ samplesMap, prevTimeMap, inHistMap, outHistMap,
THROUGHPUT_SCALE);
+ }
+
+ private void recordEndpointSample(
+ String pid, long now, long inTotal, long outTotal,
+ Map<String, LinkedList<long[]>> samplesMap, Map<String, Long>
prevTimeMap,
+ Map<String, LinkedList<Long>> inHistMap, Map<String,
LinkedList<Long>> outHistMap,
+ long scale) {
LinkedList<long[]> samples = samplesMap.computeIfAbsent(pid, k -> new
LinkedList<>());
samples.add(new long[] { now, inTotal, outTotal });
while (!samples.isEmpty() && now - samples.get(0)[0] > 2000) {
@@ -262,8 +284,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 * scale
/ deltaMs : 0;
+ long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 *
scale / deltaMs : 0;
Long lastTime = prevTimeMap.get(pid);
if (lastTime == null || now - lastTime >= 1000) {
prevTimeMap.put(pid, now);
@@ -282,8 +304,10 @@ class MetricsCollector {
String key = info.pid + "/" + cb.id;
long success = cb.successfulCalls;
long failed = cb.failedCalls;
+ // Circuit breaker history stays unscaled (scale=1) because
CircuitBreakerTab
+ // formats values as plain integers, not via formatThroughput()
recordEndpointSample(key, now, success, failed,
- cbThroughputSamples, previousCbTime, cbSuccessHistory,
cbFailHistory);
+ cbThroughputSamples, previousCbTime, cbSuccessHistory,
cbFailHistory, 1);
}
}
@@ -403,4 +427,54 @@ class MetricsCollector {
map.keySet().removeIf(k -> k.startsWith(prefix));
}
}
+
+ // --- Shared throughput formatting utilities ---
+
+ /**
+ * Round a scaled throughput value up to a nice chart-axis maximum.
Returns values that are multiples of 1, 2, or 5
+ * at the appropriate magnitude, ensuring the Y-axis labels are
human-readable.
+ */
+ static long niceMax(long rawMax) {
+ if (rawMax <= 0) {
+ return THROUGHPUT_SCALE;
+ }
+ int[] steps = { 1, 2, 5 };
+ long multiplier = THROUGHPUT_SCALE;
+ while (multiplier > 0) {
+ for (int s : steps) {
+ long candidate = s * multiplier;
+ if (candidate < 0) {
+ // overflow — fall back to rawMax
+ return rawMax;
+ }
+ if (candidate >= rawMax) {
+ return candidate;
+ }
+ }
+ long next = multiplier * 10;
+ if (next / 10 != multiplier) {
+ // overflow — fall back to rawMax
+ return rawMax;
+ }
+ multiplier = next;
+ }
+ return rawMax;
+ }
+
+ /**
+ * Format a scaled throughput value for display. Values >= 10 are shown as
integers, values >= 1 with one decimal,
+ * and sub-1 values with two decimals.
+ */
+ static String formatThroughput(long scaledValue) {
+ double v = scaledValue / (double) 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/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 55914b8cd547..b3c6195f9d03 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
@@ -504,11 +504,18 @@ class OverviewTab extends AbstractTab {
for (long v : mergedTotal) {
rawMax = Math.max(rawMax, v);
}
- long maxTp = roundUpNice(rawMax);
+ long maxTp = MetricsCollector.niceMax(rawMax);
long curTp = mergedTotal[renderPoints - 1];
- long curFailed = mergedFailed[renderPoints - 1];
+ // Clamp failed to total so the title matches the chart bars
(total comes from
+ // EWMA while failed is still delta-based, so failed can
momentarily exceed total)
+ long curFailed = Math.min(mergedFailed[renderPoints - 1], curTp);
long curOk = Math.max(0, curTp - curFailed);
+ // Format throughput values unscaled for display
+ String curTpFmt = MetricsCollector.formatThroughput(curTp);
+ String curOkFmt = MetricsCollector.formatThroughput(curOk);
+ String curFailFmt = MetricsCollector.formatThroughput(curFailed);
+
Line titleLine;
if (chartMode == CHART_SINGLE && ctx.selectedPid != null) {
IntegrationInfo chartSel = ctx.findSelectedIntegration();
@@ -517,18 +524,18 @@ class OverviewTab extends AbstractTab {
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)
@@ -547,7 +554,7 @@ class OverviewTab extends AbstractTab {
BarChart barChart = BarChart.builder()
.data(groups)
- .max(maxTp > 0 ? maxTp + 2 : 2)
+ .max(maxTp)
.barWidth(1)
.barGap(0)
.groupGap(0)
@@ -560,9 +567,11 @@ class OverviewTab extends AbstractTab {
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("%3s",
MetricsCollector.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("%3s",
MetricsCollector.formatThroughput(maxTp / 2)), dimStyle)));
} else if (row == barRows - 1) {
yLines.add(Line.from(Span.styled(" 0", dimStyle)));
} else {
@@ -897,18 +906,6 @@ class OverviewTab extends AbstractTab {
return sortStyle(column, sort);
}
- private static long roundUpNice(long value) {
- if (value <= 10) {
- return 10;
- }
- long step = (long) Math.pow(10, Math.floor(Math.log10(value)));
- long rounded = ((value + step - 1) / step) * step;
- if (rounded % 2 != 0) {
- rounded += step;
- }
- return rounded;
- }
-
private static boolean hasReadmeInSourceDir(IntegrationInfo info) {
java.nio.file.Path srcDir = FilesBrowser.resolveSourceDirectory(info);
if (srcDir != null) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorThroughputTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorThroughputTest.java
new file mode 100644
index 000000000000..27c536c73279
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorThroughputTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link MetricsCollector} throughput formatting and scaling
utilities. Verifies that sub-1.0 msg/s rates
+ * (common in development workloads) are correctly preserved and displayed.
+ */
+class MetricsCollectorThroughputTest {
+
+ @Test
+ void formatThroughputZero() {
+ assertEquals("0", MetricsCollector.formatThroughput(0));
+ }
+
+ @Test
+ void formatThroughputSubOne() {
+ // 0.20 msg/s = 20 when scaled by 100
+ assertEquals("0.20", MetricsCollector.formatThroughput(20));
+ // 0.05 msg/s = 5 when scaled by 100
+ assertEquals("0.05", MetricsCollector.formatThroughput(5));
+ }
+
+ @Test
+ void formatThroughputBetweenOneAndTen() {
+ // 1.0 msg/s = 100 when scaled by 100
+ assertEquals("1.0", MetricsCollector.formatThroughput(100));
+ // 5.5 msg/s = 550 when scaled by 100
+ assertEquals("5.5", MetricsCollector.formatThroughput(550));
+ }
+
+ @Test
+ void formatThroughputAboveTen() {
+ // 10 msg/s = 1000 when scaled by 100
+ assertEquals("10", MetricsCollector.formatThroughput(1000));
+ // 42 msg/s = 4200 when scaled by 100
+ assertEquals("42", MetricsCollector.formatThroughput(4200));
+ }
+
+ @Test
+ void niceMaxReturnsScaleForZero() {
+ assertEquals(MetricsCollector.THROUGHPUT_SCALE,
MetricsCollector.niceMax(0));
+ }
+
+ @Test
+ void niceMaxReturnsScaleForSmallValues() {
+ // 0.20 msg/s scaled = 20 -> niceMax should return 100 (= 1.0 msg/s)
+ long result = MetricsCollector.niceMax(20);
+ assertEquals(100, result);
+ }
+
+ @Test
+ void niceMaxRoundsUpToNiceNumber() {
+ // 3.5 msg/s scaled = 350 -> niceMax should return 500 (= 5.0 msg/s)
+ long result = MetricsCollector.niceMax(350);
+ assertEquals(500, result);
+ }
+
+ @Test
+ void niceMaxForLargeValues() {
+ // 75 msg/s scaled = 7500 -> niceMax should return 10000 (= 100 msg/s)
+ long result = MetricsCollector.niceMax(7500);
+ assertEquals(10000, result);
+ }
+
+ @Test
+ void niceMaxAlwaysGreaterOrEqual() {
+ // niceMax should always return a value >= the input
+ for (long v : new long[] { 1, 10, 50, 99, 100, 150, 200, 500, 1000,
5000, 10000 }) {
+ assertTrue(MetricsCollector.niceMax(v) >= v,
+ "niceMax(" + v + ") = " + MetricsCollector.niceMax(v) + "
should be >= " + v);
+ }
+ }
+
+ @Test
+ void throughputScaleIsHundred() {
+ // Verify the scale factor — tests and display logic depend on this
value
+ assertEquals(100, MetricsCollector.THROUGHPUT_SCALE);
+ }
+
+ @Test
+ void niceMaxDoesNotOverflowForLargeInput() {
+ // Very large rawMax should not cause infinite loop or negative values
+ long result = MetricsCollector.niceMax(Long.MAX_VALUE / 2);
+ assertTrue(result >= Long.MAX_VALUE / 2,
+ "niceMax should return at least rawMax for extreme values, got
" + result);
+ }
+
+ @Test
+ void niceMaxHandlesMaxLong() {
+ // Long.MAX_VALUE itself should not hang
+ long result = MetricsCollector.niceMax(Long.MAX_VALUE);
+ assertTrue(result > 0, "niceMax(Long.MAX_VALUE) should return a
positive value, got " + result);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabThroughputRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabThroughputRenderTest.java
new file mode 100644
index 000000000000..f0c348aa2cb8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabThroughputRenderTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Screen-capture render tests for the OverviewTab throughput chart. Verifies
that sub-1.0 msg/s rates are displayed
+ * correctly in the bar chart title, preventing future regressions where
integer truncation would cause the chart to
+ * show "0 msg/s" instead of e.g. "0.20 msg/s".
+ *
+ * @see <a
href="https://issues.apache.org/jira/browse/CAMEL-23865">CAMEL-23865</a>
+ */
+class OverviewTabThroughputRenderTest {
+
+ private MonitorContext ctx;
+ private MetricsCollector metrics;
+ private IntegrationInfo info;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+
+ info = new IntegrationInfo();
+ info.pid = "1234";
+ info.name = "test-app";
+ info.state = 5; // started
+ info.exchangesTotal = 10;
+ info.throughput = "0.20"; // 0.20 msg/s (e.g. timer with 5s period)
+
+ AtomicReference<List<IntegrationInfo>> data = new
AtomicReference<>(List.of(info));
+ AtomicReference<List<InfraInfo>> infraData = new
AtomicReference<>(List.of());
+ ctx = new MonitorContext(data, infraData);
+ ctx.selectedPid = "1234";
+
+ metrics = new MetricsCollector();
+ }
+
+ @AfterEach
+ void tearDown() {
+ Theme.resetForTesting();
+ }
+
+ /**
+ * Verifies that a sub-1.0 msg/s throughput rate is visible in the chart
title when throughput history contains
+ * scaled values from the EWMA backend.
+ */
+ @Test
+ void chartShowsFractionalThroughputInTitle() {
+ // Populate throughput history with scaled 0.20 msg/s values
+ // (0.20 * THROUGHPUT_SCALE = 20)
+ Map<String, LinkedList<Long>> throughputHistory =
metrics.getThroughputHistory();
+ LinkedList<Long> history = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ history.add(20L); // 0.20 msg/s scaled by 100
+ }
+ throughputHistory.put("1234", history);
+
+ Map<String, LinkedList<Long>> failedHistory =
metrics.getFailedHistory();
+ LinkedList<Long> fHistory = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ fHistory.add(0L);
+ }
+ failedHistory.put("1234", fHistory);
+
+ OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () ->
{
+ });
+ tab.chartMode = OverviewTab.CHART_ALL;
+
+ String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+ assertTrue(rendered.contains("0.20 msg/s"),
+ "Chart title should show '0.20 msg/s' for sub-1.0 throughput,
but got:\n" + rendered);
+ }
+
+ /**
+ * Verifies that zero throughput displays as "0 msg/s", not "0.00 msg/s".
+ */
+ @Test
+ void chartShowsZeroThroughput() {
+ Map<String, LinkedList<Long>> throughputHistory =
metrics.getThroughputHistory();
+ LinkedList<Long> history = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ history.add(0L);
+ }
+ throughputHistory.put("1234", history);
+
+ Map<String, LinkedList<Long>> failedHistory =
metrics.getFailedHistory();
+ failedHistory.put("1234", new LinkedList<>(history));
+
+ OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () ->
{
+ });
+ tab.chartMode = OverviewTab.CHART_ALL;
+
+ String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+ assertTrue(rendered.contains("0 msg/s"),
+ "Chart title should show '0 msg/s' for zero throughput, but
got:\n" + rendered);
+ }
+
+ /**
+ * Verifies that high throughput (>= 10 msg/s) displays as integer without
decimals.
+ */
+ @Test
+ void chartShowsIntegerThroughputForHighRates() {
+ Map<String, LinkedList<Long>> throughputHistory =
metrics.getThroughputHistory();
+ LinkedList<Long> history = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ history.add(4200L); // 42 msg/s scaled by 100
+ }
+ throughputHistory.put("1234", history);
+
+ Map<String, LinkedList<Long>> failedHistory =
metrics.getFailedHistory();
+ LinkedList<Long> fHistory = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ fHistory.add(0L);
+ }
+ failedHistory.put("1234", fHistory);
+
+ OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () ->
{
+ });
+ tab.chartMode = OverviewTab.CHART_ALL;
+
+ String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+ assertTrue(rendered.contains("42 msg/s"),
+ "Chart title should show '42 msg/s' for high throughput, but
got:\n" + rendered);
+ }
+
+ /**
+ * Verifies that the chart with sub-1 throughput data does NOT show the
integer "0 msg/s" — this was the original
+ * bug where integer truncation made the chart appear empty.
+ */
+ @Test
+ void chartDoesNotTruncateSubOneRateToZero() {
+ Map<String, LinkedList<Long>> throughputHistory =
metrics.getThroughputHistory();
+ LinkedList<Long> history = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ history.add(20L); // 0.20 msg/s
+ }
+ throughputHistory.put("1234", history);
+
+ Map<String, LinkedList<Long>> failedHistory =
metrics.getFailedHistory();
+ LinkedList<Long> fHistory = new LinkedList<>();
+ for (int i = 0; i < 30; i++) {
+ fHistory.add(0L);
+ }
+ failedHistory.put("1234", fHistory);
+
+ OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () ->
{
+ });
+ tab.chartMode = OverviewTab.CHART_ALL;
+
+ String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+ // The old bug would show "0 msg/s" here due to integer truncation
+ assertFalse(rendered.contains("Throughput: 0 msg/s"),
+ "Chart should NOT truncate 0.20 msg/s to '0 msg/s' — integer
truncation regression detected:\n"
+ + rendered);
+ }
+}