package org.github.jamm; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; /** * A memory listener that print to the <code>System.out</code> the class tree with the size information. */ final class TreePrinter implements MemoryMeterListener { private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final int ONE_KB = 1024; private static final int ONE_MB = 1024 * ONE_KB; /** * Mapping between objects and their information */ private final Map<Object, ObjectInfo> mapping = new IdentityHashMap<Object, ObjectInfo>(); /** * The maximum depth of the trees to be printed */ private final int maxDepth; /** * Specifies is some element will not be printed due to the depth limit. */ private boolean hasMissingElements; /** * The root object */ private Object root; public TreePrinter(int maxDepth) { this.maxDepth = maxDepth; } @Override public void started(Object obj) { root = obj; mapping.put(obj, ObjectInfo.newRoot(obj.getClass())); } @Override public void fieldAdded(Object obj, String fieldName, Object fieldValue) { ObjectInfo parent = mapping.get(obj); if (parent != null && parent.depth <= maxDepth - 1) { ObjectInfo field = parent.addChild(fieldName, fieldValue.getClass()); mapping.put(fieldValue, field); } else { hasMissingElements = true; } } @Override public void objectMeasured(Object current, long size) { ObjectInfo field = mapping.get(current); if (field != null) { field.size = size; } } @Override public void objectCounted(Object current) { } @Override public void done(long size) { System.out.println(mapping.get(root).toString(!hasMissingElements)); } /** * Container for the information associated to a field object. */ private static final class ObjectInfo { /** * The name for a root object */ private static final String ROOT_NAME = "root"; /** * The object name */ private final String name; /** * The object class name */ private final String className; /** * The object maxDepth. */ private final int depth; /** * The field children */ private List<ObjectInfo> children = Collections.emptyList(); /** * The object size. */ private long size; /** * The total size (lazy loaded) */ private long totalSize = -1; public ObjectInfo(String name, Class<?> clazz, int depth) { this.name = name; this.className = className(clazz); this.depth = depth; } /** * Creates a new root <code>ObjectInfo</code> for the specified class. * * @param clazz the root class * @return a new root <code>ObjectInfo</code> */ public static ObjectInfo newRoot(Class<?> clazz) { return new ObjectInfo(ROOT_NAME, clazz, 0); } /** * Adds the specified child * @param childName the name of the child * @param childClass the class of the child * @return the child information */ public ObjectInfo addChild(String childName, Class<?> childClass) { ObjectInfo child = new ObjectInfo(childName, childClass, depth + 1); if (children.isEmpty()) { children = new ArrayList<TreePrinter.ObjectInfo>(); } children.add(child); return child; } /** * Returns the total size of the object and of its children * @return the total size of the object and of its children */ public long totalSize() { if (totalSize < 0) totalSize = computeTotalSize(); return totalSize; } /** * Computes the total size of the object and of its children * @return the total size of the object and of its children */ private long computeTotalSize() { long total = size; for (ObjectInfo child : children) { total += child.totalSize(); } return total; } @Override public String toString() { return toString(false); } public String toString(boolean printTotalSize) { return append("", true, printTotalSize, new StringBuilder().append(LINE_SEPARATOR).append(LINE_SEPARATOR)).toString(); } /** * Appends the representation of this <code>ObjectInfo</code> to the specified builder. * * @param indentation the indentation to use * @param isLast <code>true</code> if this object is the last child from is parent * @param printTotalSize <code>true</code> if the total size must be printed, <code>false</code> otherwise * @param builder the <code>StringBuilder</code> to append to * @return the <code>StringBuilder</code> */ private StringBuilder append(String indentation, boolean isLast, boolean printTotalSize, StringBuilder builder) { if (!name.equals(ROOT_NAME)) { builder.append(indentation) .append('|') .append(LINE_SEPARATOR) .append(indentation) .append("+--"); } builder.append(name) .append(" [") .append(className) .append("] "); if (size != 0) { if (printTotalSize) { appendSizeTo(builder, totalSize()); builder.append(' '); } builder.append('('); appendSizeTo(builder, size); builder.append(')'); } return appendChildren(childIntentation(indentation, isLast), printTotalSize, builder.append(LINE_SEPARATOR)); } /** * Appends to the specified <code>StringBuilder</code> the String representation of the children * * @param indentation the indentation * @param printTotalSize <code>true</code> if the total size must be printed, <code>false</code> otherwise * @param builder the builder to append to */ private StringBuilder appendChildren(String indentation, boolean printTotalSize, StringBuilder builder) { for (int i = 0, m = children.size(); i < m; i++) { ObjectInfo child = children.get(i); boolean isLast = i == m - 1; child.append(indentation, isLast, printTotalSize, builder); } return builder; } /** * Returns the indentation to use for the children * * @param indentation the parent indentation * @param isLast <code>true</code> if the parent is the last child of its parent * @return the indentation to use for the children */ private static String childIntentation(String indentation, boolean isLast) { return isLast ? indentation + " " : indentation + "| "; } /** * Returns the name of the specified class. * * @param clazz the class * @return the name of the specified class */ private static String className(Class<? extends Object> clazz) { if (clazz.isArray()) { return clazz.getComponentType().getName() + "[]"; } return clazz.getName(); } private static void appendSizeTo(StringBuilder builder, long size) { if (size >= ONE_MB) { builder.append(String.format("%.2f", (double) size / ONE_MB)).append(" KB"); } else if (size >= ONE_KB) { builder.append(String.format("%.2f", (double) size / ONE_KB)).append(" KB"); } else { builder.append(size).append(" bytes"); } } } /** * Factory for <code>TreePrinter</code> instances. */ public static class Factory implements MemoryMeterListener.Factory { /** * The maximum depth of the trees to be printed */ private final int depth; /** * Creates a new <code>Factory</code> instance which create <code>TreePrinter</code> that will print the * visited trees up to the specified maxDepth. * @param maxDepth the maximum depth of the trees to be printed */ public Factory(int depth) { this.depth = depth; } @Override public MemoryMeterListener newInstance() { return new TreePrinter(depth); } }; }