This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-23870-heap-histogram
in repository https://gitbox.apache.org/repos/asf/camel.git

commit cd69ee5a4b770d16ec252670a405ddb0abd432c3
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 1 16:03:51 2026 +0200

    CAMEL-23870: Add camel cmd heap-histogram CLI command
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../core/commands/action/CamelHeapHistogram.java   | 237 +++++++++++++++++++++
 2 files changed, 238 insertions(+)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 86f19a1abd67..c09e343495f5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -110,6 +110,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("disable-processor", new 
CommandLine(new CamelProcessorDisableAction(this)))
                         .addSubcommand("enable-processor", new CommandLine(new 
CamelProcessorEnableAction(this)))
                         .addSubcommand("gc", new CommandLine(new 
CamelGCAction(this)))
+                        .addSubcommand("heap-histogram", new CommandLine(new 
CamelHeapHistogram(this)))
                         .addSubcommand("load", new CommandLine(new 
CamelLoadAction(this)))
                         .addSubcommand("logger", new CommandLine(new 
LoggerAction(this)))
                         .addSubcommand("receive", new CommandLine(new 
CamelReceiveAction(this)))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
new file mode 100644
index 000000000000..8636e4a4df22
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "heap-histogram", description = "Display class-level heap 
memory usage in a running Camel integration",
+         sortOptions = false, showDefaultValues = true,
+         footer = {
+                 "%nExamples:",
+                 "  camel cmd heap-histogram",
+                 "  camel cmd heap-histogram --sort instances",
+                 "  camel cmd heap-histogram --filter camel --top 30" })
+public class CamelHeapHistogram extends ActionWatchCommand {
+
+    public static class SortCompletionCandidates implements Iterable<String> {
+
+        public SortCompletionCandidates() {
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return List.of("bytes", "instances", "className").iterator();
+        }
+    }
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--sort" }, completionCandidates = 
SortCompletionCandidates.class,
+                        description = "Sort by bytes, instances, or 
className", defaultValue = "bytes")
+    String sort;
+
+    @CommandLine.Option(names = { "--top" },
+                        description = "Show only the top N classes", 
defaultValue = "50")
+    int top;
+
+    @CommandLine.Option(names = { "--filter" },
+                        description = "Filter class names (use all to include 
all classes)", defaultValue = "all")
+    String filter;
+
+    private volatile long pid;
+
+    public CamelHeapHistogram(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doWatchCall() throws Exception {
+        List<Row> rows = new ArrayList<>();
+
+        List<Long> pids = findPids(name);
+        if (pids.isEmpty()) {
+            return 1;
+        } else if (pids.size() > 1) {
+            printer().println("Name or pid " + name + " matches " + pids.size()
+                              + " running Camel integrations. Specify a name 
or PID that matches exactly one.");
+            return 1;
+        }
+
+        this.pid = pids.get(0);
+
+        Path outputFile = getOutputFile(Long.toString(pid));
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "heap-histogram");
+        Path f = getActionFile(Long.toString(pid));
+        try {
+            Files.writeString(f, root.toJson());
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            long totalInstances = jo.getLongOrDefault("totalInstances", 0);
+            long totalBytes = jo.getLongOrDefault("totalBytes", 0);
+
+            JsonArray arr = (JsonArray) jo.get("classes");
+            if (arr != null) {
+                for (int i = 0; i < arr.size(); i++) {
+                    JsonObject jc = (JsonObject) arr.get(i);
+
+                    Row row = new Row();
+                    row.num = jc.getIntegerOrDefault("num", 0);
+                    row.className = jc.getString("className");
+                    row.instances = jc.getLongOrDefault("instances", 0);
+                    row.bytes = jc.getLongOrDefault("bytes", 0);
+
+                    if (!matchesFilter(row)) {
+                        continue;
+                    }
+
+                    rows.add(row);
+                }
+            }
+
+            rows.sort(this::sortRow);
+
+            if (top > 0 && rows.size() > top) {
+                rows = rows.subList(0, top);
+            }
+
+            if (watch) {
+                clearScreen();
+            }
+            if (!rows.isEmpty()) {
+                printer().printf("PID: %s\tClasses: %d\tTotal Instances: 
%s\tTotal Bytes: %s\tDisplay: %d%n",
+                        pid, arr != null ? arr.size() : 0,
+                        formatNumber(totalInstances), formatBytes(totalBytes), 
rows.size());
+                printTable(rows);
+            }
+        } else {
+            printer().println("Response from running Camel with PID " + pid + 
" not received within 10 seconds");
+            return 1;
+        }
+
+        PathUtils.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    private boolean matchesFilter(Row row) {
+        if ("all".equalsIgnoreCase(filter)) {
+            return true;
+        }
+        if (row.className == null) {
+            return false;
+        }
+        if ("non-jdk".equalsIgnoreCase(filter)) {
+            return !row.className.startsWith("java.")
+                    && !row.className.startsWith("javax.")
+                    && !row.className.startsWith("jdk.")
+                    && !row.className.startsWith("sun.")
+                    && !row.className.startsWith("com.sun.")
+                    && !row.className.startsWith("[");
+        }
+        return row.className.toLowerCase().contains(filter.toLowerCase());
+    }
+
+    protected void printTable(List<Row> rows) {
+        int tw = terminalWidth();
+        int fixedWidth = 6 + 14 + 14;
+        int borderOverhead = TerminalWidthHelper.noBorderOverhead(4);
+        int nameWidth = TerminalWidthHelper.flexWidth(tw, fixedWidth, 
borderOverhead, 30, 80);
+
+        printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, 
Arrays.asList(
+                new 
Column().header("#").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> Integer.toString(r.num)),
+                new Column().header("CLASS 
NAME").dataAlign(HorizontalAlign.LEFT)
+                        .maxWidth(nameWidth, OverflowBehaviour.ELLIPSIS_LEFT)
+                        .with(r -> r.className),
+                new 
Column().header("INSTANCES").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> formatNumber(r.instances)),
+                new 
Column().header("BYTES").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> formatBytes(r.bytes)))));
+    }
+
+    protected int sortRow(Row o1, Row o2) {
+        String s = sort;
+        int negate = 1;
+        if (s.startsWith("-")) {
+            s = s.substring(1);
+            negate = -1;
+        }
+        switch (s) {
+            case "instances":
+                return Long.compare(o2.instances, o1.instances) * negate;
+            case "className":
+                return (o1.className != null
+                        ? o1.className.compareToIgnoreCase(o2.className != 
null ? o2.className : "")
+                        : 0)
+                       * negate;
+            default:
+                return Long.compare(o2.bytes, o1.bytes) * negate;
+        }
+    }
+
+    protected JsonObject waitForOutputFile(Path outputFile) {
+        return getJsonObject(outputFile);
+    }
+
+    static String formatBytes(long bytes) {
+        if (bytes < 1024) {
+            return bytes + " B";
+        } else if (bytes < 1024 * 1024) {
+            return String.format("%.1f KB", bytes / 1024.0);
+        } else if (bytes < 1024L * 1024 * 1024) {
+            return String.format("%.1f MB", bytes / (1024.0 * 1024));
+        } else {
+            return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
+        }
+    }
+
+    static String formatNumber(long num) {
+        return String.format("%,d", num);
+    }
+
+    private static class Row {
+        int num;
+        String className;
+        long instances;
+        long bytes;
+    }
+}

Reply via email to