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");

Reply via email to