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 6264c4653806 chore: camel-jbang - Events shows one category at a time
with f, and Startup is a waterfall with the slowest first on s (#29507)
6264c4653806 is described below
commit 6264c46538066afe9e8d3a622a05ab17fda35cdf
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 22:52:12 2026 +0100
chore: camel-jbang - Events shows one category at a time with f, and
Startup is a waterfall with the slowest first on s (#29507)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../dsl/jbang/core/commands/tui/EventTab.java | 43 +++++++++++++-
.../dsl/jbang/core/commands/tui/StartupTab.java | 68 ++++++++++++++++------
.../src/main/resources/tui/help/event.md | 3 +
.../src/main/resources/tui/help/startup.md | 32 +++++-----
.../jbang/core/commands/tui/EventTabOrderTest.java | 24 ++++++++
.../core/commands/tui/StartupTabRenderTest.java | 24 ++++++++
6 files changed, 157 insertions(+), 37 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
index 91fa539b245d..ac2fb44c6bea 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Rect;
@@ -42,6 +43,9 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
class EventTab extends AbstractTableTab {
+ // f: only the events of one category (exchange events drown the route and
context ones); null for all
+ private String category;
+
EventTab(MonitorContext ctx) {
super(ctx, "timestamp", "category", "type", "message");
this.sort = "timestamp";
@@ -73,7 +77,7 @@ class EventTab extends AbstractTableTab {
@Override
protected int getRowCount() {
IntegrationInfo info = ctx.findSelectedIntegration();
- return info != null ? info.events.size() : 0;
+ return info != null ? sortedEvents(info).size() : 0;
}
@Override
@@ -109,7 +113,7 @@ class EventTab extends AbstractTableTab {
Constraint.length(30),
Constraint.fill())
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Events ").build())
+ .title(category != null ? " Events [" + category + "]
" : " Events ").build())
.build();
lastTableArea = area;
@@ -124,6 +128,9 @@ class EventTab extends AbstractTableTab {
*/
List<EventInfo> sortedEvents(IntegrationInfo info) {
List<EventInfo> sorted = new ArrayList<>(info.events);
+ if (category != null) {
+ sorted.removeIf(e -> !category.equals(e.category));
+ }
if (!sortReversed) {
Collections.reverse(sorted);
}
@@ -131,6 +138,38 @@ class EventTab extends AbstractTableTab {
return sorted;
}
+ @Override
+ protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isCharIgnoreCase('f')) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ category = nextCategory(category, info != null ? info.events :
List.of());
+ tableState.select(0);
+ return true;
+ }
+ return false;
+ }
+
+ /** The category after the one shown: all, then each category the events
have, in order, then all again. */
+ static String nextCategory(String current, List<EventInfo> events) {
+ List<String> categories = events.stream().map(e ->
e.category).filter(Objects::nonNull).distinct().sorted()
+ .toList();
+ if (current == null) {
+ return categories.isEmpty() ? null : categories.get(0);
+ }
+ int i = categories.indexOf(current);
+ return i >= 0 && i + 1 < categories.size() ? categories.get(i + 1) :
null;
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ super.renderFooter(spans);
+ hint(spans, "f", "category [" + (category != null ? category : "all")
+ "]");
+ }
+
+ String category() {
+ return category;
+ }
+
private int sortEvent(EventInfo a, EventInfo b) {
int result = switch (sort) {
case "category" -> {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
index 4b54ce450239..a781bc0ad99e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -26,6 +27,7 @@ import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
import dev.tamboui.text.Text;
@@ -55,6 +57,8 @@ class StartupTab extends AbstractTab {
private volatile List<StartupStep> steps = Collections.emptyList();
private int scrollOffset;
+ // s: the steps in the order they started (a waterfall), or the slowest
first
+ private boolean slowestFirst;
private long totalDuration;
private long maxDuration;
private long minDurationColor;
@@ -120,6 +124,11 @@ class StartupTab extends AbstractTab {
loadStartupData();
return true;
}
+ if (ke.isCharIgnoreCase('s')) {
+ slowestFirst = !slowestFirst;
+ scrollOffset = 0;
+ return true;
+ }
return false;
}
@@ -199,7 +208,8 @@ class StartupTab extends AbstractTab {
return;
}
- String title = String.format(" Startup Timeline — Total: %dms, Steps:
%d ", totalDuration, steps.size());
+ String title = String.format(" Startup Timeline — Total: %dms, Steps:
%d%s ", totalDuration, steps.size(),
+ slowestFirst ? ", slowest first" : "");
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(title)
@@ -217,11 +227,18 @@ class StartupTab extends AbstractTab {
int end = Math.min(scrollOffset + visibleLines, steps.size());
- int barMaxWidth = Math.max(10, inner.width() - 30);
+ List<StartupStep> ordered = ordered(steps, slowestFirst);
+ // the time the steps span: a bar starts where its step started, as
long as it took
+ long t0 = steps.stream().mapToLong(st -> st.beginTime).filter(b -> b >
0).min().orElse(0);
+ long t1 = steps.stream().mapToLong(st -> st.beginTime +
st.duration).max().orElse(0);
+ long span = t0 > 0 ? Math.max(1, t1 - t0) : Math.max(1, maxDuration);
+ int labelWidth = Math.min(inner.width() / 2,
+ steps.stream().mapToInt(st -> 2 * st.level +
CharWidth.of(label(st))).max().orElse(10) + 2);
+ int barMaxWidth = Math.max(10, inner.width() - 9 - labelWidth - 1);
List<Line> lines = new ArrayList<>();
for (int i = scrollOffset; i < end; i++) {
- lines.add(renderStep(steps.get(i), barMaxWidth));
+ lines.add(renderStep(ordered.get(i), labelWidth, barMaxWidth, t0,
span));
}
List<Rect> hChunks = Layout.horizontal()
@@ -239,30 +256,42 @@ class StartupTab extends AbstractTab {
}
}
- private Line renderStep(StartupStep step, int maxBarWidth) {
- String indent = " ".repeat(step.level);
-
+ /**
+ * A step: how long it took, what it is (indented by its level), and its
bar on the time line of the startup, from
+ * where it started (without begin times, as an older Camel has them, from
the left).
+ */
+ private Line renderStep(StartupStep step, int labelWidth, int maxBarWidth,
long t0, long span) {
boolean isRoot = step.level == 0;
Style bandStyle = isRoot ? LABEL : colorForDuration(step.duration);
+ int offset = t0 > 0 && step.beginTime > 0
+ ? (int) Math.min(maxBarWidth - 1, Math.round((double)
(step.beginTime - t0) / span * maxBarWidth)) : 0;
+ int barWidth = Math.max(1, Math.min(maxBarWidth - offset,
+ (int) Math.round((double) step.duration / span *
maxBarWidth)));
+ String what = TuiHelper.truncate(" ".repeat(step.level) +
label(step), labelWidth);
+ return Line.from(
+ Span.styled(String.format("%7dms ", step.duration), isRoot ?
LABEL : VALUE),
+ Span.styled(what + " ".repeat(Math.max(1, labelWidth -
CharWidth.of(what))), LABEL),
+ Span.raw(" ".repeat(offset)),
+ Span.styled("█".repeat(barWidth), bandStyle));
+ }
- double ratio = maxDuration > 0 ? (double) step.duration / maxDuration
: 0;
- int barWidth = Math.max(1, (int) Math.round(ratio * maxBarWidth));
- String bar = "█".repeat(barWidth);
-
- String durationStr = step.duration + "ms";
+ private static String label(StartupStep step) {
String label = step.name != null ? step.name : "";
if (step.description != null && !step.description.isEmpty() &&
!step.description.equals(label)) {
label += " " + step.description;
}
+ return label;
+ }
- int pad = Math.max(1, 8 - durationStr.length());
-
- return Line.from(
- Span.raw(indent),
- Span.styled(bar, bandStyle),
- Span.raw(" ".repeat(pad)),
- Span.styled(durationStr, isRoot ? LABEL : VALUE),
- Span.styled(" " + label, LABEL));
+ /** The steps in the order they started (by begin time, the outer step
first), or the slowest first. */
+ static List<StartupStep> ordered(List<StartupStep> steps, boolean
slowestFirst) {
+ List<StartupStep> ordered = new ArrayList<>(steps);
+ if (slowestFirst) {
+ ordered.sort(Comparator.comparingLong((StartupStep st) ->
st.duration).reversed());
+ } else if (steps.stream().anyMatch(st -> st.beginTime > 0)) {
+ ordered.sort(Comparator.comparingLong((StartupStep st) ->
st.beginTime).thenComparingInt(st -> st.level));
+ }
+ return ordered;
}
private Style colorForDuration(long duration) {
@@ -272,6 +301,7 @@ class StartupTab extends AbstractTab {
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
+ hint(spans, "s", slowestFirst ? "start order" : "slowest first");
hintLast(spans, "PgUp/Dn", "page");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/event.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/event.md
index 4cba30dace71..927669fabb8b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/event.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/event.md
@@ -24,3 +24,6 @@ Only the most recent events are shown.
- `Up/Down` — select event
- `s` — cycle sort column
- `S` — reverse sort order
+- `f` — only the events of one category: all, then each category the
+ events have (`exchange`, `general`, `route` ...), then all again. The
+ exchange events of a busy route drown the route and context ones.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/startup.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/startup.md
index 0ebda3ba2386..9e06e68bdbe9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/startup.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/startup.md
@@ -1,8 +1,9 @@
# Startup
-The Startup tab shows a timeline of how the integration started up.
-Each step is displayed with a horizontal bar proportional to its
-duration, helping you visually identify which phases took the most time.
+The Startup tab shows a timeline of how the integration started up:
+a waterfall where each step's bar starts when the step started and is
+as long as it took, so you see what ran inside what, and what ran
+after what.
This is useful for optimizing startup time — for example, finding
components that take a long time to initialize, routes that are slow
@@ -12,25 +13,23 @@ to start, or external connections that delay readiness.
Each line shows one startup step with:
-- **Bar** — Horizontal bar proportional to duration. Longer bars = slower
steps. Color ranges from green (fast) through yellow to red (slowest steps)
- **Duration** — Time in milliseconds this step took
-- **Name** — What this step does (e.g., component initialization, route
startup)
+- **Name** — What this step does (e.g., component initialization, route
startup), indented under the step it is part of
+- **Bar** — Where the step ran on the time line of the startup. Color ranges
from green (fast) through yellow to red (slowest steps)
-Steps are indented to show parent-child relationships — a route startup
-step contains sub-steps for each processor initialization.
+The steps are in the order they started. `s` puts the slowest first,
+to find what to optimize without scrolling through the fast ones.
## Example Screen
```
- ████████████████████████████████████████ 1234ms Starting CamelContext
- ██████████████████ 620ms Start Routes
- ████████████ 412ms timer-to-log
- ████ 150ms Create Component: timer
- ██ 80ms Create Component: log
- ████ 140ms seda-consumer
- ████████ 280ms Start Consumers
- ██████ 210ms timer://hello
- ██ 70ms seda://queue
+ 1234ms Starting CamelContext ████████████████████████████████████████
+ 620ms Start Routes ████████████████████
+ 412ms timer-to-log █████████████
+ 150ms Create Component: timer █████
+ 80ms Create Component: log ███
+ 140ms seda-consumer █████
+ 280ms Start Consumers █████████
```
In this example, the total startup took 1234ms. The slowest phase
@@ -61,5 +60,6 @@ the context is fully started — the data does not change
during runtime.
- `Up/Down` — scroll through steps
- `PgUp/PgDn` — scroll by page
- `Home/End` — jump to top/end
+- `s` — the slowest steps first, or the order they started
- `F5` — reload startup data
- `Esc` — back
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
index 617ccf94b60b..8591488fd29a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
+import dev.tamboui.tui.event.KeyEvent;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,6 +47,29 @@ class EventTabOrderTest {
"ExchangeCompleted@1000", "ExchangeSent@1000",
"ExchangeSending@1000", "ExchangeCreated@1000");
}
+ @Test
+ void fShowsOneCategoryAtATime() {
+ IntegrationInfo info = new IntegrationInfo();
+ info.events.add(event("ExchangeCreated", 1000));
+ info.events.add(event("ExchangeCompleted", 1000));
+ EventInfo started = event("RouteStarted", 500);
+ started.category = "route";
+ info.events.add(started);
+ EventTab tab = new EventTab(new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())));
+ tab.ctx.selectedPid = info.pid = "1";
+
+ // all, then exchange, route, and all again
+ assertThat(tab.category()).isNull();
+ tab.handleKeyEvent(KeyEvent.ofChar('f'));
+ assertThat(tab.category()).isEqualTo("exchange");
+ tab.handleKeyEvent(KeyEvent.ofChar('f'));
+ assertThat(tab.category()).isEqualTo("route");
+ assertThat(tab.sortedEvents(info)).extracting(e ->
e.type).containsExactly("RouteStarted");
+ tab.handleKeyEvent(KeyEvent.ofChar('f'));
+ assertThat(tab.category()).isNull();
+ assertThat(tab.sortedEvents(info)).hasSize(3);
+ }
+
private static EventInfo event(String type, long timestamp) {
EventInfo e = new EventInfo();
e.type = type;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java
index e4dd9a4118f1..0c709b0b983e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTabRenderTest.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;
/**
@@ -81,4 +82,27 @@ class StartupTabRenderTest {
"Footer should contain hints");
}
+ @Test
+ void theStepsAreInTheOrderTheyStartedOrTheSlowestFirst() {
+ // as the recorder has them: a step after its parent, in the order
they ended
+ StartupTab.StartupStep routes = step("Init Routes", 1, 100, 80);
+ StartupTab.StartupStep route = step("stock-check Create Route", 2,
105, 73);
+ StartupTab.StartupStep context = step("Init CamelContext", 0, 90, 256);
+ List<StartupTab.StartupStep> steps = List.of(route, routes, context);
+
+ assertEquals(List.of(context, routes, route),
StartupTab.ordered(steps, false));
+ assertEquals(List.of(context, routes, route),
StartupTab.ordered(steps, true));
+ // without begin times (an older Camel): as recorded
+ steps.forEach(st -> st.beginTime = 0);
+ assertEquals(steps, StartupTab.ordered(steps, false));
+ }
+
+ private static StartupTab.StartupStep step(String name, int level, long
begin, long duration) {
+ StartupTab.StartupStep st = new StartupTab.StartupStep();
+ st.name = name;
+ st.level = level;
+ st.beginTime = begin;
+ st.duration = duration;
+ return st;
+ }
}