/* * Copyright (c) 2012 Aleksey Shipilev * * 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 net.shipilev.concurrent.torture; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import net.shipilev.concurrency.torture.schema.result.Env; import net.shipilev.concurrency.torture.schema.result.Kv; import net.shipilev.concurrency.torture.schema.result.ObjectFactory; import net.shipilev.concurrency.torture.schema.result.Result; import net.shipilev.concurrency.torture.schema.result.State; import net.shipilev.concurrent.torture.tests.ConcurrencyTest; import net.shipilev.concurrent.torture.tests.OneActorOneObserverTest; import net.shipilev.concurrent.torture.tests.TwoActorsOneArbiterTest; import net.shipilev.concurrent.torture.util.Environment; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** * Basic runner for concurrency tests. * * @author Aleksey Shipilev (aleksey.shipilev@oracle.com) */ public class Runner { private final File destDir; private final int time; private final int loops; private final boolean shouldYield; private final int wtime; private final int witers; private final ExecutorService pool; private volatile boolean isStopped; private final PrintWriter pw; private final TextResultPrinter printer; public Runner(Options opts) throws FileNotFoundException, JAXBException { printer = new TextResultPrinter(opts); pw = new PrintWriter(System.out, true); destDir = new File(opts.getResultDest()); destDir.mkdirs(); time = opts.getTime(); loops = opts.getLoops(); wtime = opts.getWarmupTime(); witers = opts.getWarmupIterations(); shouldYield = opts.shouldYield(); pool = Executors.newCachedThreadPool(); } public void ensureThreads(int threads) { if (Runtime.getRuntime().availableProcessors() < threads && !shouldYield) { pw.println("WARNING: This test should be run with at least " + threads + " CPUs to get reliable results, or enable yielding"); } } /** * Run the test. * This method blocks until test is complete * * @param test test to run * @param <S> test state object type * @throws InterruptedException * @throws ExecutionException */ public <S> void run(OneActorOneObserverTest<S> test) throws ExecutionException, InterruptedException { pw.println("Running " + test.getClass().getName()); ensureThreads(3); if (witers > 0) { pw.print("Warmup "); for (int c = 0; c < witers; c++) { pw.print("."); pw.flush(); run(test, wtime, true); } pw.println(); } run(test, time, false); } private <S> void run(final OneActorOneObserverTest<S> test, int time, boolean dryRun) throws InterruptedException, ExecutionException { final SingleSharedStateHolder<S> holder = new SingleSharedStateHolder<S>(); // current should be null so that injector could inject the first instance holder.current = null; isStopped = false; /* Injector thread: injects new states until interrupted. */ Future<?> s1 = pool.submit(new Runnable() { public void run() { while (!isStopped) { @SuppressWarnings("unchecked") S[] newStride = (S[]) new Object[loops]; for (int c = 0; c < loops; c++) { newStride[c] = test.newState(); } while (holder.current != null) { if (isStopped) { return; } if (shouldYield) Thread.yield(); } holder.current = newStride; } } }); /* Actor 1 thread. The rationale for its loop is as follows: a. We should be easy on checking the interrupted status, hence we do $LOOPS internally b. Thread should not observe the state object more than once */ Future<?> a1 = pool.submit(new Runnable() { public void run() { S[] last = null; int[] indices = generatePermutation(loops); while (!isStopped) { S[] cur = holder.current; if (cur != null && last != cur) { for (int l = 0; l < loops; l++) { test.actor1(cur[indices[l]]); } last = cur; } else { if (shouldYield) Thread.yield(); } } } }); /* Observer thread. The rationale for its loop is as follows: a. We should be easy on checking the interrupted status, hence we do $LOOPS internally b. Thread should not observe the state object more than once c. The overhead of doing the work inside the inner loop should be small d. $state is getting reused, so we end up marshalling it to long to count properly */ Future<Multiset<Long>> res = pool.submit(new Callable<Multiset<Long>>() { public Multiset<Long> call() { Multiset<Long> set = HashMultiset.create(); S[] last = null; byte[] state = new byte[8]; byte[][] results = new byte[loops][]; int[] indices = generatePermutation(loops); while (!isStopped) { S[] cur = holder.current; if (cur != null && last != cur) { for (int l = 0; l < loops; l++) { int index = indices[l]; test.observe(cur[index], state); results[index] = new byte[8]; System.arraycopy(state, 0, results[index], 0, 8); } last = cur; for (int i = 0; i < loops; i++) { set.add(byteArrToLong(results[i])); } // let others proceed holder.current = null; } else { if (shouldYield) Thread.yield(); } } return set; } }); TimeUnit.MILLISECONDS.sleep(time); isStopped = true; a1.get(); s1.get(); res.get(); if (!dryRun) { Result r = dump(test, res.get()); judge(r); } } public static int[] generatePermutation(int len) { int[] res = new int[len]; for (int i = 0; i < len; i++) { res[i] = i; } return shuffle(res); } public static int[] shuffle(int[] arr) { Random r = new Random(); int[] res = arr.clone(); for (int i = arr.length; i > 1; i--) { int i1 = i-1; int i2 = r.nextInt(i); int t = res[i1]; res[i1] = res[i2]; res[i2] = t; } return res; } public <S> void run(final TwoActorsOneArbiterTest<S> test) throws InterruptedException, ExecutionException { pw.println("Running " + test.getClass().getName()); ensureThreads(4); if (witers > 0) { pw.print("Warmup "); for (int c = 0; c < witers; c++) { pw.print("."); pw.flush(); run(test, wtime, true); } pw.println(); } run(test, time, false); } public <S> void run(final TwoActorsOneArbiterTest<S> test, int time, boolean dryRun) throws InterruptedException, ExecutionException { final TwoSharedStateHolder<S> holder = new TwoSharedStateHolder<S>(); // need to initialize so that actor thread will not NPE. // once injector catches up, it will push fresh state objects holder.current = test.newState(); isStopped = false; /* Injector thread: injects new states until interrupted. There are an addi.tional constraints: a. If actors results are not yet consumed, do not push the new state. This will effectively block actors from working until arbiter consumes their result. */ Future<?> s1 = pool.submit(new Runnable() { public void run() { while (!isStopped) { while (holder.t1 != null && holder.t2 != null && !isStopped) if (shouldYield) Thread.yield(); holder.current = test.newState(); } } }); /* Actor 1 thread. The rationale for its loop is as follows: a. We should be easy on checking the interrupted status, hence we do $LOOPS internally b. Thread should not observe the state object more than once c. Once thread is done with its work, it publishes the reference to state object for arbiter */ Future<?> a1 = pool.submit(new Runnable() { public void run() { S last = null; while (!isStopped) { int l = 0; while (l < loops) { S cur = holder.current; if (last != cur) { test.actor1(cur); holder.t1 = cur; last = cur; } else { if (shouldYield) Thread.yield(); } l++; } } } }); /* Actor 2 thread. The rationale for its loop is as follows: a. We should be easy on checking the interrupted status, hence we do $LOOPS internally b. Thread should not observe the state object more than once c. Once thread is done with its work, it publishes the reference to state object for arbiter */ Future<?> a2 = pool.submit(new Runnable() { public void run() { S last = null; while (!isStopped) { int l = 0; while (l < loops) { S cur = holder.current; if (last != cur) { test.actor2(cur); last = cur; holder.t2 = cur; } else { if (shouldYield) Thread.yield(); } l++; } } } }); /* Arbiter thread. The rationale for its loop is as follows: a. We should be easy on checking the interrupted status, hence we do $LOOPS internally b. Thread should not observe the state object more than once c. The overhead of doing the work inside the inner loop should be small d. $state is getting reused, so we end up marshalling it to long to count properly e. Arbiter waits until both actors have finished their work and published their results */ Future<Multiset<Long>> res = pool.submit(new Callable<Multiset<Long>>() { public Multiset<Long> call() { byte[] res = new byte[8]; Multiset<Long> set = HashMultiset.create(); byte[][] results = new byte[loops][]; while (!isStopped) { int c = 0; int l = 0; while (l < loops) { S s1 = holder.t1; S s2 = holder.t2; if (s1 == s2 && s1 != null) { test.arbitrate(s1, res); results[c] = new byte[8]; System.arraycopy(res, 0, results[c], 0, 8); c++; holder.t1 = null; holder.t2 = null; } else { if (shouldYield) Thread.yield(); } l++; } for (int i = 0; i < c; i++) { set.add(byteArrToLong(results[i])); } } return set; } }); TimeUnit.MILLISECONDS.sleep(time); isStopped = true; s1.get(); a1.get(); a2.get(); res.get(); if (!dryRun) { Result r = dump(test, res.get()); judge(r); } } private Result dump(ConcurrencyTest test, Multiset<Long> results) { ObjectFactory factory = new ObjectFactory(); Result result = factory.createResult(); result.setName(test.getClass().getName()); for (Long e : results.elementSet()) { byte[] b = longToByteArr(e); byte[] temp = new byte[test.resultSize()]; System.arraycopy(b, 0, temp, 0, test.resultSize()); b = temp; State state = factory.createState(); state.setId(Arrays.toString(b)); state.setCount(results.count(e)); result.getState().add(state); } Env env = factory.createEnv(); for (Map.Entry<String, String> entry : Environment.getEnvironment().entrySet()) { Kv kv = factory.createKv(); kv.setKey(entry.getKey()); kv.setValue(entry.getValue()); env.getProperty().add(kv); } result.setEnv(env); try { String packageName = Result.class.getPackage().getName(); JAXBContext jc = JAXBContext.newInstance(packageName); Marshaller marshaller = jc.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.marshal(result, new File(destDir + "/" + test.getClass().getName() + ".xml")); } catch (Throwable e) { e.printStackTrace(); } return result; } private void judge(Result result) { printer.parse(pw, result); pw.println(); } private byte[] longToByteArr(Long element) { ByteBuffer buf = ByteBuffer.allocate(8); buf.putLong(element); return buf.array(); } public static long byteArrToLong(byte[] b) { ByteBuffer buf = ByteBuffer.wrap(b); return buf.getLong(); } public void close() throws FileNotFoundException, JAXBException { pool.shutdownNow(); } public static class SingleSharedStateHolder<S> { volatile S[] current; } public static class TwoSharedStateHolder<S> { volatile S current; volatile S t1; volatile S t2; } }