/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.engine.utilities.general;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/**
* The class {@code MemoryUtilities} defines utility methods related to memory usage.
*/
public final class MemoryUtilities {
/**
* Instances of the class {@code MemoryUsage} represent information about the amount of memory
* used by the objects in an object graph.
*/
public static class MemoryUsage {
/**
* The number of bytes in a kilobyte.
*/
private static final long K = 1024L;
/**
* The number of bytes in a megabyte.
*/
private static final long M = K * K;
/**
* The total amount of memory being used.
*/
private long totalSize = 0L;
/**
* The number of objects whose size is included in the total.
*/
private long totalCount = 0L;
/**
* A table mapping classes to information about the amount of space used by instances of that
* class.
*/
private HashMap<Class<?>, Accumulator> classUsageMap = new HashMap<Class<?>, Accumulator>();
/**
* Initialize a newly create object to record no memory used by zero objects.
*/
public MemoryUsage() {
super();
}
/**
* Record that an instance of the given class uses the given number of bytes of memory.
*
* @param objectClass the class of object using the memory
* @param size the amount of memory used by the object
*/
public void addMemory(Class<?> objectClass, long size) {
totalSize += size;
totalCount++;
Accumulator accumulator = classUsageMap.get(objectClass);
if (accumulator == null) {
accumulator = new Accumulator();
classUsageMap.put(objectClass, accumulator);
}
accumulator.addMemory(size);
}
/**
* Add the memory used by objects in the graph represented by the given memory usage data to the
* totals maintained by this object.
*
* @param usageData the usage data to be added to the usage data maintained by this object
*/
public void addMemory(MemoryUsage usageData) {
totalSize += usageData.totalSize;
totalCount += usageData.totalCount;
for (Map.Entry<Class<?>, Accumulator> entry : usageData.classUsageMap.entrySet()) {
Class<?> usedClass = entry.getKey();
Accumulator accumulator = classUsageMap.get(usedClass);
if (accumulator == null) {
classUsageMap.put(usedClass, entry.getValue());
} else {
accumulator.addMemory(entry.getValue());
}
}
}
/**
* Write a summary of the memory used by the objects in the object graph to the given writer.
*
* @param writer the writer to which the summary is to be written
* @return
*/
public void writeSummary(PrintWriter writer) {
printSize(writer, "Total memory: ", totalSize);
writer.println();
writer.print("Total count: ");
writer.println(totalCount);
writer.println();
Class<?>[] usedClasses = classUsageMap.keySet().toArray(new Class<?>[classUsageMap.size()]);
Arrays.sort(usedClasses, new Comparator<Class<?>>() {
@Override
public int compare(Class<?> firstClass, Class<?> secondClass) {
return firstClass.getName().compareTo(secondClass.getName());
}
});
for (Class<?> usedClass : usedClasses) {
Accumulator accumulator = classUsageMap.get(usedClass);
printSize(writer, " ", accumulator.totalSize);
writer.print(" [");
writer.print(accumulator.totalCount);
writer.print("] ");
writer.println(usedClass.getName());
}
}
/**
* Write a number representing memory size to the given writer.
*
* @param writer the writer to which the size is to be written
* @param label the label to be written before the size is written
* @param size the number of bytes to be written
*/
private void printSize(PrintWriter writer, String label, long size) {
writer.print(label);
if (size < K) {
writer.print(size);
writer.print(" bytes");
} else if (size < M) {
writer.print(size / K);
writer.print(" K (");
writer.print(size);
writer.print(" bytes)");
} else {
writer.print(size / M);
writer.print(" M (");
writer.print(size);
writer.print(" bytes)");
}
}
}
/**
* Instances of the class {@code Accumulator} represent information about the amount of memory
* used by instances of a specific class.
*/
private static class Accumulator {
/**
* The total amount of memory being used.
*/
private long totalSize = 0L;
/**
* The number of objects whose size is included in the total.
*/
private long totalCount = 0L;
/**
* Initialize a newly created accumulator to be zero.
*/
public Accumulator() {
super();
}
/**
* Add the memory usage represented by the given accumulator to this accumulator.
*
* @param accumulator the accumulator whose usage is to be added
*/
public void addMemory(Accumulator accumulator) {
totalSize += accumulator.totalSize;
totalCount += accumulator.totalCount;
}
/**
* Record that the given amount of memory is being used by a single instance of a class.
*
* @param size the amount of memory being used
*/
public void addMemory(long size) {
totalSize += size;
totalCount++;
}
}
/**
* An array containing the amount by which a value must be incremented in order to round it up to
* the nearest multiple of four when the index into the array is the remainder after dividing the
* value by four.
*/
private static final long[] ROUND_UP_AMOUNT = {0L, 3L, 2L, 1L};
/**
* Return an object representing the approximate size in bytes of the object graph containing the
* given object and all objects reachable from it. The actual size of an individual object depends
* on the implementation of the virtual machine running on the current platform and cannot be
* computed accurately. However, the value returned is close enough to allow the sizes of two
* different object structures to be compared with a reasonable degree of confidence.
* <p>
* Note that this method cannot be used to find the size of primitive values. Primitive values
* will be automatically boxed and this method will return the size of the boxed primitive, not
* the size of the primitive itself.
*
* @param object the object at the root of the graph whose size is to be returned
* @return the approximate size of the object graph containing the given object
*/
public static MemoryUsage measureMemoryUsage(Object object) {
return measureMemoryUsage(object, Predicates.alwaysTrue());
}
/**
* Return an object representing the approximate size in bytes of the object graph containing the
* given object and all objects reachable from it. The actual size of an individual object depends
* on the implementation of the virtual machine running on the current platform and cannot be
* computed accurately. However, the value returned is close enough to allow the sizes of two
* different object structures to be compared with a reasonable degree of confidence.
* <p>
* Note that this method cannot be used to find the size of primitive values. Primitive values
* will be automatically boxed and this method will return the size of the boxed primitive, not
* the size of the primitive itself.
*
* @param object the object at the root of the graph whose size is to be returned
* @param isIncluded a predicate that returns {@code true} if the argument is an object that is
* part of the object graph whose size is being computed
* @return the approximate size of the object graph containing the given object
*/
public static MemoryUsage measureMemoryUsage(Object object, Predicate<Object> isIncluded) {
MemoryUsage usageData = new MemoryUsage();
HashSet<Object> visitedObjects = new HashSet<Object>();
HashSet<Object> objectsToVisit = new HashSet<Object>();
objectsToVisit.add(object);
while (!objectsToVisit.isEmpty()) {
Object nextObject = objectsToVisit.iterator().next();
objectsToVisit.remove(nextObject);
if (isIncluded.apply(nextObject) && visitedObjects.add(nextObject)) {
addObjectToGraph(nextObject, usageData, visitedObjects, objectsToVisit);
}
}
return usageData;
}
/**
* Return the approximate size in bytes of the object graph containing the given object and all
* objects reachable from it. The actual size of an individual object depends on the
* implementation of the virtual machine running on the current platform and cannot be computed
* accurately. However, the value returned is close enough to allow the sizes of two different
* object structures to be compared with a reasonable degree of confidence.
* <p>
* Note that this method cannot be used to find the size of primitive values. Primitive values
* will be automatically boxed and this method will return the size of the boxed primitive, not
* the size of the primitive itself.
*
* @param object the object at the root of the graph whose size is to be returned
* @return the approximate size of the object graph containing the given object
*/
public static long sizeOfGraph(Object object) {
return sizeOfGraph(object, Predicates.alwaysTrue());
}
/**
* Return the approximate size in bytes of the object graph containing the given object and all
* objects reachable from it. The actual size of an individual object depends on the
* implementation of the virtual machine running on the current platform and cannot be computed
* accurately. However, the value returned is close enough to allow the sizes of two different
* object structures to be compared with a reasonable degree of confidence.
* <p>
* Note that this method cannot be used to find the size of primitive values. Primitive values
* will be automatically boxed and this method will return the size of the boxed primitive, not
* the size of the primitive itself.
*
* @param object the object at the root of the graph whose size is to be returned
* @param isIncluded a predicate that returns {@code true} if the argument is an object that is
* part of the object graph whose size is being computed
* @return the approximate size of the object graph containing the given object
*/
public static long sizeOfGraph(Object object, Predicate<Object> isIncluded) {
long size = 0L;
HashSet<Object> visitedObjects = new HashSet<Object>();
HashSet<Object> objectsToVisit = new HashSet<Object>();
objectsToVisit.add(object);
while (!objectsToVisit.isEmpty()) {
Object nextObject = objectsToVisit.iterator().next();
objectsToVisit.remove(nextObject);
if (isIncluded.apply(nextObject) && visitedObjects.add(nextObject)) {
size += addObjectToGraph(nextObject, visitedObjects, objectsToVisit);
}
}
return size;
}
/**
* Compute the size of the given object and add any objects referenced by it to the set of objects
* needing to be visited.
*
* @param object the object to be added to the object graph
* @param visitedObjects the objects that are already part of the graph
* @param objectsToVisit the objects to be added to the object graph
* @return the size of the given object
*/
private static long addObjectToGraph(Object object, HashSet<Object> visitedObjects,
HashSet<Object> objectsToVisit) {
Class<?> objectClass = object.getClass();
if (objectClass.isArray()) {
Class<?> componentType = objectClass.getComponentType();
int length = Array.getLength(object);
if (!componentType.isPrimitive()) {
for (int i = 0; i < length; i++) {
Object elementValue = Array.get(object, i);
if (elementValue != null && !visitedObjects.contains(elementValue)) {
objectsToVisit.add(elementValue);
}
}
}
return 12L + roundToWord(length * getElementSize(componentType));
} else {
long size = 8L;
while (objectClass != null) {
Field[] fields = objectClass.getDeclaredFields();
for (Field field : fields) {
if (!Modifier.isStatic(field.getModifiers())) {
Class<?> fieldType = field.getType();
if (fieldType == long.class || fieldType == double.class) {
size += 8L;
} else {
size += 4L;
}
if (!fieldType.isPrimitive() && !field.isEnumConstant()) {
try {
field.setAccessible(true);
Object fieldValue = field.get(object);
if (fieldValue != null && !visitedObjects.contains(fieldValue)) {
objectsToVisit.add(fieldValue);
}
} catch (Exception exception) {
// Ignored.
}
}
}
}
objectClass = objectClass.getSuperclass();
}
return size;
}
}
/**
* Compute the size of the given object and add any objects referenced by it to the set of objects
* needing to be visited.
*
* @param object the object to be added to the object graph
* @param usageData the usage data for the object graph containing the object
* @param visitedObjects the objects that are already part of the graph
* @param objectsToVisit the objects to be added to the object graph
* @return the size of the given object
*/
private static void addObjectToGraph(Object object, MemoryUsage usageData,
HashSet<Object> visitedObjects, HashSet<Object> objectsToVisit) {
Class<?> objectClass = object.getClass();
if (objectClass.isArray()) {
Class<?> componentType = objectClass.getComponentType();
int length = Array.getLength(object);
if (!componentType.isPrimitive()) {
for (int i = 0; i < length; i++) {
Object elementValue = Array.get(object, i);
if (elementValue != null && !visitedObjects.contains(elementValue)
&& !(elementValue instanceof Class)) {
objectsToVisit.add(elementValue);
}
}
}
usageData.addMemory(objectClass, 12L + roundToWord(length * getElementSize(componentType)));
} else {
long size = 8L;
Class<?> currentClass = objectClass;
while (currentClass != null) {
Field[] fields = currentClass.getDeclaredFields();
for (Field field : fields) {
if (!Modifier.isStatic(field.getModifiers())) {
Class<?> fieldType = field.getType();
if (fieldType == long.class || fieldType == double.class) {
size += 8L;
} else {
size += 4L;
}
if (!fieldType.isPrimitive() && !field.isEnumConstant()) {
try {
field.setAccessible(true);
Object fieldValue = field.get(object);
if (fieldValue != null && !visitedObjects.contains(fieldValue)
&& !(fieldValue instanceof Class)) {
objectsToVisit.add(fieldValue);
}
} catch (Exception exception) {
// Ignored.
}
}
}
}
currentClass = currentClass.getSuperclass();
}
usageData.addMemory(objectClass, size);
}
}
/**
* Return the approximate number of bytes required to store a value of the given type in an array.
*
* @param componentType the type of the value to be stored
* @return the approximate number of bytes required to store a value of the given type in an array
*/
private static long getElementSize(Class<?> componentType) {
if (componentType.isPrimitive()) {
if (componentType == boolean.class) {
return 1L;
} else if (componentType == byte.class) {
return 1L;
} else if (componentType == char.class) {
return 2L;
} else if (componentType == short.class) {
return 2L;
} else if (componentType == int.class) {
return 4L;
} else if (componentType == long.class) {
return 8L;
} else if (componentType == float.class) {
return 4L;
} else if (componentType == double.class) {
return 8L;
}
}
return 4L;
}
/**
* Return the smallest multiple of four (4) that is greater than or equal to the given value.
*
* @param value the value to be rounded up to the nearest multiple of four
* @return the smallest multiple of four that is greater than or equal to the value
*/
private static long roundToWord(long value) {
return value + ROUND_UP_AMOUNT[(int) (value % 4)];
}
/**
* Prevent the creation of instances of this class.
*/
private MemoryUtilities() {
super();
}
}