This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-23870-heap-histogram in repository https://gitbox.apache.org/repos/asf/camel.git
commit 73c913d3d54e94ebd0104b452e3bdca809531db2 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Jul 1 16:20:24 2026 +0200 chore: add detail panel to heap histogram tab with package summary and JAR origin Co-Authored-By: Claude <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../jbang/core/commands/tui/HeapHistogramTab.java | 230 +++++++++++++++++++-- 1 file changed, 210 insertions(+), 20 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java index 3030f3b11e33..c620c298f6ce 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java @@ -30,6 +30,7 @@ import dev.tamboui.style.Style; import dev.tamboui.terminal.Frame; import dev.tamboui.text.Line; import dev.tamboui.text.Span; +import dev.tamboui.text.Text; import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.widgets.block.Block; @@ -58,11 +59,12 @@ class HeapHistogramTab implements MonitorTab { private String sort = "bytes"; private int sortIndex = 2; private boolean sortReversed; - private int filter; // 0=all, 1=camel + private int filter; private List<HeapEntry> allEntries = Collections.emptyList(); private long totalInstances; private long totalBytes; private String lastPid; + private List<ClasspathTab.JarEntry> classpathEntries = Collections.emptyList(); HeapHistogramTab(MonitorContext ctx) { this.ctx = ctx; @@ -74,6 +76,7 @@ class HeapHistogramTab implements MonitorTab { if (pid != null && !pid.equals(lastPid)) { lastPid = pid; allEntries = Collections.emptyList(); + classpathEntries = Collections.emptyList(); } if (allEntries.isEmpty()) { loadHeapHistogram(); @@ -83,6 +86,7 @@ class HeapHistogramTab implements MonitorTab { @Override public void onIntegrationChanged() { allEntries = Collections.emptyList(); + classpathEntries = Collections.emptyList(); lastPid = null; loadHeapHistogram(); } @@ -150,7 +154,7 @@ class HeapHistogramTab implements MonitorTab { if (loading.get() && allEntries.isEmpty()) { frame.renderWidget( Paragraph.builder() - .text(dev.tamboui.text.Text.from( + .text(Text.from( Line.from(Span.styled(" Loading heap histogram...", Style.EMPTY.dim())))) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) .title(" Heap Histogram ").build()) @@ -162,10 +166,11 @@ class HeapHistogramTab implements MonitorTab { List<HeapEntry> visible = sortedEntries(); List<Rect> chunks = Layout.vertical() - .constraints(Constraint.length(1), Constraint.fill()) + .constraints(Constraint.length(1), Constraint.percentage(60), Constraint.fill()) .split(area); renderSummary(frame, chunks.get(0), visible); renderTable(frame, chunks.get(1), visible); + renderDetail(frame, chunks.get(2), visible); } private void renderSummary(Frame frame, Rect area, List<HeapEntry> visible) { @@ -227,6 +232,103 @@ class HeapHistogramTab implements MonitorTab { frame.renderStatefulWidget(table, area, tableState); } + private void renderDetail(Frame frame, Rect area, List<HeapEntry> visible) { + Integer sel = tableState.selected(); + if (sel == null || sel < 0 || sel >= visible.size()) { + frame.renderWidget( + Paragraph.builder() + .text(Text.from(Line.from(Span.styled(" Select a class to see details", Style.EMPTY.dim())))) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .title(" Detail ").build()) + .build(), + area); + return; + } + + HeapEntry entry = visible.get(sel); + String className = entry.className != null ? entry.className : ""; + String pkg = extractPackage(className); + + List<Line> lines = new ArrayList<>(); + + // Class info + lines.add(Line.from( + Span.styled(" Class: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled(className, Style.EMPTY.fg(Color.CYAN)))); + lines.add(Line.from( + Span.styled(" Package: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled(pkg.isEmpty() ? "(none)" : pkg, Style.EMPTY.fg(Color.WHITE)))); + lines.add(Line.from( + Span.styled(" Instances: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled(formatNumber(entry.instances), Style.EMPTY.fg(Color.WHITE)), + Span.styled(" Bytes: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled(formatBytes(entry.bytes), Style.EMPTY.fg(Color.WHITE)))); + + // Package summary + if (!pkg.isEmpty()) { + long pkgInstances = 0; + long pkgBytes = 0; + int pkgClasses = 0; + for (HeapEntry e : allEntries) { + if (e.className != null && extractPackage(e.className).equals(pkg)) { + pkgInstances += e.instances; + pkgBytes += e.bytes; + pkgClasses++; + } + } + lines.add(Line.from(Span.raw(""))); + lines.add(Line.from( + Span.styled(" Package Summary ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled("(" + pkg + ")", Style.EMPTY.dim()))); + lines.add(Line.from( + Span.styled(" Classes: ", Style.EMPTY.fg(Color.YELLOW)), + Span.styled(formatNumber(pkgClasses), Style.EMPTY.fg(Color.WHITE)), + Span.styled(" Instances: ", Style.EMPTY.fg(Color.YELLOW)), + Span.styled(formatNumber(pkgInstances), Style.EMPTY.fg(Color.WHITE)), + Span.styled(" Bytes: ", Style.EMPTY.fg(Color.YELLOW)), + Span.styled(formatBytes(pkgBytes), Style.EMPTY.fg(Color.WHITE)))); + } + + // JAR info + ClasspathTab.JarEntry jar = findJar(className); + if (jar != null) { + lines.add(Line.from(Span.raw(""))); + if (jar.groupId() != null) { + lines.add(Line.from( + Span.styled(" JAR: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled(jar.groupId() + ":" + jar.artifactId() + ":" + jar.version(), + Style.EMPTY.fg(Color.GREEN)))); + } else { + lines.add(Line.from( + Span.styled(" JAR: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled(jar.display(), Style.EMPTY.fg(Color.GREEN)))); + } + if (jar.fullPath() != null) { + String path = jar.fullPath(); + int maxW = area.width() - 18; + if (maxW > 0 && path.length() > maxW) { + path = "..." + path.substring(path.length() - maxW + 3); + } + lines.add(Line.from( + Span.styled(" ", Style.EMPTY), + Span.styled(path, Style.EMPTY.dim()))); + } + } else if (isBuiltinClass(className)) { + lines.add(Line.from(Span.raw(""))); + lines.add(Line.from( + Span.styled(" JAR: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled("JDK (built-in)", Style.EMPTY.fg(Color.GREEN)))); + } + + String title = " Detail: " + TuiHelper.truncate(className, area.width() - 14) + " "; + frame.renderWidget( + Paragraph.builder() + .text(Text.from(lines)) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build()) + .build(), + area); + } + @Override public void renderFooter(List<Span> spans) { hint(spans, "Esc", "back"); @@ -291,29 +393,19 @@ class HeapHistogramTab implements MonitorTab { - **INSTANCES** — Number of live instances of this class on the heap - **BYTES** — Total bytes consumed by all instances of this class - ## Example Screen + ## Detail Panel + + The detail panel below the table shows additional context for the selected class: - ``` - # CLASS NAME INSTANCES BYTES - 1 [B 45,230 12.5 MB - 2 [C 38,100 8.2 MB - 3 java.lang.String 38,050 1.8 MB - 4 java.util.HashMap$Node 12,400 595.0 KB - 5 java.lang.Object[] 8,200 450.2 KB - ``` + - **Package Summary** — Total classes, instances, and bytes for all classes in the same package + - **JAR** — The Maven artifact (groupId:artifactId:version) and file path of the JAR containing the class ## Filter Modes - **all** (default) — Show all classes + - **non-jdk** — Exclude JDK classes (java.*, javax.*, jdk.*, sun.*, com.sun.*, arrays) - **camel** — Show only classes from `org.apache.camel` packages - ## What To Look For - - - **Large byte counts at the top**: Normal for byte arrays (`[B`) and char arrays (`[C`) — these back Strings and buffers - - **Unexpected classes with high counts**: May indicate a memory leak — objects being created but not released - - **Growing instance counts on refresh**: Press F5 repeatedly to spot classes whose instance counts keep growing, which suggests a leak - - **Camel-specific classes**: Use the `camel` filter to focus on Camel's own objects. High counts of Exchange, Message, or Endpoint objects may indicate route issues - ## Keys | Key | Action | @@ -321,7 +413,7 @@ class HeapHistogramTab implements MonitorTab { | Up/Down | Select class | | s | Cycle sort column (bytes, instances, className) | | S | Reverse sort order | - | f | Toggle filter (all / camel) | + | f | Toggle filter (all / non-jdk / camel) | | F5 | Refresh heap histogram | | PgUp/PgDn | Scroll by page | | Esc | Back | @@ -366,6 +458,60 @@ class HeapHistogramTab implements MonitorTab { return e.className != null && e.className.contains("org.apache.camel"); } + private static boolean isBuiltinClass(String className) { + if (className == null || className.isEmpty()) { + return false; + } + return className.startsWith("java.") + || className.startsWith("javax.") + || className.startsWith("jdk.") + || className.startsWith("sun.") + || className.startsWith("com.sun.") + || className.startsWith("["); + } + + private static String extractPackage(String className) { + if (className == null || className.isEmpty()) { + return ""; + } + if (className.startsWith("[")) { + // array type — extract element class if object array + int idx = className.lastIndexOf('L'); + if (idx >= 0) { + String element = className.substring(idx + 1).replace(";", ""); + int dot = element.lastIndexOf('.'); + return dot >= 0 ? element.substring(0, dot) : ""; + } + return ""; + } + int dot = className.lastIndexOf('.'); + return dot >= 0 ? className.substring(0, dot) : ""; + } + + private ClasspathTab.JarEntry findJar(String className) { + if (classpathEntries.isEmpty() || className == null || className.isEmpty()) { + return null; + } + String pkg = extractPackage(className); + if (pkg.isEmpty()) { + return null; + } + // try progressively shorter prefixes of the package against groupId + ClasspathTab.JarEntry best = null; + int bestLen = -1; + for (ClasspathTab.JarEntry jar : classpathEntries) { + if (jar.groupId() == null) { + continue; + } + String gid = jar.groupId(); + if (pkg.startsWith(gid) && gid.length() > bestLen) { + best = jar; + bestLen = gid.length(); + } + } + return best; + } + private static int compareStr(String a, String b) { if (a == null && b == null) { return 0; @@ -440,16 +586,60 @@ class HeapHistogramTab implements MonitorTab { result.add(entry); } + // load classpath if not already loaded + List<ClasspathTab.JarEntry> cpEntries = classpathEntries; + if (cpEntries.isEmpty()) { + cpEntries = loadClasspathEntries(pid); + } + + List<ClasspathTab.JarEntry> finalCp = cpEntries; if (ctx.runner != null) { ctx.runner.runOnRenderThread(() -> { allEntries = result; totalInstances = ti; totalBytes = tb; + classpathEntries = finalCp; lastPid = pid; }); } } + private List<ClasspathTab.JarEntry> loadClasspathEntries(String pid) { + Path outputFile = ctx.getOutputFile(pid); + PathUtils.deleteFile(outputFile); + + JsonObject action = new JsonObject(); + action.put("action", "jvm"); + + Path actionFile = ctx.getActionFile(pid); + PathUtils.writeTextSafely(action.toJson(), actionFile); + + JsonObject response = pollJsonResponse(outputFile, 5000); + PathUtils.deleteFile(outputFile); + + if (response == null) { + return Collections.emptyList(); + } + + Object cp = response.get("classpath"); + List<String> paths = new ArrayList<>(); + if (cp instanceof JsonArray jArr) { + for (Object item : jArr) { + paths.add(String.valueOf(item)); + } + } + + if (paths.isEmpty()) { + return Collections.emptyList(); + } + + List<ClasspathTab.JarEntry> parsed = new ArrayList<>(); + for (String path : paths) { + parsed.add(ClasspathTab.parseJarEntry(path)); + } + return parsed; + } + static String formatBytes(long bytes) { if (bytes < 1024) { return bytes + " B";
