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:33 UTC
(camel) branch feature/CAMEL-23870-heap-histogram created (now d36b0b212761)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a change to branch feature/CAMEL-23870-heap-histogram
in repository https://gitbox.apache.org/repos/asf/camel.git
at d36b0b212761 CAMEL-23870: add generated doc page for heap-histogram CLI command
This branch includes the following new commits:
new ea88102e3928 CAMEL-23870: Add HeapHistogram dev console and TUI panel
new cd69ee5a4b77 CAMEL-23870: Add camel cmd heap-histogram CLI command
new 9756440d0195 chore: use Locale.US consistently across TUI and CLI for number formatting
new 8a9fae587ca2 chore: compute heap histogram totals from visible entries only
new 73c913d3d54e chore: add detail panel to heap histogram tab with package summary and JAR origin
new d7cb626c7ca4 chore: reset table selection when heap histogram filter changes
new 8a4275992e9e chore: move heap histogram summary into table title
new 06119e092897 chore: update heap histogram F1 help text and MCP description
new cc9811acaf85 CAMEL-23870: add generated docs and metadata for heap-histogram command
new acf59ede4738 CAMEL-23870: add heap histogram MCP tool for camel ask
new d36b0b212761 CAMEL-23870: add generated doc page for heap-histogram CLI command
The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(camel) 09/11: CAMEL-23870: add generated docs and metadata for heap-histogram command
Posted by da...@apache.org.
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 cc9811acaf85e39acfd9cdd101e234f02cdf3c91
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:26:55 2026 +0200
CAMEL-23870: add generated docs and metadata for heap-histogram command
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 1 +
.../src/generated/resources/META-INF/camel-jbang-commands-metadata.json | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
index 4e261d317b2d..c5cfec7993db 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
@@ -22,6 +22,7 @@ camel cmd [options]
| xref:jbang-commands/camel-jbang-cmd-disable-processor.adoc[disable-processor] | Disable Camel processor
| xref:jbang-commands/camel-jbang-cmd-enable-processor.adoc[enable-processor] | Enable Camel processor
| xref:jbang-commands/camel-jbang-cmd-gc.adoc[gc] | Trigger Java Memory Garbage Collector
+| xref:jbang-commands/camel-jbang-cmd-heap-histogram.adoc[heap-histogram] | Display class-level heap memory usage in a running Camel integration
| xref:jbang-commands/camel-jbang-cmd-load.adoc[load] | Loads new source files into an existing Camel
| xref:jbang-commands/camel-jbang-cmd-logger.adoc[logger] | List or change logging levels
| xref:jbang-commands/camel-jbang-cmd-receive.adoc[receive] | Receive and dump messages from remote endpoints
diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 9a8c2a2680a7..0a05c64223ab 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
{ "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" [...]
{ "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...]
{ "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
- { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...]
+ { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
{ "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To [...]
(camel) 01/11: CAMEL-23870: Add HeapHistogram dev console and TUI panel
Posted by da...@apache.org.
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();
(camel) 07/11: chore: move heap histogram summary into table title
Posted by da...@apache.org.
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 8a4275992e9ef2cd2dc5ed4283de5b4cf9952d1a
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:25:06 2026 +0200
chore: move heap histogram summary into table title
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../jbang/core/commands/tui/HeapHistogramTab.java | 39 ++++++++--------------
1 file changed, 13 insertions(+), 26 deletions(-)
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
index 78e97418d3f2..13ef05e0b230 100644
--- 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
@@ -167,31 +167,10 @@ class HeapHistogramTab implements MonitorTab {
List<HeapEntry> visible = sortedEntries();
List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.length(1), Constraint.percentage(60), Constraint.fill())
+ .constraints(Constraint.percentage(60), Constraint.fill())
.split(area);
- renderSummary(frame, chunks.get(0), visible);
- renderTable(frame, chunks.get(1), visible);
- renderDetail(frame, chunks.get(2), visible);
- }
-
- private void renderSummary(Frame frame, Rect area, List<HeapEntry> visible) {
- long visibleInstances = 0;
- long visibleBytes = 0;
- for (HeapEntry e : visible) {
- visibleInstances += e.instances;
- visibleBytes += e.bytes;
- }
-
- List<Span> spans = new ArrayList<>();
- spans.add(Span.styled(" Classes: ", 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("Instances: ", Style.EMPTY.fg(Color.YELLOW).bold()));
- spans.add(Span.styled(formatNumber(visibleInstances), Style.EMPTY.fg(Color.WHITE)));
- spans.add(Span.raw(" "));
- spans.add(Span.styled("Bytes: ", Style.EMPTY.fg(Color.YELLOW).bold()));
- spans.add(Span.styled(formatBytes(visibleBytes), Style.EMPTY.fg(Color.WHITE)));
- frame.renderWidget(Paragraph.from(Line.from(spans)), area);
+ renderTable(frame, chunks.get(0), visible);
+ renderDetail(frame, chunks.get(1), visible);
}
private void renderTable(Frame frame, Rect area, List<HeapEntry> visible) {
@@ -210,8 +189,16 @@ class HeapHistogramTab implements MonitorTab {
Cell.from(""), Cell.from(""), Cell.from("")));
}
- String title = String.format(" Heap Histogram [%d] sort:%s filter:%s ",
- visible.size(), sort, FILTER_LABELS[filter]);
+ long visibleInstances = 0;
+ long visibleBytes = 0;
+ for (HeapEntry e : visible) {
+ visibleInstances += e.instances;
+ visibleBytes += e.bytes;
+ }
+
+ String title = String.format(" Heap Histogram [%d] instances:%s bytes:%s sort:%s filter:%s ",
+ visible.size(), formatNumber(visibleInstances), formatBytes(visibleBytes),
+ sort, FILTER_LABELS[filter]);
Table table = Table.builder()
.rows(rows)
(camel) 06/11: chore: reset table selection when heap histogram filter changes
Posted by da...@apache.org.
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 d7cb626c7ca4ce425c45ad6d3b672dddbf1735c4
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:22:38 2026 +0200
chore: reset table selection when heap histogram filter changes
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java | 1 +
1 file changed, 1 insertion(+)
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
index c620c298f6ce..78e97418d3f2 100644
--- 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
@@ -105,6 +105,7 @@ class HeapHistogramTab implements MonitorTab {
}
if (ke.isCharIgnoreCase('f')) {
filter = (filter + 1) % FILTER_LABELS.length;
+ tableState.select(0);
return true;
}
if (ke.isKey(KeyCode.F5)) {
(camel) 11/11: CAMEL-23870: add generated doc page for heap-histogram CLI command
Posted by da...@apache.org.
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 d36b0b2127615c7c1bb83f136902b1884d76bc3f
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:29:16 2026 +0200
CAMEL-23870: add generated doc page for heap-histogram CLI command
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../camel-jbang-cmd-heap-histogram.adoc | 29 ++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-histogram.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-histogram.adoc
new file mode 100644
index 000000000000..f36a78e95cdf
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-histogram.adoc
@@ -0,0 +1,29 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd heap-histogram
+
+Display class-level heap memory usage in a running Camel integration
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd heap-histogram [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--filter` | Filter class names (use all to include all classes) | all | String
+| `--sort` | Sort by bytes, instances, or className | bytes | String
+| `--top` | Show only the top N classes | 50 | int
+| `--watch` | Execute periodically and showing output fullscreen | | boolean
+| `-h,--help` | Display the help and sub-commands | | boolean
+|===
+
+
(camel) 05/11: chore: add detail panel to heap histogram tab with package summary and JAR origin
Posted by da...@apache.org.
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 73c913d3d54e94ebd0104b452e3bdca809531db2
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:20:24 2026 +0200
chore: add detail panel to heap histogram tab with package summary and JAR origin
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../jbang/core/commands/tui/HeapHistogramTab.java | 230 +++++++++++++++++++--
1 file changed, 210 insertions(+), 20 deletions(-)
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
index 3030f3b11e33..c620c298f6ce 100644
--- 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
@@ -30,6 +30,7 @@ import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.widgets.block.Block;
@@ -58,11 +59,12 @@ class HeapHistogramTab implements MonitorTab {
private String sort = "bytes";
private int sortIndex = 2;
private boolean sortReversed;
- private int filter; // 0=all, 1=camel
+ private int filter;
private List<HeapEntry> allEntries = Collections.emptyList();
private long totalInstances;
private long totalBytes;
private String lastPid;
+ private List<ClasspathTab.JarEntry> classpathEntries = Collections.emptyList();
HeapHistogramTab(MonitorContext ctx) {
this.ctx = ctx;
@@ -74,6 +76,7 @@ class HeapHistogramTab implements MonitorTab {
if (pid != null && !pid.equals(lastPid)) {
lastPid = pid;
allEntries = Collections.emptyList();
+ classpathEntries = Collections.emptyList();
}
if (allEntries.isEmpty()) {
loadHeapHistogram();
@@ -83,6 +86,7 @@ class HeapHistogramTab implements MonitorTab {
@Override
public void onIntegrationChanged() {
allEntries = Collections.emptyList();
+ classpathEntries = Collections.emptyList();
lastPid = null;
loadHeapHistogram();
}
@@ -150,7 +154,7 @@ class HeapHistogramTab implements MonitorTab {
if (loading.get() && allEntries.isEmpty()) {
frame.renderWidget(
Paragraph.builder()
- .text(dev.tamboui.text.Text.from(
+ .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())
@@ -162,10 +166,11 @@ class HeapHistogramTab implements MonitorTab {
List<HeapEntry> visible = sortedEntries();
List<Rect> chunks = Layout.vertical()
- .constraints(Constraint.length(1), Constraint.fill())
+ .constraints(Constraint.length(1), Constraint.percentage(60), Constraint.fill())
.split(area);
renderSummary(frame, chunks.get(0), visible);
renderTable(frame, chunks.get(1), visible);
+ renderDetail(frame, chunks.get(2), visible);
}
private void renderSummary(Frame frame, Rect area, List<HeapEntry> visible) {
@@ -227,6 +232,103 @@ class HeapHistogramTab implements MonitorTab {
frame.renderStatefulWidget(table, area, tableState);
}
+ private void renderDetail(Frame frame, Rect area, List<HeapEntry> visible) {
+ Integer sel = tableState.selected();
+ if (sel == null || sel < 0 || sel >= visible.size()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(" Select a class to see details", Style.EMPTY.dim()))))
+ .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Detail ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ HeapEntry entry = visible.get(sel);
+ String className = entry.className != null ? entry.className : "";
+ String pkg = extractPackage(className);
+
+ List<Line> lines = new ArrayList<>();
+
+ // Class info
+ lines.add(Line.from(
+ Span.styled(" Class: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(className, Style.EMPTY.fg(Color.CYAN))));
+ lines.add(Line.from(
+ Span.styled(" Package: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(pkg.isEmpty() ? "(none)" : pkg, Style.EMPTY.fg(Color.WHITE))));
+ lines.add(Line.from(
+ Span.styled(" Instances: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(formatNumber(entry.instances), Style.EMPTY.fg(Color.WHITE)),
+ Span.styled(" Bytes: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(formatBytes(entry.bytes), Style.EMPTY.fg(Color.WHITE))));
+
+ // Package summary
+ if (!pkg.isEmpty()) {
+ long pkgInstances = 0;
+ long pkgBytes = 0;
+ int pkgClasses = 0;
+ for (HeapEntry e : allEntries) {
+ if (e.className != null && extractPackage(e.className).equals(pkg)) {
+ pkgInstances += e.instances;
+ pkgBytes += e.bytes;
+ pkgClasses++;
+ }
+ }
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" Package Summary ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled("(" + pkg + ")", Style.EMPTY.dim())));
+ lines.add(Line.from(
+ Span.styled(" Classes: ", Style.EMPTY.fg(Color.YELLOW)),
+ Span.styled(formatNumber(pkgClasses), Style.EMPTY.fg(Color.WHITE)),
+ Span.styled(" Instances: ", Style.EMPTY.fg(Color.YELLOW)),
+ Span.styled(formatNumber(pkgInstances), Style.EMPTY.fg(Color.WHITE)),
+ Span.styled(" Bytes: ", Style.EMPTY.fg(Color.YELLOW)),
+ Span.styled(formatBytes(pkgBytes), Style.EMPTY.fg(Color.WHITE))));
+ }
+
+ // JAR info
+ ClasspathTab.JarEntry jar = findJar(className);
+ if (jar != null) {
+ lines.add(Line.from(Span.raw("")));
+ if (jar.groupId() != null) {
+ lines.add(Line.from(
+ Span.styled(" JAR: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(jar.groupId() + ":" + jar.artifactId() + ":" + jar.version(),
+ Style.EMPTY.fg(Color.GREEN))));
+ } else {
+ lines.add(Line.from(
+ Span.styled(" JAR: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled(jar.display(), Style.EMPTY.fg(Color.GREEN))));
+ }
+ if (jar.fullPath() != null) {
+ String path = jar.fullPath();
+ int maxW = area.width() - 18;
+ if (maxW > 0 && path.length() > maxW) {
+ path = "..." + path.substring(path.length() - maxW + 3);
+ }
+ lines.add(Line.from(
+ Span.styled(" ", Style.EMPTY),
+ Span.styled(path, Style.EMPTY.dim())));
+ }
+ } else if (isBuiltinClass(className)) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(
+ Span.styled(" JAR: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.styled("JDK (built-in)", Style.EMPTY.fg(Color.GREEN))));
+ }
+
+ String title = " Detail: " + TuiHelper.truncate(className, area.width() - 14) + " ";
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(lines))
+ .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+ .build(),
+ area);
+ }
+
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
@@ -291,29 +393,19 @@ class HeapHistogramTab implements MonitorTab {
- **INSTANCES** — Number of live instances of this class on the heap
- **BYTES** — Total bytes consumed by all instances of this class
- ## Example Screen
+ ## Detail Panel
+
+ The detail panel below the table shows additional context for the selected class:
- ```
- # 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
- ```
+ - **Package Summary** — Total classes, instances, and bytes for all classes in the same package
+ - **JAR** — The Maven artifact (groupId:artifactId:version) and file path of the JAR containing the class
## Filter Modes
- **all** (default) — Show all classes
+ - **non-jdk** — Exclude JDK classes (java.*, javax.*, jdk.*, sun.*, com.sun.*, arrays)
- **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 |
@@ -321,7 +413,7 @@ class HeapHistogramTab implements MonitorTab {
| Up/Down | Select class |
| s | Cycle sort column (bytes, instances, className) |
| S | Reverse sort order |
- | f | Toggle filter (all / camel) |
+ | f | Toggle filter (all / non-jdk / camel) |
| F5 | Refresh heap histogram |
| PgUp/PgDn | Scroll by page |
| Esc | Back |
@@ -366,6 +458,60 @@ class HeapHistogramTab implements MonitorTab {
return e.className != null && e.className.contains("org.apache.camel");
}
+ private static boolean isBuiltinClass(String className) {
+ if (className == null || className.isEmpty()) {
+ return false;
+ }
+ return className.startsWith("java.")
+ || className.startsWith("javax.")
+ || className.startsWith("jdk.")
+ || className.startsWith("sun.")
+ || className.startsWith("com.sun.")
+ || className.startsWith("[");
+ }
+
+ private static String extractPackage(String className) {
+ if (className == null || className.isEmpty()) {
+ return "";
+ }
+ if (className.startsWith("[")) {
+ // array type — extract element class if object array
+ int idx = className.lastIndexOf('L');
+ if (idx >= 0) {
+ String element = className.substring(idx + 1).replace(";", "");
+ int dot = element.lastIndexOf('.');
+ return dot >= 0 ? element.substring(0, dot) : "";
+ }
+ return "";
+ }
+ int dot = className.lastIndexOf('.');
+ return dot >= 0 ? className.substring(0, dot) : "";
+ }
+
+ private ClasspathTab.JarEntry findJar(String className) {
+ if (classpathEntries.isEmpty() || className == null || className.isEmpty()) {
+ return null;
+ }
+ String pkg = extractPackage(className);
+ if (pkg.isEmpty()) {
+ return null;
+ }
+ // try progressively shorter prefixes of the package against groupId
+ ClasspathTab.JarEntry best = null;
+ int bestLen = -1;
+ for (ClasspathTab.JarEntry jar : classpathEntries) {
+ if (jar.groupId() == null) {
+ continue;
+ }
+ String gid = jar.groupId();
+ if (pkg.startsWith(gid) && gid.length() > bestLen) {
+ best = jar;
+ bestLen = gid.length();
+ }
+ }
+ return best;
+ }
+
private static int compareStr(String a, String b) {
if (a == null && b == null) {
return 0;
@@ -440,16 +586,60 @@ class HeapHistogramTab implements MonitorTab {
result.add(entry);
}
+ // load classpath if not already loaded
+ List<ClasspathTab.JarEntry> cpEntries = classpathEntries;
+ if (cpEntries.isEmpty()) {
+ cpEntries = loadClasspathEntries(pid);
+ }
+
+ List<ClasspathTab.JarEntry> finalCp = cpEntries;
if (ctx.runner != null) {
ctx.runner.runOnRenderThread(() -> {
allEntries = result;
totalInstances = ti;
totalBytes = tb;
+ classpathEntries = finalCp;
lastPid = pid;
});
}
}
+ private List<ClasspathTab.JarEntry> loadClasspathEntries(String pid) {
+ Path outputFile = ctx.getOutputFile(pid);
+ PathUtils.deleteFile(outputFile);
+
+ JsonObject action = new JsonObject();
+ action.put("action", "jvm");
+
+ Path actionFile = ctx.getActionFile(pid);
+ PathUtils.writeTextSafely(action.toJson(), actionFile);
+
+ JsonObject response = pollJsonResponse(outputFile, 5000);
+ PathUtils.deleteFile(outputFile);
+
+ if (response == null) {
+ return Collections.emptyList();
+ }
+
+ Object cp = response.get("classpath");
+ List<String> paths = new ArrayList<>();
+ if (cp instanceof JsonArray jArr) {
+ for (Object item : jArr) {
+ paths.add(String.valueOf(item));
+ }
+ }
+
+ if (paths.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<ClasspathTab.JarEntry> parsed = new ArrayList<>();
+ for (String path : paths) {
+ parsed.add(ClasspathTab.parseJarEntry(path));
+ }
+ return parsed;
+ }
+
static String formatBytes(long bytes) {
if (bytes < 1024) {
return bytes + " B";
(camel) 08/11: chore: update heap histogram F1 help text and MCP description
Posted by da...@apache.org.
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 06119e092897aa63b9a202488ff1b8ff4281d5ff
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:26:19 2026 +0200
chore: update heap histogram F1 help text and MCP description
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../dsl/jbang/core/commands/tui/HeapHistogramTab.java | 15 +++++++++++++--
.../camel/dsl/jbang/core/commands/tui/McpFacade.java | 2 +-
2 files changed, 14 insertions(+), 3 deletions(-)
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
index 13ef05e0b230..1ceec94648a1 100644
--- 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
@@ -381,12 +381,15 @@ class HeapHistogramTab implements MonitorTab {
- **INSTANCES** — Number of live instances of this class on the heap
- **BYTES** — Total bytes consumed by all instances of this class
+ The title bar shows total classes, instances, and bytes for the current filter.
+
## Detail Panel
The detail panel below the table shows additional context for the selected class:
+ - **Class** — Full class name, package, instance count and bytes
- **Package Summary** — Total classes, instances, and bytes for all classes in the same package
- - **JAR** — The Maven artifact (groupId:artifactId:version) and file path of the JAR containing the class
+ - **JAR** — The Maven artifact (groupId:artifactId:version) and file path of the JAR containing the class. JDK classes show "JDK (built-in)"
## Filter Modes
@@ -394,12 +397,20 @@ class HeapHistogramTab implements MonitorTab {
- **non-jdk** — Exclude JDK classes (java.*, javax.*, jdk.*, sun.*, com.sun.*, arrays)
- **camel** — Show only classes from `org.apache.camel` packages
+ ## What To Look For
+
+ - **Large byte counts at the top**: Normal for byte arrays and char arrays — these back Strings and buffers
+ - **Unexpected classes with high counts**: May indicate a memory leak
+ - **Growing instance counts on refresh**: Press F5 repeatedly to spot classes whose counts keep growing
+ - **Package summary**: Use the detail panel to see total memory for an entire package
+ - **JAR origin**: Identify which dependency owns the memory-heavy classes
+
## Keys
| Key | Action |
|-----|--------|
| Up/Down | Select class |
- | s | Cycle sort column (bytes, instances, className) |
+ | s | Cycle sort column (className, instances, bytes) |
| S | Reverse sort order |
| f | Toggle filter (all / non-jdk / camel) |
| F5 | Refresh heap histogram |
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 dcf7e153c0d8..11096df9a093 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
@@ -112,7 +112,7 @@ class McpFacade {
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"),
+ "Class-level heap memory analysis showing instance counts, byte usage, package summary, and JAR origin 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)"),
(camel) 10/11: CAMEL-23870: add heap histogram MCP tool for camel ask
Posted by da...@apache.org.
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 acf59ede47382c89cf2b5f7ba33a2c7704ba98e2
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:28:13 2026 +0200
CAMEL-23870: add heap histogram MCP tool for camel ask
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 8c4eace1783a..3f86c4ecd9a6 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -317,6 +317,16 @@ public class RuntimeTools {
return runtimeService.executeAction(p.pid(), "thread-dump", null);
}
+ @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false),
+ description = """
+ Get a class-level heap histogram showing instance counts and byte usage per class. \
+ Useful for diagnosing memory leaks and understanding which classes dominate heap usage.""")
+ public JsonObject camel_runtime_heap_histogram(
+ @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
+ RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid);
+ return runtimeService.executeAction(p.pid(), "heap-histogram", null);
+ }
+
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false),
description = """
Get the message history trace of the last completed exchange. \
(camel) 04/11: chore: compute heap histogram totals from visible entries only
Posted by da...@apache.org.
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 8a9fae587ca213251eb0db85bde4123e6b0a3048
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:12:33 2026 +0200
chore: compute heap histogram totals from visible entries only
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../jbang/core/commands/action/CamelHeapHistogram.java | 12 +++++++++---
.../dsl/jbang/core/commands/tui/HeapHistogramTab.java | 18 +++++++++++-------
2 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
index 4e5cf33b6e2f..c58ae90c67d5 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
@@ -137,9 +137,15 @@ public class CamelHeapHistogram extends ActionWatchCommand {
clearScreen();
}
if (!rows.isEmpty()) {
- printer().printf("PID: %s\tClasses: %d\tTotal Instances: %s\tTotal Bytes: %s\tDisplay: %d%n",
- pid, arr != null ? arr.size() : 0,
- formatNumber(totalInstances), formatBytes(totalBytes), rows.size());
+ long visibleInstances = 0;
+ long visibleBytes = 0;
+ for (Row r : rows) {
+ visibleInstances += r.instances;
+ visibleBytes += r.bytes;
+ }
+ printer().printf("PID: %s\tClasses: %d\tInstances: %s\tBytes: %s%n",
+ pid, rows.size(),
+ formatNumber(visibleInstances), formatBytes(visibleBytes));
printTable(rows);
}
} else {
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
index 8af8db0cd01f..3030f3b11e33 100644
--- 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
@@ -169,18 +169,22 @@ class HeapHistogramTab implements MonitorTab {
}
private void renderSummary(Frame frame, Rect area, List<HeapEntry> visible) {
+ long visibleInstances = 0;
+ long visibleBytes = 0;
+ for (HeapEntry e : visible) {
+ visibleInstances += e.instances;
+ visibleBytes += e.bytes;
+ }
+
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.styled("Instances: ", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.styled(formatNumber(visibleInstances), 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)));
+ spans.add(Span.styled("Bytes: ", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.styled(formatBytes(visibleBytes), Style.EMPTY.fg(Color.WHITE)));
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
}
(camel) 02/11: CAMEL-23870: Add camel cmd heap-histogram CLI command
Posted by da...@apache.org.
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 cd69ee5a4b770d16ec252670a405ddb0abd432c3
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:03:51 2026 +0200
CAMEL-23870: Add camel cmd heap-histogram CLI command
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../core/commands/action/CamelHeapHistogram.java | 237 +++++++++++++++++++++
2 files changed, 238 insertions(+)
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 86f19a1abd67..c09e343495f5 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -110,6 +110,7 @@ public class CamelJBangMain implements Callable<Integer> {
.addSubcommand("disable-processor", new CommandLine(new CamelProcessorDisableAction(this)))
.addSubcommand("enable-processor", new CommandLine(new CamelProcessorEnableAction(this)))
.addSubcommand("gc", new CommandLine(new CamelGCAction(this)))
+ .addSubcommand("heap-histogram", new CommandLine(new CamelHeapHistogram(this)))
.addSubcommand("load", new CommandLine(new CamelLoadAction(this)))
.addSubcommand("logger", new CommandLine(new LoggerAction(this)))
.addSubcommand("receive", new CommandLine(new CamelReceiveAction(this)))
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
new file mode 100644
index 000000000000..8636e4a4df22
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
@@ -0,0 +1,237 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "heap-histogram", description = "Display class-level heap memory usage in a running Camel integration",
+ sortOptions = false, showDefaultValues = true,
+ footer = {
+ "%nExamples:",
+ " camel cmd heap-histogram",
+ " camel cmd heap-histogram --sort instances",
+ " camel cmd heap-histogram --filter camel --top 30" })
+public class CamelHeapHistogram extends ActionWatchCommand {
+
+ public static class SortCompletionCandidates implements Iterable<String> {
+
+ public SortCompletionCandidates() {
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return List.of("bytes", "instances", "className").iterator();
+ }
+ }
+
+ @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+ String name = "*";
+
+ @CommandLine.Option(names = { "--sort" }, completionCandidates = SortCompletionCandidates.class,
+ description = "Sort by bytes, instances, or className", defaultValue = "bytes")
+ String sort;
+
+ @CommandLine.Option(names = { "--top" },
+ description = "Show only the top N classes", defaultValue = "50")
+ int top;
+
+ @CommandLine.Option(names = { "--filter" },
+ description = "Filter class names (use all to include all classes)", defaultValue = "all")
+ String filter;
+
+ private volatile long pid;
+
+ public CamelHeapHistogram(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doWatchCall() throws Exception {
+ List<Row> rows = new ArrayList<>();
+
+ List<Long> pids = findPids(name);
+ if (pids.isEmpty()) {
+ return 1;
+ } else if (pids.size() > 1) {
+ printer().println("Name or pid " + name + " matches " + pids.size()
+ + " running Camel integrations. Specify a name or PID that matches exactly one.");
+ return 1;
+ }
+
+ this.pid = pids.get(0);
+
+ Path outputFile = getOutputFile(Long.toString(pid));
+ PathUtils.deleteFile(outputFile);
+
+ JsonObject root = new JsonObject();
+ root.put("action", "heap-histogram");
+ Path f = getActionFile(Long.toString(pid));
+ try {
+ Files.writeString(f, root.toJson());
+ } catch (Exception e) {
+ // ignore
+ }
+
+ JsonObject jo = waitForOutputFile(outputFile);
+ if (jo != null) {
+ long totalInstances = jo.getLongOrDefault("totalInstances", 0);
+ long totalBytes = jo.getLongOrDefault("totalBytes", 0);
+
+ JsonArray arr = (JsonArray) jo.get("classes");
+ if (arr != null) {
+ for (int i = 0; i < arr.size(); i++) {
+ JsonObject jc = (JsonObject) arr.get(i);
+
+ Row row = new Row();
+ row.num = jc.getIntegerOrDefault("num", 0);
+ row.className = jc.getString("className");
+ row.instances = jc.getLongOrDefault("instances", 0);
+ row.bytes = jc.getLongOrDefault("bytes", 0);
+
+ if (!matchesFilter(row)) {
+ continue;
+ }
+
+ rows.add(row);
+ }
+ }
+
+ rows.sort(this::sortRow);
+
+ if (top > 0 && rows.size() > top) {
+ rows = rows.subList(0, top);
+ }
+
+ if (watch) {
+ clearScreen();
+ }
+ if (!rows.isEmpty()) {
+ printer().printf("PID: %s\tClasses: %d\tTotal Instances: %s\tTotal Bytes: %s\tDisplay: %d%n",
+ pid, arr != null ? arr.size() : 0,
+ formatNumber(totalInstances), formatBytes(totalBytes), rows.size());
+ printTable(rows);
+ }
+ } else {
+ printer().println("Response from running Camel with PID " + pid + " not received within 10 seconds");
+ return 1;
+ }
+
+ PathUtils.deleteFile(outputFile);
+
+ return 0;
+ }
+
+ private boolean matchesFilter(Row row) {
+ if ("all".equalsIgnoreCase(filter)) {
+ return true;
+ }
+ if (row.className == null) {
+ return false;
+ }
+ if ("non-jdk".equalsIgnoreCase(filter)) {
+ return !row.className.startsWith("java.")
+ && !row.className.startsWith("javax.")
+ && !row.className.startsWith("jdk.")
+ && !row.className.startsWith("sun.")
+ && !row.className.startsWith("com.sun.")
+ && !row.className.startsWith("[");
+ }
+ return row.className.toLowerCase().contains(filter.toLowerCase());
+ }
+
+ protected void printTable(List<Row> rows) {
+ int tw = terminalWidth();
+ int fixedWidth = 6 + 14 + 14;
+ int borderOverhead = TerminalWidthHelper.noBorderOverhead(4);
+ int nameWidth = TerminalWidthHelper.flexWidth(tw, fixedWidth, borderOverhead, 30, 80);
+
+ printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+ new Column().header("#").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> Integer.toString(r.num)),
+ new Column().header("CLASS NAME").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(nameWidth, OverflowBehaviour.ELLIPSIS_LEFT)
+ .with(r -> r.className),
+ new Column().header("INSTANCES").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> formatNumber(r.instances)),
+ new Column().header("BYTES").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> formatBytes(r.bytes)))));
+ }
+
+ protected int sortRow(Row o1, Row o2) {
+ String s = sort;
+ int negate = 1;
+ if (s.startsWith("-")) {
+ s = s.substring(1);
+ negate = -1;
+ }
+ switch (s) {
+ case "instances":
+ return Long.compare(o2.instances, o1.instances) * negate;
+ case "className":
+ return (o1.className != null
+ ? o1.className.compareToIgnoreCase(o2.className != null ? o2.className : "")
+ : 0)
+ * negate;
+ default:
+ return Long.compare(o2.bytes, o1.bytes) * negate;
+ }
+ }
+
+ protected JsonObject waitForOutputFile(Path outputFile) {
+ return getJsonObject(outputFile);
+ }
+
+ 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);
+ }
+
+ private static class Row {
+ int num;
+ String className;
+ long instances;
+ long bytes;
+ }
+}
(camel) 03/11: chore: use Locale.US consistently across TUI and CLI for number formatting
Posted by da...@apache.org.
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 9756440d0195ce1f4e07f01bed857efda54edb0d
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Jul 1 16:10:59 2026 +0200
chore: use Locale.US consistently across TUI and CLI for number formatting
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../org/apache/camel/impl/console/HeapHistogramDevConsole.java | 2 +-
.../camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java | 8 ++++----
.../apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java | 4 ++--
.../camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java | 8 ++++----
.../org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java | 2 +-
.../apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java | 8 ++++----
6 files changed, 16 insertions(+), 16 deletions(-)
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
index e07a5f0b1a97..80487b3d5ba9 100644
--- 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
@@ -128,7 +128,7 @@ public class HeapHistogramDevConsole extends AbstractDevConsole {
String className = parts[3];
if (limit > 0 && count >= limit) {
- break;
+ continue;
}
JsonObject jo = new JsonObject();
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
index 8636e4a4df22..4e5cf33b6e2f 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
@@ -216,16 +216,16 @@ public class CamelHeapHistogram extends ActionWatchCommand {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
- return String.format("%.1f KB", bytes / 1024.0);
+ return String.format(java.util.Locale.US, "%.1f KB", bytes / 1024.0);
} else if (bytes < 1024L * 1024 * 1024) {
- return String.format("%.1f MB", bytes / (1024.0 * 1024));
+ return String.format(java.util.Locale.US, "%.1f MB", bytes / (1024.0 * 1024));
} else {
- return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
+ return String.format(java.util.Locale.US, "%.1f GB", bytes / (1024.0 * 1024 * 1024));
}
}
static String formatNumber(long num) {
- return String.format("%,d", num);
+ return String.format(java.util.Locale.US, "%,d", num);
}
private static class Row {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
index adcfe23c7a30..51062724af1b 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
@@ -312,7 +312,7 @@ class FilesBrowser {
static String fileType(Path path) {
String name = path.getFileName().toString();
- String lower = name.toLowerCase(Locale.ROOT);
+ String lower = name.toLowerCase(Locale.US);
if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) {
return "camel";
}
@@ -336,7 +336,7 @@ class FilesBrowser {
private static String fileEmoji(Path path) {
String name = path.getFileName().toString();
- String lower = name.toLowerCase(Locale.ROOT);
+ String lower = name.toLowerCase(Locale.US);
if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) {
return "🐪";
}
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
index 3fe2004ce698..8af8db0cd01f 100644
--- 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
@@ -450,16 +450,16 @@ class HeapHistogramTab implements MonitorTab {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
- return String.format("%.1f KB", bytes / 1024.0);
+ return String.format(java.util.Locale.US, "%.1f KB", bytes / 1024.0);
} else if (bytes < 1024L * 1024 * 1024) {
- return String.format("%.1f MB", bytes / (1024.0 * 1024));
+ return String.format(java.util.Locale.US, "%.1f MB", bytes / (1024.0 * 1024));
} else {
- return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
+ return String.format(java.util.Locale.US, "%.1f GB", bytes / (1024.0 * 1024 * 1024));
}
}
static String formatNumber(long num) {
- return String.format("%,d", num);
+ return String.format(java.util.Locale.US, "%,d", num);
}
static class HeapEntry {
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 66f6e776b248..dcf7e153c0d8 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
@@ -356,7 +356,7 @@ class McpFacade {
KeyModifiers mods = KeyModifiers.of(ctrl, false, shift);
- KeyCode code = switch (remainder.toLowerCase(Locale.ROOT)) {
+ KeyCode code = switch (remainder.toLowerCase(Locale.US)) {
case "enter", "return" -> KeyCode.ENTER;
case "esc", "escape" -> KeyCode.ESCAPE;
case "tab" -> KeyCode.TAB;
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
index 9fe3b138075a..b0b33dc3574e 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
@@ -170,7 +170,7 @@ class TapeRecorder {
default -> null;
};
}
- String lower = key.toLowerCase(Locale.ROOT);
+ String lower = key.toLowerCase(Locale.US);
if (lower.startsWith("ctrl+")) {
return "Ctrl+" + key.substring(5);
@@ -189,14 +189,14 @@ class TapeRecorder {
}
if (lower.matches("f\\d{1,2}")) {
- return key.toUpperCase(Locale.ROOT);
+ return key.toUpperCase(Locale.US);
}
return null;
}
private static String capitalize(String s) {
- return s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1);
+ return s.substring(0, 1).toUpperCase(Locale.US) + s.substring(1);
}
static String formatSleep(long ms) {
@@ -205,7 +205,7 @@ class TapeRecorder {
}
if (ms >= 1000) {
double seconds = ms / 1000.0;
- String formatted = String.format(Locale.ROOT, "%.1f", seconds);
+ String formatted = String.format(Locale.US, "%.1f", seconds);
return formatted + "s";
}
long rounded = (ms / 100) * 100;