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();
+ }
}