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 5edbf4cd5f68 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 (#29495)
5edbf4cd5f68 is described below
commit 5edbf4cd5f682a14cd8bf594e43c9d1c74b1eabe
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 23:18:50 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 (#29495)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../dsl/jbang/core/commands/tui/MemoryTab.java | 28 ++++++++++++---
.../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 | 41 ++++++++++++++++++++++
.../core/commands/tui/ProcessTabRenderTest.java | 18 ++++++++++
.../core/commands/tui/SourceTabJumpLinksTest.java | 22 ++++++++++++
.../core/commands/tui/TimelineTabRenderTest.java | 11 ++++++
8 files changed, 173 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..1e78cad795b7 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,34 @@ 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);
+ String half = formatBytes(ceiling / 2);
+ // the half label can be the wider one: 1G is 512M halfway
+ int axisW = Math.max(top.length(), half.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 +336,15 @@ class MemoryTab extends AbstractTab {
}
}
+ if (axisW > 0) {
+ Style axis = Style.EMPTY.dim();
+ buf.setString(box.x() + axisW - 1 - top.length(), inner.y(), top,
axis);
+ if (chartH >= 5) {
+ 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 6afbb89d9f13..2c0f488e1fab 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;
@@ -765,8 +767,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) {
@@ -875,6 +878,11 @@ class TimelineTab extends AbstractTableTab {
return lines;
}
+ /** 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));
+ }
+
/** A sparkline of values (-1 is unknown, blank), relative to the highest.
*/
static String spark(long[] values, long max) {
StringBuilder sb = new StringBuilder();
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..9eea2ee345eb 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,46 @@ 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 theHalfOfOneGigabyteFitsTheAxis() {
+ info.heapMemUsed = 600L * 1024 * 1024;
+ info.heapMemCommitted = 1024L * 1024 * 1024;
+ info.heapMemMax = 2048L * 1024 * 1024;
+ MetricsCollector metrics = new MetricsCollector();
+ LinkedList<Long> history = new LinkedList<>();
+ for (int i = 0; i < 100; i++) {
+ history.add(600L * 1024 * 1024);
+ }
+ metrics.getHeapMemHistory().put("1234", history);
+
+ String rendered = TuiTestHelper.renderToString(new MemoryTab(ctx,
metrics), 160, 50);
+
+ // 512M is wider than 1G: the axis is as wide as the half, and neither
label overwrites the border
+ assertTrue(rendered.contains("│ 1G"), rendered);
+ assertTrue(rendered.contains("│512M"), 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 d55de400d4c6..af5c0ed1348b 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
@@ -470,4 +470,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);
+ }
}