package org.github.jamm;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assume.assumeThat;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.junit.Ignore;
import org.junit.Test;

/**
* Numbers here are for 64-bit Sun JVM.  Good luck with anything else.
*/
public class MemoryMeterTest
{
    // JVM memory structure is like follows
    // a ref and a header for all objects
    // always align to 8 byte boundary no matter what
    // separate fields for each class in the family tree
    // for a specific class, group: (depending on the jvm there may be optimized packing)
    //   - longs, doubles (aligned on 8 byte boundary)
    //   - ints, floats (aligned on 4 byte boundary)
    //   - shorts, chars (aligned on 2 byte boundary)
    //   - bytes, booleans (aligned on 1 byte boundary)
    //   - padding
    //   - last come refs
    //
    // for array, last comes the array body (refs or primitives)
    //
    // Mac OS X seems very good at packing and seems to use 4 byte references even on 64-bit!!!

    static final int REFERENCE_SIZE = sizeOfReference();
    static final int HEADER_SIZE = sizeOfHeader();
    static final int OBJECT_SIZE = pad(HEADER_SIZE + REFERENCE_SIZE);

    public static int sizeOfReference() {
        MemoryMeter meter = new MemoryMeter();

        // this is probably the safest way to get the reference size
        // does imply that we are testing non-pure
        // but there is so much variability, this at least will give
        // consistent answers from an "approximate" data source
        long objectArraySize = meter.measure(new Object[123]);
        if (meter.measure(new long[123]) == objectArraySize) return 8;
        if (meter.measure(new int[123]) == objectArraySize) return 4;

        // OK there is strangeness going on... we'll ask some well known sources
        try {
            Class<?> unsafe = Class.forName("sun.misc.Unsafe");
            Field unsafeField = unsafe.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            Object theUnsafe = unsafeField.get(null);
            Method addressSize = unsafe.getMethod("addressSize");
            Object size = addressSize.invoke(theUnsafe);
            int i = ((Number) size).intValue();
            return i;
        } catch (ClassNotFoundException e) {
            // ignore
        } catch (NoSuchFieldException e) {
            // ignore
        } catch (IllegalAccessException e) {
            // ignore
        } catch (NoSuchMethodException e) {
            // ignore
        } catch (InvocationTargetException e) {
            // ignore
        }
        String sadm = System.getProperty("sun.arch.data.model");
        if (sadm != null) {
            if ("32".equals(sadm)) return 4;
            if ("64".equals(sadm)) return 8;
            if ("128".equals(sadm)) return 16;
        }
        String oa = System.getProperty("os.arch");
        if (oa != null) {
            if (oa.contains("64")) return 8;
            if (oa.contains("32")) return 4;
            if ("x86".equals(oa)) return 4;
            if ("amd".equals(oa)) return 4;
        }
        String jvi = System.getProperty("java.vm.info");
        if (jvi != null) {
            if (jvi.contains("x86-32")) return 4;
        }
        String jvn = System.getProperty("java.vm.name");
        if (jvn != null) {
            if (jvn.toLowerCase().contains("64-bit")) return 8;
            if (jvn.toLowerCase().contains("32-bit")) return 4;
        }
        // ok we'll pretend it's 32-bit
        return 4;
    }

    @SuppressWarnings("unused")
    static class ByteHolder {
        private byte value;
    }

    @SuppressWarnings("unused")
    static class TwoByteHolder {
        private byte value;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class ThreeByteHolder {
        private byte value;
        private byte other1;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class FourByteHolder {
        private byte value;
        private byte other1;
        private byte other2;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class FiveByteHolder {
        private byte value;
        private byte other1;
        private byte other2;
        private byte other3;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class SixByteHolder {
        private byte value;
        private byte other1;
        private byte other2;
        private byte other3;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class SevenByteHolder {
        private byte value;
        private byte other1;
        private byte other2;
        private byte other3;
        private byte other4;
        private byte other5;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class EightByteHolder {
        private byte value;
        private byte other1;
        private byte other2;
        private byte other3;
        private byte other4;
        private byte other5;
        private byte other6;
        private byte overflow;
    }

    @SuppressWarnings("unused")
    static class NineByteHolder {
        private byte value;
        private byte other1;
        private byte other2;
        private byte other3;
        private byte other4;
        private byte other5;
        private byte other6;
        private byte other7;
        private byte overflow;
    }

    public static int sizeOfHeader() {
        // The Mac seems to pack things right in!
        MemoryMeter meter = new MemoryMeter();
        long b0 = meter.measure(new Object()) - REFERENCE_SIZE;
        long b1 = meter.measure(new ByteHolder()) - REFERENCE_SIZE;
        long b2 = meter.measure(new TwoByteHolder()) - REFERENCE_SIZE;
        long b3 = meter.measure(new ThreeByteHolder()) - REFERENCE_SIZE;
        long b4 = meter.measure(new FourByteHolder()) - REFERENCE_SIZE;
        long b5 = meter.measure(new FiveByteHolder()) - REFERENCE_SIZE;
        long b6 = meter.measure(new SixByteHolder()) - REFERENCE_SIZE;
        long b7 = meter.measure(new SevenByteHolder()) - REFERENCE_SIZE;
        long b8 = meter.measure(new EightByteHolder()) - REFERENCE_SIZE;
        long b9 = meter.measure(new NineByteHolder()) - REFERENCE_SIZE;
        if (b0 == b8 && b0 != b9) return (int)b0 - 8;
        if (b0 == b7 && b0 != b8) return (int)b0 - 7;
        if (b0 == b6 && b0 != b7) return (int)b0 - 6;
        if (b0 == b5 && b0 != b6) return (int)b0 - 5;
        if (b0 == b4 && b0 != b5) return (int)b0 - 4;
        if (b0 == b3 && b0 != b4) return (int)b0 - 3;
        if (b0 == b2 && b0 != b3) return (int)b0 - 2;
        if (b0 == b1 && b0 != b2) return (int)b0 - 1;
        return (int)b0;
    }

    public static int pad(int size) {
        if (size % 8 == 0) return size;
        return (size | 7) + 1;
    }

    public static int objectSize(int numLongDoubles, int numIntFloats, int numShortChars, int numByteBools, int numRefs) {
        int size = REFERENCE_SIZE + HEADER_SIZE;
        size += numLongDoubles * 8;
        size += numIntFloats * 4;
        size += numShortChars * 2;
        size += numByteBools;
        // now pad to ref-size boundary
        if (size % REFERENCE_SIZE != 0) {
            size = (size | (REFERENCE_SIZE - 1)) + 1;
        }
        size += numRefs * REFERENCE_SIZE;
        return pad(size);
    }

    public static int arraySize(int numElements) {
        int size = REFERENCE_SIZE + HEADER_SIZE + 4;
        if (numElements > 0) {
        // now pad to ref-size boundary
        if (size % REFERENCE_SIZE != 0) {
            size = (size | (REFERENCE_SIZE - 1)) + 1;
        }
        size += numElements * REFERENCE_SIZE;
        }
        return pad(size);
    }

    public static int byteArraySize(int numElements) {
        int size = REFERENCE_SIZE + HEADER_SIZE + 4;
        if (numElements > 0) {
        // now pad to ref-size boundary
        if (size % REFERENCE_SIZE != 0) {
            size = (size | (REFERENCE_SIZE - 1)) + 1;
        }
        size += numElements;
        }
        return pad(size);
    }

    public static int charArraySize(int numElements) {
        int size = REFERENCE_SIZE + HEADER_SIZE + 4;
        if (numElements > 0) {
        // now pad to ref-size boundary
        if (size % REFERENCE_SIZE != 0) {
            size = (size | (REFERENCE_SIZE - 1)) + 1;
        }
        size += numElements * 2;
        }
        return pad(size);
    }

    public static int intArraySize(int numElements) {
        int size = REFERENCE_SIZE + HEADER_SIZE + 4;
        if (numElements > 0) {
        // now pad to ref-size boundary
        if (size % REFERENCE_SIZE != 0) {
            size = (size | (REFERENCE_SIZE - 1)) + 1;
        }
        size += numElements * 4;
        }
        return pad(size);
    }

    public static int longArraySize(int numElements) {
        int size = REFERENCE_SIZE + HEADER_SIZE + 4;
        if (numElements > 0) {
        // now pad to ref-size boundary
        if (size % REFERENCE_SIZE != 0) {
            size = (size | (REFERENCE_SIZE - 1)) + 1;
        }
        size += numElements * 8;
        }
        return pad(size);
    }

    @Test
    public void testObjectArraySizes() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of Object[0]", arraySize(0), meter.measure(new Object[0]));

        assertEquals("Shallow size of Object[1]", arraySize(1), meter.measure(new Object[1]));

        assertEquals("Shallow size of Object[256]", arraySize(256), meter.measure(new Object[256]));
    }

    @Test
    public void testByteArraySizes() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of byte[0]", byteArraySize(0), meter.measure(new byte[0]));

        assertEquals("Shallow size of byte[1]", byteArraySize(1), meter.measure(new byte[1]));

        assertEquals("Shallow size of byte[256]", byteArraySize(256), meter.measure(new byte[256]));
    }

    @Test
    public void testCharArraySizes() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of char[0]", charArraySize(0), meter.measure(new char[0]));

        assertEquals("Shallow size of char[1]", charArraySize(1), meter.measure(new char[1]));

        assertEquals("Shallow size of char[256]", charArraySize(256), meter.measure(new char[256]));
    }

    @Test
    public void testIntArraySizes() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of int[0]", intArraySize(0), meter.measure(new int[0]));

        assertEquals("Shallow size of int[1]", intArraySize(1), meter.measure(new int[1]));

        assertEquals("Shallow size of int[256]", intArraySize(256), meter.measure(new int[256]));
    }

    @Test
    public void testLongArraySizes() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of long[0]", longArraySize(0), meter.measure(new long[0]));

        assertEquals("Shallow size of long[1]", longArraySize(1), meter.measure(new long[1]));

        assertEquals("Shallow size of long[256]", longArraySize(256), meter.measure(new long[256]));
    }

    @SuppressWarnings("unused")
    static class LongHolder {
        private long value;
    }

    @SuppressWarnings("unused")
    static class IntHolder {
        private int value;
    }

    @SuppressWarnings("unused")
    static class CharHolder {
        private char value;
    }

    @SuppressWarnings("unused")
    static class TwoCharHolder {
        private char value;
        private char other;
    }

    @SuppressWarnings("unused")
    static class ThreeCharHolder {
        private char value;
        private char other;
        private char overflow;
    }

    @SuppressWarnings("unused")
    static class IntCharHolder {
        private int value;
        private char other;
    }

    @SuppressWarnings("unused")
    static class LongIntHolder {
        private long value;
        private int other;
    }

    @SuppressWarnings("unused")
    static class LongIntHolder2 extends LongHolder {
        private int other;
    }

    @Test
    public void testMacOSX_x86_64() {
        // Mac OS X seems to have a way to stash 4 bytes away in a plain object
        MemoryMeter meter = new MemoryMeter();
        assumeThat(System.getProperty("os.name"), is("Mac OS X"));
        assumeThat(System.getProperty("os.arch"), is("x86_64"));
        assertEquals("no embedded long field", 24, meter.measure(new LongHolder()));
        assertEquals("Embedded int field", 16, meter.measure(new IntHolder()));
        assertEquals("Embedded char field", 16, meter.measure(new CharHolder()));
        assertEquals("Embedded char field * 2", 16, meter.measure(new TwoCharHolder()));
        assertEquals("Embedded char field * 3", 24, meter.measure(new ThreeCharHolder()));
        assertEquals("Embedded int field only", 24, meter.measure(new IntCharHolder()));
        assertEquals("Only 4 bytes available", 24, meter.measure(new FiveByteHolder()));
        assertEquals("4 bytes always available", 24, meter.measure(new LongIntHolder()));
        assertEquals("4 bytes not available if parent has a field", 32, meter.measure(new LongIntHolder2()));
        assertEquals(meter.measure(new int[16384]), meter.measure(new Object[16384]));
    }

    @Test
    public void testMacOSX_i386() {
        // Mac OS X seems to have a way to stash 4 bytes away in a plain object
        MemoryMeter meter = new MemoryMeter();
        assumeThat(System.getProperty("os.name"), is("Mac OS X"));
        assumeThat(System.getProperty("os.arch"), is("i386"));
        assertEquals("Room for 1 long", 16, meter.measure(new LongHolder()));
        assertEquals("Room for 1 int", 16, meter.measure(new IntHolder()));
        assertEquals("Room for 1 char", 16, meter.measure(new CharHolder()));
        assertEquals("Room for 2 chars", 16, meter.measure(new TwoCharHolder()));
        assertEquals("Room for 3 chars", 16, meter.measure(new ThreeCharHolder()));
        assertEquals("Room for 1 int and 1 char", 16, meter.measure(new IntCharHolder()));
        assertEquals("Room for 5 bytes", 16, meter.measure(new FiveByteHolder()));
        assertEquals("Room for 7 bytes", 16, meter.measure(new SevenByteHolder()));
        assertEquals("Room for 8 bytes", 16, meter.measure(new EightByteHolder()));
        assertEquals("Room for 9 bytes", 24, meter.measure(new NineByteHolder()));
        assertEquals("Room for 1 long and 1 int", 24, meter.measure(new LongIntHolder()));
        assertEquals("4 bytes not available to child classes", 24, meter.measure(new LongIntHolder2()));
        assertEquals(meter.measure(new int[16384]), meter.measure(new Object[16384]));
    }

    @Test
    public void testPrimitives() {

        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of Object", OBJECT_SIZE, meter.measure(new Object()));
        assertEquals("Deep size of Object", OBJECT_SIZE, meter.measureDeep(new Object()));

        assertEquals("Shallow size of Long", objectSize(1, 0, 0, 0, 0), meter.measure(new Long(0)));
        assertEquals("Deep size of Long", objectSize(1, 0, 0, 0, 0), meter.measureDeep(new Long(0)));

        assertEquals("Shallow size of Integer", objectSize(0, 1, 0, 0, 0), meter.measure(new Integer(0)));
        assertEquals("Deep size of Integer", objectSize(0, 1, 0, 0, 0), meter.measureDeep(new Integer(0)));

        assertEquals("Shallow size of empty String", objectSize(0, 0, 4, 0, 0), meter.measure(""));
        assertEquals("Deep size of empty String", objectSize(0, 0, 4, 0, 0) + charArraySize(0), meter.measureDeep(""));
        assertEquals("Shallow size of one-character String", objectSize(0, 0, 4, 0, 0), meter.measure("a"));
        assertEquals("Deep size of one-character String", objectSize(0, 0, 4, 0, 0) + charArraySize(1), meter.measureDeep("a"));

        assertEquals("Shallow size of empty array of objects", arraySize(0), meter.measure(new Object[0]));
        Object[] objects = new Object[100];
        assertEquals("Shallow size of Object[100] containing all nulls", arraySize(100), meter.measure(objects));
        assertEquals("Deep size of Object[100] containing all nulls", arraySize(100), meter.measureDeep(objects));
        for(int i = 0; i < objects.length; i++) {
            objects[i] = new Object();
        }
        assertEquals("Shallow size of Object[100] containing new Object()s", arraySize(100) + OBJECT_SIZE * 100, meter.measureDeep(objects));
    }

    @Test
    public void testByteBuffer() {
        ByteBuffer empty = ByteBuffer.allocate(0);
        ByteBuffer one = ByteBuffer.allocate(1);
        ByteBuffer emptyOne = (ByteBuffer) one.duplicate().position(1);

        MemoryMeter m1 = new MemoryMeter();
        MemoryMeter m2 = m1.omitSharedBufferOverhead();

        // from Object
        // ref*2
        // from Buffer
        // long
        // int * 4
        // from ByteBuffer
        // int
        // boolean * 3
        // padding
        // byte[]
        int BYTEBUFFER_SIZE = objectSize(1, 5, 0, 3, 1);

        assertEquals("Shallow empty ByteBuffer", BYTEBUFFER_SIZE, m1.measure(empty));
        assertEquals("Deep empty ByteBuffer", BYTEBUFFER_SIZE + byteArraySize(0), m1.measureDeep(empty));
        // 8 is apparently the minimum number of bytes allocated
        assertEquals("Deep 1-byte ByteBuffer", BYTEBUFFER_SIZE + byteArraySize(1), m1.measureDeep(one));
        assertEquals("Deep duplicated 1-byte ByteBuffer", BYTEBUFFER_SIZE + byteArraySize(1), m1.measureDeep(emptyOne));

        // there are hard-coded 64-bit arch values when omiting shared buffer overhead
        assertEquals(BYTEBUFFER_SIZE, m2.measure(empty));
        assertEquals(BYTEBUFFER_SIZE, m2.measureDeep(empty));
        assertEquals(BYTEBUFFER_SIZE + 1, m2.measureDeep(one)); // as of 0.2.4 we don't count the bytes!!!
        assertEquals(BYTEBUFFER_SIZE, m2.measureDeep(emptyOne));
    }

    @Test
    public void testCycle() throws Exception {
        MemoryMeter meter = new MemoryMeter();

        Recursive dummy = new Recursive();
        assertEquals("Shallow size of Recursive object", objectSize(0, 1, 0, 0, 1), meter.measure(dummy));
        assertEquals("Deep size of Recursive is shallow size when child==null", meter.measure(dummy), meter.measureDeep(dummy));
        dummy.child = dummy;
        assertEquals("Deep size of Recursive is shallow size when child==this", meter.measure(dummy), meter.measureDeep(dummy));
    }

    @Test
    public void testInheritance() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("Shallow size of Parent", objectSize(0, 1, 0, 0, 0), meter.measure(new Parent()));
        assertEquals("Deep size of Parent", objectSize(0, 1, 0, 0, 0), meter.measureDeep(new Parent()));
        assertEquals("Shallow size of Child", objectSize(0, 2, 0, 0, 0), meter.measure(new Child()));
        assertEquals("Deep size of Parent", objectSize(0, 2, 0, 0, 0), meter.measureDeep(new Child()));
    }

    @Test
    @Ignore("These vary quite radically depending on the JVM.")
    public void testCollections() {
        MemoryMeter meter = new MemoryMeter();

        assertEquals("sizeOf ArrayList",
                objectSize(0, 2, 0, 0, 1) // the object itself
                        + arraySize(10), // the backing array's initial load factor
                meter.measureDeep(new ArrayList<Object>()));
        assertEquals("sizeOf HashMap",
                objectSize(0, 4, 0, 0, 4) // the object itself
                        + arraySize(16), // the backing array
                meter.measureDeep(new HashMap<Object, Object>()));
        assertEquals("sizeOf LinkedHashMap",
                objectSize(0, 4, 0, 1, 5)  // the object itself
                        + arraySize(16) // the inherited backing array
                        + objectSize(0, 1, 0, 0, 5), // the first node
                meter.measureDeep(new LinkedHashMap<Object, Object>()));

        // I give up for the ones below!
        assertEquals("sizeOf ReentrantReadWriteLock", 176, meter.measureDeep(new ReentrantReadWriteLock()));
        assertEquals("sizeOf ConcurrentSkipListMap", 192, meter.measureDeep(new ConcurrentSkipListMap<Object, Object>()));
    }

    @Test
    public void testDeep() {
        MemoryMeter meter = new MemoryMeter();

        Recursive root = new Recursive();
        Recursive recursive = root;
        for (int i = 0; i < 100000; i++) {
            recursive.child = new Recursive();
            recursive = recursive.child;
        }
        assertEquals(objectSize(0, 1, 0, 0, 1) * 100001, meter.measureDeep(root));
    }

    @SuppressWarnings("unused")
    private static class Parent {
        private int i;
    }

    @SuppressWarnings("unused")
    private static class Child extends Parent {
        private int j;
    }

    @SuppressWarnings("unused")
    private static class Recursive {
        int i;
        Recursive child = null;
    }
    
    @Test
    public void testIgnoreKnownSingletons() {
    	MemoryMeter meter = new MemoryMeter();
    	
    	long classFieldSize = meter.measureDeep(new HasClassField());
    	long enumFieldSize = meter.measureDeep(new HasEnumField());
    	
    	meter = meter.ignoreKnownSingletons();
    	
    	assertNotEquals(classFieldSize, meter.measureDeep(new HasClassField()));
    	assertNotEquals(enumFieldSize, meter.measureDeep(new HasEnumField()));
    }
    
    @Test
    public void testIgnoreNonStrongReferences() {
    	MemoryMeter meter = new MemoryMeter();
    	
    	long classFieldSize = meter.measureDeep(new HasReferenceField());
    	
    	meter = meter.ignoreNonStrongReferences();
    	
    	assertNotEquals(classFieldSize, meter.measureDeep(new HasClassField()));
    }
    
    @SuppressWarnings("unused")
    private static class HasClassField {
    	private Class<?> cls = String.class;
    }
    
    @SuppressWarnings("unused")
    private static class HasEnumField {
    	enum Giant {Fee, Fi, Fo, Fum}
    	
    	private Giant grunt = Giant.Fee;
    }
    
    @SuppressWarnings("unused")
    private static class HasReferenceField {
    	private SoftReference<Date> ref = new SoftReference<Date>(new Date());
    }

    @Test
    public void testUnmeteredAnnotationOnFields() {
        MemoryMeter meter = new MemoryMeter();

        String s = "test";

        long stringSize = meter.measureDeep(s);
        long withoutSize = meter.measureDeep(new WithoutAnnotationField(null));
        assertEquals(stringSize + withoutSize, meter.measureDeep(new WithoutAnnotationField(s)));

        long withSize = meter.measureDeep(new WithAnnotationField(null));
        assertEquals(withSize, meter.measureDeep(new WithAnnotationField(s)));
    }

    @Test
    public void testUnmeteredTypeAnnotation() {
        MemoryMeter meter = new MemoryMeter();

        String s = "test";
        assertEquals(0, meter.measureDeep(new WithTypeAnnotation(s)));
    }

    @Test
    public void testUnmeteredAnnotationOnParent() {
        MemoryMeter meter = new MemoryMeter();

        String s = "test";
        assertEquals(0, meter.measureDeep(new WithParentWithAnnotation(s)));
    }

    @Test
    public void testUnmeteredAnnotationOnFieldParent() {
        MemoryMeter meter = new MemoryMeter();

        long withoutSize = meter.measureDeep(new WithFieldWithAnnotatedParent(null));

        WithParentWithAnnotation field = new WithParentWithAnnotation("test");
        long withSize = meter.measureDeep(new WithFieldWithAnnotatedParent(field));
        assertEquals(withoutSize, withSize);
    }

    @Test
    public void testUnmeteredAnnotationOnFieldInterface() {
        MemoryMeter meter = new MemoryMeter();

        long withoutSize = meter.measureDeep(new WithFieldAnnotatedInterface(null));

        AnnotatedInterface field = new AnnotatedInterface() {
        };
        long withSize = meter.measureDeep(new WithFieldAnnotatedInterface(field));
        assertEquals(withoutSize, withSize);
    }

    @SuppressWarnings("unused")
    private static class WithoutAnnotationField {
        private String s;

        public WithoutAnnotationField(String s) {
            this.s = s;
        }
    }

    private static class WithAnnotationField {

        @org.github.jamm.Unmetered
        private String s;

        public WithAnnotationField(String s) {
            this.s = s;
        }
    }

    @Unmetered
    @SuppressWarnings("unused")
    private static class WithTypeAnnotation {
        private String s;

        public WithTypeAnnotation(String s) {
            this.s = s;
        }
    }

    private static class WithParentWithAnnotation extends WithTypeAnnotation {

        public WithParentWithAnnotation(String s) {
            super(s);
        }
    }

    @SuppressWarnings("unused")
    private static class WithFieldWithAnnotatedParent {

        private final WithParentWithAnnotation field;

        public WithFieldWithAnnotatedParent(WithParentWithAnnotation field) {
            this.field = field;
        }
    }

    @Unmetered
    private interface AnnotatedInterface {

    }

    @SuppressWarnings("unused")
    private static class WithFieldAnnotatedInterface {

        private final AnnotatedInterface field;

        public WithFieldAnnotatedInterface(AnnotatedInterface field) {
            this.field = field;
        }
    }
}
