This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch chore-tui-small-bugs-charts in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1cc1431cf62864a351de2c0d00658d8eed6e5155 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 9 16:16:07 2026 +0100 chore: camel-jbang - Process shows each argument after the jar, Source Info is about the open file, the heap chart has an axis, and a millisecond of jitter is no spike - Process: the arguments after -jar (run and the files) were joined to the -jar line and cut off. -jar and -cp take one value, and the arguments after it have a line each. - Source: the Info panel described the file under the cursor of the list, also while the viewer had the focus on another file (opened with g or from another tab). It describes the open file then. - Memory: the heap chart said "<peak> committed" when the top was the most used in its history; it says what is used and committed, and has a y axis (the top, the half and 0). - Timeline: the mean time sparkline drew 1 ms as a full bar on a route of 0 and 1 ms. Its height is at least 10 ms. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/MemoryTab.java | 27 ++++++++++++++++--- .../dsl/jbang/core/commands/tui/ProcessTab.java | 31 +++++++++++++++++----- .../dsl/jbang/core/commands/tui/SourceTab.java | 29 +++++++++++++++----- .../dsl/jbang/core/commands/tui/TimelineTab.java | 12 +++++++-- .../core/commands/tui/MemoryTabRenderTest.java | 22 +++++++++++++++ .../core/commands/tui/ProcessTabRenderTest.java | 18 +++++++++++++ .../core/commands/tui/SourceTabJumpLinksTest.java | 22 +++++++++++++++ .../core/commands/tui/TimelineTabRenderTest.java | 11 ++++++++ 8 files changed, 153 insertions(+), 19 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java index 06ccd219bd10..07110641e977 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java @@ -259,23 +259,32 @@ class MemoryTab extends AbstractTab { return; } - // Use committed as the scale ceiling + // the top of the chart: the committed heap, or the most used in the history when that was more (the heap + // shrinks); the axis says which height it is long observedMax = hist.stream().mapToLong(Long::longValue).max().orElse(1); long ceiling = info.heapMemCommitted > 0 ? info.heapMemCommitted : observedMax; if (observedMax > ceiling) { ceiling = observedMax; } - String title = String.format(" Heap Usage (%s / %s committed) ", formatBytes(info.heapMemUsed), formatBytes(ceiling)); + String title = String.format(" Heap Usage (%s used, %s committed) ", formatBytes(info.heapMemUsed), + formatBytes(info.heapMemCommitted > 0 ? info.heapMemCommitted : ceiling)); // Render the block border first Block block = Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build(); frame.renderWidget(block, area); - Rect inner = block.inner(area); + Rect box = block.inner(area); - if (inner.isEmpty() || ceiling <= 0) { + if (box.isEmpty() || ceiling <= 0) { return; } + // the y axis at the left: the top, the half and 0 + String top = formatBytes(ceiling); + int axisW = top.length() + 1; + if (box.width() <= axisW + 10) { + axisW = 0; + } + Rect inner = new Rect(box.x() + axisW, box.y(), box.width() - axisW, box.height()); int chartW = inner.width(); int chartH = inner.height() - 1; @@ -325,6 +334,16 @@ class MemoryTab extends AbstractTab { } } + if (axisW > 0) { + Style axis = Style.EMPTY.dim(); + buf.setString(box.x(), inner.y(), top, axis); + if (chartH >= 5) { + String half = formatBytes(ceiling / 2); + buf.setString(box.x() + axisW - 1 - half.length(), inner.y() + chartH / 2, half, axis); + } + buf.setString(box.x() + axisW - 2, inner.y() + chartH - 1, "0", axis); + } + // Render time axis on the bottom row of the inner area (inside the block border) renderTimeAxis(frame, inner, info, chartW); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java index 6fc5c2cd078d..f5aab1d8d794 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java @@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.ArrayList; import java.util.List; +import java.util.Set; import dev.tamboui.layout.Constraint; import dev.tamboui.layout.Layout; @@ -270,16 +271,34 @@ class ProcessTab extends AbstractTab { } } - private static List<String> splitCommandLine(String cmdLine) { + // options of the JVM whose value is the next token, after which come the main class or jar and its arguments + private static final Set<String> ONE_VALUE_OPTIONS = Set.of("-jar", "-cp", "-classpath", "--class-path"); + + /** + * The command line a line per option, with its value: an option of the JVM that takes one value (-jar, -cp) has + * just that one, and the arguments after it (the main class, the files of camel run) a line each, so none is cut + * off at the end of a long -jar line. + */ + static List<String> splitCommandLine(String cmdLine) { List<String> parts = new ArrayList<>(); - for (String token : cmdLine.split("\\s+")) { - if (token.startsWith("-") && !parts.isEmpty()) { + boolean oneValue = false; + boolean arguments = false; + for (String token : cmdLine.trim().split("\\s+")) { + if (parts.isEmpty()) { + parts.add(token); + } else if (token.startsWith("-")) { parts.add(token); - } else if (parts.isEmpty()) { + oneValue = ONE_VALUE_OPTIONS.contains(token); + arguments = false; + } else if (oneValue) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " " + token); + oneValue = false; + arguments = true; + } else if (arguments) { parts.add(token); } else { - String last = parts.get(parts.size() - 1); - parts.set(parts.size() - 1, last + " " + token); + // the values of an option, such as --name foo + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " " + token); } } return parts; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java index af9f5b566109..a7e361624029 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java @@ -1096,15 +1096,30 @@ class SourceTab extends AbstractTab { private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + /** + * The file the Info panel describes, the one the focus is on: the open file when the viewer has the focus, else the + * one under the cursor of the list (the open file can be another one, opened with g or from another tab). + */ + Path infoPath() { + String viewed = sourceViewer.isVisible() && focusOnViewer ? sourceViewer.getCurrentFilePath() : null; + if (viewed != null) { + return Path.of(viewed); + } + Integer sel = listState.selected(); + return sel != null && sel < entries.size() ? Path.of(entries.get(sel).path()) : null; + } + private void renderInfoPanel(Frame frame, Rect area) { List<Line> lines = new ArrayList<>(); + Path path = infoPath(); Integer sel = listState.selected(); - if (sel != null && sel < entries.size()) { - FilesBrowser.FileEntry entry = entries.get(sel); - Path path = Path.of(entry.path()); + boolean directory = path != null && sel != null && sel < entries.size() + && entries.get(sel).directory() && path.toString().equals(entries.get(sel).path()); + if (path != null) { + String name = path.getFileName() != null ? path.getFileName().toString() : ""; try { BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); - if (!entry.directory()) { + if (!directory) { lines.add(Line.from( Span.styled(" Size: ", Style.EMPTY.dim()), Span.raw(FilesBrowser.formatFileSize(attrs.size())))); @@ -1114,11 +1129,11 @@ class SourceTab extends AbstractTab { lines.add(Line.from( Span.styled(" Modified: ", Style.EMPTY.dim()), Span.raw(modified))); - if (!entry.directory()) { + if (!directory) { String ext = ""; - int dot = entry.name().lastIndexOf('.'); + int dot = name.lastIndexOf('.'); if (dot > 0) { - ext = entry.name().substring(dot + 1); + ext = name.substring(dot + 1); } if (!ext.isEmpty()) { lines.add(Line.from( diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java index 641d1231a954..3b4153546b59 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java @@ -56,6 +56,8 @@ import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint; */ class TimelineTab extends AbstractTableTab { + // the mean time sparkline is at least this many milliseconds high: below it, a time is noise + static final long MIN_MEAN_SCALE_MILLIS = 10; static final int[] WINDOWS = { 60, 120, 300 }; private static final String BLOCKS = "▁▂▃▄▅▆▇█"; private static final int CAUSE_ROWS = 4; @@ -742,8 +744,9 @@ class TimelineTab extends AbstractTableTab { } List<Span> meanLine = new ArrayList<>(); meanLine.add(Span.styled(pad(" mean time", label), Theme.label())); - // relative to twice the average, so a steady time is a calm line and a slow second stands out - long scale = n > 0 ? Math.max(maxMean, 2 * sumMean / n) : 0; + // relative to twice the average, so a steady time is a calm line and a slow second stands out; at least + // MIN_MEAN_SCALE_MILLIS, so a millisecond of jitter on a fast route is not a full bar + long scale = n > 0 ? meanScale(maxMean, sumMean / n) : 0; meanLine.add(Span.styled(spark(mean, scale), Theme.info())); meanLine.add(Span.raw(" " + (n > 0 ? "avg " + ChaosTab.shortTime(sumMean / n) : "no exchange"))); if (max > 0) { @@ -853,6 +856,11 @@ class TimelineTab extends AbstractTableTab { } /** A sparkline of values (-1 is unknown, blank), relative to the highest. */ + /** The height of the mean time sparkline: twice the average or the slowest second, at least 10 ms. */ + static long meanScale(long maxMean, long avg) { + return Math.max(MIN_MEAN_SCALE_MILLIS, Math.max(maxMean, 2 * avg)); + } + static String spark(long[] values, long max) { StringBuilder sb = new StringBuilder(); for (long v : values) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTabRenderTest.java index f9f2029e80f8..0b15d581997f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTabRenderTest.java @@ -17,6 +17,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.concurrent.atomic.AtomicReference; @@ -74,6 +75,27 @@ class MemoryTabRenderTest { assertTrue(rendered.contains("Heap"), "Should show Heap memory section header"); } + @Test + void theHeapChartSaysWhatTheTopIsAndWhatIsCommitted() { + info.heapMemUsed = 38L * 1024 * 1024; + info.heapMemCommitted = 48L * 1024 * 1024; + info.heapMemMax = 512L * 1024 * 1024; + MetricsCollector metrics = new MetricsCollector(); + // the heap was 64M a while ago, and has shrunk since + LinkedList<Long> history = new LinkedList<>(); + for (int i = 0; i < 100; i++) { + history.add((i < 50 ? 64L : 38L) * 1024 * 1024); + } + metrics.getHeapMemHistory().put("1234", history); + + String rendered = TuiTestHelper.renderToString(new MemoryTab(ctx, metrics), 160, 50); + + assertTrue(rendered.contains("Heap Usage (38M used, 48M committed)"), rendered); + // the y axis: the top of the chart is the most the heap used, then the half + assertTrue(rendered.contains("│64M"), rendered); + assertTrue(rendered.contains("│32M"), rendered); + } + @Test void renderShowsThreadCount() { info.threadCount = 42; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java index d521d7402208..43182a9da38d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTabRenderTest.java @@ -24,6 +24,7 @@ import dev.tamboui.text.Span; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -102,4 +103,21 @@ class ProcessTabRenderTest { assertTrue(footer.contains("Esc"), "Footer should contain Esc hint"); } + @Test + void theArgumentsAfterTheJarHaveALineEach() { + List<String> parts = ProcessTab.splitCommandLine( + "/usr/bin/java -Xmx512m -XX:+AutoCreateSharedArchive -jar /opt/camel/lib/camel-launcher.jar run README.md" + + " application.properties orders.camel.yaml --logging-color=true" + + " --name=orders --port 8080"); + + assertEquals(List.of("/usr/bin/java", "-Xmx512m", "-XX:+AutoCreateSharedArchive", + "-jar /opt/camel/lib/camel-launcher.jar", "run", "README.md", "application.properties", + "orders.camel.yaml", "--logging-color=true", "--name=orders", "--port 8080"), parts); + } + + @Test + void theMainClassAfterTheClasspathHasALineOfItsOwn() { + assertEquals(List.of("java", "-cp a.jar:b.jar", "com.acme.Main", "--verbose"), + ProcessTab.splitCommandLine("java -cp a.jar:b.jar com.acme.Main --verbose")); + } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java index 13be5dae4a48..b7da0484a30b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java @@ -257,6 +257,28 @@ class SourceTabJumpLinksTest { assertThat(tab.computeJumpLinks(billing).get(4).filePath()).isEqualTo(orders.toString()); } + @Test + void theInfoPanelDescribesTheOpenFileWhenTheViewerHasTheFocus() throws Exception { + Files.writeString(dir.resolve("application.properties"), "greeting=hello\n"); + Path routes = Files.writeString(dir.resolve("orders.camel.yaml"), """ + - route: + id: orders + from: + uri: direct:orders + steps: + - to: + uri: log:orders + """); + SourceTab tab = newTab(); + assertThat(tab.loadDirectory(dir)).isTrue(); + // the list has its cursor on its first entry, not on the routes + assertThat(tab.infoPath()).isNotNull().isNotEqualTo(routes); + + // the routes are opened another way (as g does): the list keeps its cursor, the viewer has the focus + tab.openFileForReplay(routes); + assertThat(tab.infoPath()).isEqualTo(routes); + } + private static SourceTab newTab() { return new SourceTab(new MonitorContext(new AtomicReference<>(List.of()), new AtomicReference<>(List.of()))); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java index c5b277032565..f90675123177 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java @@ -426,4 +426,15 @@ class TimelineTabRenderTest { } throw new AssertionError("no line with " + text); } + + @Test + void aMillisecondOfJitterIsNoFullBar() { + // a fast route: 0 and 1 ms seconds are noise at the bottom, not spikes + assertThat(TimelineTab.meanScale(1, 0)).isEqualTo(10); + assertThat(TimelineTab.spark(new long[] { 0, 1, 0, 1 }, TimelineTab.meanScale(1, 0))) + .doesNotContain("█"); + // a slow second still stands out against a steady time + assertThat(TimelineTab.meanScale(400, 50)).isEqualTo(400); + assertThat(TimelineTab.meanScale(60, 50)).isEqualTo(100); + } }
