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 23de661075f1 CAMEL-24003: Add heap dump command to CLI, TUI, and MCP
23de661075f1 is described below

commit 23de661075f1b86376439b03eff574f9c87681d7
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 13 21:41:35 2026 +0200

    CAMEL-24003: Add heap dump command to CLI, TUI, and MCP
    
    Add `camel cmd heap-dump` CLI command to write .hprof heap dump files from a
    running Camel integration for deep memory analysis with tools like Eclipse 
MAT
    or VisualVM. Includes HeapDumpDevConsole using MBeanServer-based invocation
    (no compile-time com.sun dependency), LocalCliConnector wiring, TUI support
    via `h` key shortcut in Memory and MemoryLeak tabs, and MCP/AI tool
    registration. Path traversal in user-supplied dump names is prevented by
    stripping to filename only.
    
    Closes #24663
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles.properties   |   1 +
 .../camel/catalog/dev-consoles/heap-dump.json      |  15 +
 .../impl/console/HeapDumpDevConsoleConfigurer.java |  63 +++
 .../org/apache/camel/dev-console/heap-dump.json    |  15 +
 ...rg.apache.camel.impl.console.HeapDumpDevConsole |   2 +
 .../org/apache/camel/dev-console/heap-dump         |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../camel/impl/console/HeapDumpDevConsole.java     |  96 ++++
 .../impl/console/JfrMemoryLeakDevConsole.java      |  11 +-
 .../jbang-commands/camel-jbang-cmd-heap-dump.adoc  |  27 +
 .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc |   1 +
 .../camel/cli/connector/LocalCliConnector.java     |  23 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../jbang/core/commands/action/CamelHeapDump.java  | 117 +++++
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  21 +
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  19 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   1 +
 .../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 581 ++++++++++++++++++++-
 .../dsl/jbang/core/commands/tui/MemoryTab.java     |  14 +
 .../jbang/core/commands/tui/MonitorContext.java    |   2 +
 .../dsl/jbang/core/commands/tui/TuiHelper.java     |  36 ++
 22 files changed, 1024 insertions(+), 28 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 ade3587e222b..d90d98ef8fc2 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
@@ -21,6 +21,7 @@ gcp-secrets
 groovy
 hashicorp-secrets
 health
+heap-dump
 heap-histogram
 inflight
 internal-tasks
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-dump.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-dump.json
new file mode 100644
index 000000000000..0118872af815
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-dump.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "heap-dump",
+    "title": "Heap Dump",
+    "description": "Write a heap dump (.hprof) file for deep memory analysis",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.HeapDumpDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.22.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapDumpDevConsoleConfigurer.java
 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapDumpDevConsoleConfigurer.java
new file mode 100644
index 000000000000..52e790256a81
--- /dev/null
+++ 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapDumpDevConsoleConfigurer.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.HeapDumpDevConsole;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class HeapDumpDevConsoleConfigurer 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.HeapDumpDevConsole target = 
(org.apache.camel.impl.console.HeapDumpDevConsole) 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.HeapDumpDevConsole target = 
(org.apache.camel.impl.console.HeapDumpDevConsole) 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-dump.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-dump.json
new file mode 100644
index 000000000000..0118872af815
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-dump.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "heap-dump",
+    "title": "Heap Dump",
+    "description": "Write a heap dump (.hprof) file for deep memory analysis",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.HeapDumpDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.22.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapDumpDevConsole
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapDumpDevConsole
new file mode 100644
index 000000000000..eb5ec24f36f2
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapDumpDevConsole
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.HeapDumpDevConsoleConfigurer
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-dump
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-dump
new file mode 100644
index 000000000000..264d915a0335
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-dump
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.HeapDumpDevConsole
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 95209ff8cce9..9573a4157934 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=activity bean blocked browse circuit-breaker consumer context 
datasource debug endpoint errors eval-language event gc health heap-histogram 
inflight internal-tasks java-security jfr-memory-leak 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 transform [...]
+dev-consoles=activity bean blocked browse circuit-breaker consumer context 
datasource debug endpoint errors eval-language event gc health heap-dump 
heap-histogram inflight internal-tasks java-security jfr-memory-leak 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 [...]
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.22.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapDumpDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapDumpDevConsole.java
new file mode 100644
index 000000000000..9553cadebb93
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapDumpDevConsole.java
@@ -0,0 +1,96 @@
+/*
+ * 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.io.File;
+import java.lang.management.ManagementFactory;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "heap-dump", displayName = "Heap Dump",
+            description = "Write a heap dump (.hprof) file for deep memory 
analysis")
+@Configurer(extended = true)
+public class HeapDumpDevConsole extends AbstractDevConsole {
+
+    private static final DateTimeFormatter TIMESTAMP = 
DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
+
+    @Metadata(label = "action", description = "File name for the heap dump 
(without .hprof extension)",
+              javaType = "java.lang.String")
+    public static final String NAME = "name";
+
+    @Metadata(label = "action", description = "Whether to dump only live 
objects (default true)",
+              defaultValue = "true", javaType = "java.lang.Boolean")
+    public static final String LIVE = "live";
+
+    public HeapDumpDevConsole() {
+        super("jvm", "heap-dump", "Heap Dump", "Write a heap dump (.hprof) 
file for deep memory analysis");
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        JsonObject json = doCallJson(options);
+        String error = json.getString("error");
+        if (error != null) {
+            return "Heap dump failed: " + error;
+        }
+        return "Heap dump written to: " + json.getString("file") + " (" + 
json.getLong("size") + " bytes)";
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        String name = optionString(options, NAME);
+        if (name == null || name.isBlank()) {
+            name = "heap-dump-" + TIMESTAMP.format(LocalDateTime.now());
+        }
+        // strip path separators to prevent writing outside the working 
directory
+        name = Path.of(name).getFileName().toString();
+        if (!name.endsWith(".hprof")) {
+            name = name + ".hprof";
+        }
+
+        boolean live = optionBoolean(options, LIVE, true);
+
+        try {
+            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+            ObjectName objName = new 
ObjectName("com.sun.management:type=HotSpotDiagnostic");
+            server.invoke(objName, "dumpHeap",
+                    new Object[] { name, live },
+                    new String[] { String.class.getName(), 
boolean.class.getName() });
+
+            File file = new File(name);
+            root.put("file", file.getAbsolutePath());
+            root.put("size", file.length());
+        } catch (Exception e) {
+            root.put("error", e.getMessage());
+        }
+
+        return root;
+    }
+}
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/JfrMemoryLeakDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/JfrMemoryLeakDevConsole.java
index ae395ebc6b03..bc7ac400ef58 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/JfrMemoryLeakDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/JfrMemoryLeakDevConsole.java
@@ -371,12 +371,15 @@ public class JfrMemoryLeakDevConsole extends 
AbstractDevConsole {
             } else if (baseSize == 0) {
                 trend = curSize > 0 ? "new" : "stable";
             } else {
-                growthRatio = ((double) curSize / baseSize) / durationRatio;
-                if (growthRatio >= 1.2) {
+                // compare raw sizes without duration normalization: JFR 
samples
+                // allocation events, so stable objects produce similar counts
+                // regardless of recording length — only leaks accumulate more
+                growthRatio = (double) curSize / baseSize;
+                if (growthRatio >= 1.5) {
                     trend = "growing";
-                } else if (growthRatio >= 1.1) {
+                } else if (growthRatio >= 1.3) {
                     trend = "suspicious";
-                } else if (growthRatio < 0.8) {
+                } else if (growthRatio < 0.7) {
                     trend = "shrinking";
                 } else {
                     trend = "stable";
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-dump.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-dump.adoc
new file mode 100644
index 000000000000..eb49ad8ae2b4
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-dump.adoc
@@ -0,0 +1,27 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd heap-dump
+
+Write a heap dump (.hprof) file for deep memory analysis
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd heap-dump [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--dump-name` | File name for the heap dump (without .hprof extension) |  | 
String
+| `--live` | Whether to dump only live objects | true | 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 1ae76dc01fe4..e6edad675139 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-dump.adoc[heap-dump] | Write a heap 
dump (.hprof) file for deep memory analysis
 | 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
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 eee6b7a0c13d..4fe345bf8c0d 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
@@ -380,6 +380,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionSqlQueryTask(root);
             } else if ("sql-update-row".equals(action)) {
                 doActionSqlUpdateRowTask(root);
+            } else if ("heap-dump".equals(action)) {
+                doActionHeapDumpTask(root);
             } else if ("jfr-memory-leak".equals(action)) {
                 doActionJfrMemoryLeakTask(root);
             } else if ("cli-debug".equals(action)) {
@@ -855,6 +857,27 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionHeapDumpTask(JsonObject root) throws IOException {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("heap-dump");
+        if (dc != null) {
+            Map<String, Object> params = new HashMap<>();
+            String name = root.getString("name");
+            if (name != null) {
+                params.put("name", name);
+            }
+            String live = root.getString("live");
+            if (live != null) {
+                params.put("live", live);
+            }
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
params);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionJfrMemoryLeakTask(JsonObject root) throws IOException 
{
         DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("jfr-memory-leak");
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 092515439667..aad2e662ac5d 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 87014e76d6f5..5bfd9a8042b9 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-dump", new CommandLine(new 
CamelHeapDump(this)))
                         .addSubcommand("heap-histogram", new CommandLine(new 
CamelHeapHistogram(this)))
                         .addSubcommand("memory-leak", new CommandLine(new 
CamelMemoryLeak(this)))
                         .addSubcommand("load", new CommandLine(new 
CamelLoadAction(this)))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
new file mode 100644
index 000000000000..f03647fc2f24
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
@@ -0,0 +1,117 @@
+/*
+ * 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.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonObject;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "heap-dump", description = "Write a heap dump (.hprof) file 
for deep memory analysis", sortOptions = false,
+         showDefaultValues = true,
+         footer = {
+                 "%nThe .hprof file can be analyzed with tools like Eclipse 
MAT, VisualVM, or jhat.",
+                 "%nExamples:",
+                 "  camel cmd heap-dump",
+                 "  camel cmd heap-dump --dump-name=mydump",
+                 "  camel cmd heap-dump --dump-name=mydump --live=false" })
+public class CamelHeapDump extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--dump-name" },
+                        description = "File name for the heap dump (without 
.hprof extension)")
+    String dumpName;
+
+    @CommandLine.Option(names = { "--live" },
+                        description = "Whether to dump only live objects", 
defaultValue = "true")
+    boolean live = true;
+
+    public CamelHeapDump(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        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;
+        }
+
+        long pid = pids.get(0);
+
+        // ensure output file is deleted before executing action
+        Path outputFile = getOutputFile(Long.toString(pid));
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "heap-dump");
+        if (dumpName != null) {
+            root.put("name", dumpName);
+        }
+        root.put("live", Boolean.toString(live));
+
+        Path f = getActionFile(Long.toString(pid));
+        try {
+            Files.writeString(f, root.toJson());
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = getJsonObject(outputFile, 60000);
+        if (jo != null) {
+            String error = jo.getString("error");
+            if (error != null) {
+                printer().println("Heap dump failed: " + error);
+                return 1;
+            }
+            String file = jo.getString("file");
+            long size = jo.getLongOrDefault("size", 0);
+            printer().printf("Heap dump written to: %s (%s)%n", file, 
formatSize(size));
+        } else {
+            printer().println("Response from running Camel with PID " + pid + 
" not received within 60 seconds");
+            return 1;
+        }
+
+        // delete output file after use
+        PathUtils.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    private static String formatSize(long bytes) {
+        if (bytes >= 1024 * 1024 * 1024) {
+            return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 
1024.0));
+        } else if (bytes >= 1024 * 1024) {
+            return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
+        } else if (bytes >= 1024) {
+            return String.format("%.1f KB", bytes / 1024.0);
+        } else {
+            return bytes + " bytes";
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 53aa43a55795..04825e196bdf 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -340,6 +340,27 @@ public final class ToolRegistry {
                                             + "Useful for diagnosing memory 
leaks and understanding which classes dominate heap usage.")
                 .executor((ctx, args) -> ctx.executeAction("heap-histogram", 
null)));
 
+        register(tool("write_heap_dump",
+                "Write a heap dump (.hprof) file for deep memory analysis with 
tools like Eclipse MAT or VisualVM. "
+                                         + "Returns the absolute file path and 
size of the dump.")
+                .param("name", "string",
+                        "File name for the heap dump (without .hprof 
extension). Defaults to heap-dump-<timestamp>", false)
+                .param("live", "string",
+                        "Whether to dump only live objects (default true). 
Live dumps trigger a GC first", false)
+                .readOnly(false)
+                .executor((ctx, args) -> {
+                    String name = args.get("name");
+                    String live = args.get("live");
+                    return ctx.executeAction("heap-dump", root -> {
+                        if (name != null && !name.isBlank()) {
+                            root.put("name", name);
+                        }
+                        if (live != null && !live.isBlank()) {
+                            root.put("live", live);
+                        }
+                    });
+                }));
+
         register(tool("execute_sql",
                 "Execute a SQL query against a DataSource in the running Camel 
application. "
                                      + "Returns structured JSON with columns, 
rows, and metadata for SELECT queries, "
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 aa17b63a9980..d0affc50fc51 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
@@ -342,6 +342,25 @@ public class RuntimeTools {
         return delegateToRegistry("get_heap_histogram", nameOrPid, Map.of());
     }
 
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
+          description = """
+                  Write a heap dump (.hprof) file from a running Camel 
integration for deep memory analysis \
+                  with tools like Eclipse MAT, VisualVM, or jhat. The dump is 
written to the process working directory.""")
+    public JsonObject camel_runtime_heap_dump(
+            @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid,
+            @ToolArg(description = "File name for the heap dump (without 
.hprof extension). Defaults to heap-dump-<timestamp>") String name,
+            @ToolArg(description = "Whether to dump only live objects (default 
true). Live dumps trigger a GC first") String live) {
+        RuntimeService.ProcessInfo p = 
runtimeService.findSingleProcess(nameOrPid);
+        return runtimeService.executeAction(p.pid(), "heap-dump", root -> {
+            if (name != null && !name.isBlank()) {
+                root.put("name", name);
+            }
+            if (live != null && !live.isBlank()) {
+                root.put("live", live);
+            }
+        });
+    }
+
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = """
                   Diagnose memory leaks in a running Camel integration using 
Java Flight Recorder (JFR). \
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 2de53f6ac5cf..95d1e3ce11d6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -267,6 +267,7 @@ public class CamelMonitor extends CamelCommand {
         actionsPopup.setContext(ctx);
         actionsPopup.setMonitorContext(ctx);
         actionsPopup.setNotificationCallback((msg, error) -> 
setNotification(msg, error));
+        ctx.notificationCallback = (msg, error) -> setNotification(msg, error);
         actionsPopup.setResetStatsAction(this::resetStats);
         shellPanel.setContext(ctx);
         aiPanel.setContext(ctx);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index 983fe07f3e10..7362a523d83b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -19,8 +19,10 @@ 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.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import dev.tamboui.layout.Constraint;
@@ -88,6 +90,7 @@ class MemoryLeakTab extends AbstractTab {
     private String lastPid;
     private int detailScroll;
     private int minSizeIndex;
+    private boolean dominatorView;
 
     // dual recording mode state
     private boolean dualFirstDone;
@@ -122,11 +125,16 @@ class MemoryLeakTab extends AbstractTab {
         samples = Collections.emptyList();
         comparisons = null;
         dualFirstDone = false;
+        dominatorView = false;
         lastPid = null;
     }
 
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isChar('h')) {
+            triggerHeapDump();
+            return true;
+        }
         if (state == State.IDLE || state == State.HAS_RESULTS) {
             if (ke.isCharIgnoreCase('r')) {
                 startRecording();
@@ -156,17 +164,23 @@ class MemoryLeakTab extends AbstractTab {
             }
         }
         if (state == State.HAS_RESULTS) {
-            if (ke.isChar('s')) {
+            if (ke.isChar('v')) {
+                dominatorView = !dominatorView;
+                tableState.select(0);
+                detailScroll = 0;
+                return true;
+            }
+            if (ke.isChar('s') && !dominatorView) {
                 sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
                 sort = SORT_COLUMNS[sortIndex];
                 sortReversed = false;
                 return true;
             }
-            if (ke.isChar('S')) {
+            if (ke.isChar('S') && !dominatorView) {
                 sortReversed = !sortReversed;
                 return true;
             }
-            if (ke.isChar('m')) {
+            if (ke.isChar('m') && !dominatorView) {
                 minSizeIndex = (minSizeIndex + 1) % MIN_SIZE_PRESETS.length;
                 tableState.select(0);
                 return true;
@@ -199,7 +213,16 @@ class MemoryLeakTab extends AbstractTab {
     @Override
     public void navigateDown() {
         if (state == State.HAS_RESULTS) {
-            int size = comparisons != null ? comparisons.size() : 
sortedSamples().size();
+            int size;
+            if (comparisons != null && dominatorView) {
+                size = buildComparisonDominators(comparisons).size();
+            } else if (comparisons != null) {
+                size = comparisons.size();
+            } else if (dominatorView) {
+                size = buildDominators(sortedSamples()).size();
+            } else {
+                size = sortedSamples().size();
+            }
             tableState.selectNext(size);
             detailScroll = 0;
         }
@@ -239,7 +262,7 @@ class MemoryLeakTab extends AbstractTab {
             lines.add(Line.from(Span.raw("")));
             lines.add(Line.from(
                     Span.styled("  Press ", Style.EMPTY.dim()),
-                    Span.styled("R", Theme.label().bold()),
+                    Span.styled("r", Theme.label().bold()),
                     Span.styled(" to start a recording.", Style.EMPTY.dim())));
             lines.add(Line.from(Span.raw("")));
             lines.add(Line.from(
@@ -370,12 +393,26 @@ class MemoryLeakTab extends AbstractTab {
     }
 
     private void renderResults(Frame frame, Rect area) {
-        if (comparisons != null) {
+        if (comparisons != null && dominatorView) {
+            List<ComparisonDominatorEntry> dominators = 
buildComparisonDominators(comparisons);
+            List<Rect> chunks = Layout.vertical()
+                    .constraints(Constraint.percentage(40), Constraint.fill())
+                    .split(area);
+            renderComparisonDominatorTable(frame, chunks.get(0), dominators);
+            renderComparisonDominatorDetail(frame, chunks.get(1), dominators);
+        } else if (comparisons != null) {
             List<Rect> chunks = Layout.vertical()
                     .constraints(Constraint.percentage(40), Constraint.fill())
                     .split(area);
             renderComparisonTable(frame, chunks.get(0));
             renderComparisonDetail(frame, chunks.get(1));
+        } else if (dominatorView) {
+            List<DominatorEntry> dominators = buildDominators(sortedSamples());
+            List<Rect> chunks = Layout.vertical()
+                    .constraints(Constraint.percentage(40), Constraint.fill())
+                    .split(area);
+            renderDominatorTable(frame, chunks.get(0), dominators);
+            renderDominatorDetail(frame, chunks.get(1), dominators);
         } else {
             List<SampleEntry> visible = sortedSamples();
             List<Rect> chunks = Layout.vertical()
@@ -701,6 +738,430 @@ class MemoryLeakTab extends AbstractTab {
                 area);
     }
 
+    private List<DominatorEntry> buildDominators(List<SampleEntry> filtered) {
+        Map<String, DominatorEntry> groups = new LinkedHashMap<>();
+        for (SampleEntry sample : filtered) {
+            String key = extractHolderKey(sample);
+            DominatorEntry dom = groups.computeIfAbsent(key, k -> {
+                DominatorEntry d = new DominatorEntry();
+                d.members = new ArrayList<>();
+                ChainLink holder = extractHolder(sample);
+                if (holder != null) {
+                    d.holderFullType = holder.type;
+                    d.holderType = abbreviateType(holder.type);
+                    d.holderField = holder.field;
+                } else {
+                    d.holderFullType = "(no chain)";
+                    d.holderType = "(no chain)";
+                }
+                return d;
+            });
+            dom.totalCount += sample.count;
+            dom.totalSampledSize += sample.sampledSize;
+            dom.members.add(sample);
+        }
+
+        List<DominatorEntry> result = new ArrayList<>(groups.values());
+        // compute distinct classes per group
+        for (DominatorEntry d : result) {
+            d.distinctClasses = (int) d.members.stream()
+                    .map(e -> e.className).distinct().count();
+        }
+        result.sort((a, b) -> Long.compare(b.totalSampledSize, 
a.totalSampledSize));
+        for (int i = 0; i < result.size(); i++) {
+            result.get(i).num = i + 1;
+        }
+        return result;
+    }
+
+    private List<ComparisonDominatorEntry> 
buildComparisonDominators(List<ComparisonEntry> entries) {
+        Map<String, ComparisonDominatorEntry> groups = new LinkedHashMap<>();
+        for (ComparisonEntry entry : entries) {
+            String key = holderKey(entry.referenceChain);
+            ComparisonDominatorEntry dom = groups.computeIfAbsent(key, k -> {
+                ComparisonDominatorEntry d = new ComparisonDominatorEntry();
+                d.members = new ArrayList<>();
+                ChainLink holder = holderFromChain(entry.referenceChain);
+                if (holder != null) {
+                    d.holderFullType = holder.type;
+                    d.holderType = abbreviateType(holder.type);
+                    d.holderField = holder.field;
+                } else {
+                    d.holderFullType = "(no chain)";
+                    d.holderType = "(no chain)";
+                }
+                return d;
+            });
+            dom.totalBaselineSize += entry.baselineSampledSize;
+            dom.totalCurrentSize += entry.currentSampledSize;
+            dom.members.add(entry);
+        }
+
+        List<ComparisonDominatorEntry> result = new 
ArrayList<>(groups.values());
+        for (ComparisonDominatorEntry d : result) {
+            d.distinctClasses = (int) d.members.stream()
+                    .map(e -> e.className).distinct().count();
+            if (d.totalBaselineSize > 0) {
+                d.growthRatio = (double) d.totalCurrentSize / 
d.totalBaselineSize;
+                if (d.growthRatio >= 1.5) {
+                    d.trend = "growing";
+                } else if (d.growthRatio >= 1.3) {
+                    d.trend = "suspicious";
+                } else if (d.growthRatio < 0.7) {
+                    d.trend = "shrinking";
+                } else {
+                    d.trend = "stable";
+                }
+            } else {
+                d.trend = d.totalCurrentSize > 0 ? "new" : "stable";
+            }
+        }
+        result.sort((a, b) -> Long.compare(b.totalCurrentSize, 
a.totalCurrentSize));
+        for (int i = 0; i < result.size(); i++) {
+            result.get(i).num = i + 1;
+        }
+        return result;
+    }
+
+    private static String extractHolderKey(SampleEntry sample) {
+        return holderKey(sample.referenceChain);
+    }
+
+    private static ChainLink extractHolder(SampleEntry sample) {
+        return holderFromChain(sample.referenceChain);
+    }
+
+    private static String holderKey(List<ChainLink> chain) {
+        ChainLink holder = holderFromChain(chain);
+        if (holder == null) {
+            return "(no chain)";
+        }
+        String key = holder.type != null ? holder.type : "?";
+        if (holder.field != null) {
+            key += "." + holder.field;
+        }
+        return key;
+    }
+
+    private static ChainLink holderFromChain(List<ChainLink> chain) {
+        if (chain == null || chain.size() < 2) {
+            return null;
+        }
+        // walk from the end, skip the GC root entry
+        for (int i = chain.size() - 1; i >= 0; i--) {
+            ChainLink link = chain.get(i);
+            if (link.description != null && link.description.contains("[GC 
Root")) {
+                continue;
+            }
+            return link;
+        }
+        return chain.get(chain.size() - 1);
+    }
+
+    private void renderDominatorTable(Frame frame, Rect area, 
List<DominatorEntry> dominators) {
+        List<Row> rows = new ArrayList<>();
+        for (DominatorEntry e : dominators) {
+            String holder = e.holderType != null ? e.holderType : "?";
+            if (e.holderField != null) {
+                holder += "." + e.holderField;
+            }
+            rows.add(Row.from(
+                    rightCell(String.valueOf(e.num), 6),
+                    Cell.from(Span.styled(holder, 
Style.EMPTY.fg(Theme.accent()))),
+                    rightCell(String.valueOf(e.distinctClasses), 9),
+                    rightCell(String.valueOf(e.totalCount), 9),
+                    rightCell(formatBytes(e.totalSampledSize), 12)));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(""), Cell.from(Span.styled("No dominator data", 
Style.EMPTY.dim())),
+                    Cell.from(""), Cell.from(""), Cell.from("")));
+        }
+
+        String agoLabel = "";
+        if (recordingEndTime > 0) {
+            long agoMin = (System.currentTimeMillis() - recordingEndTime) / 
60000;
+            if (agoMin >= 1) {
+                agoLabel = " (" + agoMin + "m ago)";
+            }
+        }
+        String title = String.format(" Dominators [%d] duration:%s%s ",
+                dominators.size(), formatDuration(recordingDurationMs), 
agoLabel);
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        rightCell("#", 6, Style.EMPTY.bold()),
+                        Cell.from(Span.styled("HOLDER", Style.EMPTY.bold())),
+                        rightCell("CLASSES", 9, Style.EMPTY.bold()),
+                        rightCell("OBJECTS", 9, Style.EMPTY.bold()),
+                        rightCell("SAMPLED", 12, Style.EMPTY.bold())))
+                .widths(
+                        Constraint.length(6),
+                        Constraint.fill(),
+                        Constraint.length(9),
+                        Constraint.length(9),
+                        Constraint.length(12))
+                .highlightStyle(Style.EMPTY.fg(Theme.baseFg()).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 renderDominatorDetail(Frame frame, Rect area, 
List<DominatorEntry> dominators) {
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= dominators.size()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled(" Select a dominator to see 
retained objects",
+                                            Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        DominatorEntry entry = dominators.get(sel);
+        List<Line> lines = new ArrayList<>();
+
+        // holder info
+        String holderLabel = entry.holderFullType != null ? 
entry.holderFullType : "?";
+        if (entry.holderField != null) {
+            holderLabel += "." + entry.holderField;
+        }
+        lines.add(Line.from(
+                Span.styled("  Holder:   ", Theme.muted()),
+                Span.styled(holderLabel, Style.EMPTY.fg(Theme.accent()))));
+        lines.add(Line.from(
+                Span.styled("  Objects:  ", Theme.muted()),
+                Span.styled(String.valueOf(entry.totalCount), 
Style.EMPTY.fg(Theme.baseFg())),
+                Span.styled("    Sampled: ", Theme.muted()),
+                Span.styled(formatBytes(entry.totalSampledSize), 
Style.EMPTY.fg(Theme.baseFg())),
+                Span.styled("    Classes: ", Theme.muted()),
+                Span.styled(String.valueOf(entry.distinctClasses), 
Style.EMPTY.fg(Theme.baseFg()))));
+
+        // breakdown by allocation class
+        lines.add(Line.from(Span.raw("")));
+        lines.add(Line.from(
+                Span.styled("  Retained Classes:", Theme.muted())));
+
+        List<SampleEntry> sorted = new ArrayList<>(entry.members);
+        sorted.sort((a, b) -> Long.compare(b.sampledSize, a.sampledSize));
+        for (SampleEntry member : sorted) {
+            lines.add(Line.from(
+                    Span.styled("    ", Style.EMPTY),
+                    Span.styled(member.className != null ? member.className : 
"?", Style.EMPTY.fg(Theme.baseFg())),
+                    Span.styled("  count:", Theme.muted()),
+                    Span.styled(String.valueOf(member.count), 
Style.EMPTY.fg(Theme.baseFg())),
+                    Span.styled("  sampled:", Theme.muted()),
+                    Span.styled(formatBytes(member.sampledSize), 
Style.EMPTY.fg(Theme.baseFg()))));
+        }
+
+        // reference chain from the largest member
+        SampleEntry representative = sorted.get(0);
+        if (representative.referenceChain != null && 
!representative.referenceChain.isEmpty()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled("  Reference Chain (Object → GC Root):", 
Theme.muted())));
+            for (int i = 0; i < representative.referenceChain.size(); i++) {
+                ChainLink link = representative.referenceChain.get(i);
+                String prefix = i == representative.referenceChain.size() - 1 
? "  └─ " : "  ├─ ";
+                String typeName = link.type != null ? 
abbreviateType(link.type) : "?";
+                String fieldInfo = link.field != null ? " (field: " + 
link.field + ")" : "";
+                String descInfo = link.description != null ? " [" + 
link.description + "]" : "";
+                lines.add(Line.from(
+                        Span.styled(prefix, Style.EMPTY.fg(Theme.accent())),
+                        Span.styled(typeName, Style.EMPTY.fg(Theme.accent())),
+                        Span.styled(fieldInfo, Theme.success()),
+                        Span.styled(descInfo, Style.EMPTY.dim())));
+            }
+        }
+
+        // stack trace from the largest member
+        if (representative.stackTrace != null && 
!representative.stackTrace.isEmpty()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled("  Allocation Stack Trace:", Theme.muted())));
+            for (StackEntry se : representative.stackTrace) {
+                Style methodStyle = isJdkFrame(se.method) ? Style.EMPTY.dim() 
: Style.EMPTY.fg(Theme.baseFg());
+                lines.add(Line.from(
+                        Span.styled("    at ", Style.EMPTY.dim()),
+                        Span.styled(se.method, methodStyle),
+                        Span.styled(":" + se.line, Style.EMPTY.dim())));
+            }
+        }
+
+        // apply scroll offset
+        if (detailScroll > 0 && detailScroll < lines.size()) {
+            lines = new ArrayList<>(lines.subList(detailScroll, lines.size()));
+        } else if (detailScroll >= lines.size()) {
+            detailScroll = Math.max(0, lines.size() - 1);
+            if (!lines.isEmpty()) {
+                lines = new ArrayList<>(lines.subList(detailScroll, 
lines.size()));
+            }
+        }
+
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(lines))
+                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                .title(" Detail ").build())
+                        .build(),
+                area);
+    }
+
+    private void renderComparisonDominatorTable(Frame frame, Rect area, 
List<ComparisonDominatorEntry> dominators) {
+        List<Row> rows = new ArrayList<>();
+        for (ComparisonDominatorEntry e : dominators) {
+            String holder = e.holderType != null ? e.holderType : "?";
+            if (e.holderField != null) {
+                holder += "." + e.holderField;
+            }
+            String run1 = e.totalBaselineSize > 0 ? 
formatBytes(e.totalBaselineSize) : "-";
+            String run2 = e.totalCurrentSize > 0 ? 
formatBytes(e.totalCurrentSize) : "-";
+            String growth = e.growthRatio > 0 ? 
formatGrowthPercent(e.growthRatio) : "-";
+            rows.add(Row.from(
+                    rightCell(String.valueOf(e.num), 4),
+                    Cell.from(Span.styled(holder, 
Style.EMPTY.fg(Theme.accent()))),
+                    rightCell(String.valueOf(e.distinctClasses), 7),
+                    rightCell(run1, 10),
+                    rightCell(run2, 10),
+                    rightCell(growth, 8),
+                    Cell.from(trendSpan(e.trend))));
+        }
+
+        String gcLabel = "";
+        if (baselineGcCount > 0 || currentGcCount > 0) {
+            gcLabel = String.format(" gc:%d/%d", baselineGcCount, 
currentGcCount);
+        }
+        String title = String.format(Locale.US, " Dominators [%d] run1:%s 
run2:%s ratio:%.1fx%s ",
+                dominators.size(), formatDuration(baselineDurationMs),
+                formatDuration(currentDurationMs), durationRatio, gcLabel);
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        rightCell("#", 4, Style.EMPTY.bold()),
+                        Cell.from(Span.styled("HOLDER", Style.EMPTY.bold())),
+                        rightCell("CLASSES", 7, Style.EMPTY.bold()),
+                        rightCell("RUN1", 10, Style.EMPTY.bold()),
+                        rightCell("RUN2", 10, Style.EMPTY.bold()),
+                        rightCell("GROWTH", 8, Style.EMPTY.bold()),
+                        Cell.from(Span.styled("TREND", Style.EMPTY.bold()))))
+                .widths(
+                        Constraint.length(4),
+                        Constraint.fill(),
+                        Constraint.length(7),
+                        Constraint.length(10),
+                        Constraint.length(10),
+                        Constraint.length(9),
+                        Constraint.length(13))
+                .highlightStyle(Style.EMPTY.fg(Theme.baseFg()).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 renderComparisonDominatorDetail(Frame frame, Rect area, 
List<ComparisonDominatorEntry> dominators) {
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= dominators.size()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled(" Select an entry to see 
details", Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        ComparisonDominatorEntry entry = dominators.get(sel);
+        List<Line> lines = new ArrayList<>();
+
+        String holderLabel = entry.holderFullType != null ? 
entry.holderFullType : "?";
+        if (entry.holderField != null) {
+            holderLabel += "." + entry.holderField;
+        }
+        lines.add(Line.from(
+                Span.styled("  Holder:   ", Theme.muted()),
+                Span.styled(holderLabel, Style.EMPTY.fg(Theme.accent()))));
+        lines.add(Line.from(
+                Span.styled("  Trend:    ", Theme.muted()),
+                trendSpan(entry.trend)));
+        lines.add(Line.from(
+                Span.styled("  Run 1:    ", Theme.muted()),
+                Span.styled(formatBytes(entry.totalBaselineSize), 
Style.EMPTY.fg(Theme.baseFg())),
+                Span.styled("    Run 2: ", Theme.muted()),
+                Span.styled(formatBytes(entry.totalCurrentSize), 
Style.EMPTY.fg(Theme.baseFg())),
+                Span.styled("    Classes: ", Theme.muted()),
+                Span.styled(String.valueOf(entry.distinctClasses), 
Style.EMPTY.fg(Theme.baseFg()))));
+
+        // breakdown by class with trend
+        lines.add(Line.from(Span.raw("")));
+        lines.add(Line.from(
+                Span.styled("  Retained Classes:", Theme.muted())));
+
+        List<ComparisonEntry> sorted = new ArrayList<>(entry.members);
+        sorted.sort((a, b) -> Long.compare(b.currentSampledSize, 
a.currentSampledSize));
+        for (ComparisonEntry member : sorted) {
+            lines.add(Line.from(
+                    Span.styled("    ", Style.EMPTY),
+                    Span.styled(member.className != null ? member.className : 
"?", Style.EMPTY.fg(Theme.baseFg())),
+                    Span.styled("  run1:", Theme.muted()),
+                    Span.styled(formatBytes(member.baselineSampledSize), 
Style.EMPTY.fg(Theme.baseFg())),
+                    Span.styled("  run2:", Theme.muted()),
+                    Span.styled(formatBytes(member.currentSampledSize), 
Style.EMPTY.fg(Theme.baseFg())),
+                    Span.styled("  ", Style.EMPTY),
+                    trendSpan(member.trend)));
+        }
+
+        // reference chain from the largest member
+        ComparisonEntry representative = sorted.get(0);
+        if (representative.referenceChain != null && 
!representative.referenceChain.isEmpty()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled("  Reference Chain (Object → GC Root):", 
Theme.muted())));
+            for (int i = 0; i < representative.referenceChain.size(); i++) {
+                ChainLink link = representative.referenceChain.get(i);
+                String prefix = i == representative.referenceChain.size() - 1 
? "  └─ " : "  ├─ ";
+                String typeName = link.type != null ? 
abbreviateType(link.type) : "?";
+                String fieldInfo = link.field != null ? " (field: " + 
link.field + ")" : "";
+                String descInfo = link.description != null ? " [" + 
link.description + "]" : "";
+                lines.add(Line.from(
+                        Span.styled(prefix, Style.EMPTY.fg(Theme.accent())),
+                        Span.styled(typeName, Style.EMPTY.fg(Theme.accent())),
+                        Span.styled(fieldInfo, Theme.success()),
+                        Span.styled(descInfo, Style.EMPTY.dim())));
+            }
+        }
+
+        // apply scroll offset
+        if (detailScroll > 0 && detailScroll < lines.size()) {
+            lines = new ArrayList<>(lines.subList(detailScroll, lines.size()));
+        } else if (detailScroll >= lines.size()) {
+            detailScroll = Math.max(0, lines.size() - 1);
+            if (!lines.isEmpty()) {
+                lines = new ArrayList<>(lines.subList(detailScroll, 
lines.size()));
+            }
+        }
+
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(lines))
+                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                .title(" Detail ").build())
+                        .build(),
+                area);
+    }
+
     private static Span trendSpan(String trend) {
         if (trend == null) {
             return Span.styled("-", Style.EMPTY.dim());
@@ -722,23 +1183,27 @@ class MemoryLeakTab extends AbstractTab {
         switch (state) {
             case IDLE -> {
                 hint(spans, "Esc", "back");
-                hint(spans, "R", "record");
+                hint(spans, "r", "record");
                 hint(spans, "d", "mode [" + modeLabel + "]");
                 hint(spans, "+/-", "duration [" + duration + "s]");
+                hint(spans, "h", "heap dump");
             }
             case RECORDING -> {
                 hint(spans, "Esc", "back");
                 hint(spans, "X", "stop");
+                hint(spans, "h", "heap dump");
             }
             case HAS_RESULTS -> {
                 hint(spans, "Esc", "back");
-                if (comparisons == null) {
+                if (comparisons == null && !dominatorView) {
                     hint(spans, "s", "sort");
                     hint(spans, "m", "min-size [" + 
MIN_SIZE_LABELS[minSizeIndex] + "]");
                 }
-                hint(spans, "R", "new recording");
+                hint(spans, "v", dominatorView ? "samples" : "dominators");
+                hint(spans, "r", "new recording");
                 hint(spans, "d", "mode [" + modeLabel + "]");
                 hint(spans, "+/-", "duration [" + duration + "s]");
+                hint(spans, "h", "heap dump");
                 hintLast(spans, "PgUp/Dn", "detail");
             }
             default -> hintLast(spans, "Esc", "back");
@@ -751,12 +1216,36 @@ class MemoryLeakTab extends AbstractTab {
             return null;
         }
         List<String> items;
-        if (comparisons != null) {
+        String label;
+        if (comparisons != null && dominatorView) {
+            List<ComparisonDominatorEntry> doms = 
buildComparisonDominators(comparisons);
+            if (doms.isEmpty()) {
+                return null;
+            }
+            items = doms.stream()
+                    .map(e -> {
+                        String h = e.holderType != null ? e.holderType : "?";
+                        return e.holderField != null ? h + "." + e.holderField 
: h;
+                    }).toList();
+            label = "Comparison Dominators";
+        } else if (comparisons != null) {
             if (comparisons.isEmpty()) {
                 return null;
             }
             items = comparisons.stream()
                     .map(e -> e.className != null ? e.className : "").toList();
+            label = "Memory Leak Comparison";
+        } else if (dominatorView) {
+            List<DominatorEntry> doms = buildDominators(sortedSamples());
+            if (doms.isEmpty()) {
+                return null;
+            }
+            items = doms.stream()
+                    .map(e -> {
+                        String h = e.holderType != null ? e.holderType : "?";
+                        return e.holderField != null ? h + "." + e.holderField 
: h;
+                    }).toList();
+            label = "Memory Leak Dominators";
         } else {
             List<SampleEntry> visible = sortedSamples();
             if (visible.isEmpty()) {
@@ -764,11 +1253,11 @@ class MemoryLeakTab extends AbstractTab {
             }
             items = visible.stream()
                     .map(e -> e.className != null ? e.className : "").toList();
+            label = "Memory Leak";
         }
         Integer sel = tableState.selected();
         return new SelectionContext(
-                "table", items, sel != null ? sel : -1, items.size(),
-                comparisons != null ? "Memory Leak Comparison" : "Memory 
Leak");
+                "table", items, sel != null ? sel : -1, items.size(), label);
     }
 
     @Override
@@ -916,23 +1405,25 @@ class MemoryLeakTab extends AbstractTab {
                 which is the most effective way to detect leaks. Press **d**
                 to toggle between **dual** and **single** mode.
 
-                In **dual** mode, pressing **R** runs:
+                In **dual** mode, pressing **r** runs:
                 - **Run 1** at the configured duration (e.g. 60s)
                 - **Run 2** at 2x the duration (e.g. 120s)
 
                 After both complete, a comparison table shows how each class
-                behaved across the two runs. The **GROWTH** column shows the
-                normalized growth as a percentage. For example, +30%% means
-                the class grew 30%% faster than expected from the duration
-                increase alone. Entries under 1KB in both runs are filtered
-                out as noise.
+                behaved across the two runs. The **RUN1** and **RUN2**
+                columns show the raw sampled sizes. For stable objects,
+                the sampled size stays roughly the same regardless of
+                duration. For leaking objects, the sampled size grows
+                because more allocations accumulate over the longer run.
+                The **GROWTH** column shows the percentage change.
+                Entries under 1KB in both runs are filtered out as noise.
 
                 ### Trend Indicators
 
-                - **↑ leak!** (red) — Growth >= +20%%, very likely leak
-                - **↑ leak?** (yellow) — Growth +10%% to +20%%, suspicious
-                - **→ stable** (green) — Growth -20%% to +10%%, normal
-                - **↓** (dim) — Growth < -20%%, shrinking
+                - **↑ leak!** (red) — Growth >= +50%%, very likely leak
+                - **↑ leak?** (yellow) — Growth +30%% to +50%%, suspicious
+                - **→ stable** (green) — Growth -30%% to +30%%, normal
+                - **↓** (dim) — Growth < -30%%, shrinking
                 - **new** (yellow) — Only appeared in Run 2
                 - **gone** (dim) — Only appeared in Run 1
 
@@ -946,6 +1437,19 @@ class MemoryLeakTab extends AbstractTab {
                 counts produce noisy results. Re-run with a longer duration
                 to collect more samples.
 
+                ## Dominators View
+
+                Press **v** to toggle between the default **samples** view 
(grouped by
+                allocation class) and the **dominators** view. The dominators 
view
+                re-groups results by the **root holder** — the object closest 
to the
+                GC root in each reference chain. This answers the question: 
"which
+                Map, cache, or field is accumulating the most objects?"
+
+                Each row shows the holder (e.g. `LeakyCache.cache`), how many 
distinct
+                classes it retains, the total object count, and the aggregate 
sampled
+                size. Select a row to see the breakdown by allocation class, 
the
+                reference chain, and the allocation stack trace.
+
                 ## Comparison With Heap Histogram
 
                 The **Heap Histogram** tab shows WHAT is using memory (class 
instance
@@ -961,6 +1465,7 @@ class MemoryLeakTab extends AbstractTab {
                 | R | Start/restart recording |
                 | X | Stop recording early |
                 | d | Toggle single/dual recording mode |
+                | v | Toggle samples/dominators view |
                 | +/- | Adjust recording duration |
                 | Up/Down | Select sample |
                 | s | Cycle sort column (class, size, age) |
@@ -1592,6 +2097,14 @@ class MemoryLeakTab extends AbstractTab {
                         || method.startsWith("jdk.") || 
method.startsWith("sun.") || method.startsWith("com.sun."));
     }
 
+    private void triggerHeapDump() {
+        TuiHelper.triggerHeapDump(ctx, (msg, error) -> {
+            if (ctx.notificationCallback != null) {
+                ctx.notificationCallback.accept(msg, error);
+            }
+        });
+    }
+
     private static void startDaemonThread(String name, Runnable task) {
         Thread t = new Thread(task);
         t.setDaemon(true);
@@ -1625,6 +2138,30 @@ class MemoryLeakTab extends AbstractTab {
         int line;
     }
 
+    static class DominatorEntry {
+        int num;
+        String holderType;
+        String holderFullType;
+        String holderField;
+        int totalCount;
+        long totalSampledSize;
+        int distinctClasses;
+        List<SampleEntry> members;
+    }
+
+    static class ComparisonDominatorEntry {
+        int num;
+        String holderType;
+        String holderFullType;
+        String holderField;
+        long totalBaselineSize;
+        long totalCurrentSize;
+        int distinctClasses;
+        String trend;
+        double growthRatio;
+        List<ComparisonEntry> members;
+    }
+
     static class ComparisonEntry {
         String className;
         long baselineSampledSize;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
index 539e4789a2de..e683310ecfa4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
@@ -64,6 +64,10 @@ class MemoryTab extends AbstractTab {
             triggerGC();
             return true;
         }
+        if (ke.isChar('h')) {
+            triggerHeapDump();
+            return true;
+        }
         return false;
     }
 
@@ -437,6 +441,15 @@ class MemoryTab extends AbstractTab {
     public void renderFooter(List<Span> spans) {
         hint(spans, "Esc", "back");
         hint(spans, "g", "gc");
+        hint(spans, "h", "heap dump");
+    }
+
+    private void triggerHeapDump() {
+        TuiHelper.triggerHeapDump(ctx, (msg, error) -> {
+            if (ctx.notificationCallback != null) {
+                ctx.notificationCallback.accept(msg, error);
+            }
+        });
     }
 
     private void triggerGC() {
@@ -540,6 +553,7 @@ class MemoryTab extends AbstractTab {
                 ## Keys
 
                 - `g` — trigger garbage collection on the running integration 
(sends a GC request to the JVM — useful for testing if high usage is just 
uncollected garbage)
+                - `h` — write a heap dump (.hprof) file for deep analysis with 
tools like Eclipse MAT or VisualVM
                 - `Esc` — back
                 """;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 995c9b1f0bcf..e298e7664358 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
 
 import dev.tamboui.tui.TuiRunner;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -39,6 +40,7 @@ class MonitorContext {
     int logPinPercent;
     boolean logPinVisible;
     boolean ratePerMinute;
+    BiConsumer<String, Boolean> notificationCallback;
 
     MonitorContext(
                    AtomicReference<List<IntegrationInfo>> data,
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 941f8f5b2ba7..8554c89c1ae9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -23,6 +23,7 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.function.BiConsumer;
 import java.util.function.Function;
 
 import dev.tamboui.layout.Rect;
@@ -33,6 +34,7 @@ import dev.tamboui.text.CharWidth;
 import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.support.PatternHelper;
 import org.apache.camel.util.FileUtil;
@@ -900,4 +902,38 @@ final class TuiHelper {
         }
         return false;
     }
+
+    static void triggerHeapDump(MonitorContext ctx, BiConsumer<String, 
Boolean> notifyCallback) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return;
+        }
+        notifyCallback.accept("Writing heap dump...", false);
+        String pid = info.pid;
+        Thread t = new Thread(() -> {
+            Path outputFile = ctx.getOutputFile(pid);
+            PathUtils.deleteFile(outputFile);
+            JsonObject root = new JsonObject();
+            root.put("action", "heap-dump");
+            Path actionFile = ctx.getActionFile(pid);
+            PathUtils.writeTextSafely(root.toJson(), actionFile);
+            JsonObject jo = pollJsonResponse(outputFile, 60000);
+            if (jo != null) {
+                String error = jo.getString("error");
+                if (error != null) {
+                    notifyCallback.accept("Heap dump failed: " + error, true);
+                } else {
+                    String file = jo.getString("file");
+                    long size = jo.getLongOrDefault("size", 0);
+                    notifyCallback.accept("Heap dump: " + file + " (" + 
formatBytes(size) + ")", false);
+                }
+            } else {
+                notifyCallback.accept("Heap dump: no response within 60s", 
true);
+            }
+            PathUtils.deleteFile(outputFile);
+        });
+        t.setDaemon(true);
+        t.setName("heap-dump-" + pid);
+        t.start();
+    }
 }

Reply via email to