This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new d8023329b947 CAMEL-23870: Add heap histogram dev console, TUI panel,
CLI command, and MCP tool (#24364)
d8023329b947 is described below
commit d8023329b9474e80c588f9fe0c74959d0721b340
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 1 18:16:06 2026 +0200
CAMEL-23870: Add heap histogram dev console, TUI panel, CLI command, and
MCP tool (#24364)
* CAMEL-23870: Add HeapHistogram dev console and TUI panel
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23870: Add camel cmd heap-histogram CLI command
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: use Locale.US consistently across TUI and CLI for number formatting
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: compute heap histogram totals from visible entries only
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: add detail panel to heap histogram tab with package summary and
JAR origin
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: reset table selection when heap histogram filter changes
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: move heap histogram summary into table title
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: update heap histogram F1 help text and MCP description
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23870: add generated docs and metadata for heap-histogram command
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23870: add heap histogram MCP tool for camel ask
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23870: add generated doc page for heap-histogram CLI command
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23870: add generated catalog files for heap-histogram dev console
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* chore: fix TUI render tests to match CSS theme styles
Tests were asserting pre-CSS-theme colors (YELLOW for hint keys, GREEN
for success) but the CSS engine resolves to BLACK and LIGHT_GREEN.
Also fixed renderNoSelection tests to match current prompt text.
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../apache/camel/catalog/dev-consoles.properties | 1 +
.../camel/catalog/dev-consoles/heap-histogram.json | 15 +
.../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-jbang-cmd-heap-histogram.adoc | 29 +
.../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 1 +
.../camel/cli/connector/LocalCliConnector.java | 14 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../core/commands/action/CamelHeapHistogram.java | 243 ++++++++
.../dsl/jbang/core/commands/mcp/RuntimeTools.java | 10 +
.../dsl/jbang/core/commands/tui/FilesBrowser.java | 4 +-
.../jbang/core/commands/tui/HeapHistogramTab.java | 664 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/McpFacade.java | 8 +-
.../dsl/jbang/core/commands/tui/PopupManager.java | 26 +-
.../dsl/jbang/core/commands/tui/TabRegistry.java | 24 +-
.../dsl/jbang/core/commands/tui/TapeRecorder.java | 8 +-
.../commands/tui/MonitorContextRenderTest.java | 32 +-
.../core/commands/tui/OverviewTabRenderTest.java | 4 +-
23 files changed, 1288 insertions(+), 60 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index aff1673d5525..b714441d0ead 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -20,6 +20,7 @@ gcp-secrets
groovy
hashicorp-secrets
health
+heap-histogram
inflight
internal-tasks
java-security
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-histogram.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-histogram.json
new file mode 100644
index 000000000000..af0809c2bbf5
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/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/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..80487b3d5ba9
--- /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) {
+ continue;
+ }
+
+ 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/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
+|===
+
+
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-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-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 [...]
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..c58ae90c67d5
--- /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,243 @@
+/*
+ * 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()) {
+ 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 {
+ 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(java.util.Locale.US, "%.1f KB", bytes /
1024.0);
+ } else if (bytes < 1024L * 1024 * 1024) {
+ return String.format(java.util.Locale.US, "%.1f MB", bytes /
(1024.0 * 1024));
+ } else {
+ return String.format(java.util.Locale.US, "%.1f GB", bytes /
(1024.0 * 1024 * 1024));
+ }
+ }
+
+ static String formatNumber(long num) {
+ return String.format(java.util.Locale.US, "%,d", num);
+ }
+
+ private static class Row {
+ int num;
+ String className;
+ long instances;
+ long bytes;
+ }
+}
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. \
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
new file mode 100644
index 000000000000..1ceec94648a1
--- /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,664 @@
+/*
+ * 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.text.Text;
+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;
+ 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;
+ }
+
+ @Override
+ public void onTabSelected() {
+ String pid = ctx.selectedPid;
+ if (pid != null && !pid.equals(lastPid)) {
+ lastPid = pid;
+ allEntries = Collections.emptyList();
+ classpathEntries = Collections.emptyList();
+ }
+ if (allEntries.isEmpty()) {
+ loadHeapHistogram();
+ }
+ }
+
+ @Override
+ public void onIntegrationChanged() {
+ allEntries = Collections.emptyList();
+ classpathEntries = 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;
+ tableState.select(0);
+ 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(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.percentage(60), Constraint.fill())
+ .split(area);
+ renderTable(frame, chunks.get(0), visible);
+ renderDetail(frame, chunks.get(1), visible);
+ }
+
+ 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("")));
+ }
+
+ 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)
+ .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);
+ }
+
+ 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");
+ 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
+
+ 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. JDK classes show "JDK (built-in)"
+
+ ## 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 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 (className, instances, bytes) |
+ | S | Reverse sort order |
+ | f | Toggle filter (all / non-jdk / 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 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;
+ }
+ 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);
+ }
+
+ // 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";
+ } else if (bytes < 1024 * 1024) {
+ return String.format(java.util.Locale.US, "%.1f KB", bytes /
1024.0);
+ } else if (bytes < 1024L * 1024 * 1024) {
+ return String.format(java.util.Locale.US, "%.1f MB", bytes /
(1024.0 * 1024));
+ } else {
+ return String.format(java.util.Locale.US, "%.1f GB", bytes /
(1024.0 * 1024 * 1024));
+ }
+ }
+
+ static String formatNumber(long num) {
+ return String.format(java.util.Locale.US, "%,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..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
@@ -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,
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)"),
@@ -354,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/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();
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;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
index 875b1fd43e93..4ba1170134ba 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
@@ -46,8 +46,6 @@ class MonitorContextRenderTest {
String rendered = HealthTabRenderTest.bufferToString(buffer);
assertTrue(rendered.contains("No integration selected"),
"Should render 'No integration selected' in the block title");
- assertTrue(rendered.contains("Select an integration"),
- "Should render the selection prompt text");
}
@Test
@@ -59,38 +57,21 @@ class MonitorContextRenderTest {
MonitorContext.renderNoSelection(frame, area);
String rendered = HealthTabRenderTest.bufferToString(buffer);
- // The block title should appear in the buffer
assertTrue(rendered.contains("No integration selected"),
"Should render the block title");
- // The prompt text should appear somewhere in the buffer
- assertTrue(rendered.contains("Select an integration"),
- "Should render the prompt text");
}
@Test
- void renderNoSelectionPromptIsDimmed() {
- Rect area = new Rect(0, 0, 80, 10);
+ void renderNoSelectionShowsHintKeys() {
+ Rect area = new Rect(0, 0, 80, 20);
Buffer buffer = Buffer.empty(area);
Frame frame = Frame.forTesting(buffer);
MonitorContext.renderNoSelection(frame, area);
- // Check that the "Select" text cell has DIM modifier
- boolean foundDim = false;
- for (int y = 0; y < buffer.height(); y++) {
- for (int x = 0; x < buffer.width(); x++) {
- var cell = buffer.get(x, y);
- if ("S".equals(cell.symbol()) &&
cell.style().effectiveModifiers()
- .contains(dev.tamboui.style.Modifier.DIM)) {
- foundDim = true;
- break;
- }
- }
- if (foundDim) {
- break;
- }
- }
- assertTrue(foundDim, "Prompt text should use DIM modifier");
+ String rendered = HealthTabRenderTest.bufferToString(buffer);
+ assertTrue(rendered.contains("Overview"), "Should render the Overview
hint");
+ assertTrue(rendered.contains("Help"), "Should render the Help hint");
}
@Test
@@ -104,13 +85,12 @@ class MonitorContextRenderTest {
}
@Test
- void hintKeyUsesYellowBoldStyle() {
+ void hintKeyIsBoldStyled() {
List<Span> spans = new ArrayList<>();
MonitorContext.hint(spans, "s", "sort");
Span keySpan = spans.get(0);
assertTrue(keySpan.style().fg().isPresent(), "Key span should have a
foreground color");
- assertEquals(Color.YELLOW, keySpan.style().fg().get(), "Key span
should be YELLOW");
assertTrue(keySpan.style().effectiveModifiers().contains(dev.tamboui.style.Modifier.BOLD),
"Key span should be BOLD");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
index 22bc0fbd92e0..dd2f32e67a56 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
@@ -114,8 +114,8 @@ class OverviewTabRenderTest {
Frame frame = Frame.forTesting(buffer);
tab.render(frame, area);
- boolean foundGreen = TuiTestHelper.findCellWithColor(buffer, "R",
Color.GREEN);
- assertTrue(foundGreen, "Running status should be rendered in GREEN");
+ boolean foundGreen = TuiTestHelper.findCellWithColor(buffer, "R",
Color.LIGHT_GREEN);
+ assertTrue(foundGreen, "Running status should be rendered in
LIGHT_GREEN");
}
@Test