/* * Copyright (C) 2011-2014 Chris Vest (mr.chrisvest@gmail.com) * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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 stormpot.simulations; import org.HdrHistogram.Histogram; import stormpot.*; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static stormpot.AlloKit.*; public abstract class Sim { private static final Timeout SHUTDOWN_TIMEOUT = new Timeout(10, TimeUnit.SECONDS); protected enum Param { size(Integer.TYPE), expiration(Expiration.class), backgroundExpirationEnabled(Boolean.TYPE), preciseLeakDetectionEnabled(Boolean.TYPE), metricsRecorder(MetricsRecorder.class), threadFactory(ThreadFactory.class); private final Class<?> type; Param(Class<?> type) { this.type = type; } void set(Config<GenericPoolable> config, Object value) throws Exception { String firstNameChar = name().substring(0, 1); String restNameChars = name().substring(1); String setterName = "set" + firstNameChar.toUpperCase() + restNameChars; Method setter = Config.class.getMethod(setterName, type); setter.invoke(config, value); } } protected enum Output { none, summary, detailed } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) protected @interface Simulation { long measurementTime() default 10; TimeUnit measurementTimeUnit() default TimeUnit.SECONDS; Class<? extends Pool>[] pools() default {QueuePool.class, BlazePool.class}; Output output() default Output.detailed; } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) protected @interface Conf { Param value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) protected @interface Agent { String name() default ""; long initialDelay() default 0; TimeUnit initialDelayUnit() default TimeUnit.MILLISECONDS; } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) protected @interface Agents { Agent[] value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) protected @interface AgentPause { String name() default ""; TimeUnit unit() default TimeUnit.MILLISECONDS; } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) protected @interface AllocationCost { TimeUnit value() default TimeUnit.MILLISECONDS; } private static class Link<T> { final Link<T> tail; final T value; private Link(Link<T> tail, T value) { this.tail = tail; this.value = value; } } @SuppressWarnings("unchecked") public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); String cmdClass = System.getProperty("sun.java.command").replaceFirst("^.*\\s+", ""); Class<?> klass = Class.forName(cmdClass); Sim sim = (Sim) klass.newInstance(); // Find all the pool implementation constructors. List<Constructor<Pool<GenericPoolable>>> ctors = new ArrayList<>(); Simulation simulation = klass.getAnnotation(Simulation.class); if (simulation != null) { for (Class<? extends Pool> type : simulation.pools()) { Constructor<?> constructor = type.getConstructor(Config.class); ctors.add((Constructor<Pool<GenericPoolable>>) constructor); } } else { Constructor<?> constructor = QueuePool.class.getConstructor(Config.class); ctors.add((Constructor<Pool<GenericPoolable>>) constructor); constructor = BlazePool.class.getConstructor(Config.class); ctors.add((Constructor<Pool<GenericPoolable>>) constructor); } // Go through all fields, constructing a set of configurations. // Run each configuration for each pool implementation. AtomicBoolean enableAllocationCost = new AtomicBoolean(); Collection<Config<GenericPoolable>> configurations = buildConfigurations(klass, sim, enableAllocationCost); for (Config<GenericPoolable> config : configurations) { for (Constructor<Pool<GenericPoolable>> ctor : ctors) { Output output = simulation == null? Output.detailed : simulation.output(); simulate(sim, ctor, config, enableAllocationCost, output); } } long elapsedMillis = System.currentTimeMillis() - start; System.out.printf("Done, %.3f seconds.%n", elapsedMillis / 1000.0); System.exit(0); } private static Collection<Config<GenericPoolable>> buildConfigurations( Class<?> klass, Sim sim, AtomicBoolean enableAllocationCost) throws Exception { // Find all fields annotated with @Conf // Group them by their Param category // Produce a Config for every permutation of each group // Building the groups EnumMap<Param, List<Object>> groups = new EnumMap<>(Param.class); for (Field field : klass.getDeclaredFields()) { Conf conf = field.getAnnotation(Conf.class); if (conf != null) { Param param = conf.value(); Object value = field.get(sim); List<Object> values = groups.get(param); if (values == null) { values = new LinkedList<>(); groups.put(param, values); } values.add(value); } } // Sequence the groups Link<Map.Entry<Param, List<Object>>> head = null; for (Map.Entry<Param, List<Object>> entry : groups.entrySet()) { head = new Link<>(head, entry); } // Building Config permutations Config<GenericPoolable> baseConfig = new Config<>(); CountingAllocator allocator = buildAllocator(sim, enableAllocationCost); baseConfig.setAllocator(allocator); List<Config<GenericPoolable>> result = new LinkedList<>(); addConfigPermutations(head, baseConfig, result); return result; } private static CountingAllocator buildAllocator( final Sim sim, final AtomicBoolean enableAllocationCost) { Class<?> klass = sim.getClass(); TimeUnit allocationTimeUnit = null; Method allocationTimeMethod = null; for (Method method : klass.getDeclaredMethods()) { AllocationCost allocationCost = method.getAnnotation(AllocationCost.class); if (allocationCost != null) { assert allocationTimeUnit == null: "Found more than one @AllocationCost method"; allocationTimeUnit = allocationCost.value(); allocationTimeMethod = method; } } if (allocationTimeMethod == null) { return allocator(); } final TimeUnit unit = allocationTimeUnit; final Method method = allocationTimeMethod; return allocator(alloc(new Action() { @Override public GenericPoolable apply(Slot slot, GenericPoolable obj) throws Exception { if (enableAllocationCost.get()) { Number time = (Number) method.invoke(sim); unit.sleep(time.longValue()); } return $new.apply(slot, obj); } })); } private static void addConfigPermutations( Link<Map.Entry<Param, List<Object>>> head, Config<GenericPoolable> baseConfig, List<Config<GenericPoolable>> result) throws Exception { if (head == null) { // Base case result.add(baseConfig); } else { // Reduction Param param = head.value.getKey(); List<Object> values = head.value.getValue(); for (Object value : values) { if (value.getClass().isArray()) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { Config<GenericPoolable> mutation = baseConfig.clone(); param.set(mutation, Array.get(value, i)); addConfigPermutations(head.tail, mutation, result); } } else { Config<GenericPoolable> mutation = baseConfig.clone(); param.set(mutation, value); addConfigPermutations(head.tail, mutation, result); } } } } private static void simulate( Sim sim, Constructor<Pool<GenericPoolable>> ctor, Config<GenericPoolable> config, AtomicBoolean enableAllocationCost, Output output) throws Exception { DependencyResolver deps = new DependencyResolver(); deps.add(config); deps.add(config.getAllocator()); deps.add(config.getExpiration()); deps.add(config.getMetricsRecorder()); deps.add(config.getThreadFactory()); deps.add(output); String poolTypeName = ctor.getDeclaringClass().getSimpleName(); System.out.printf("Simulating %s %s for %s%n", sim.getClass().getSimpleName(), describe(config), poolTypeName); long measurementTimeMillis = getMeasurementTimeMillis(sim); int iterations = 100; long warmupTimeMillis = measurementTimeMillis / iterations; for (int i = 1; i <= iterations; i++) { boolean isWarmup = i < iterations; long runTime = isWarmup? warmupTimeMillis : measurementTimeMillis; // Reset internal state. enableAllocationCost.set(false); Pool<GenericPoolable> pool = ctor.newInstance(config); sim = sim.getClass().newInstance(); deps.replace(pool); // Wait for the pool to boot up. int size = config.getSize(); List<GenericPoolable> objs = new ArrayList<>(); for (int j = 0; j < size; j++) { objs.add(pool.claim(new Timeout(30, TimeUnit.MINUTES))); } for (GenericPoolable obj : objs) { obj.release(); } // Then run. enableAllocationCost.set(true); sim.run(deps, runTime, isWarmup? Output.none : output); // Finally clean up a bit. if (!pool.shutdown().await(SHUTDOWN_TIMEOUT)) { ManagedPool managedPool = (ManagedPool) pool; System.err.printf( "Shutdown timed out! Pool type = %s, config = %s, " + "object leaks detected = %s%n", poolTypeName, describe(config), managedPool.getLeakedObjectsCount()); } } } private static long getMeasurementTimeMillis(Sim sim) { Class<?> klass = sim.getClass(); Simulation simulation = klass.getAnnotation(Simulation.class); if (simulation != null) { long time = simulation.measurementTime(); TimeUnit unit = simulation.measurementTimeUnit(); return unit.toMillis(time); } return 10000; } private static String describe(Config<GenericPoolable> config) { int size = config.getSize(); Expiration<? super GenericPoolable> expiration = config.getExpiration(); boolean backgroundExpirationEnabled = config.isBackgroundExpirationEnabled(); boolean preciseLeakDetectionEnabled = config.isPreciseLeakDetectionEnabled(); MetricsRecorder metricsRecorder = config.getMetricsRecorder(); ThreadFactory threadFactory = config.getThreadFactory(); return String.format( "{%n\tsize = %s" + "%n\texpiration = %s%n\t" + "backgroundExpirationEnabled = %s%n\t" + "preciseLeakDetectionEnabled = %s%n\t" + "metricsRecorder = %s%n\t" + "threadFactory = %s%n}", size, expiration, backgroundExpirationEnabled, preciseLeakDetectionEnabled, metricsRecorder, threadFactory); } private void run( DependencyResolver deps, long runTimeMillis, Output output) throws InterruptedException { // Find all agents and their corresponding configuration fields, and create // a thread for each. ControlSignal controlSignal = new ControlSignal(); // Agents are defined by the @Agent or @Agents annotations. // Their timings are defined by the @AgentPause annotation, linked by their name. Method[] methods = getClass().getMethods(); List<AgentRunner> agents = new ArrayList<>(); Map<String, IterationPause> iterationPauseIndex = new HashMap<>(); for (Method method : methods) { AgentPause agentPause = method.getAnnotation(AgentPause.class); if (agentPause != null) { IterationPause pause = new IterationPause(agentPause, this, method); iterationPauseIndex.put(agentPause.name(), pause); } } for (Method method : methods) { Agent agent = method.getAnnotation(Agent.class); if (agent != null) { startAgent(deps, controlSignal, agents, iterationPauseIndex, method, agent); } Agents agentsAnnotation = method.getAnnotation(Agents.class); if (agentsAnnotation != null) { for (Agent subAgent : agentsAnnotation.value()) { startAgent(deps, controlSignal, agents, iterationPauseIndex, method, subAgent); } } } if (agents.isEmpty()) { System.err.println("No public @Agent or @Agents annotated methods found!"); return; } // Then start them all, and run them for as long as need be. controlSignal.start(); Thread.sleep(runTimeMillis); controlSignal.stop(); controlSignal.awaitDone(agents.size()); // Finally print their results, if need be. if (output != Output.none) { Histogram sum = newHistogram(); for (AgentRunner agent : agents) { sum.add(agent.histogram); if (output == Output.detailed) { agent.printResults(); } } if (agents.size() > 1 || output == Output.summary) { System.out.println("Latency results sum:"); printHistogram(sum); System.out.println(); } } } private void startAgent( DependencyResolver deps, ControlSignal controlSignal, List<AgentRunner> agents, Map<String, IterationPause> iterationPauseIndex, Method method, Agent agent) { IterationPause iterationPause = iterationPauseIndex.get(agent.name()); AgentRunner runner = new AgentRunner( controlSignal, deps, agent, this, method, iterationPause); Thread thread = new Thread(runner, "Agent[" + agent.name() + "]"); thread.start(); agents.add(runner); } private static class AgentRunner implements Runnable { private final ControlSignal controlSignal; private final long initialDelayTime; private final TimeUnit initialDelayUnit; private final String agentName; private final Sim sim; private final Method agentMethod; private final IterationPause iterationPause; private final Histogram histogram; private final Object[] argumentList; public AgentRunner( ControlSignal controlSignal, DependencyResolver deps, Agent agent, Sim sim, Method agentMethod, IterationPause iterationPause) { this.controlSignal = controlSignal; initialDelayTime = agent.initialDelay(); initialDelayUnit = agent.initialDelayUnit(); agentName = agent.name(); this.sim = sim; this.agentMethod = agentMethod; this.iterationPause = iterationPause; histogram = newHistogram(); argumentList = deps.resolve(agentMethod.getParameterTypes()); } @Override public void run() { try { runUnsafe(); } catch (Exception e) { throw new RuntimeException("Agent[" + agentName + "] failed", e); } } private void runUnsafe() throws Exception { controlSignal.awaitStart(); if (initialDelayUnit != null) { initialDelayUnit.sleep(initialDelayTime); } long lastPauseTime = 0; while (!controlSignal.stopped()) { long start = System.nanoTime(); callAgentMethod(); long elapsed = System.nanoTime() - start; long micros = elapsed / 1000; histogram.recordValueWithExpectedInterval(micros, lastPauseTime); lastPauseTime = iterationPause.pause(micros); } controlSignal.done(); } private void callAgentMethod() { try { agentMethod.invoke(sim, argumentList); } catch (Exception e) { throw new RuntimeException("Agent[" + agentName + "] method invocation failure", e); } } public void printResults() { System.out.printf("Latency results for Agent[%s]:%n", agentName); printHistogram(histogram); System.out.println(); } } private static Histogram newHistogram() { return new Histogram(TimeUnit.MINUTES.toMicros(10), 3); } private static void printHistogram(Histogram histogram) { histogram.outputPercentileDistribution(System.out, 1, 1000.0); } private static class IterationPause { private final Sim sim; private final Method pauseMethod; private final TimeUnit pauseUnit; public IterationPause(AgentPause agentPause, Sim sim, Method pauseMethod) { this.sim = sim; this.pauseMethod = pauseMethod; this.pauseUnit = agentPause.unit(); } public long pause(long spentTimeMicros) throws Exception { Number result = (Number) pauseMethod.invoke(sim); long pauseTime = result.longValue(); long upauseTime = pauseUnit.toMicros(pauseTime); TimeUnit.MICROSECONDS.sleep(upauseTime - spentTimeMicros); return upauseTime; } } private static class ControlSignal { private final CountDownLatch startLatch; private final Semaphore doneCounter; private volatile boolean stopped; private ControlSignal() { startLatch = new CountDownLatch(1); doneCounter = new Semaphore(0); } public void awaitStart() throws InterruptedException { startLatch.await(); } public void start() { startLatch.countDown(); } public boolean stopped() { return stopped; } public void stop() { stopped = true; } public void done() { doneCounter.release(); } public void awaitDone(int doneCounts) throws InterruptedException { doneCounter.acquire(doneCounts); } } private static class DependencyResolver { private final List<Object> dependencies; private DependencyResolver() { dependencies = new ArrayList<>(); } public void add(Object obj) { if (obj != null) { dependencies.add(obj); } } public Object[] resolve(Class<?>[] types) { Object[] values = new Object[types.length]; for (int i = 0; i < values.length; i++) { values[i] = resolve(types[i]); } return values; } private <T> T resolve(Class<T> type) { for (Object dependency : dependencies) { if (type.isInstance(dependency)) { return type.cast(dependency); } } return null; } public void replace(Object obj) { int size = dependencies.size(); for (int i = 0; i < size; i++) { if (dependencies.get(i).getClass() == obj.getClass()) { dependencies.set(i, obj); return; } } add(obj); } } }