Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Double the performance of measuring collections #68

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/org/github/jamm/ClassMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.github.jamm;

import org.github.jamm.accessors.FieldAccessor;

import java.lang.reflect.Field;
import java.util.ArrayList;

/**
* Stores metadata about a class to be used when measuring objects.
* <p>
* CONTRACT:
* There is an implicit assumption that calling methods on an instance of this class is for an
* object of the same class as what was passed into the constructor during initialization.
*/
public class ClassMetadata {

/**
* Reusable empty array to minimize the cache size as many classes will end up with no
* measurable fields. This includes primitive wrapper classes or any custom classes which
* don't contain any fields that pass the field filter.
*/
public static final Field[] NO_FIELDS = new Field[0];
public final int shallowSize;

/**
* The declared fields that pass the fieldFilter. These are used to discover nested objects.
*/
private final Field[] measurableFields;

public ClassMetadata(int shallowSize, Class<?> clazz, FieldFilter fieldFilter) {
this.shallowSize = shallowSize;
measurableFields = getMeasurableFields(clazz, fieldFilter);
}

/**
* Adds the applicable nested child objects from the parentObject into the measurement stack.
*
* @param parentObject The parent object from which to fetch the nested child objects
* @param fieldAccessor The accessor for fetching the field value
* @param classFilter The FieldAndClassFilter that determines whether a child object should be ignored
* @param stack The measurement stack to add the child objects into
*/
public void addFieldReferences(
Object parentObject,
FieldAccessor fieldAccessor,
FieldAndClassFilter classFilter,
MeasurementStack stack
) {
MemoryMeterListener listener = stack.listener();
// We only need to check the classFilter since the field filter was already applied when
// this cache entry was initialized
for (Field field : measurableFields) {
Object child = getFieldValue(fieldAccessor, parentObject, field, listener);
if (child != null && !classFilter.ignore(child.getClass())) {
stack.pushObject(parentObject, field.getName(), child);
}
}
}

/**
* Retrieves the field value if possible.
*
* @param obj the object for which the field value must be retrieved
* @param field the field for which the value must be retrieved
* @param listener the {@code MemoryMeterListener}
* @return the field value if it was possible to retrieve it
* @throws CannotAccessFieldException if the field could not be accessed
*/
private static Object getFieldValue(
FieldAccessor accessor,
Object obj,
Field field,
MemoryMeterListener listener
) {
try {
return accessor.getFieldValue(obj, field);
} catch (CannotAccessFieldException e) {
listener.failedToAccessField(obj, field.getName(), field.getType());
throw e;
}
}

/**
* Gets the fields which should be measured when fetching nested child objects.
*
* @param clazz The class for which to discover the declared fields to be measured
* @param fieldFilter The field filter which determines which fields to ignore
* @return An array of the fields to be measured
*/
private static Field[] getMeasurableFields(Class<?> clazz, FieldFilter fieldFilter) {
ArrayList<Field> fields = new ArrayList<>();
Class<?> type = clazz;
while (type != null) {
for (Field field : type.getDeclaredFields()) {
if (!fieldFilter.ignore(clazz, field)) {
fields.add(field);
}
}
type = type.getSuperclass();
}
// Share the empty array when there aren't any measurable fields in order to minimize cache size
return fields.isEmpty() ? NO_FIELDS : fields.toArray(new Field[fields.size()]);
}
}
15 changes: 8 additions & 7 deletions src/org/github/jamm/MeasurementStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,17 @@ void pushRoot(Object object) {
}

/**
* Push the specified array element into the stack.
* Push all the eligible elements from the array into the stack.
*
* @param array the array
* @param index the element index
*/
void pushArrayElement(Object[] array, int index) {
Object child = array[index];
if (child != null && !classFilter.ignore(child.getClass()) && tracker.add(child)) {
stack.push(child);
listener.arrayElementAdded(array, index, child);
void pushArrayElements(Object[] array) {
for (int i = 0; i < array.length; i++) {
Object element = array[i];
if (element != null && !classFilter.ignore(element.getClass()) && tracker.add(element)) {
stack.push(element);
listener.arrayElementAdded(array, i, element);
}
}
}

Expand Down
129 changes: 44 additions & 85 deletions src/org/github/jamm/MemoryMeter.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.github.jamm;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.github.jamm.accessors.FieldAccessor;
import org.github.jamm.listeners.NoopMemoryMeterListener;
Expand Down Expand Up @@ -218,10 +219,10 @@ private MemoryMeter(Builder builder) {

/**
* Create a new {@link MemoryMeter} instance from the different component it needs to measure object graph.
* <p>Unless there is a specific need to override some of the {@code MemoryMeter} logic people should only create
* {@code MemoryMeter} instances through {@code MemoryMeter.builder()}. This constructor provides a way to modify part of the
* <p>Unless there is a specific need to override some of the {@code MemoryMeter} logic people should only create
* {@code MemoryMeter} instances through {@code MemoryMeter.builder()}. This constructor provides a way to modify part of the
* logic being used by allowing to use specific implementations for the strategy or filters.</p>
*
*
* @param strategy the {@code MemoryMeterStrategy} to use for measuring object shallow size.
* @param classFilter the filter used to filter out classes from the measured object graph
* @param fieldFilter the filter used to filter out fields from the measured object graph
Expand Down Expand Up @@ -450,6 +451,7 @@ public long measureDeep(Object object) {
* @param bbMode the mode that should be used to measure ByteBuffers.
* @return the memory usage of @param object including referenced objects
*/

public long measureDeep(Object object, ByteBufferMode bbMode) {

if (object == null) {
Expand All @@ -459,6 +461,7 @@ public long measureDeep(Object object, ByteBufferMode bbMode) {
if (classFilter.ignore(object.getClass()))
return 0;

Map<Class<?>, ClassMetadata> cache = new HashMap<>();
MemoryMeterListener listener = listenerFactory.newInstance();

// track stack manually, so we can handle deeper hierarchies than recursion
Expand All @@ -469,44 +472,55 @@ public long measureDeep(Object object, ByteBufferMode bbMode) {
while (!stack.isEmpty()) {

Object current = stack.pop();
Class<?> cls = current.getClass();

// Deal with optimizations first.
if (StringMeter.ENABLED && current instanceof String) {
String s = (String) current;
long size1 = measureDeep(s, listener);
total += size1;
if (StringMeter.ENABLED && cls == String.class) {
total += measureDeep((String) current, listener);
continue;
}

if (current instanceof Measurable) {
Measurable measurable = (Measurable) current;
total += measure(measurable, listener);
measurable.addChildrenTo(stack);
continue;
}

long size = strategy.measure(current);
listener.objectMeasured(current, size);
total += size;

Class<?> cls = current.getClass();

if (cls.isArray()) {
if (!cls.getComponentType().isPrimitive())
addArrayElements((Object[]) current, stack);
} else {
if (current instanceof ByteBuffer && bbMode.isSlab((ByteBuffer) current)) {
ByteBuffer buffer = (ByteBuffer) current;
if (!buffer.isDirect()) { // If direct we should simply not measure the fields
long remaining = buffer.remaining();
listener.byteBufferRemainingMeasured(buffer, remaining);
total += remaining;
}
continue;
long size = strategy.measure(current);
listener.objectMeasured(current, size);
total += size;
if (!cls.getComponentType().isPrimitive()){
stack.pushArrayElements((Object[]) current);
}
addFields(current, cls, stack);
continue;
}

ClassMetadata metadata = cache.get(cls);
if (metadata == null) {
// Casting to int is safe as the class data model can't support more than about
// 65K fields per class (including all superclass fields) so the shallow size
// can't approach anywhere near the max value of integers (max 2 GB shallow size)
int shallowSize = (int)strategy.measure(current);
metadata = new ClassMetadata(shallowSize, cls, fieldFilter);
cache.put(cls, metadata);
}
}
listener.objectMeasured(current, metadata.shallowSize);
total += metadata.shallowSize;

if (current instanceof ByteBuffer && bbMode.isSlab((ByteBuffer) current)) {
ByteBuffer buffer = (ByteBuffer) current;
if (!buffer.isDirect()) { // If direct we should simply not measure the fields
long remaining = buffer.remaining();
listener.byteBufferRemainingMeasured(buffer, remaining);
total += remaining;
}
continue;
}

metadata.addFieldReferences(current, ACCESSOR, classFilter, stack);
}
listener.done(total);
return total;
}
Expand All @@ -523,68 +537,13 @@ private long measure(Measurable measurable, MemoryMeterListener listener) {
return size;
}

private void addFields(Object obj, Class<?> cls, MeasurementStack stack) {
Class<?> type = cls;
while (type != null) {
addDeclaredFields(obj, type, stack);
type = type.getSuperclass();
}
}

private void addDeclaredFields(Object obj, Class<?> type, MeasurementStack stack) {
for (Field field : type.getDeclaredFields()) {
if (!fieldFilter.ignore(obj.getClass(), field)) {
addField(obj, field, stack);
}
}
}

/**
* Adds the object field value to the stack.
*
* @param obj the object from which the field value must be retrieved
* @param field the field
* @param stack
*/
private void addField(Object obj, Field field, MeasurementStack stack) {
Object child = getFieldValue(obj, field, stack.listener());

if (child != null && (!classFilter.ignore(child.getClass()))) {
stack.pushObject(obj, field.getName(), child);
}
}

/**
* Retrieves the field value if possible.
*
* @param obj the object for which the field value must be retrieved
* @param field the field for which the value must be retrieved
* @param listener the {@code MemoryMeterListener}
* @return the field value if it was possible to retrieve it
* @throws CannotAccessFieldException if the field could not be accessed
*/
private Object getFieldValue(Object obj, Field field, MemoryMeterListener listener) {
try {
return ACCESSOR.getFieldValue(obj, field);
} catch (CannotAccessFieldException e) {
listener.failedToAccessField(obj, field.getName(), field.getType());
throw e;
}
}

private void addArrayElements(Object[] array, MeasurementStack stack) {
for (int i = 0; i < array.length; i++) {
stack.pushArrayElement(array, i);
}
}

/**
* Builder for {@code MemoryMeter} instances
*/
public static final class Builder {

/**
* The strategy to perform shallow measurements and its fallback strategies in case the required classes are not available.
* The strategy to perform shallow measurements and its fallback strategies in case the required classes are not available.
*/
private List<Guess> guesses = BEST;
private boolean ignoreOuterClassReference;
Expand Down Expand Up @@ -625,7 +584,7 @@ public Builder withGuessing(Guess strategy, Guess... fallbacks) {

/**
* Ignores the outer class reference from non-static inner classes.
* <p>In practice this is only useful if the top class provided to {@code MemoryMeter.measureDeep} is an inner
* <p>In practice this is only useful if the top class provided to {@code MemoryMeter.measureDeep} is an inner
* class and we wish to ignore the outer class in the measurement.</p>
*
* @return this builder
Expand Down
Loading