net.shipilev.concurrent.torture.Runner.java Source code

Java tutorial

Introduction

Here is the source code for net.shipilev.concurrent.torture.Runner.java

Source

/*
 * 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;
    }

}