This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch fix/camel-23865-tui-throughput-regression in repository https://gitbox.apache.org/repos/asf/camel.git
commit 82f0158097e486ac1600030559b1016948cf0102 Author: Guillaume Nodet <[email protected]> AuthorDate: Tue Jul 7 17:45:52 2026 +0000 CAMEL-23865: Re-apply TUI chart throughput scaling lost during refactoring The CAMEL-23865 fix added EWMA smoothing to LoadThroughput (backend) and throughput scaling to the TUI charts. The backend fix survived, but the TUI-side changes (100x scaling, formatThroughput, niceMax) were lost when commit 065fd8314d9 ("Improve mouse support") rewrote MetricsCollector, OverviewTab, and EndpointsTab. This re-applies the TUI throughput scaling: - MetricsCollector: use EWMA throughput from status JSON scaled by 100x instead of integer division that truncated sub-1 rates to 0 - OverviewTab: format chart title with fractional msg/s display - EndpointsTab: same formatting fix for endpoint rate labels - Add screen-capture render tests to prevent future regressions Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/EndpointsTab.java | 8 +- .../jbang/core/commands/tui/MetricsCollector.java | 79 +++++++-- .../dsl/jbang/core/commands/tui/OverviewTab.java | 27 +-- .../tui/MetricsCollectorThroughputTest.java | 99 +++++++++++ .../tui/OverviewTabThroughputRenderTest.java | 186 +++++++++++++++++++++ 5 files changed, 372 insertions(+), 27 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..e166e19054e3 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); } } @@ -262,8 +275,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); @@ -403,4 +416,44 @@ 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 (true) { + for (int s : steps) { + long candidate = s * multiplier; + if (candidate >= rawMax) { + return candidate; + } + } + multiplier *= 10; + } + } + + /** + * 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 4af1263a93b8..8392b5b10e62 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,16 @@ 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]; 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 +522,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 +552,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 +565,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 { 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..9f506533ada2 --- /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,99 @@ +/* + * 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); + } +} 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); + } +}
