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