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 b4f37e4197a1 CAMEL-23976: TUI - display percentile latency stats
(p50/p95/p99)
b4f37e4197a1 is described below
commit b4f37e4197a1feffe3233b1a48cd68d668c5d6d5
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 10 09:55:14 2026 +0200
CAMEL-23976: TUI - display percentile latency stats (p50/p95/p99)
CAMEL-23976: TUI - document percentile latency stats in F1 help
CAMEL-23976: TUI - keep Route tab title when toggling top mode
CAMEL-23976: TUI - add top mode toggle to Overview tab
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/diagram/RouteDiagramHelper.java | 3 +
.../camel/diagram/RouteDiagramLayoutEngine.java | 3 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 4 +-
.../dsl/jbang/core/commands/tui/DiagramTab.java | 37 ++
.../jbang/core/commands/tui/IntegrationInfo.java | 8 +
.../dsl/jbang/core/commands/tui/OverviewTab.java | 401 +++++++++++++++------
.../dsl/jbang/core/commands/tui/ProcessorInfo.java | 3 +
.../dsl/jbang/core/commands/tui/RouteInfo.java | 3 +
.../dsl/jbang/core/commands/tui/RoutesTab.java | 194 ++++++++--
.../dsl/jbang/core/commands/tui/StatusParser.java | 14 +
.../core/commands/tui/RoutesTabRenderTest.java | 1 -
11 files changed, 541 insertions(+), 130 deletions(-)
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
index 603b827594f7..798569710d62 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
@@ -110,6 +110,9 @@ public final class RouteDiagramHelper {
stat.minProcessingTime =
ls.getLongOrDefault("minProcessingTime", 0);
stat.lastProcessingTime =
ls.getLongOrDefault("lastProcessingTime", -1);
stat.deltaProcessingTime =
ls.getLongOrDefault("deltaProcessingTime", -1);
+ stat.p50ProcessingTime =
ls.getLongOrDefault("p50ProcessingTime", -1);
+ stat.p95ProcessingTime =
ls.getLongOrDefault("p95ProcessingTime", -1);
+ stat.p99ProcessingTime =
ls.getLongOrDefault("p99ProcessingTime", -1);
stat.lastCreatedExchangeTimestamp =
ls.getLongOrDefault("lastCreatedExchangeTimestamp", -1);
stat.lastCompletedExchangeTimestamp =
ls.getLongOrDefault("lastCompletedExchangeTimestamp", -1);
stat.lastFailedExchangeTimestamp =
ls.getLongOrDefault("lastFailedExchangeTimestamp", -1);
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
index de3656d6b085..309b88638786 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
@@ -145,6 +145,9 @@ public class RouteDiagramLayoutEngine {
public long minProcessingTime;
public long lastProcessingTime;
public long deltaProcessingTime;
+ public long p50ProcessingTime = -1;
+ public long p95ProcessingTime = -1;
+ public long p99ProcessingTime = -1;
public long lastCreatedExchangeTimestamp;
public long lastCompletedExchangeTimestamp;
public long lastFailedExchangeTimestamp;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index c9879b88a309..8f7a92c87a9f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -1395,13 +1395,11 @@ public class CamelMonitor extends CamelCommand {
return;
}
- // Route and Top labels are the same display width so toggling Top
mode does not shift the bar.
- String routesLabel = tabRegistry.routesTab().isTopMode() ? " Top " :
"Route";
Line[] labels = {
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW,
"1", "Overview")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2",
"Log")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "3",
"Diagram")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "4",
routesLabel)),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "4",
"Route")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS,
"5", "Endpoint")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HTTP, "6",
"HTTP")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HEALTH, "7",
"Health")),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index cca6df972afe..90472df22908 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -475,6 +475,19 @@ class DiagramTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" Min: ", Theme.muted()),
Span.raw(String.format("%" + tw + "d ms",
route.minTime))));
+ if (route.p50Time >= 0) {
+ int pw = numWidth(route.p50Time, route.p95Time,
route.p99Time);
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" p50: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
route.p50Time))));
+ lines.add(Line.from(
+ Span.styled(" p95: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
route.p95Time))));
+ lines.add(Line.from(
+ Span.styled(" p99: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
route.p99Time))));
+ }
}
if (route.sinceLastCompleted != null || route.sinceLastFailed !=
null) {
@@ -619,6 +632,20 @@ class DiagramTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" Last: ", Theme.muted()),
Span.raw(String.format("%" + tw + "d ms",
stat.lastProcessingTime))));
+ if (stat.p50ProcessingTime >= 0) {
+ int pw = numWidth(stat.p50ProcessingTime,
stat.p95ProcessingTime,
+ stat.p99ProcessingTime);
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" p50: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
stat.p50ProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" p95: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
stat.p95ProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" p99: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
stat.p99ProcessingTime))));
+ }
if (stat.lastCompletedExchangeTimestamp > 0 ||
stat.lastFailedExchangeTimestamp > 0) {
long now = System.currentTimeMillis();
@@ -1143,6 +1170,11 @@ class DiagramTab extends AbstractTab {
ri.put("maxTime", route.maxTime);
ri.put("minTime", route.minTime);
}
+ if (route.p50Time >= 0) {
+ ri.put("p50Time", route.p50Time);
+ ri.put("p95Time", route.p95Time);
+ ri.put("p99Time", route.p99Time);
+ }
if (route.sinceLastCompleted != null) {
ri.put("sinceLastSuccess", route.sinceLastCompleted);
}
@@ -1170,6 +1202,11 @@ class DiagramTab extends AbstractTab {
ni.put("minTime", stat.minProcessingTime);
ni.put("lastTime", stat.lastProcessingTime);
}
+ if (stat.p50ProcessingTime >= 0) {
+ ni.put("p50Time", stat.p50ProcessingTime);
+ ni.put("p95Time", stat.p95ProcessingTime);
+ ni.put("p99Time", stat.p99ProcessingTime);
+ }
result.put("nodeInfo", ni);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 2820a1956d93..93820acd05a8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -42,6 +42,14 @@ class IntegrationInfo {
String inflightLoad01;
String inflightLoad05;
String inflightLoad15;
+ long meanTime;
+ long maxTime;
+ long minTime;
+ long lastTime;
+ long deltaTime;
+ long p50Time = -1;
+ long p95Time = -1;
+ long p99Time = -1;
String last;
String delta;
String sinceLastStarted;
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 6f51d09d4494..899023dfdc25 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
@@ -77,6 +77,7 @@ class OverviewTab extends AbstractTab {
private static final int MAX_SPARKLINE_POINTS = 300;
private static final String[] SORT_COLUMNS = { "pid", "name", "version",
"status", "total", "fail" };
private static final String[] INFRA_SORT_COLUMNS = { "service", "version",
"port", "status" };
+ private static final String[] TOP_SORT_COLUMNS = { "mean", "max", "min",
"last", "delta", "p50", "p95", "p99" };
static final int CHART_ALL = 0;
static final int CHART_SINGLE = 1;
@@ -108,6 +109,10 @@ class OverviewTab extends AbstractTab {
private String infraSort = "service";
private int infraSortIndex = 0;
private boolean infraSortReversed;
+ boolean topMode;
+ private String topSort = "mean";
+ private int topSortIndex;
+ private boolean topSortReversed;
OverviewTab(
MonitorContext ctx,
@@ -133,6 +138,10 @@ class OverviewTab extends AbstractTab {
infraSortIndex = (infraSortIndex + 1) %
INFRA_SORT_COLUMNS.length;
infraSort = INFRA_SORT_COLUMNS[infraSortIndex];
infraSortReversed = false;
+ } else if (topMode) {
+ topSortIndex = (topSortIndex + 1) % TOP_SORT_COLUMNS.length;
+ topSort = TOP_SORT_COLUMNS[topSortIndex];
+ topSortReversed = false;
} else {
sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
sort = SORT_COLUMNS[sortIndex];
@@ -143,11 +152,17 @@ class OverviewTab extends AbstractTab {
if (ke.isChar('S')) {
if (infraFocused) {
infraSortReversed = !infraSortReversed;
+ } else if (topMode) {
+ topSortReversed = !topSortReversed;
} else {
sortReversed = !sortReversed;
}
return true;
}
+ if (ke.isCharIgnoreCase('t') && !infraFocused) {
+ topMode = !topMode;
+ return true;
+ }
if (ke.isChar('i') && !ctx.infraData.get().isEmpty()) {
infraFocused = !infraFocused;
if (infraFocused && infraTableState.selected() == null) {
@@ -312,121 +327,244 @@ class OverviewTab extends AbstractTab {
}
List<Row> rows = new ArrayList<>();
+ Row header;
+ Constraint[] widths;
int rowIndex = 0;
- for (IntegrationInfo info : infos) {
- boolean isEven = (rowIndex++ % 2 == 0);
- Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) : Style.EMPTY;
- if (info.vanishing) {
- long elapsed = System.currentTimeMillis() - info.vanishStart;
- float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
- int gray = (int) (100 * fade);
- Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
-
- String vanishName = TuiIcons.labeled(TuiIcons.CAMEL, info.name
!= null ? info.name : "");
- rows.add(Row.from(
- Cell.from(Span.styled(info.pid, dimStyle)),
- Cell.from(Span.styled(vanishName, dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled(TuiIcons.STOPPED + " Stopped",
Theme.error().dim())),
- Cell.from(Span.styled(info.ago != null ? info.ago :
"", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle)),
- Cell.from(Span.styled("", dimStyle))).style(rowBg));
- } else {
- String stateText = extractState(info.state);
- if (stoppingPids.contains(info.pid) ||
"Terminating".equals(stateText)) {
- stateText = "Stopping";
- } else if ("Terminated".equals(stateText)) {
- stateText = "Stopped";
- } else if ("Running".equals(stateText) && info.routeStarted ==
0 && info.routeTotal > 0) {
- stateText = "Stopped";
+ if (topMode) {
+ for (IntegrationInfo info : infos) {
+ boolean isEven = (rowIndex++ % 2 == 0);
+ Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) :
Style.EMPTY;
+
+ if (info.vanishing) {
+ long elapsed = System.currentTimeMillis() -
info.vanishStart;
+ float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
+ int gray = (int) (100 * fade);
+ Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
+ String vanishName = TuiIcons.labeled(TuiIcons.CAMEL,
info.name != null ? info.name : "");
+ rows.add(Row.from(
+ Cell.from(Span.styled(vanishName, dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("",
dimStyle))).style(rowBg));
+ } else {
+ String platformIcon = TuiIcons.runtimeIcon(info.platform
!= null ? info.platform : "");
+ String nameText = platformIcon + " " + (info.name != null
? info.name : "");
+ List<Span> nameSpans = new ArrayList<>();
+ nameSpans.add(Span.styled(nameText, Theme.info()));
+ if (info.devMode) {
+ nameSpans.add(Span.styled(" [dev]", Theme.label()));
+ }
+ Line nameLine = Line.from(nameSpans);
+ Style failStyle = info.failed > 0 ? Theme.error().bold() :
Style.EMPTY;
+ String throughputDisplay = info.throughput;
+ if (throughputDisplay == null ||
"0.00".equals(throughputDisplay)) {
+ LinkedList<Long> tpHist =
throughputHistory.get(info.pid);
+ if (tpHist != null && !tpHist.isEmpty()) {
+ long tp = tpHist.getLast();
+ if (tp > 0) {
+ throughputDisplay =
String.format(java.util.Locale.US, "%.2f", (double) tp);
+ }
+ }
+ }
+ rows.add(Row.from(
+ Cell.from(nameLine),
+ rightCell(info.exchangesTotal > 0 ?
String.valueOf(info.meanTime) : "", 6,
+ TuiHelper.topTimeStyle(info.meanTime)),
+ rightCell(info.exchangesTotal > 0 ?
String.valueOf(info.maxTime) : "", 6,
+ TuiHelper.topTimeStyle(info.maxTime)),
+ rightCell(info.exchangesTotal > 0 ?
String.valueOf(info.minTime) : "", 6),
+ rightCell(info.exchangesTotal > 0 ?
String.valueOf(info.lastTime) : "", 6),
+ rightCell(info.deltaTime != 0 ?
String.valueOf(info.deltaTime) : "", 6,
+ TuiHelper.topDeltaStyle(info.deltaTime)),
+ rightCell(info.p50Time >= 0 ?
String.valueOf(info.p50Time) : "", 6),
+ rightCell(info.p95Time >= 0 ?
String.valueOf(info.p95Time) : "", 6),
+ rightCell(info.p99Time >= 0 ?
String.valueOf(info.p99Time) : "", 6),
+ rightCell(String.valueOf(info.exchangesTotal), 8),
+ rightCell(String.valueOf(info.failed), 6,
failStyle),
+ rightCell(String.valueOf(info.inflight), 8),
+ rightCell(throughputDisplay != null ?
throughputDisplay : "", 8),
+ rightCell(TuiHelper.formatLoad(
+ info.inflightLoad01, info.inflightLoad05,
info.inflightLoad15), 12))
+ .style(rowBg));
}
- Style statusStyle = switch (stateText) {
- case "Started", "Running" -> Theme.success();
- case "Stopped" -> Theme.error();
- default -> Theme.warning();
- };
+ }
- Style failStyle = info.failed > 0 ? Theme.error().bold() :
Style.EMPTY;
+ header = Row.from(
+ Cell.from(Span.styled("NAME", Style.EMPTY.bold())),
+ rightCell(topSortLabel("MEAN", "mean"), 6,
topSortStyle("mean")),
+ rightCell(topSortLabel("MAX", "max"), 6,
topSortStyle("max")),
+ rightCell(topSortLabel("MIN", "min"), 6,
topSortStyle("min")),
+ rightCell(topSortLabel("LAST", "last"), 6,
topSortStyle("last")),
+ rightCell(topSortLabel("DELTA", "delta"), 6,
topSortStyle("delta")),
+ rightCell(topSortLabel("P50", "p50"), 6,
topSortStyle("p50")),
+ rightCell(topSortLabel("P95", "p95"), 6,
topSortStyle("p95")),
+ rightCell(topSortLabel("P99", "p99"), 6,
topSortStyle("p99")),
+ rightCell("TOTAL", 8, Style.EMPTY.bold()),
+ rightCell("FAIL", 6, Style.EMPTY.bold()),
+ rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
+ rightCell("MSG/S", 8, Style.EMPTY.bold()),
+ rightCell("LOAD", 12, Style.EMPTY.bold()));
+
+ widths = new Constraint[] {
+ Constraint.fill(),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(8),
+ Constraint.length(6),
+ Constraint.length(8),
+ Constraint.length(8),
+ Constraint.length(13)
+ };
+ } else {
+ boolean hasPercentiles = infos.stream().anyMatch(i -> i.p50Time >=
0);
+ for (IntegrationInfo info : infos) {
+ boolean isEven = (rowIndex++ % 2 == 0);
+ Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) :
Style.EMPTY;
+
+ if (info.vanishing) {
+ long elapsed = System.currentTimeMillis() -
info.vanishStart;
+ float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
+ int gray = (int) (100 * fade);
+ Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
+
+ String vanishName = TuiIcons.labeled(TuiIcons.CAMEL,
info.name != null ? info.name : "");
+ rows.add(Row.from(
+ Cell.from(Span.styled(info.pid, dimStyle)),
+ Cell.from(Span.styled(vanishName, dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled(TuiIcons.STOPPED + "
Stopped", Theme.error().dim())),
+ Cell.from(Span.styled(info.ago != null ? info.ago
: "", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("", dimStyle)),
+ Cell.from(Span.styled("",
dimStyle))).style(rowBg));
+ } else {
+ String stateText = extractState(info.state);
+ if (stoppingPids.contains(info.pid) ||
"Terminating".equals(stateText)) {
+ stateText = "Stopping";
+ } else if ("Terminated".equals(stateText)) {
+ stateText = "Stopped";
+ } else if ("Running".equals(stateText) &&
info.routeStarted == 0 && info.routeTotal > 0) {
+ stateText = "Stopped";
+ }
+ Style statusStyle = switch (stateText) {
+ case "Started", "Running" -> Theme.success();
+ case "Stopped" -> Theme.error();
+ default -> Theme.warning();
+ };
- String sinceLastDisplay = formatSinceLast(info);
+ Style failStyle = info.failed > 0 ? Theme.error().bold() :
Style.EMPTY;
- boolean hasDoc = info.readmeFiles != null &&
!info.readmeFiles.isEmpty();
- if (!hasDoc) {
- hasDoc = hasReadmeInSourceDir(info);
- }
- String platformIcon = TuiIcons.runtimeIcon(info.platform !=
null ? info.platform : "");
- String nameText = platformIcon + " " + (info.name != null ?
info.name : "");
- List<Span> nameSpans = new ArrayList<>();
- nameSpans.add(Span.styled(nameText, Theme.info()));
- if (info.devMode) {
- nameSpans.add(Span.styled(" [dev]", Theme.label()));
- }
- if (hasDoc) {
- nameSpans.add(Span.styled(" " + TuiIcons.README,
Style.EMPTY));
- }
- Line nameLine = Line.from(nameSpans);
- String throughputDisplay = info.throughput;
- if (throughputDisplay == null ||
"0.00".equals(throughputDisplay)) {
- LinkedList<Long> tpHist = throughputHistory.get(info.pid);
- if (tpHist != null && !tpHist.isEmpty()) {
- long tp = tpHist.getLast();
- if (tp > 0) {
- throughputDisplay =
String.format(java.util.Locale.US, "%.2f", (double) tp);
+ String sinceLastDisplay = formatSinceLast(info);
+
+ boolean hasDoc = info.readmeFiles != null &&
!info.readmeFiles.isEmpty();
+ if (!hasDoc) {
+ hasDoc = hasReadmeInSourceDir(info);
+ }
+ String platformIcon = TuiIcons.runtimeIcon(info.platform
!= null ? info.platform : "");
+ String nameText = platformIcon + " " + (info.name != null
? info.name : "");
+ List<Span> nameSpans = new ArrayList<>();
+ nameSpans.add(Span.styled(nameText, Theme.info()));
+ if (info.devMode) {
+ nameSpans.add(Span.styled(" [dev]", Theme.label()));
+ }
+ if (hasDoc) {
+ nameSpans.add(Span.styled(" " + TuiIcons.README,
Style.EMPTY));
+ }
+ Line nameLine = Line.from(nameSpans);
+ String throughputDisplay = info.throughput;
+ if (throughputDisplay == null ||
"0.00".equals(throughputDisplay)) {
+ LinkedList<Long> tpHist =
throughputHistory.get(info.pid);
+ if (tpHist != null && !tpHist.isEmpty()) {
+ long tp = tpHist.getLast();
+ if (tp > 0) {
+ throughputDisplay =
String.format(java.util.Locale.US, "%.2f", (double) tp);
+ }
}
}
+ String timingCol;
+ if (hasPercentiles && info.p50Time >= 0) {
+ timingCol = info.p50Time + "/" + info.p95Time + "/" +
info.p99Time;
+ } else if (info.exchangesTotal > 0) {
+ timingCol = info.minTime + "/" + info.maxTime + "/" +
info.meanTime;
+ } else {
+ timingCol = "";
+ }
+ rows.add(Row.from(
+ Cell.from(info.pid),
+ Cell.from(nameLine),
+ Cell.from(info.camelVersion != null ?
info.camelVersion : ""),
+ centerCell(info.ready != null ? info.ready : "",
5),
+ Cell.from(Span.styled(stateText, statusStyle)),
+ Cell.from(info.ago != null ? info.ago : ""),
+ rightCell(info.routeStarted + "/" +
info.routeTotal, 7),
+ rightCell(throughputDisplay != null ?
throughputDisplay : "", 8),
+ rightCell(String.valueOf(info.exchangesTotal), 8),
+ rightCell(String.valueOf(info.failed), 6,
failStyle),
+ rightCell(String.valueOf(info.inflight), 8),
+ rightCell(timingCol, 14),
+ Cell.from(sinceLastDisplay)).style(rowBg));
}
- rows.add(Row.from(
- Cell.from(info.pid),
- Cell.from(nameLine),
- Cell.from(info.camelVersion != null ?
info.camelVersion : ""),
- centerCell(info.ready != null ? info.ready : "", 5),
- Cell.from(Span.styled(stateText, statusStyle)),
- Cell.from(info.ago != null ? info.ago : ""),
- rightCell(info.routeStarted + "/" + info.routeTotal,
7),
- rightCell(throughputDisplay != null ?
throughputDisplay : "", 8),
- rightCell(String.valueOf(info.exchangesTotal), 8),
- rightCell(String.valueOf(info.failed), 6, failStyle),
- rightCell(String.valueOf(info.inflight), 8),
- Cell.from(sinceLastDisplay)).style(rowBg));
}
- }
- Row header = Row.from(
- Cell.from(Span.styled(sortLabel("PID", "pid"),
sortStyle("pid"))),
- Cell.from(Span.styled(sortLabel("NAME", "name"),
sortStyle("name"))),
- Cell.from(Span.styled(sortLabel("VERSION", "version"),
sortStyle("version"))),
- centerCell("READY", 5, Style.EMPTY.bold()),
- Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
- Cell.from(Span.styled("AGE", Style.EMPTY.bold())),
- rightCell("ROUTE", 7, Style.EMPTY.bold()),
- rightCell("MSG/S", 8, Style.EMPTY.bold()),
- rightCell(sortLabel("TOTAL", "total"), 8, sortStyle("total")),
- rightCell(sortLabel("FAIL", "fail"), 6, sortStyle("fail")),
- rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
- Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())));
+ String timingHeader = hasPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
+ header = Row.from(
+ Cell.from(Span.styled(sortLabel("PID", "pid"),
sortStyle("pid"))),
+ Cell.from(Span.styled(sortLabel("NAME", "name"),
sortStyle("name"))),
+ Cell.from(Span.styled(sortLabel("VERSION", "version"),
sortStyle("version"))),
+ centerCell("READY", 5, Style.EMPTY.bold()),
+ Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
+ Cell.from(Span.styled("AGE", Style.EMPTY.bold())),
+ rightCell("ROUTE", 7, Style.EMPTY.bold()),
+ rightCell("MSG/S", 8, Style.EMPTY.bold()),
+ rightCell(sortLabel("TOTAL", "total"), 8,
sortStyle("total")),
+ rightCell(sortLabel("FAIL", "fail"), 6, sortStyle("fail")),
+ rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
+ rightCell(timingHeader, 14, Style.EMPTY.bold()),
+ Cell.from(Span.styled("SINCE-LAST", Style.EMPTY.bold())));
+
+ widths = new Constraint[] {
+ Constraint.length(8),
+ Constraint.fill(),
+ Constraint.length(16),
+ Constraint.length(5),
+ Constraint.length(10),
+ Constraint.length(8),
+ Constraint.length(7),
+ Constraint.length(8),
+ Constraint.length(8),
+ Constraint.length(6),
+ Constraint.length(8),
+ Constraint.length(14),
+ Constraint.length(13)
+ };
+ }
Table.Builder tableBuilder = Table.builder()
.rows(rows)
.header(header)
- .widths(
- Constraint.length(8),
- Constraint.fill(),
- Constraint.length(16),
- Constraint.length(5),
- Constraint.length(10),
- Constraint.length(8),
- Constraint.length(7),
- Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
- Constraint.length(13))
+ .widths(widths)
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(infraCount > 0 ? " Integrations " : " Overview
").build());
@@ -873,6 +1011,7 @@ class OverviewTab extends AbstractTab {
}
if (!ctx.isInfraSelected()) {
hint(spans, "s", "sort");
+ hint(spans, "t", topMode ? "top [on]" : "top [off]");
int effectiveMode = (chartMode == CHART_SINGLE && ctx.selectedPid
== null) ? CHART_ALL : chartMode;
hint(spans, "a", "chart " + switch (effectiveMode) {
case CHART_ALL -> "[all]";
@@ -904,7 +1043,7 @@ class OverviewTab extends AbstractTab {
List<IntegrationInfo> sortedInfos() {
List<IntegrationInfo> infos = new ArrayList<>(ctx.data.get());
- infos.sort(this::sortCompare);
+ infos.sort(topMode ? this::topSortCompare : this::sortCompare);
return infos;
}
@@ -988,6 +1127,32 @@ class OverviewTab extends AbstractTab {
return sortReversed ? -result : result;
}
+ private int topSortCompare(IntegrationInfo a, IntegrationInfo b) {
+ if (a.vanishing != b.vanishing) {
+ return a.vanishing ? 1 : -1;
+ }
+ int result = switch (topSort) {
+ case "mean" -> Long.compare(b.meanTime, a.meanTime);
+ case "max" -> Long.compare(b.maxTime, a.maxTime);
+ case "min" -> Long.compare(b.minTime, a.minTime);
+ case "last" -> Long.compare(b.lastTime, a.lastTime);
+ case "delta" -> Long.compare(Math.abs(b.deltaTime),
Math.abs(a.deltaTime));
+ case "p50" -> Long.compare(b.p50Time, a.p50Time);
+ case "p95" -> Long.compare(b.p95Time, a.p95Time);
+ case "p99" -> Long.compare(b.p99Time, a.p99Time);
+ default -> 0;
+ };
+ return topSortReversed ? -result : result;
+ }
+
+ private String topSortLabel(String label, String column) {
+ return sortLabel(label, column, topSort, topSortReversed);
+ }
+
+ private Style topSortStyle(String column) {
+ return sortStyle(column, topSort);
+ }
+
private int infraSortCompare(InfraInfo a, InfraInfo b) {
if (a.vanishing != b.vanishing) {
return a.vanishing ? 1 : -1;
@@ -1076,12 +1241,30 @@ class OverviewTab extends AbstractTab {
- **INFLIGHT** — Exchanges currently being processed right
now. A consistently high inflight count may indicate slow downstream services
- **SINCE-LAST** — Time since the last exchange activity,
shown as up to three values separated by `/`: started/completed/failed. For
example, `1s/3s/1m14s` means the last exchange started 1s ago, the last
completed 3s ago, and the last failure was 1m14s ago. Values are omitted when
there is no activity of that type
+ ## Percentile Latency (P50/P95/P99)
+
+ When **Extended** statistics level is enabled, a timing column
shows
+ percentile latencies instead of MIN/MAX/MEAN:
+
+ - **P50** — Median processing time (50th percentile). Half of
all exchanges completed faster than this
+ - **P95** — 95th percentile. 95% of exchanges completed faster
than this. Useful for SLA monitoring
+ - **P99** — 99th percentile. Only 1% of exchanges were slower.
Highlights worst-case tail latency
+
+ Percentiles are computed over a sliding window of recent
exchanges, making
+ them more meaningful than MIN/MAX for understanding real-world
performance.
+ With very few messages (e.g., 10), P95 and P99 may equal the
MAX value since
+ there aren't enough samples to differentiate.
+
+ To enable Extended statistics, set
`camel.main.load-statistics-enabled = true`
+ in your application configuration. Without Extended
statistics, the column
+ shows MIN/MAX/MEAN instead.
+
## Example Screen
```
- PID NAME VERSION STATUS AGE MSG/S TOTAL
FAIL INFLIGHT SINCE-LAST
- 73136 camel-demo 4.21.0 Running 2m30s 1.00 142 0
0 0s
- 64628 my-routes 4.21.0 Running 1h15m 0.50 2850 3
1 2s
+ PID NAME VERSION STATUS AGE MSG/S TOTAL
FAIL INFLIGHT P50/P95/P99 SINCE-LAST
+ 73136 camel-demo 4.21.0 Running 2m30s 1.00 142 0
0 1/10/31 0s
+ 64628 my-routes 4.21.0 Running 1h15m 0.50 2850 3
1 2/15/42 2s
```
## Sparkline Chart
@@ -1176,6 +1359,16 @@ class OverviewTab extends AbstractTab {
row.put("throughput", info.throughput);
row.put("routeStarted", info.routeStarted);
row.put("routeTotal", info.routeTotal);
+ row.put("meanTime", info.meanTime);
+ row.put("maxTime", info.maxTime);
+ row.put("minTime", info.minTime);
+ row.put("lastTime", info.lastTime);
+ row.put("deltaTime", info.deltaTime);
+ if (info.p50Time >= 0) {
+ row.put("p50Time", info.p50Time);
+ row.put("p95Time", info.p95Time);
+ row.put("p99Time", info.p99Time);
+ }
rows.add(row);
}
result.put("rows", rows);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
index c9c97436cec0..d122004668c5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
@@ -27,6 +27,9 @@ class ProcessorInfo {
long maxTime;
long lastTime;
long deltaTime;
+ long p50Time = -1;
+ long p95Time = -1;
+ long p99Time = -1;
long inflight;
String sinceLastStarted;
String sinceLastCompleted;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
index 8f309088d39a..f8513a655ca4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
@@ -37,6 +37,9 @@ class RouteInfo {
long maxTime;
long lastTime;
long deltaTime;
+ long p50Time = -1;
+ long p95Time = -1;
+ long p99Time = -1;
String load01;
String load05;
String load15;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 85ffdba6bc77..5fedcb7452cb 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -53,7 +53,7 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
class RoutesTab extends AbstractTab {
private static final String[] ROUTE_SORT_COLUMNS = { "name", "from",
"status", "total", "failed" };
- private static final String[] ROUTE_TOP_SORT_COLUMNS = { "mean", "max",
"min", "last", "delta" };
+ private static final String[] ROUTE_TOP_SORT_COLUMNS = { "mean", "max",
"min", "last", "delta", "p50", "p95", "p99" };
// Route sort state
private String routeSort = "name";
@@ -90,10 +90,6 @@ class RoutesTab extends AbstractTab {
super(ctx);
}
- boolean isTopMode() {
- return routeTopMode;
- }
-
boolean isShowDiagram() {
return diagram.isShowDiagram();
}
@@ -577,6 +573,9 @@ class RoutesTab extends AbstractTab {
rightCell(route.total > 0 ?
String.valueOf(route.lastTime) : "", 6),
rightCell(route.deltaTime != 0 ?
String.valueOf(route.deltaTime) : "", 6,
topDeltaStyle(route.deltaTime)),
+ rightCell(route.p50Time >= 0 ?
String.valueOf(route.p50Time) : "", 6),
+ rightCell(route.p95Time >= 0 ?
String.valueOf(route.p95Time) : "", 6),
+ rightCell(route.p99Time >= 0 ?
String.valueOf(route.p99Time) : "", 6),
rightCell(String.valueOf(route.total), 8),
rightCell(String.valueOf(route.failed), 6, failStyle),
rightCell(String.valueOf(route.inflight), 8),
@@ -584,6 +583,29 @@ class RoutesTab extends AbstractTab {
rightCell(formatLoad(route.load01, route.load05,
route.load15), 12)));
}
+ IntegrationInfo selTop = ctx.findSelectedIntegration();
+ if (selTop != null && selTop.exchangesTotal > 0) {
+ Style ts = Theme.label();
+ routeRows.add(Row.from(
+ Cell.from(Span.styled("TOTAL", ts)),
+ Cell.from(""),
+ rightCell(String.valueOf(selTop.meanTime), 6, ts),
+ rightCell(String.valueOf(selTop.maxTime), 6, ts),
+ rightCell(String.valueOf(selTop.minTime), 6, ts),
+ rightCell(String.valueOf(selTop.lastTime), 6, ts),
+ rightCell(selTop.deltaTime != 0 ?
String.valueOf(selTop.deltaTime) : "", 6, ts),
+ rightCell(selTop.p50Time >= 0 ?
String.valueOf(selTop.p50Time) : "", 6, ts),
+ rightCell(selTop.p95Time >= 0 ?
String.valueOf(selTop.p95Time) : "", 6, ts),
+ rightCell(selTop.p99Time >= 0 ?
String.valueOf(selTop.p99Time) : "", 6, ts),
+ rightCell(String.valueOf(selTop.exchangesTotal), 8,
ts),
+ rightCell(String.valueOf(selTop.failed), 6,
+ selTop.failed > 0 ? Theme.error().bold() : ts),
+ rightCell(String.valueOf(selTop.inflight), 8, ts),
+ rightCell(selTop.throughput != null ?
selTop.throughput : "", 8, ts),
+ rightCell(TuiHelper.formatLoad(
+ selTop.inflightLoad01, selTop.inflightLoad05,
selTop.inflightLoad15), 12, ts)));
+ }
+
routeTable = Table.builder()
.rows(routeRows)
.header(Row.from(
@@ -594,6 +616,9 @@ class RoutesTab extends AbstractTab {
rightCell(routeTopSortLabel("MIN", "min"), 6,
routeTopSortStyle("min")),
rightCell(routeTopSortLabel("LAST", "last"), 6,
routeTopSortStyle("last")),
rightCell(routeTopSortLabel("DELTA", "delta"), 6,
routeTopSortStyle("delta")),
+ rightCell(routeTopSortLabel("P50", "p50"), 6,
routeTopSortStyle("p50")),
+ rightCell(routeTopSortLabel("P95", "p95"), 6,
routeTopSortStyle("p95")),
+ rightCell(routeTopSortLabel("P99", "p99"), 6,
routeTopSortStyle("p99")),
rightCell("TOTAL", 8, Style.EMPTY.bold()),
rightCell("FAIL", 6, Style.EMPTY.bold()),
rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
@@ -607,6 +632,9 @@ class RoutesTab extends AbstractTab {
Constraint.length(6),
Constraint.length(6),
Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
Constraint.length(8),
Constraint.length(6),
Constraint.length(8),
@@ -615,9 +643,11 @@ class RoutesTab extends AbstractTab {
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Top Routes ").build())
+ .title(" Routes ").build())
.build();
} else {
+ boolean hasPercentiles = sortedRoutes.stream().anyMatch(r ->
r.p50Time >= 0);
+
List<Row> routeRows = new ArrayList<>();
for (RouteInfo route : sortedRoutes) {
Style stateStyle = "Started".equals(route.state)
@@ -630,6 +660,15 @@ class RoutesTab extends AbstractTab {
String sinceLastRoute = formatSinceLastRoute(route);
+ String timingCol;
+ if (hasPercentiles && route.p50Time >= 0) {
+ timingCol = route.p50Time + "/" + route.p95Time + "/" +
route.p99Time;
+ } else if (route.total > 0) {
+ timingCol = route.minTime + "/" + route.maxTime + "/" +
route.meanTime;
+ } else {
+ timingCol = "";
+ }
+
routeRows.add(Row.from(
Cell.from(Span.styled(route.routeId != null ?
route.routeId : "", Style.EMPTY.fg(Theme.accent()))),
Cell.from(routeFromLabel(route)),
@@ -640,12 +679,36 @@ class RoutesTab extends AbstractTab {
rightCell(String.valueOf(route.total), 8),
rightCell(String.valueOf(route.failed), 6, failStyle),
rightCell(String.valueOf(route.inflight), 8),
- rightCell(route.total > 0
- ? route.minTime + "/" + route.maxTime + "/" +
route.meanTime
- : "", 14),
+ rightCell(timingCol, 14),
Cell.from(sinceLastRoute)));
}
+ IntegrationInfo selDef = ctx.findSelectedIntegration();
+ if (selDef != null && selDef.exchangesTotal > 0) {
+ Style ts = Theme.label();
+ String totalTimingCol;
+ if (hasPercentiles && selDef.p50Time >= 0) {
+ totalTimingCol = selDef.p50Time + "/" + selDef.p95Time +
"/" + selDef.p99Time;
+ } else {
+ totalTimingCol = selDef.minTime + "/" + selDef.maxTime +
"/" + selDef.meanTime;
+ }
+ routeRows.add(Row.from(
+ Cell.from(Span.styled("TOTAL", ts)),
+ Cell.from(""),
+ Cell.from(""),
+ Cell.from(""),
+ Cell.from(""),
+ rightCell(selDef.throughput != null ?
selDef.throughput : "", 8, ts),
+ rightCell(String.valueOf(selDef.exchangesTotal), 8,
ts),
+ rightCell(String.valueOf(selDef.failed), 6,
+ selDef.failed > 0 ? Theme.error().bold() : ts),
+ rightCell(String.valueOf(selDef.inflight), 8, ts),
+ rightCell(totalTimingCol, 14, ts),
+ Cell.from("")));
+ }
+
+ String timingHeader = hasPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
+
routeTable = Table.builder()
.rows(routeRows)
.header(Row.from(
@@ -658,7 +721,7 @@ class RoutesTab extends AbstractTab {
rightCell(routeSortLabel("TOTAL", "total"), 8,
routeSortStyle("total")),
rightCell(routeSortLabel("FAIL", "failed"), 6,
routeSortStyle("failed")),
rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
- rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()),
+ rightCell(timingHeader, 14, Style.EMPTY.bold()),
Cell.from(Span.styled("SINCE-LAST",
Style.EMPTY.bold()))))
.widths(
Constraint.length(24),
@@ -885,6 +948,19 @@ class RoutesTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" Min: ", Theme.muted()),
Span.raw(String.format("%" + tw + "d ms",
route.minTime))));
+ if (route.p50Time >= 0) {
+ int pw = numWidth(route.p50Time, route.p95Time,
route.p99Time);
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" p50: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
route.p50Time))));
+ lines.add(Line.from(
+ Span.styled(" p95: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
route.p95Time))));
+ lines.add(Line.from(
+ Span.styled(" p99: ", Theme.muted()),
+ Span.raw(String.format("%" + pw + "d ms",
route.p99Time))));
+ }
}
if (route.sinceLastCompleted != null || route.sinceLastFailed !=
null) {
@@ -1025,6 +1101,20 @@ class RoutesTab extends AbstractTab {
lines.add(Line.from(
Span.styled(" Last: ", Style.EMPTY.dim()),
Span.raw(String.format("%" + tw + "d ms",
stat.lastProcessingTime))));
+ if (stat.p50ProcessingTime >= 0) {
+ int pw = numWidth(stat.p50ProcessingTime,
stat.p95ProcessingTime,
+ stat.p99ProcessingTime);
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" p50: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + pw + "d ms",
stat.p50ProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" p95: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + pw + "d ms",
stat.p95ProcessingTime))));
+ lines.add(Line.from(
+ Span.styled(" p99: ", Style.EMPTY.dim()),
+ Span.raw(String.format("%" + pw + "d ms",
stat.p99ProcessingTime))));
+ }
if (stat.lastCompletedExchangeTimestamp > 0 ||
stat.lastFailedExchangeTimestamp > 0) {
long now = System.currentTimeMillis();
@@ -1118,6 +1208,9 @@ class RoutesTab extends AbstractTab {
rightCell(proc.total > 0 ?
String.valueOf(proc.lastTime) : "", 6),
rightCell(proc.deltaTime != 0 ?
String.valueOf(proc.deltaTime) : "", 6,
topDeltaStyle(proc.deltaTime)),
+ rightCell(proc.p50Time >= 0 ?
String.valueOf(proc.p50Time) : "", 6),
+ rightCell(proc.p95Time >= 0 ?
String.valueOf(proc.p95Time) : "", 6),
+ rightCell(proc.p99Time >= 0 ?
String.valueOf(proc.p99Time) : "", 6),
rightCell(String.valueOf(proc.total), 8),
rightCell(String.valueOf(proc.failed), 6,
proc.failed > 0 ? Theme.error() : Style.EMPTY),
@@ -1135,6 +1228,9 @@ class RoutesTab extends AbstractTab {
rightCell(routeTopSortLabel("MIN", "min"), 6,
routeTopSortStyle("min")),
rightCell(routeTopSortLabel("LAST", "last"), 6,
routeTopSortStyle("last")),
rightCell(routeTopSortLabel("DELTA", "delta"), 6,
routeTopSortStyle("delta")),
+ rightCell(routeTopSortLabel("P50", "p50"), 6,
routeTopSortStyle("p50")),
+ rightCell(routeTopSortLabel("P95", "p95"), 6,
routeTopSortStyle("p95")),
+ rightCell(routeTopSortLabel("P99", "p99"), 6,
routeTopSortStyle("p99")),
rightCell("TOTAL", 8, Style.EMPTY.bold()),
rightCell("FAIL", 6, Style.EMPTY.bold()),
rightCell("INFLIGHT", 8, Style.EMPTY.bold())))
@@ -1147,17 +1243,31 @@ class RoutesTab extends AbstractTab {
Constraint.length(6),
Constraint.length(6),
Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
+ Constraint.length(6),
Constraint.length(8),
Constraint.length(6),
Constraint.length(9))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Top Processors [" + route.routeId + "]
").build())
+ .title(" Processors [" + route.routeId + "]
").build())
.build();
} else {
+ boolean hasProcPercentiles = route.p50Time >= 0
+ || route.processors.stream().anyMatch(p -> p.p50Time >= 0);
+
List<Row> rows = new ArrayList<>();
// Synthetic top row representing the route itself
Style routeStyle = route.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String routeTimingCol;
+ if (hasProcPercentiles && route.p50Time >= 0) {
+ routeTimingCol = route.p50Time + "/" + route.p95Time + "/" +
route.p99Time;
+ } else if (route.total > 0) {
+ routeTimingCol = route.minTime + "/" + route.maxTime + "/" +
route.meanTime;
+ } else {
+ routeTimingCol = "";
+ }
rows.add(Row.from(
Cell.from(" route"),
Cell.from(Span.styled(route.from != null ? route.from :
route.routeId, routeStyle)),
@@ -1166,15 +1276,22 @@ class RoutesTab extends AbstractTab {
rightCell(String.valueOf(route.failed), 6,
route.failed > 0 ? Theme.error() : Style.EMPTY),
rightCell(String.valueOf(route.inflight), 8),
- rightCell(route.total > 0
- ? route.minTime + "/" + route.maxTime + "/" +
route.meanTime
- : "", 14),
+ rightCell(routeTimingCol, 14),
Cell.from("")));
for (ProcessorInfo proc : route.processors) {
String indent = " ".repeat(proc.level);
Style nameStyle = proc.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String procTimingCol;
+ if (hasProcPercentiles && proc.p50Time >= 0) {
+ procTimingCol = proc.p50Time + "/" + proc.p95Time + "/" +
proc.p99Time;
+ } else if (proc.total > 0) {
+ procTimingCol = proc.minTime + "/" + proc.maxTime + "/" +
proc.meanTime;
+ } else {
+ procTimingCol = "";
+ }
+
rows.add(Row.from(
Cell.from(" " + (proc.processor != null ?
proc.processor : "")),
Cell.from(Span.styled(indent + (proc.id != null ?
proc.id : ""), nameStyle)),
@@ -1183,11 +1300,10 @@ class RoutesTab extends AbstractTab {
rightCell(String.valueOf(proc.failed), 6,
proc.failed > 0 ? Theme.error() : Style.EMPTY),
rightCell(String.valueOf(proc.inflight), 8),
- rightCell(proc.total > 0
- ? proc.minTime + "/" + proc.maxTime + "/" +
proc.meanTime
- : "", 14),
+ rightCell(procTimingCol, 14),
Cell.from("")));
}
+ String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" :
"MIN/MAX/MEAN";
table = Table.builder()
.rows(rows)
@@ -1198,7 +1314,7 @@ class RoutesTab extends AbstractTab {
rightCell("TOTAL", 8, Style.EMPTY.bold()),
rightCell("FAIL", 6, Style.EMPTY.bold()),
rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
- rightCell("MIN/MAX/MEAN", 14, Style.EMPTY.bold()),
+ rightCell(procTimingHeader, 14,
Style.EMPTY.bold()),
Cell.from("")))
.widths(
Constraint.length(20),
@@ -1256,6 +1372,9 @@ class RoutesTab extends AbstractTab {
case "min" -> Long.compare(b.minTime, a.minTime);
case "last" -> Long.compare(b.lastTime, a.lastTime);
case "delta" -> Long.compare(b.deltaTime, a.deltaTime);
+ case "p50" -> Long.compare(b.p50Time, a.p50Time);
+ case "p95" -> Long.compare(b.p95Time, a.p95Time);
+ case "p99" -> Long.compare(b.p99Time, a.p99Time);
default -> 0;
};
return routeTopSortReversed ? -result : result;
@@ -1268,6 +1387,9 @@ class RoutesTab extends AbstractTab {
case "min" -> Long.compare(b.minTime, a.minTime);
case "last" -> Long.compare(b.lastTime, a.lastTime);
case "delta" -> Long.compare(b.deltaTime, a.deltaTime);
+ case "p50" -> Long.compare(b.p50Time, a.p50Time);
+ case "p95" -> Long.compare(b.p95Time, a.p95Time);
+ case "p99" -> Long.compare(b.p99Time, a.p99Time);
default -> 0;
};
return routeTopSortReversed ? -result : result;
@@ -1280,6 +1402,9 @@ class RoutesTab extends AbstractTab {
case "min" -> proc.minTime;
case "last" -> proc.lastTime;
case "delta" -> Math.abs(proc.deltaTime);
+ case "p50" -> proc.p50Time;
+ case "p95" -> proc.p95Time;
+ case "p99" -> proc.p99Time;
default -> proc.meanTime;
};
}
@@ -1590,13 +1715,33 @@ class RoutesTab extends AbstractTab {
- **MAX** — Slowest exchange processing time in milliseconds.
A very high MAX compared to MEAN suggests occasional slow outliers
- **SINCE-LAST** — Time since the last exchange activity on
this route, shown as up to three values separated by `/`:
started/completed/failed (e.g., `1s/3s/1m14s`). Values are omitted when there
is no activity of that type
+ ## Percentile Latency (P50/P95/P99)
+
+ When **Extended** statistics level is enabled, the timing
columns show
+ percentile latencies instead of MIN/MEAN/MAX — both for routes
and processors:
+
+ - **P50** — Median processing time (50th percentile). Half of
all exchanges completed faster than this
+ - **P95** — 95th percentile. 95% of exchanges completed faster
than this. Useful for SLA monitoring
+ - **P99** — 99th percentile. Only 1% of exchanges were slower.
Highlights worst-case tail latency
+
+ Percentiles are computed over a sliding window of recent
exchanges, making
+ them more meaningful than MIN/MAX for understanding real-world
performance.
+ With very few messages (e.g., 10), P95 and P99 may equal the
MAX value since
+ there aren't enough samples to differentiate.
+
+ To enable Extended statistics, set
`camel.main.load-statistics-enabled = true`
+ in your application configuration. Without Extended
statistics, the columns
+ show MIN/MEAN/MAX instead.
+
+ The TOTAL summary row shows the overall percentiles across all
routes.
+
## Example Screen
```
- ROUTE FROM STATUS COVER MSG/S
TOTAL FAIL INFLIGHT MIN MEAN MAX SINCE-LAST
- timer-to-log timer://hello?p=2000 Started 5/5 0.50
142 0 0 0 0 2 1s
- timer-to-seda timer://pump?p=3000 Started 2/2 0.33
95 0 0 0 0 2 2s
- seda-consumer seda://queue Started 1/1 0.33
95 0 0 0 0 0 2s
+ ROUTE FROM STATUS COVER MSG/S
TOTAL FAIL INFLIGHT P50/P95/P99 SINCE-LAST
+ timer-to-log timer://hello?p=2000 Started 5/5 0.50
142 0 0 1/10/31 1s
+ timer-to-seda timer://pump?p=3000 Started 2/2 0.33
95 0 0 0/1/2 2s
+ seda-consumer seda://queue Started 1/1 0.33
95 0 0 0/0/1 2s
```
## Top Mode
@@ -1726,6 +1871,11 @@ class RoutesTab extends AbstractTab {
row.put("min", ri.minTime);
row.put("last", ri.lastTime);
row.put("throughput", ri.throughput);
+ if (ri.p50Time >= 0) {
+ row.put("p50", ri.p50Time);
+ row.put("p95", ri.p95Time);
+ row.put("p99", ri.p99Time);
+ }
if (ri.group != null) {
row.put("group", ri.group);
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index 4b911df11bb2..a25102a2563a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -98,8 +98,16 @@ final class StatusParser {
info.inflightLoad01 = objToString(stats.get("load01"));
info.inflightLoad05 = objToString(stats.get("load05"));
info.inflightLoad15 = objToString(stats.get("load15"));
+ info.meanTime = Math.max(0,
objToLong(stats.get("meanProcessingTime")));
+ info.maxTime = Math.max(0,
objToLong(stats.get("maxProcessingTime")));
+ info.minTime = Math.max(0,
objToLong(stats.get("minProcessingTime")));
+ info.p50Time = objToLong(stats.get("p50ProcessingTime"));
+ info.p95Time = objToLong(stats.get("p95ProcessingTime"));
+ info.p99Time = objToLong(stats.get("p99ProcessingTime"));
info.last = objToString(stats.get("lastProcessingTime"));
+ info.lastTime = Math.max(0,
objToLong(stats.get("lastProcessingTime")));
info.delta = objToString(stats.get("deltaProcessingTime"));
+ info.deltaTime = objToLong(stats.get("deltaProcessingTime"));
long tsStarted =
objToLong(stats.get("lastCreatedExchangeTimestamp"));
if (tsStarted > 0) {
info.sinceLastStarted = TimeUtils.printSince(tsStarted);
@@ -185,6 +193,9 @@ final class StatusParser {
ri.maxTime = Math.max(0,
objToLong(rs.get("maxProcessingTime")));
ri.lastTime = Math.max(0,
objToLong(rs.get("lastProcessingTime")));
ri.deltaTime = objToLong(rs.get("deltaProcessingTime"));
+ ri.p50Time = objToLong(rs.get("p50ProcessingTime"));
+ ri.p95Time = objToLong(rs.get("p95ProcessingTime"));
+ ri.p99Time = objToLong(rs.get("p99ProcessingTime"));
ri.load01 = objToString(rs.get("load01"));
ri.load05 = objToString(rs.get("load05"));
ri.load15 = objToString(rs.get("load15"));
@@ -221,6 +232,9 @@ final class StatusParser {
pi.maxTime = Math.max(0,
objToLong(ps.get("maxProcessingTime")));
pi.lastTime =
objToLong(ps.get("lastProcessingTime"));
pi.deltaTime =
objToLong(ps.get("deltaProcessingTime"));
+ pi.p50Time =
objToLong(ps.get("p50ProcessingTime"));
+ pi.p95Time =
objToLong(ps.get("p95ProcessingTime"));
+ pi.p99Time =
objToLong(ps.get("p99ProcessingTime"));
pi.inflight =
objToLong(ps.get("exchangesInflight"));
long tsStarted =
objToLong(ps.get("lastCreatedExchangeTimestamp"));
if (tsStarted > 0) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
index 5883842921f0..bab0aed3b429 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
@@ -205,7 +205,6 @@ class RoutesTabRenderTest {
// Press 't' to toggle top mode
tab.handleKeyEvent(KeyEvent.ofChar('t', KeyModifiers.NONE));
- assertTrue(tab.isTopMode(), "Should be in top mode after pressing
't'");
String topRender = renderToString(tab, 140, 30);
assertTrue(topRender.contains("MEAN"), "Top mode should show MEAN
header");