You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/07/01 14:29:34 UTC
(camel) 01/11: CAMEL-23870: Add HeapHistogram dev console and TUI panel
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch feature/CAMEL-23870-heap-histogram
in repository https://gitbox.apache.org/repos/asf/camel.git
commit ea88102e3928620171e06be34d6d840ccd597ec5
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:01:16 2026 +0200
CAMEL-23870: Add HeapHistogram dev console and TUI panel
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../console/HeapHistogramDevConsoleConfigurer.java | 63 +++
.../apache/camel/dev-console/heap-histogram.json | 15 +
...ache.camel.impl.console.HeapHistogramDevConsole | 2 +
.../org/apache/camel/dev-console/heap-histogram | 2 +
.../org/apache/camel/dev-consoles.properties | 2 +-
.../impl/console/HeapHistogramDevConsole.java | 178 ++++++++
.../camel/cli/connector/LocalCliConnector.java | 14 +
.../jbang/core/commands/tui/HeapHistogramTab.java | 471 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/McpFacade.java | 6 +-
.../dsl/jbang/core/commands/tui/PopupManager.java | 26 +-
.../dsl/jbang/core/commands/tui/TabRegistry.java | 24 +-
11 files changed, 779 insertions(+), 24 deletions(-)
diff --git a/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapHistogramDevConsoleConfigurer.java b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapHistogramDevConsoleConfigurer.java
new file mode 100644
index 000000000000..707bd8e5f6c7
--- /dev/null
+++ b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapHistogramDevConsoleConfigurer.java
@@ -0,0 +1,63 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.impl.console;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.impl.console.HeapHistogramDevConsole;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class HeapHistogramDevConsoleConfigurer extends org.apache.camel.support.component.PropertyConfigurerSupport implements GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter {
+
+ private static final Map<String, Object> ALL_OPTIONS;
+ static {
+ Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("CamelContext", org.apache.camel.CamelContext.class);
+ ALL_OPTIONS = map;
+ }
+
+ @Override
+ public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) {
+ org.apache.camel.impl.console.HeapHistogramDevConsole target = (org.apache.camel.impl.console.HeapHistogramDevConsole) obj;
+ switch (ignoreCase ? name.toLowerCase() : name) {
+ case "camelcontext":
+ case "camelContext": target.setCamelContext(property(camelContext, org.apache.camel.CamelContext.class, value)); return true;
+ default: return false;
+ }
+ }
+
+ @Override
+ public Map<String, Object> getAllOptions(Object target) {
+ return ALL_OPTIONS;
+ }
+
+ @Override
+ public Class<?> getOptionType(String name, boolean ignoreCase) {
+ switch (ignoreCase ? name.toLowerCase() : name) {
+ case "camelcontext":
+ case "camelContext": return org.apache.camel.CamelContext.class;
+ default: return null;
+ }
+ }
+
+ @Override
+ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+ org.apache.camel.impl.console.HeapHistogramDevConsole target = (org.apache.camel.impl.console.HeapHistogramDevConsole) obj;
+ switch (ignoreCase ? name.toLowerCase() : name) {
+ case "camelcontext":
+ case "camelContext": return target.getCamelContext();
+ default: return null;
+ }
+ }
+}
+
diff --git a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-histogram.json b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-histogram.json
new file mode 100644
index 000000000000..af0809c2bbf5
--- /dev/null
+++ b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-histogram.json
@@ -0,0 +1,15 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "heap-histogram",
+ "title": "Heap Histogram",
+ "description": "Displays class-level heap memory usage",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.HeapHistogramDevConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.21.0-SNAPSHOT"
+ }
+}
+
diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapHistogramDevConsole b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapHistogramDevConsole
new file mode 100644
index 000000000000..6059841ac7de
--- /dev/null
+++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapHistogramDevConsole
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.HeapHistogramDevConsoleConfigurer
diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-histogram b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-histogram
new file mode 100644
index 000000000000..2254ff32042b
--- /dev/null
+++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-histogram
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.HeapHistogramDevConsole
diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index fe5d76fe5112..c429fc6982f4 100644
--- a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health inflight internal-tasks java-security jvm log memory message-history processor producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source sql-query sql-trace startup-recorder system-properties thread top trace transformers type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health heap-histogram inflight internal-tasks java-security jvm log memory message-history processor producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source sql-query sql-trace startup-recorder system-properties thread top trace transformers type-converters variables
groupId=org.apache.camel
artifactId=camel-console
version=4.21.0-SNAPSHOT
diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapHistogramDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapHistogramDevConsole.java
new file mode 100644
index 000000000000..e07a5f0b1a97
--- /dev/null
+++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapHistogramDevConsole.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.impl.console;
+
+import java.lang.management.ManagementFactory;
+import java.util.Map;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "heap-histogram", displayName = "Heap Histogram", description = "Displays class-level heap memory usage")
+@Configurer(extended = true)
+public class HeapHistogramDevConsole extends AbstractDevConsole {
+
+ public HeapHistogramDevConsole() {
+ super("jvm", "heap-histogram", "Heap Histogram", "Displays class-level heap memory usage");
+ }
+
+ @Override
+ protected String doCallText(Map<String, Object> options) {
+ String histogram = invokeGcClassHistogram();
+ if (histogram == null) {
+ return "Heap histogram not available (DiagnosticCommand MBean not found)";
+ }
+ int limit = getLimit(options);
+ if (limit > 0) {
+ return truncateText(histogram, limit);
+ }
+ return histogram;
+ }
+
+ @Override
+ protected JsonObject doCallJson(Map<String, Object> options) {
+ JsonObject root = new JsonObject();
+
+ String histogram = invokeGcClassHistogram();
+ if (histogram == null) {
+ root.put("classes", new JsonArray());
+ root.put("totalInstances", 0);
+ root.put("totalBytes", 0);
+ return root;
+ }
+
+ int limit = getLimit(options);
+ return parseHistogram(histogram, limit);
+ }
+
+ private static int getLimit(Map<String, Object> options) {
+ Object val = options.get("limit");
+ if (val != null) {
+ try {
+ return Integer.parseInt(val.toString());
+ } catch (NumberFormatException e) {
+ // use default
+ }
+ }
+ return 100;
+ }
+
+ private static String invokeGcClassHistogram() {
+ try {
+ MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+ ObjectName name = new ObjectName("com.sun.management:type=DiagnosticCommand");
+ String result = (String) server.invoke(
+ name, "gcClassHistogram",
+ new Object[] { null },
+ new String[] { String[].class.getName() });
+ return result;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static JsonObject parseHistogram(String histogram, int limit) {
+ JsonObject root = new JsonObject();
+ JsonArray arr = new JsonArray();
+ root.put("classes", arr);
+
+ long totalInstances = 0;
+ long totalBytes = 0;
+ int count = 0;
+
+ String[] lines = histogram.split("\n");
+ for (String line : lines) {
+ line = line.trim();
+ if (line.isEmpty() || line.startsWith("-") || line.startsWith("num")) {
+ continue;
+ }
+ if (line.startsWith("Total")) {
+ String[] parts = line.split("\\s+");
+ if (parts.length >= 3) {
+ totalInstances = parseLong(parts[1]);
+ totalBytes = parseLong(parts[2]);
+ }
+ continue;
+ }
+
+ String[] parts = line.split("\\s+");
+ if (parts.length < 4) {
+ continue;
+ }
+
+ // parts[0]=num: parts[1]=#instances parts[2]=#bytes parts[3]=class
+ String numStr = parts[0].replace(":", "");
+ int num = (int) parseLong(numStr);
+ long instances = parseLong(parts[1]);
+ long bytes = parseLong(parts[2]);
+ String className = parts[3];
+
+ if (limit > 0 && count >= limit) {
+ break;
+ }
+
+ JsonObject jo = new JsonObject();
+ jo.put("num", num);
+ jo.put("instances", instances);
+ jo.put("bytes", bytes);
+ jo.put("className", className);
+ arr.add(jo);
+ count++;
+ }
+
+ root.put("totalInstances", totalInstances);
+ root.put("totalBytes", totalBytes);
+
+ return root;
+ }
+
+ private static long parseLong(String s) {
+ try {
+ return Long.parseLong(s);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static String truncateText(String histogram, int limit) {
+ StringBuilder sb = new StringBuilder();
+ int count = 0;
+ String[] lines = histogram.split("\n");
+ for (String line : lines) {
+ String trimmed = line.trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("-") || trimmed.startsWith("num")) {
+ sb.append(line).append('\n');
+ continue;
+ }
+ if (trimmed.startsWith("Total")) {
+ sb.append(line).append('\n');
+ continue;
+ }
+ if (count < limit) {
+ sb.append(line).append('\n');
+ count++;
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index d8d562fb77a9..2e336aec1cf7 100644
--- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -335,6 +335,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
doActionJvmTask();
} else if ("thread-dump".equals(action)) {
doActionThreadDumpTask();
+ } else if ("heap-histogram".equals(action)) {
+ doActionHeapHistogramTask();
} else if ("top-processors".equals(action)) {
doActionTopProcessorsTask();
} else if ("source".equals(action)) {
@@ -836,6 +838,18 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
}
}
+ private void doActionHeapHistogramTask() throws IOException {
+ DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+ .resolveById("heap-histogram");
+ if (dc != null) {
+ JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON);
+ LOG.trace("Updating output file: {}", outputFile);
+ IOHelper.writeText(json.toJson(), outputFile);
+ } else {
+ IOHelper.writeText("{}", outputFile);
+ }
+ }
+
private void doActionKafkaTask() throws IOException {
DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
.resolveById("kafka");
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
new file mode 100644
index 000000000000..3fe2004ce698
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -0,0 +1,471 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class HeapHistogramTab implements MonitorTab {
+
+ private static final String[] SORT_COLUMNS = { "className", "instances", "bytes" };
+ private static final String[] FILTER_LABELS = { "all", "non-jdk", "camel" };
+
+ private final MonitorContext ctx;
+ private final TableState tableState = new TableState();
+ private final AtomicBoolean loading = new AtomicBoolean(false);
+
+ private String sort = "bytes";
+ private int sortIndex = 2;
+ private boolean sortReversed;
+ private int filter; // 0=all, 1=camel
+ private List<HeapEntry> allEntries = Collections.emptyList();
+ private long totalInstances;
+ private long totalBytes;
+ private String lastPid;
+
+ HeapHistogramTab(MonitorContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void onTabSelected() {
+ String pid = ctx.selectedPid;
+ if (pid != null && !pid.equals(lastPid)) {
+ lastPid = pid;
+ allEntries = Collections.emptyList();
+ }
+ if (allEntries.isEmpty()) {
+ loadHeapHistogram();
+ }
+ }
+
+ @Override
+ public void onIntegrationChanged() {
+ allEntries = Collections.emptyList();
+ lastPid = null;
+ loadHeapHistogram();
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isChar('s')) {
+ sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+ sort = SORT_COLUMNS[sortIndex];
+ sortReversed = false;
+ return true;
+ }
+ if (ke.isChar('S')) {
+ sortReversed = !sortReversed;
+ return true;
+ }
+ if (ke.isCharIgnoreCase('f')) {
+ filter = (filter + 1) % FILTER_LABELS.length;
+ return true;
+ }
+ if (ke.isKey(KeyCode.F5)) {
+ loadHeapHistogram();
+ return true;
+ }
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ for (int i = 0; i < 20 && tableState.selected() != null && tableState.selected() > 0; i++) {
+ tableState.selectPrevious();
+ }
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ List<HeapEntry> visible = sortedEntries();
+ for (int i = 0; i < 20; i++) {
+ tableState.selectNext(visible.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ return false;
+ }
+
+ @Override
+ public void navigateUp() {
+ tableState.selectPrevious();
+ }
+
+ @Override
+ public void navigateDown() {
+ List<HeapEntry> visible = sortedEntries();
+ tableState.selectNext(visible.size());
+ }
+
+ @Override
+ public void render(Frame frame, Rect area) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ renderNoSelection(frame, area);
+ return;
+ }
+
+ if (loading.get() && allEntries.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(dev.tamboui.text.Text.from(
+ Line.from(Span.styled(" Loading heap histogram...", Style.EMPTY.dim()))))
+ .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Heap Histogram ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ List<HeapEntry> visible = sortedEntries();
+
+ List<Rect> chunks = Layout.vertical()
+ .constraints(Constraint.length(1), Constraint.fill())
+ .split(area);
+ renderSummary(frame, chunks.get(0), visible);
+ renderTable(frame, chunks.get(1), visible);
+ }
+
+ private void renderSummary(Frame frame, Rect area, List<HeapEntry> visible) {
+ List<Span> spans = new ArrayList<>();
+ spans.add(Span.styled(" Classes: ", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.styled(String.valueOf(allEntries.size()), Style.EMPTY.fg(Color.WHITE)));
+ spans.add(Span.raw(" "));
+ spans.add(Span.styled("Showing: ", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.styled(String.valueOf(visible.size()), Style.EMPTY.fg(Color.WHITE)));
+ spans.add(Span.raw(" "));
+ spans.add(Span.styled("Total Instances: ", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.styled(formatNumber(totalInstances), Style.EMPTY.fg(Color.WHITE)));
+ spans.add(Span.raw(" "));
+ spans.add(Span.styled("Total Bytes: ", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.styled(formatBytes(totalBytes), Style.EMPTY.fg(Color.WHITE)));
+ frame.renderWidget(Paragraph.from(Line.from(spans)), area);
+ }
+
+ private void renderTable(Frame frame, Rect area, List<HeapEntry> visible) {
+ List<Row> rows = new ArrayList<>();
+ for (HeapEntry e : visible) {
+ rows.add(Row.from(
+ rightCell(String.valueOf(e.num), 6),
+ Cell.from(Span.styled(e.className != null ? e.className : "", Style.EMPTY.fg(Color.CYAN))),
+ rightCell(formatNumber(e.instances), 14),
+ rightCell(formatBytes(e.bytes), 14)));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(Row.from(
+ Cell.from(Span.styled("No data", Style.EMPTY.dim())),
+ Cell.from(""), Cell.from(""), Cell.from("")));
+ }
+
+ String title = String.format(" Heap Histogram [%d] sort:%s filter:%s ",
+ visible.size(), sort, FILTER_LABELS[filter]);
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ rightCell("#", 6, Style.EMPTY.bold()),
+ Cell.from(Span.styled(sortLabel("CLASS NAME", "className"), sortStyle("className"))),
+ rightCell(sortLabel("INSTANCES", "instances"), 14, sortStyle("instances")),
+ rightCell(sortLabel("BYTES", "bytes"), 14, sortStyle("bytes"))))
+ .widths(
+ Constraint.length(6),
+ Constraint.fill(),
+ Constraint.length(14),
+ Constraint.length(14))
+ .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+ .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+ .build();
+
+ frame.renderStatefulWidget(table, area, tableState);
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ hint(spans, "Esc", "back");
+ hint(spans, "s", "sort");
+ hint(spans, "f", "filter [" + FILTER_LABELS[filter] + "]");
+ hintLast(spans, "F5", "refresh");
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ List<HeapEntry> visible = sortedEntries();
+ if (visible.isEmpty()) {
+ return null;
+ }
+ List<String> items = visible.stream().map(e -> e.className != null ? e.className : "").toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "Heap Histogram");
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ List<HeapEntry> entries = sortedEntries();
+ if (entries.isEmpty()) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Heap Histogram");
+ JsonArray rows = new JsonArray();
+ for (HeapEntry e : entries) {
+ JsonObject row = new JsonObject();
+ row.put("num", e.num);
+ row.put("className", e.className);
+ row.put("instances", e.instances);
+ row.put("bytes", e.bytes);
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", allEntries.size());
+ result.put("totalInstances", totalInstances);
+ result.put("totalBytes", totalBytes);
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Heap Histogram
+
+ The Heap Histogram tab shows class-level memory usage in the JVM heap,
+ similar to `jcmd <pid> GC.class_histogram`. Each row represents a class
+ with the number of live instances and total bytes consumed.
+
+ This is useful for diagnosing memory leaks, finding unexpected object
+ retention, and understanding which classes dominate heap usage.
+
+ ## Table Columns
+
+ - **#** — Rank by bytes (from the raw JVM histogram)
+ - **CLASS NAME** — Fully qualified class name. Array types use JVM notation (e.g., `[B` = byte array, `[Ljava.lang.Object;` = Object array)
+ - **INSTANCES** — Number of live instances of this class on the heap
+ - **BYTES** — Total bytes consumed by all instances of this class
+
+ ## Example Screen
+
+ ```
+ # CLASS NAME INSTANCES BYTES
+ 1 [B 45,230 12.5 MB
+ 2 [C 38,100 8.2 MB
+ 3 java.lang.String 38,050 1.8 MB
+ 4 java.util.HashMap$Node 12,400 595.0 KB
+ 5 java.lang.Object[] 8,200 450.2 KB
+ ```
+
+ ## Filter Modes
+
+ - **all** (default) — Show all classes
+ - **camel** — Show only classes from `org.apache.camel` packages
+
+ ## What To Look For
+
+ - **Large byte counts at the top**: Normal for byte arrays (`[B`) and char arrays (`[C`) — these back Strings and buffers
+ - **Unexpected classes with high counts**: May indicate a memory leak — objects being created but not released
+ - **Growing instance counts on refresh**: Press F5 repeatedly to spot classes whose instance counts keep growing, which suggests a leak
+ - **Camel-specific classes**: Use the `camel` filter to focus on Camel's own objects. High counts of Exchange, Message, or Endpoint objects may indicate route issues
+
+ ## Keys
+
+ | Key | Action |
+ |-----|--------|
+ | Up/Down | Select class |
+ | s | Cycle sort column (bytes, instances, className) |
+ | S | Reverse sort order |
+ | f | Toggle filter (all / camel) |
+ | F5 | Refresh heap histogram |
+ | PgUp/PgDn | Scroll by page |
+ | Esc | Back |
+ """;
+ }
+
+ private List<HeapEntry> sortedEntries() {
+ List<HeapEntry> result = new ArrayList<>();
+ for (HeapEntry e : allEntries) {
+ if (filter == 1 && isJavaClass(e)) {
+ continue;
+ }
+ if (filter == 2 && !isCamelClass(e)) {
+ continue;
+ }
+ result.add(e);
+ }
+ result.sort((a, b) -> {
+ int cmp = switch (sort) {
+ case "instances" -> Long.compare(b.instances, a.instances);
+ case "className" -> compareStr(a.className, b.className);
+ default -> Long.compare(b.bytes, a.bytes);
+ };
+ return sortReversed ? -cmp : cmp;
+ });
+ return result;
+ }
+
+ private static boolean isJavaClass(HeapEntry e) {
+ if (e.className == null) {
+ return false;
+ }
+ return e.className.startsWith("java.")
+ || e.className.startsWith("javax.")
+ || e.className.startsWith("jdk.")
+ || e.className.startsWith("sun.")
+ || e.className.startsWith("com.sun.")
+ || e.className.startsWith("[");
+ }
+
+ private static boolean isCamelClass(HeapEntry e) {
+ return e.className != null && e.className.contains("org.apache.camel");
+ }
+
+ private static int compareStr(String a, String b) {
+ if (a == null && b == null) {
+ return 0;
+ }
+ if (a == null) {
+ return -1;
+ }
+ if (b == null) {
+ return 1;
+ }
+ return a.compareToIgnoreCase(b);
+ }
+
+ private String sortLabel(String label, String column) {
+ return MonitorContext.sortLabel(label, column, sort, sortReversed);
+ }
+
+ private Style sortStyle(String column) {
+ return MonitorContext.sortStyle(column, sort);
+ }
+
+ private void loadHeapHistogram() {
+ if (ctx.selectedPid == null || ctx.runner == null) {
+ return;
+ }
+ if (!loading.compareAndSet(false, true)) {
+ return;
+ }
+ String pid = ctx.selectedPid;
+ ctx.runner.scheduler().execute(() -> {
+ try {
+ loadHeapHistogramInBackground(pid);
+ } finally {
+ loading.set(false);
+ }
+ });
+ }
+
+ private void loadHeapHistogramInBackground(String pid) {
+ Path outputFile = ctx.getOutputFile(pid);
+ PathUtils.deleteFile(outputFile);
+
+ JsonObject root = new JsonObject();
+ root.put("action", "heap-histogram");
+
+ Path actionFile = ctx.getActionFile(pid);
+ PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+ JsonObject jo = pollJsonResponse(outputFile, 5000);
+ PathUtils.deleteFile(outputFile);
+
+ if (jo == null) {
+ return;
+ }
+
+ long ti = jo.getLongOrDefault("totalInstances", 0);
+ long tb = jo.getLongOrDefault("totalBytes", 0);
+
+ JsonArray arr = (JsonArray) jo.get("classes");
+ if (arr == null) {
+ return;
+ }
+
+ List<HeapEntry> result = new ArrayList<>();
+ for (int i = 0; i < arr.size(); i++) {
+ JsonObject ej = (JsonObject) arr.get(i);
+ HeapEntry entry = new HeapEntry();
+ entry.num = ej.getIntegerOrDefault("num", 0);
+ entry.className = ej.getString("className");
+ entry.instances = ej.getLongOrDefault("instances", 0);
+ entry.bytes = ej.getLongOrDefault("bytes", 0);
+ result.add(entry);
+ }
+
+ if (ctx.runner != null) {
+ ctx.runner.runOnRenderThread(() -> {
+ allEntries = result;
+ totalInstances = ti;
+ totalBytes = tb;
+ lastPid = pid;
+ });
+ }
+ }
+
+ static String formatBytes(long bytes) {
+ if (bytes < 1024) {
+ return bytes + " B";
+ } else if (bytes < 1024 * 1024) {
+ return String.format("%.1f KB", bytes / 1024.0);
+ } else if (bytes < 1024L * 1024 * 1024) {
+ return String.format("%.1f MB", bytes / (1024.0 * 1024));
+ } else {
+ return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
+ }
+ }
+
+ static String formatNumber(long num) {
+ return String.format("%,d", num);
+ }
+
+ static class HeapEntry {
+ int num;
+ String className;
+ long instances;
+ long bytes;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 674c4a3f0f3e..66f6e776b248 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -90,8 +90,8 @@ class McpFacade {
static final String[] MORE_TAB_NAMES = {
"Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
- "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "SQL Query", "SQL Trace", "Spans", "Process",
- "Startup", "Threads"
+ "Consumers", "DataSource", "Heap Histogram", "Inflight", "Memory", "Metrics", "SQL Query", "SQL Trace",
+ "Spans", "Process", "Startup", "Threads"
};
static final Map<String, String> TAB_DESCRIPTIONS = Map.ofEntries(
@@ -111,6 +111,8 @@ class McpFacade {
Map.entry("Configuration", "Application configuration properties"),
Map.entry("Consumers", "Consumer statistics (polling and event-driven consumers)"),
Map.entry("DataSource", "JDBC DataSource pool statistics (active, idle, max connections)"),
+ Map.entry("Heap Histogram",
+ "Class-level heap memory analysis showing instance counts and byte usage per class"),
Map.entry("Inflight", "Currently in-flight exchanges being processed"),
Map.entry("Memory", "JVM memory usage (heap/non-heap), GC stats, and thread counts"),
Map.entry("Metrics", "Micrometer metrics (counters, gauges, timers, distribution summaries)"),
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index ba1c87e859ed..7687999001cc 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -176,7 +176,7 @@ class PopupManager {
return true;
}
if (ke.isDown()) {
- morePopupState.selectNext(16);
+ morePopupState.selectNext(17);
return true;
}
int shortcutSel = morePopupShortcut(ke);
@@ -239,7 +239,7 @@ class PopupManager {
void renderMorePopup(Frame frame, Rect area) {
int popupW = 22;
- int popupH = 18;
+ int popupH = 19;
// Position just below the "0 More▾" tab label
int dividerW = CharWidth.of(" | ");
int tabBarX = 0;
@@ -268,6 +268,7 @@ class PopupManager {
ListItem.from(Line.from(Span.raw(" Confi"), Span.styled("g", keyStyle), Span.raw("uration"))),
ListItem.from(Line.from(Span.raw(" Co"), Span.styled("n", keyStyle), Span.raw("sumers"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("D", keyStyle), Span.raw("ataSource"))),
+ ListItem.from(Line.from(Span.raw(" "), Span.styled("H", keyStyle), Span.raw("eap Histogram"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("I", keyStyle), Span.raw("nflight"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("M", keyStyle), Span.raw("emory"))),
ListItem.from(Line.from(Span.raw(" M"), Span.styled("e", keyStyle), Span.raw("trics"))),
@@ -397,33 +398,36 @@ class PopupManager {
if (ke.isChar('d')) {
return 6;
}
- if (ke.isChar('i')) {
+ if (ke.isChar('h')) {
return 7;
}
- if (ke.isChar('m')) {
+ if (ke.isChar('i')) {
return 8;
}
- if (ke.isChar('e')) {
+ if (ke.isChar('m')) {
return 9;
}
- if (ke.isChar('q')) {
+ if (ke.isChar('e')) {
return 10;
}
- if (ke.isChar('r')) {
+ if (ke.isChar('q')) {
return 11;
}
- if (ke.isChar('o')) {
+ if (ke.isChar('r')) {
return 12;
}
- if (ke.isChar('p')) {
+ if (ke.isChar('o')) {
return 13;
}
- if (ke.isChar('s')) {
+ if (ke.isChar('p')) {
return 14;
}
- if (ke.isChar('t')) {
+ if (ke.isChar('s')) {
return 15;
}
+ if (ke.isChar('t')) {
+ return 16;
+ }
return -1;
}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 4fb2bf453383..e0cf19d7e1f7 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -86,6 +86,7 @@ class TabRegistry {
private ProcessTab processTab;
private OverviewTab overviewTab;
private DataSourceTab dataSourceTab;
+ private HeapHistogramTab heapHistogramTab;
private SqlQueryTab sqlQueryTab;
private SqlTraceTab sqlTraceTab;
@@ -105,6 +106,7 @@ class TabRegistry {
routesTab = new RoutesTab(ctx);
consumersTab = new ConsumersTab(ctx);
dataSourceTab = new DataSourceTab(ctx);
+ heapHistogramTab = new HeapHistogramTab(ctx);
sqlQueryTab = new SqlQueryTab(ctx);
sqlTraceTab = new SqlTraceTab(ctx);
endpointsTab = new EndpointsTab(ctx, dataService.metrics());
@@ -129,7 +131,7 @@ class TabRegistry {
resetIntegrationTabState);
sqlTraceTab.setEditSqlAction(sql -> {
- selectMoreTab(10); // switch to SQL Query tab
+ selectMoreTab(11); // switch to SQL Query tab
sqlQueryTab.setInputValue("sql", sql);
});
}
@@ -216,15 +218,16 @@ class TabRegistry {
case 4 -> configurationTab;
case 5 -> consumersTab;
case 6 -> dataSourceTab;
- case 7 -> inflightTab;
- case 8 -> memoryTab;
- case 9 -> metricsTab;
- case 10 -> sqlQueryTab;
- case 11 -> sqlTraceTab;
- case 12 -> spansTab;
- case 13 -> processTab;
- case 14 -> startupTab;
- case 15 -> threadsTab;
+ case 7 -> heapHistogramTab;
+ case 8 -> inflightTab;
+ case 9 -> memoryTab;
+ case 10 -> metricsTab;
+ case 11 -> sqlQueryTab;
+ case 12 -> sqlTraceTab;
+ case 13 -> spansTab;
+ case 14 -> processTab;
+ case 15 -> startupTab;
+ case 16 -> threadsTab;
default -> null;
};
if (activeMoreTab != null) {
@@ -247,6 +250,7 @@ class TabRegistry {
configurationTab.onIntegrationChanged();
consumersTab.onIntegrationChanged();
dataSourceTab.onIntegrationChanged();
+ heapHistogramTab.onIntegrationChanged();
sqlQueryTab.onIntegrationChanged();
sqlTraceTab.onIntegrationChanged();
circuitBreakerTab.onIntegrationChanged();