package org.github.jamm; import org.junit.*; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * UGLY AS SIN, BUT QUICK TO WRITE AND WORKS. * We generate java source code for random data classes (with parent hierarchy), write it to a temp dir, * call javac on it, and instrument it. */ public class GuessTest { @Test public void testDeepNecessaryClasses() { final MemoryMeter instrument = new MemoryMeter().withTrackerProvider(TRACKER_PROVIDER); final MemoryMeter guess = new MemoryMeter().withGuessing(MemoryMeter.Guess.ALWAYS_SPEC).withTrackerProvider(TRACKER_PROVIDER); Assert.assertTrue("MemoryMeter not initialised", MemoryMeter.hasInstrumentation()); final List<Object> objects = new ArrayList<Object>(); { final ConcurrentSkipListMap<Long, Long> map = new ConcurrentSkipListMap<Long, Long>(); for (long i = 0 ; i < 100 ; i++) map.put(i, i); objects.add(map); } int failures = 0; for (final Object obj : objects) { long instrumented = instrument.measureDeep(obj); long guessed = guess.measureDeep(obj); if (instrumented != guessed) { System.err.println(String.format("Guessed %d, instrumented %d for %s", guessed, instrumented, obj.getClass().getName())); failures++; } } Assert.assertEquals("Not all guesses matched the instrumented values. See output for details.", 0, failures); } @Test public void testProblemClasses() throws InterruptedException, ExecutionException, IOException, IllegalAccessException, InstantiationException { final MemoryMeter instrument = new MemoryMeter(); final MemoryMeter guess = new MemoryMeter().withGuessing(MemoryMeter.Guess.ALWAYS_UNSAFE); Assert.assertTrue("MemoryMeter not initialised", MemoryMeter.hasInstrumentation()); List<Def> defs = new ArrayList<Def>(); defs.add(Def.parse("{long*1}->{float*1}")); defs.add(Def.parse("{long*1}->{byte*4}")); defs.add(Def.parse("{long*1}->{byte*7}")); defs.add(Def.parse("{long*1}->{byte*9}")); defs.add(Def.parse("{long*1}->{float*1}->{long*1}->{float*1}")); final List<GeneratedClass> classes = compile(defs); int failures = 0; for (final GeneratedClass clazz : classes) { Object obj = clazz.clazz.newInstance(); long instrumented = instrument.measure(obj); long guessed = guess.measure(obj); if (instrumented != guessed) { System.err.println(String.format("Guessed %d, instrumented %d for %s", guessed, instrumented, clazz.description)); failures++; } } Assert.assertEquals("Not all guesses matched the instrumented values. See output for details.", 0, failures); } @Test public void testRandomClasses() throws InterruptedException, ExecutionException { final int testsPerCPU = 100; final MemoryMeter instrument = new MemoryMeter(); final MemoryMeter guess = new MemoryMeter().withGuessing(MemoryMeter.Guess.ALWAYS_UNSAFE); Assert.assertTrue("MemoryMeter not initialised", MemoryMeter.hasInstrumentation()); final List<Future<Integer>> results = new ArrayList<Future<Integer>>(); for (int i = 0 ; i < Runtime.getRuntime().availableProcessors() ; i++) { results.add(EXEC.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { final List<GeneratedClass> classes = randomClasses(testsPerCPU); int failures = 0; for (final GeneratedClass clazz : classes) { Object obj = clazz.clazz.newInstance(); long instrumented = instrument.measure(obj); long guessed = guess.measure(obj); if (instrumented != guessed) { System.err.println(String.format("Guessed %d, instrumented %d for %s", guessed, instrumented, clazz.description)); failures++; } } return failures; } })); } int failures = 0; for (Future<Integer> result : results) failures += result.get(); Assert.assertEquals("Not all guesses matched the instrumented values. See output for details.", 0, failures); } @Test public void testRandomArrays() throws InterruptedException, ExecutionException { final MemoryMeter instrument = new MemoryMeter(); final MemoryMeter guess = new MemoryMeter().withGuessing(MemoryMeter.Guess.ALWAYS_UNSAFE); Assert.assertTrue("MemoryMeter not initialised", MemoryMeter.hasInstrumentation()); final List<Future<Boolean>> results = new ArrayList<Future<Boolean>>(); for (int i = 0 ; i < 10000 ; i++) { results.add(EXEC.submit(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Object obj = Array.newInstance(TYPES[rnd.nextInt(TYPES.length)].clazz, rnd.nextInt(1000)); long instrumented = instrument.measure(obj); long guessed = guess.measure(obj); if (instrumented != guessed) { System.err.println(String.format("%s of length %d. Guessed %d, instrumented %d", obj.getClass(), Array.getLength(obj), guessed, instrumented)); return Boolean.FALSE; } return Boolean.TRUE; } })); } for (Future<Boolean> result : results) Assert.assertTrue("Failed test - see output for details", result.get()); } private static final Types[] TYPES = Types.values(); private static final Random rnd = new Random(); private static final AtomicInteger id = new AtomicInteger(); private static final MyClassLoader CL = new MyClassLoader(); private static final File tempDir = new File(System.getProperty("java.io.tmpdir"), "testclasses"); private static final Callable<Set<Object>> TRACKER_PROVIDER = new TrackerProvider(); private static final ExecutorService CONSUME_PROCESS_OUTPUT = Executors.newCachedThreadPool(new DaemonThreadFactory()); private static final ExecutorService EXEC = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); static { // clear out the temp dir tempDir.mkdirs(); for (File file : tempDir.listFiles()) { if (!file.delete() || file.exists()) throw new IllegalStateException(); } } // declare all the primitive types private static enum Types { BOOLEAN(boolean.class), BYTE(byte.class), CHAR(char.class), SHORT(short.class), INT(int.class), FLOAT(float.class), LONG(long.class), DOUBLE(double.class), OBJECT(Object.class); final Class<?> clazz; final String name; Types(Class<?> clazz) { this.clazz = clazz; this.name = clazz.getSimpleName(); } } // a class loader for loading our random classes; permits loading arbitrary bytes to arbitrary class name private static final class MyClassLoader extends ClassLoader { Class<?> load(String name, byte[] bytes) { return super.defineClass(name, bytes, 0, bytes.length); } } // a simple data-only compiled class (defined by us) and its readable description private static final class GeneratedClass { final Class<?> clazz; final String description; private GeneratedClass(Class<?> clazz, String description) { this.clazz = clazz; this.description = description; } } // a simple data-only class-hierarchy definition (our representation) private static final class Def { // a type and a number of occurrences private static final class TypeDef { final Types type; final int count; private TypeDef(Types type, int count) { this.type = type; this.count = count; } } // a single class - just a set of TypeDef private static final class ClassDef { final TypeDef[] typedefs; private ClassDef(TypeDef[] typedefs) { this.typedefs = typedefs; } } // the class hierarchy; classdefs[x] is an ancestor of classdefs[x+1..] final ClassDef[] classdefs; private Def(ClassDef[] classdefs) { this.classdefs = classdefs; } // generate a random class hierarchy private static Def random() { ClassDef[] classdefs = new ClassDef[1 + rnd.nextInt(4)]; for (int d = 0 ; d != classdefs.length ; d++) { final List<TypeDef> typedefs = new ArrayList<TypeDef>(); int fieldCount = rnd.nextInt(100); int f = 0; while (f < fieldCount) { Types type = TYPES[rnd.nextInt(TYPES.length)]; int fc = 1 + rnd.nextInt(fieldCount - f); typedefs.add(new TypeDef(type, fc)); f += fc; } classdefs[d] = new ClassDef(typedefs.toArray(new TypeDef[0])); } return new Def(classdefs); } // parse one of our readable descriptions into a Def private static Def parse(String description) { final Pattern clazz = Pattern.compile("\\{([a-zO]+\\*[0-9]+ ?)+\\}"); final Pattern type = Pattern.compile("([a-zO]+)\\*([0-9]+)"); Matcher cm = clazz.matcher(description); List<ClassDef> classdefs = new ArrayList<ClassDef>(); while (cm.find()) { Matcher tm = type.matcher(cm.group()); List<TypeDef> typedefs = new ArrayList<TypeDef>(); while (tm.find()) { typedefs.add(new TypeDef( Types.valueOf(tm.group(1).toUpperCase()), Integer.parseInt(tm.group(2)))); } classdefs.add(new ClassDef(typedefs.toArray(new TypeDef[0]))); } return new Def(classdefs.toArray(new ClassDef[0])); } // transform the definition into a Java declaration, with associated files on disk Decl declare() throws IOException { String prev = null; List<Decl.ClassDecl> parts = new ArrayList<Decl.ClassDecl>(); for (ClassDef classdef : classdefs) { String name = "Test" + id.incrementAndGet(); StringBuilder decl = new StringBuilder("public class "); decl.append(name); if (prev != null) { decl.append(" extends "); decl.append(prev); } decl.append(" {\n"); int field = 0; for (TypeDef typedef : classdef.typedefs) { for (int i = 0 ; i < typedef.count ; i++) { decl.append("public "); decl.append(typedef.type.name); decl.append(" field"); decl.append(field++); decl.append(";\n"); } } decl.append("}"); File src = new File(tempDir, name + ".java"); if (src.exists()) throw new IllegalStateException(); final FileWriter writer = new FileWriter(src); writer.append(decl); writer.close(); File trg = new File(tempDir, name + ".class"); parts.add(new Decl.ClassDecl(src, trg, name, decl.toString())); prev = name; } return new Decl(parts.toArray(new Decl.ClassDecl[0]), this); } // generate a simple description - these can be parsed by Def.parse() String description() { final StringBuilder description = new StringBuilder(); for (ClassDef classdef : classdefs) { if (description.length() > 0) description.append("->"); description.append("{"); boolean first = true; for (TypeDef typedef : classdef.typedefs) { if (!first) description.append(" "); description.append(typedef.type); description.append("*"); description.append(typedef.count); first = false; } description.append("}"); } return description.toString(); } } // translate a def into a concrete declaration with source files private static final class Decl { @SuppressWarnings("unused") private static final class ClassDecl { final File srcfile; final File binfile; final String name; final String declaration; private ClassDecl(File srcfile, File binfile, String name, String declaration) { this.srcfile = srcfile; this.binfile = binfile; this.name = name; this.declaration = declaration; } } final ClassDecl[] classdecls; final Def def; private Decl(ClassDecl[] classdecls, Def def) { this.classdecls = classdecls; this.def = def; } } // compile the provided defs by declaring them in source files and calling javac private static List<GeneratedClass> compile(List<Def> defs) throws IOException, ExecutionException, InterruptedException { final List<String> args = new ArrayList<String>(); args.addAll(Arrays.asList("javac", "-d", tempDir.getAbsolutePath())); final List<Decl> decls = new ArrayList<Decl>(); for (Def def : defs) decls.add(def.declare()); for (Decl decl : decls) for (Decl.ClassDecl classdecl : decl.classdecls) args.add(classdecl.srcfile.getAbsolutePath()); // compile final Process p = new ProcessBuilder(args.toArray(new String[0])).start(); final Future<String> stdout = CONSUME_PROCESS_OUTPUT.submit(new ConsumeOutput(p.getInputStream())); final Future<String> stderr = CONSUME_PROCESS_OUTPUT.submit(new ConsumeOutput(p.getErrorStream())); try { p.waitFor(); } catch (InterruptedException e) { throw new IllegalStateException(); } final List<GeneratedClass> generated = new ArrayList<GeneratedClass>(); // load for (Decl decl : decls) { Class<?> loaded = null; for (Decl.ClassDecl classdecl : decl.classdecls) { File trg = classdecl.binfile; if (!trg.exists()) { System.out.println(stdout.get()); System.err.println(stderr.get()); } ByteArrayOutputStream buffer = new ByteArrayOutputStream(); BufferedInputStream in = new BufferedInputStream(new FileInputStream(trg)); int i; while ( (i = in.read()) >= 0) buffer.write(i); in.close(); loaded = CL.load(classdecl.name, buffer.toByteArray()); } generated.add(new GeneratedClass(loaded, decl.def.description())); } return generated; } // generate some random classes private static List<GeneratedClass> randomClasses(int count) throws IOException, ExecutionException, InterruptedException { // define final List<Def> defs = new ArrayList<Def>(); while (defs.size() < count) defs.add(Def.random()); return compile(defs); } // consume process output into a string private static final class ConsumeOutput implements Callable<String> { final BufferedReader in; final StringBuilder sb = new StringBuilder(); private ConsumeOutput(InputStream in) { this.in = new BufferedReader(new InputStreamReader(in)); } @Override public String call() throws Exception { try { String line; while (null != (line = in.readLine())) { sb.append(line); sb.append("\n"); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } } private static final class TrackerProvider implements Callable<Set<Object>> { @Override public Set<Object> call() throws Exception { return new HashSet<Object>(); } }; private static final class DaemonThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } } }