/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.avro.io;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.avro.FooBarSpecificRecord;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Field;
import org.apache.avro.TypeEnum;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.reflect.ReflectData;
import org.apache.avro.reflect.ReflectDatumReader;
import org.apache.avro.reflect.ReflectDatumWriter;
import org.apache.avro.specific.SpecificDatumReader;
import org.apache.avro.specific.SpecificDatumWriter;
import org.apache.avro.specific.SpecificRecordBase;
import org.apache.avro.util.Utf8;


/**
 * Performance tests for various low level operations of
 * Avro encoding and decoding.
 */
public class Perf {
  private static final int COUNT = 250000; // needs to be a multiple of 4
  private static final int CYCLES = 800;

  /**
   * Use a fixed value seed for random number generation
   * to allow for better cross-run comparisons.
   */
  private static final long SEED = 19781210;

  protected static Random newRandom() {
    return new Random(SEED);
  }

  private static class TestDescriptor {
    Class<? extends Test> test;
    String param;
    TestDescriptor(Class<? extends Test> test, String param) {
      this.test = test;
      this.param = param;
    }
    void add(List<TestDescriptor> typeList) {
      ALL_TESTS.put(param, this);
      typeList.add(this);
    }
  }

  private static final List<TestDescriptor> BASIC = new ArrayList<TestDescriptor>();
  private static final List<TestDescriptor> RECORD = new ArrayList<TestDescriptor>();
  private static final List<TestDescriptor> GENERIC = new ArrayList<TestDescriptor>();
  private static final List<TestDescriptor> GENERIC_ONETIME = new ArrayList<TestDescriptor>();
  private static final List<TestDescriptor> SPECIFIC = new ArrayList<TestDescriptor>();
  private static final List<TestDescriptor> REFLECT = new ArrayList<TestDescriptor>();
  private static final LinkedHashMap<String, TestDescriptor> ALL_TESTS;
  private static final LinkedHashMap<String, List<TestDescriptor>> BATCHES;
  static {
    ALL_TESTS = new LinkedHashMap<String, TestDescriptor>();
    BATCHES = new LinkedHashMap<String, List<TestDescriptor>>();
    BATCHES.put("-basic", BASIC);
    new TestDescriptor(IntTest.class, "-i").add(BASIC);
    new TestDescriptor(SmallLongTest.class, "-ls").add(BASIC);
    new TestDescriptor(LongTest.class, "-l").add(BASIC);
    new TestDescriptor(FloatTest.class, "-f").add(BASIC);
    new TestDescriptor(DoubleTest.class, "-d").add(BASIC);
    new TestDescriptor(BoolTest.class, "-b").add(BASIC);
    new TestDescriptor(BytesTest.class, "-by").add(BASIC);
    new TestDescriptor(StringTest.class, "-s").add(BASIC);
    new TestDescriptor(ArrayTest.class, "-a").add(BASIC);
    new TestDescriptor(MapTest.class, "-m").add(BASIC);
    BATCHES.put("-record", RECORD);
    new TestDescriptor(RecordTest.class, "-R").add(RECORD);
    new TestDescriptor(ValidatingRecord.class, "-Rv").add(RECORD);
    new TestDescriptor(ResolvingRecord.class, "-Rr").add(RECORD);
    new TestDescriptor(RecordWithDefault.class, "-Rd").add(RECORD);
    new TestDescriptor(RecordWithOutOfOrder.class, "-Ro").add(RECORD);
    new TestDescriptor(RecordWithPromotion.class, "-Rp").add(RECORD);
    BATCHES.put("-generic", GENERIC);
    new TestDescriptor(GenericTest.class, "-G").add(GENERIC);
    new TestDescriptor(GenericStrings.class, "-Gs").add(GENERIC);
    new TestDescriptor(GenericNested.class, "-Gn").add(GENERIC);
    new TestDescriptor(GenericNestedFake.class, "-Gf").add(GENERIC);
    new TestDescriptor(GenericWithDefault.class, "-Gd").add(GENERIC);
    new TestDescriptor(GenericWithOutOfOrder.class, "-Go").add(GENERIC);
    new TestDescriptor(GenericWithPromotion.class, "-Gp").add(GENERIC);
    BATCHES.put("-generic-onetime", GENERIC_ONETIME);
    new TestDescriptor(GenericOneTimeDecoderUse.class, "-Gotd").add(GENERIC_ONETIME);
    new TestDescriptor(GenericOneTimeReaderUse.class, "-Gotr").add(GENERIC_ONETIME);
    new TestDescriptor(GenericOneTimeUse.class, "-Got").add(GENERIC_ONETIME);
    new TestDescriptor(FooBarSpecificRecordTest.class, "-Sf").add(SPECIFIC);
    BATCHES.put("-reflect", REFLECT);
    new TestDescriptor(ReflectRecordTest.class, "-REFr").add(REFLECT);
    new TestDescriptor(ReflectBigRecordTest.class, "-REFbr").add(REFLECT);
    new TestDescriptor(ReflectFloatTest.class, "-REFf").add(REFLECT);
    new TestDescriptor(ReflectDoubleTest.class, "-REFd").add(REFLECT);
    new TestDescriptor(ReflectIntArrayTest.class, "-REFia").add(REFLECT);
    new TestDescriptor(ReflectLongArrayTest.class, "-REFla").add(REFLECT);
    new TestDescriptor(ReflectDoubleArrayTest.class, "-REFda").add(REFLECT);
    new TestDescriptor(ReflectFloatArrayTest.class, "-REFfa").add(REFLECT);
    new TestDescriptor(ReflectNestedFloatArrayTest.class, "-REFnf").add(REFLECT);
    new TestDescriptor(ReflectNestedObjectArrayTest.class, "-REFno").add(REFLECT);
    new TestDescriptor(ReflectNestedLargeFloatArrayTest.class, "-REFnlf").add(REFLECT);
    new TestDescriptor(ReflectNestedLargeFloatArrayBlockedTest.class, "-REFnlfb").add(REFLECT);
  }

  private static void usage() {
    StringBuilder usage = new StringBuilder("Usage: Perf { -nowrite | -noread | ");
    StringBuilder details = new StringBuilder();
    details.append(" -nowrite   (do not execute write tests)\n");
    details.append(" -noread   (do not execute write tests)\n");
    for (Map.Entry<String, List<TestDescriptor>> entry : BATCHES.entrySet()) {
      List<TestDescriptor> lt = entry.getValue();
      String param = entry.getKey();
      String paramName = param.substring(1);
      usage.append(param).append(" | ");
      details.append(" ").append(param).append("   (executes all ").append(paramName).append(" tests):\n");
      for (TestDescriptor t : lt) {
        usage.append(t.param).append(" | ");
        details.append("      ").append(t.param).append("  (").append(t.test.getSimpleName()).append(")\n");
      }
    }
    usage.setLength(usage.length() - 2);
    usage.append("}\n");
    System.out.println(usage.toString());
    System.out.print(details.toString());
  }

  public static void main(String[] args) throws Exception {
    List<Test> tests = new ArrayList<Test>();
    boolean writeTests = true;
    boolean readTests = true;
    for (String a : args) {
      TestDescriptor t = ALL_TESTS.get(a);
      if (null != t) {
        tests.add(t.test.newInstance());
        continue;
      }
      List<TestDescriptor> lt = BATCHES.get(a);
      if (null != lt) {
        for (TestDescriptor td : lt) {
          tests.add(td.test.newInstance());
        }
        continue;
      }
      if ("-nowrite".equals(a)) {
        writeTests = false;
        continue;
      }
      if ("-noread".equals(a)) {
        readTests = false;
        continue;
      }
      usage();
      System.exit(1);
    }
    if (tests.isEmpty()) {
      for (Map.Entry<String, TestDescriptor> entry : ALL_TESTS.entrySet()) {
        TestDescriptor t = entry.getValue();
        Test test = t.test.newInstance();
        tests.add(test);
      }
    }
    System.out.println("Executing tests: \n" + tests +  "\n readTests:" +
        readTests + "\n writeTests:" + writeTests + "\n cycles=" + CYCLES);

    for (int k = 0; k < tests.size(); k++) {
      Test t = tests.get(k);
      try {
        // get everything to compile once
        t.init();
        if (t.isReadTest()) {
          t.readTest();
        }
        if (t.isWriteTest()) {
          t.writeTest();
        }
        t.reset();
      } catch (Exception e) {
        System.out.println("Failed to execute test: " + t.getClass().getSimpleName());
        throw e;
      }
    }

    printHeader();

    for (int k = 0; k < tests.size(); k++) {
      Test t = tests.get(k);
      // warmup JVM
      t.init();
      if (t.isReadTest() && readTests) {
        for (int i = 0; i < t.cycles/2; i++) {
          t.readTest();
        }
      }
      if (t.isWriteTest() && writeTests) {
        for (int i = 0; i < t.cycles/2; i++) {
          t.writeTest();
        }
      }
      t.reset();
      // test
      long s = 0;
      System.gc();
      t.init();
      if (t.isReadTest() && readTests) {
        for (int i = 0; i < t.cycles; i++) {
          s += t.readTest();
        }
        printResult(s, t, t.name + "Read");
      }
      s = 0;
      if (t.isWriteTest() && writeTests) {
        for (int i = 0; i < t.cycles; i++) {
          s += t.writeTest();
        }
        printResult(s, t, t.name + "Write");
      }
      t.reset();
    }
  }

  private static final void printHeader() {
    String header = String.format(
        "%60s     time    M entries/sec   M bytes/sec  bytes/cycle",
        "test name");
    System.out.println(header.toString());
  }

  private static final void printResult(long s, Test t, String name) {
    s /= 1000;
    double entries = (t.cycles * (double) t.count);
    double bytes = t.cycles * (double) t.encodedSize;
    StringBuilder result = new StringBuilder();
    result.append(String.format("%42s: %6d ms  ", name, (s/1000)));
    result.append(String.format("%10.3f   %11.3f   %11d",
        (entries / s), (bytes/ s),  t.encodedSize));
    System.out.println(result.toString());
  }

  private abstract static class Test {

    /**
     * Name of the test.
     */
    public final String name;
    public final int count;
    public final int cycles;
    public long encodedSize = 0;
    protected boolean isReadTest = true;
    protected boolean isWriteTest = true;
    static DecoderFactory decoder_factory = new DecoderFactory();
    static EncoderFactory encoder_factory = new EncoderFactory();

    public Test(String name, int cycles, int count) {
      this.name = name;
      this.cycles = cycles;
      this.count = count;
    }

    /**
     * Reads data from a Decoder and returns the time taken in nanoseconds.
     */
    abstract long readTest() throws IOException;

    /**
     * Writes data to an Encoder and returns the time taken in nanoseconds.
     */
    abstract long writeTest() throws IOException;

    final boolean isWriteTest() {
      return isWriteTest;
    }

    final boolean isReadTest() {
      return isReadTest;
    }

    /** initializes data for read and write tests **/
    abstract void init() throws IOException;

    /** clears generated data arrays and other large objects created during initialization **/
    abstract void reset();

    @Override
    public String toString() {
      return this.getClass().getSimpleName();
    }

  }

  /** the basic test writes a simple schema directly to an encoder or
   * reads from an array.  It does not use GenericDatumReader or any
   * higher level constructs, just manual serialization.
   */
  private static abstract class BasicTest extends Test {
    protected final Schema schema;
    protected byte[] data;
    BasicTest(String name, String json) throws IOException {
      this(name, json, 1);
    }
    BasicTest(String name, String json, int factor) throws IOException {
      super(name, CYCLES, COUNT/factor);
      this.schema = new Schema.Parser().parse(json);
    }

    @Override
    public final long readTest() throws IOException {
      long t = System.nanoTime();
      Decoder d = getDecoder();
      readInternal(d);
      return (System.nanoTime() - t);
    }

    @Override
    public final long writeTest() throws IOException {
      long t = System.nanoTime();
      Encoder e = getEncoder();
      writeInternal(e);
      e.flush();
      return (System.nanoTime() - t);
    }

    protected Decoder getDecoder() throws IOException {
      return newDecoder();
    }

    private Encoder getEncoder() throws IOException {
      return newEncoder(getOutputStream());
    }

    protected Decoder newDecoder() {
      return decoder_factory.binaryDecoder(data, null);
    }

    protected Encoder newEncoder(ByteArrayOutputStream out) throws IOException {
      Encoder e = encoder_factory.binaryEncoder(out, null);
//    Encoder e = encoder_factory.directBinaryEncoder(out, null);
//    Encoder e = encoder_factory.blockingBinaryEncoder(out, null);
//    Encoder e = new LegacyBinaryEncoder(out);
      return e;
    }

    private ByteArrayOutputStream getOutputStream() {
      return new ByteArrayOutputStream((int)(encodedSize > 0 ? encodedSize : count));
    }

    @Override
    void init() throws IOException {
      genSourceData();
      ByteArrayOutputStream baos = getOutputStream();
      Encoder e = newEncoder(baos);
      writeInternal(e);
      e.flush();
      data = baos.toByteArray();
      encodedSize = data.length;
      //System.out.println(this.getClass().getSimpleName() + " encodedSize=" + encodedSize);
    }

    abstract void genSourceData();
    abstract void readInternal(Decoder d) throws IOException;
    abstract void writeInternal(Encoder e) throws IOException;
  }

  static class IntTest extends BasicTest {
    protected int[] sourceData = null;
    public IntTest() throws IOException {
      this("Int", "{ \"type\": \"int\"} ");
    }

    private IntTest(String name, String schema) throws IOException {
      super(name, schema);
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new int[count];
      for (int i = 0; i < sourceData.length; i+=4) {
        sourceData[i] = r.nextInt(50); // fits in 1 byte
        sourceData[i+1] = r.nextInt(5000); // fits in 2 bytes
        sourceData[i+2] = r.nextInt(500000); // fits in 3 bytes
        sourceData[i+3] = r.nextInt(150000000); // most in 4, some in 5
      }
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count/4; i++) {
        d.readInt();
        d.readInt();
        d.readInt();
        d.readInt();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length; i+=4) {
        e.writeInt(sourceData[i]);
        e.writeInt(sourceData[i+1]);
        e.writeInt(sourceData[i+2]);
        e.writeInt(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  // This is the same data as ReadInt, but using readLong.
  static class SmallLongTest extends IntTest {
    public SmallLongTest() throws IOException {
      super("SmallLong", "{ \"type\": \"long\"} ");
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count/4; i++) {
        d.readLong();
        d.readLong();
        d.readLong();
        d.readLong();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length; i+=4) {
        e.writeLong(sourceData[i]);
        e.writeLong(sourceData[i+1]);
        e.writeLong(sourceData[i+2]);
        e.writeLong(sourceData[i+3]);
      }
    }
  }

  // this tests reading Longs that are sometimes very large
  static class LongTest extends BasicTest {
    private long[] sourceData = null;
    public LongTest() throws IOException {
      super("Long", "{ \"type\": \"long\"} ");
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new long[count];
      for (int i = 0; i < sourceData.length; i+=4) {
        sourceData[i] = r.nextLong() % 0x7FL; // half fit in 1, half in 2
        sourceData[i+1] = r.nextLong() % 0x1FFFFFL; // half fit in <=3, half in 4
        sourceData[i+2] = r.nextLong() % 0x3FFFFFFFFL; // half in <=5, half in 6
        sourceData[i+3] = r.nextLong() % 0x1FFFFFFFFFFFFL; // half in <=8, half in 9
      }
      // last 16, make full size
      for (int i = sourceData.length - 16; i < sourceData.length; i ++) {
        sourceData[i] = r.nextLong();
      }
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count/4; i++) {
        d.readLong();
        d.readLong();
        d.readLong();
        d.readLong();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length;i+=4) {
        e.writeLong(sourceData[i]);
        e.writeLong(sourceData[i+1]);
        e.writeLong(sourceData[i+2]);
        e.writeLong(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class FloatTest extends BasicTest {
    float[] sourceData = null;
    public FloatTest() throws IOException {
      this("Float", "{ \"type\": \"float\"} ");
    }
    public FloatTest(String name, String schema) throws IOException {
      super(name, schema);
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new float[count];
      for (int i = 0; i < sourceData.length;) {
        sourceData[i++] = r.nextFloat();
      }
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count; i+=4) {
        d.readFloat();
        d.readFloat();
        d.readFloat();
        d.readFloat();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length;i+=4) {
        e.writeFloat(sourceData[i]);
        e.writeFloat(sourceData[i+1]);
        e.writeFloat(sourceData[i+2]);
        e.writeFloat(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class DoubleTest extends BasicTest {
    double[] sourceData = null;
    public DoubleTest() throws IOException {
      super("Double", "{ \"type\": \"double\"} ");
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new double[count];
      for (int i = 0; i < sourceData.length;) {
        sourceData[i++] = r.nextDouble();
      }
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count; i+=4) {
        d.readDouble();
        d.readDouble();
        d.readDouble();
        d.readDouble();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length;i+=4) {
        e.writeDouble(sourceData[i]);
        e.writeDouble(sourceData[i+1]);
        e.writeDouble(sourceData[i+2]);
        e.writeDouble(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class BoolTest extends BasicTest {
    boolean[] sourceData = null;
    public BoolTest() throws IOException {
      super("Boolean", "{ \"type\": \"boolean\"} ");
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new boolean[count];
      for (int i = 0; i < sourceData.length;) {
        sourceData[i++] = r.nextBoolean();
      }
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count/4; i++) {
        d.readBoolean();
        d.readBoolean();
        d.readBoolean();
        d.readBoolean();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length;i+=4) {
        e.writeBoolean(sourceData[i]);
        e.writeBoolean(sourceData[i+1]);
        e.writeBoolean(sourceData[i+2]);
        e.writeBoolean(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class BytesTest extends BasicTest {
    byte[][] sourceData = null;
    public BytesTest() throws IOException {
      super("Bytes", "{ \"type\": \"bytes\"} ", 5);
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new byte[count][];
      for (int i = 0; i < sourceData.length;) {
        byte[] data = new byte[r.nextInt(70)];
        r.nextBytes(data);
        sourceData[i++] = data;
      }
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      ByteBuffer bb = ByteBuffer.allocate(70);
      for (int i = 0; i < count/4; i++) {
        d.readBytes(bb);
        d.readBytes(bb);
        d.readBytes(bb);
        d.readBytes(bb);
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length;i+=4) {
        e.writeBytes(sourceData[i]);
        e.writeBytes(sourceData[i+1]);
        e.writeBytes(sourceData[i+2]);
        e.writeBytes(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  private static String randomString(Random r) {
    char[] data = new char[r.nextInt(70)];
    for (int j = 0; j < data.length; j++) {
      data[j] = (char)('a' + r.nextInt('z'-'a'));
    }
    return new String(data);
  }

  static class StringTest extends BasicTest {
    String[] sourceData = null;
    public StringTest() throws IOException {
      super("String", "{ \"type\": \"string\"} ", 5);
    }

    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new String[count];
      for (int i = 0; i < sourceData.length;)
        sourceData[i++] = randomString(r);
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      Utf8 utf = new Utf8();
      for (int i = 0; i < count/4; i++) {
        d.readString(utf).toString();
        d.readString(utf).toString();
        d.readString(utf).toString();
        d.readString(utf).toString();
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length;i+=4) {
        e.writeString(sourceData[i]);
        e.writeString(sourceData[i+1]);
        e.writeString(sourceData[i+2]);
        e.writeString(sourceData[i+3]);
      }
    }

    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class ArrayTest extends FloatTest {
    public ArrayTest() throws IOException {
      super("Array",
          "{ \"type\": \"array\", \"items\": " +
          " { \"type\": \"record\", \"name\":\"Foo\", \"fields\": " +
          "  [{\"name\":\"bar\", \"type\":" +
          "    {\"type\": \"array\", \"items\": " +
          "     { \"type\": \"record\", \"name\":\"Vals\", \"fields\": [" +
          "      {\"name\":\"f1\", \"type\":\"float\"}," +
          "      {\"name\":\"f2\", \"type\":\"float\"}," +
          "      {\"name\":\"f3\", \"type\":\"float\"}," +
          "      {\"name\":\"f4\", \"type\":\"float\"}]" +
          "     }" +
          "    }" +
          "   }]}}");
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      d.readArrayStart();
      for (long i = d.readArrayStart(); i != 0; i = d.arrayNext()) {
        for (long j = 0; j < i; j++) {
          d.readFloat();
          d.readFloat();
          d.readFloat();
          d.readFloat();
        }
      }
      d.arrayNext();
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      int items = sourceData.length/4;
      e.writeArrayStart();
      e.setItemCount(1);
      e.startItem();
      e.writeArrayStart();
      e.setItemCount(items);
      for (int i = 0; i < sourceData.length;i+=4) {
        e.startItem();
        e.writeFloat(sourceData[i]);
        e.writeFloat(sourceData[i+1]);
        e.writeFloat(sourceData[i+2]);
        e.writeFloat(sourceData[i+3]);
      }
      e.writeArrayEnd();
      e.writeArrayEnd();
    }
  }

  static class MapTest extends FloatTest {
    public MapTest() throws IOException {
      super("Map", "{ \"type\": \"map\", \"values\": " +
          "  { \"type\": \"record\", \"name\":\"Vals\", \"fields\": [" +
          "   {\"name\":\"f1\", \"type\":\"float\"}," +
          "   {\"name\":\"f2\", \"type\":\"float\"}," +
          "   {\"name\":\"f3\", \"type\":\"float\"}," +
          "   {\"name\":\"f4\", \"type\":\"float\"}]" +
          "  }} ");
    }

    @Override
    void readInternal(Decoder d) throws IOException {
      Utf8 key = new Utf8();
      for (long i = d.readMapStart(); i != 0; i = d.mapNext()) {
        for (long j = 0; j < i; j++) {
          key = d.readString(key);
          d.readFloat();
          d.readFloat();
          d.readFloat();
          d.readFloat();
        }
      }
    }

    @Override
    void writeInternal(Encoder e) throws IOException {
      int items = sourceData.length/4;
      e.writeMapStart();
      e.setItemCount(items);
      Utf8 foo = new Utf8("foo");
      for (int i = 0; i < sourceData.length;i+=4) {
        e.startItem();
        e.writeString(foo);
        e.writeFloat(sourceData[i]);
        e.writeFloat(sourceData[i+1]);
        e.writeFloat(sourceData[i+2]);
        e.writeFloat(sourceData[i+3]);
      }
      e.writeMapEnd();
    }
  }

  private static final String RECORD_SCHEMA =
    "{ \"type\": \"record\", \"name\": \"R\", \"fields\": [\n"
    + "{ \"name\": \"f1\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f2\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f3\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f4\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f5\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f6\", \"type\": \"int\" }\n"
    + "] }";

  private static final String NESTED_RECORD_SCHEMA =
    "{ \"type\": \"record\", \"name\": \"R\", \"fields\": [\n"
    + "{ \"name\": \"f1\", \"type\": \n" +
        "{ \"type\": \"record\", \"name\": \"D\", \"fields\": [\n" +
          "{\"name\": \"dbl\", \"type\": \"double\" }]\n" +
        "} },\n"
    + "{ \"name\": \"f2\", \"type\": \"D\" },\n"
    + "{ \"name\": \"f3\", \"type\": \"D\" },\n"
    + "{ \"name\": \"f4\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f5\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f6\", \"type\": \"int\" }\n"
    + "] }";

  private static class Rec {
    double f1;
    double f2;
    double f3;
    int f4;
    int f5;
    int f6;
    Rec() {

    }
    Rec(Random r) {
      f1 = r.nextDouble();
      f2 = r.nextDouble();
      f3 = r.nextDouble();
      f4 = r.nextInt();
      f5 = r.nextInt();
      f6 = r.nextInt();
    }
  }

  static class RecordTest extends BasicTest {
    Rec[] sourceData = null;
    public RecordTest() throws IOException {
      this("Record");
    }
    public RecordTest(String name) throws IOException {
      super(name, RECORD_SCHEMA, 6);
    }
    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new Rec[count];
      for (int i = 0; i < sourceData.length; i++) {
        sourceData[i] = new Rec(r);
      }
    }
    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count; i++) {
        d.readDouble();
        d.readDouble();
        d.readDouble();
        d.readInt();
        d.readInt();
        d.readInt();
      }
    }
    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length; i++) {
        Rec r = sourceData[i];
        e.writeDouble(r.f1);
        e.writeDouble(r.f2);
        e.writeDouble(r.f3);
        e.writeInt(r.f4);
        e.writeInt(r.f5);
        e.writeInt(r.f6);
      }
    }
    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class ValidatingRecord extends RecordTest {
    ValidatingRecord() throws IOException {
      super("ValidatingRecord");
    }
    @Override
    protected Decoder getDecoder() throws IOException {
      return new ValidatingDecoder(schema, super.getDecoder());
    }
    @Override
    protected Encoder newEncoder(ByteArrayOutputStream out) throws IOException {
      return encoder_factory.validatingEncoder(schema, super.newEncoder(out));
    }
  }

  static class ResolvingRecord extends RecordTest {
    public ResolvingRecord() throws IOException {
      super("ResolvingRecord");
      isWriteTest = false;
    }
    @Override
    protected Decoder getDecoder() throws IOException {
      return new ResolvingDecoder(schema, schema, super.getDecoder());
    }
  }

  private static final String RECORD_SCHEMA_WITH_DEFAULT =
    "{ \"type\": \"record\", \"name\": \"R\", \"fields\": [\n"
    + "{ \"name\": \"f1\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f2\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f3\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f4\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f5\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f6\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f7\", \"type\": \"string\", "
      + "\"default\": \"undefined\" },\n"
    + "{ \"name\": \"f8\", \"type\": \"string\","
      + "\"default\": \"undefined\" }\n"
    + "] }";

  private static final String RECORD_SCHEMA_WITH_OUT_OF_ORDER =
    "{ \"type\": \"record\", \"name\": \"R\", \"fields\": [\n"
    + "{ \"name\": \"f1\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f3\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f5\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f2\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f4\", \"type\": \"int\" },\n"
    + "{ \"name\": \"f6\", \"type\": \"int\" }\n"
    + "] }";

  private static final String RECORD_SCHEMA_WITH_PROMOTION =
    "{ \"type\": \"record\", \"name\": \"R\", \"fields\": [\n"
    + "{ \"name\": \"f1\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f2\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f3\", \"type\": \"double\" },\n"
    + "{ \"name\": \"f4\", \"type\": \"long\" },\n"
    + "{ \"name\": \"f5\", \"type\": \"long\" },\n"
    + "{ \"name\": \"f6\", \"type\": \"long\" }\n"
    + "] }";


  /**
   * Tests the performance of introducing default values.
   */
  static class RecordWithDefault extends RecordTest {
    private final Schema readerSchema;
    public RecordWithDefault() throws IOException {
      super("RecordWithDefault");
      readerSchema = new Schema.Parser().parse(RECORD_SCHEMA_WITH_DEFAULT);
      isWriteTest = false;
    }
    @Override
    protected Decoder getDecoder() throws IOException {
      return new ResolvingDecoder(schema, readerSchema, super.getDecoder());
    }
    @Override
    protected void readInternal(Decoder d) throws IOException {
      ResolvingDecoder r = (ResolvingDecoder) d;
      Field[] ff = r.readFieldOrder();
      for (int i = 0; i < count; i++) {
        for (int j = 0; j < ff.length; j++) {
          Field f = ff[j];
          switch (f.pos()) {
          case 0:
          case 1:
          case 2:
            r.readDouble();
            break;
          case 3:
          case 4:
          case 5:
            r.readInt();
            break;
          case 6:
          case 7:
            r.readString(null);
            break;
          }
        }
      }
    }
  }

  /**
   * Tests the performance of resolving a change in field order.
   */
  static class RecordWithOutOfOrder extends RecordTest {
    private final Schema readerSchema;
    public RecordWithOutOfOrder() throws IOException {
      super("RecordWithOutOfOrder");
      readerSchema = new Schema.Parser().parse(RECORD_SCHEMA_WITH_OUT_OF_ORDER);
      isWriteTest = false;
    }
    @Override
    protected Decoder getDecoder() throws IOException {
      return new ResolvingDecoder(schema, readerSchema, super.getDecoder());
    }
    @Override
    protected void readInternal(Decoder d) throws IOException {
      ResolvingDecoder r = (ResolvingDecoder) d;
      Field[] ff = r.readFieldOrder();
      for (int i = 0; i < count; i++) {
        for (int j = 0; j < ff.length; j++) {
          Field f = ff[j];
          switch (f.pos()) {
          case 0:
          case 1:
          case 3:
            r.readDouble();
            break;
          case 2:
          case 4:
          case 5:
            r.readInt();
            break;
          }
        }
      }
    }
  }

  /**
   * Tests the performance of resolving a type promotion.
   */
  static class RecordWithPromotion extends RecordTest {
    private final Schema readerSchema;
    public RecordWithPromotion() throws IOException {
      super("RecordWithPromotion");
      readerSchema = new Schema.Parser().parse(RECORD_SCHEMA_WITH_PROMOTION);
      isWriteTest = false;
    }
    @Override
    protected Decoder getDecoder() throws IOException {
      return new ResolvingDecoder(schema, readerSchema, super.getDecoder());
    }
    @Override
    protected void readInternal(Decoder d) throws IOException {
      ResolvingDecoder r = (ResolvingDecoder) d;
      Field[] ff = r.readFieldOrder();
      for (int i = 0; i < count; i++) {
        for (int j = 0; j < ff.length; j++) {
          Field f = ff[j];
          switch (f.pos()) {
          case 0:
          case 1:
          case 2:
            r.readDouble();
            break;
          case 3:
          case 4:
          case 5:
            r.readLong();
            break;
          }
        }
      }
    }
  }

  static class GenericTest extends BasicTest {
    GenericRecord[] sourceData = null;
    protected final GenericDatumReader<Object> reader;
    public GenericTest() throws IOException {
      this("Generic");
    }
    protected GenericTest(String name) throws IOException {
      this(name, RECORD_SCHEMA);
    }
    protected GenericTest(String name, String writerSchema) throws IOException {
      super(name, writerSchema, 12);
      reader = newReader();
    }
    protected GenericDatumReader<Object> getReader() {
      return reader;
    }
    protected GenericDatumReader<Object> newReader() {
      return new GenericDatumReader<Object>(schema);
    }
    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new GenericRecord[count];
      for (int i = 0; i < sourceData.length; i++) {
        GenericRecord rec = new GenericData.Record(schema);
        rec.put(0, r.nextDouble());
        rec.put(1, r.nextDouble());
        rec.put(2, r.nextDouble());
        rec.put(3, r.nextInt());
        rec.put(4, r.nextInt());
        rec.put(5, r.nextInt());
        sourceData[i] = rec;
      }
    }
    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count; i++) {
        getReader().read(null, d);
      }
    }
    @Override
    void writeInternal(Encoder e) throws IOException {
      GenericDatumWriter<Object> writer = new GenericDatumWriter<Object>(schema);
      for (int i = 0; i < sourceData.length; i++) {
        GenericRecord rec = sourceData[i];
        writer.write(rec, e);
      }
    }
    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  private static final String GENERIC_STRINGS =
    "{ \"type\": \"record\", \"name\": \"R\", \"fields\": [\n"
    + "{ \"name\": \"f1\", \"type\": \"string\" },\n"
    + "{ \"name\": \"f2\", \"type\": \"string\" },\n"
    + "{ \"name\": \"f3\", \"type\": \"string\" }\n"
    + "] }";

  static class GenericStrings extends GenericTest {
    public GenericStrings() throws IOException {
      super("GenericStrings", GENERIC_STRINGS);
    }
    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new GenericRecord[count];
      for (int i = 0; i < sourceData.length; i++) {
        GenericRecord rec = new GenericData.Record(schema);
        rec.put(0, randomString(r));
        rec.put(1, randomString(r));
        rec.put(2, randomString(r));
        sourceData[i] = rec;
      }
    }
  }

  static class GenericNested extends GenericTest {
    public GenericNested() throws IOException {
      super("GenericNested_", NESTED_RECORD_SCHEMA);
    }
    @Override
    void genSourceData() {
      sourceData = generateGenericNested(schema, count);
    }
  }
  static GenericRecord[] generateGenericNested(Schema schema, int count) {
    Random r = newRandom();
    GenericRecord[] sourceData = new GenericRecord[count];
    Schema doubleSchema = schema.getFields().get(0).schema();
    for (int i = 0; i < sourceData.length; i++) {
      GenericRecord rec = new GenericData.Record(schema);
      GenericRecord inner;
      inner = new GenericData.Record(doubleSchema);
      inner.put(0, r.nextDouble());
      rec.put(0, inner);
      inner = new GenericData.Record(doubleSchema);
      inner.put(0, r.nextDouble());
      rec.put(1, inner);
      inner = new GenericData.Record(doubleSchema);
      inner.put(0, r.nextDouble());
      rec.put(2, inner);
      rec.put(3, r.nextInt());
      rec.put(4, r.nextInt());
      rec.put(5, r.nextInt());
      sourceData[i] = rec;
    }
    return sourceData;
  }

  static class GenericNestedFake extends BasicTest {
    //reads and writes generic data, but not using
    //GenericDatumReader or GenericDatumWriter
    GenericRecord[] sourceData = null;
    public GenericNestedFake() throws IOException {
      super("GenericNestedFake_", NESTED_RECORD_SCHEMA, 12);
    }
    @Override
    void readInternal(Decoder d) throws IOException {
      Schema doubleSchema = schema.getFields().get(0).schema();
      for (int i = 0; i < count; i++) {
        GenericRecord rec = new GenericData.Record(schema);
        GenericRecord inner;
        inner = new GenericData.Record(doubleSchema);
        inner.put(0, d.readDouble());
        rec.put(0, inner);
        inner = new GenericData.Record(doubleSchema);
        inner.put(0, d.readDouble());
        rec.put(1, inner);
        inner = new GenericData.Record(doubleSchema);
        inner.put(0, d.readDouble());
        rec.put(2, inner);
        rec.put(3, d.readInt());
        rec.put(4, d.readInt());
        rec.put(5, d.readInt());
      }
    }
    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length; i++) {
        GenericRecord rec = sourceData[i];
        GenericRecord inner;
        inner = (GenericRecord)rec.get(0);
        e.writeDouble((Double)inner.get(0));
        inner = (GenericRecord)rec.get(1);
        e.writeDouble((Double)inner.get(0));
        inner = (GenericRecord)rec.get(2);
        e.writeDouble((Double)inner.get(0));
        e.writeInt((Integer)rec.get(3));
        e.writeInt((Integer)rec.get(4));
        e.writeInt((Integer)rec.get(5));
      }
    }
    @Override
    void genSourceData() {
      sourceData = generateGenericNested(schema, count);
    }
    @Override
    void reset() {
      data = null;
      sourceData = null;
    }

  }

  private static abstract class GenericResolving extends GenericTest {
    protected GenericResolving(String name)
    throws IOException {
      super(name);
      isWriteTest = false;
    }
    @Override
    protected GenericDatumReader<Object> newReader() {
      return new GenericDatumReader<Object>(schema, getReaderSchema());
    }
    protected abstract Schema getReaderSchema();
  }

  static class GenericWithDefault extends GenericResolving {
    GenericWithDefault() throws IOException {
      super("GenericWithDefault_");
    }
    @Override
    protected Schema getReaderSchema() {
      return  new Schema.Parser().parse(RECORD_SCHEMA_WITH_DEFAULT);
    }
  }

  static class GenericWithOutOfOrder extends GenericResolving {
    GenericWithOutOfOrder() throws IOException {
      super("GenericWithOutOfOrder_");
    }
    @Override
    protected Schema getReaderSchema() {
      return new Schema.Parser().parse(RECORD_SCHEMA_WITH_OUT_OF_ORDER);
    }
  }

  static class GenericWithPromotion extends GenericResolving {
    GenericWithPromotion() throws IOException {
      super("GenericWithPromotion_");
    }
    @Override
    protected Schema getReaderSchema() {
      return new Schema.Parser().parse(RECORD_SCHEMA_WITH_PROMOTION);
    }
  }

  static class GenericOneTimeDecoderUse extends GenericTest {
    public GenericOneTimeDecoderUse() throws IOException {
      super("GenericOneTimeDecoderUse_");
      isWriteTest = false;
    }
    @Override
    protected Decoder getDecoder() {
      return newDecoder();
    }
  }

  static class GenericOneTimeReaderUse extends GenericTest {
    public GenericOneTimeReaderUse() throws IOException {
      super("GenericOneTimeReaderUse_");
      isWriteTest = false;
    }
    @Override
    protected GenericDatumReader<Object> getReader() {
      return newReader();
    }
  }

  static class GenericOneTimeUse extends GenericTest {
    public GenericOneTimeUse() throws IOException {
      super("GenericOneTimeUse_");
      isWriteTest = false;
    }
    @Override
    protected GenericDatumReader<Object> getReader() {
      return newReader();
    }
    @Override
    protected Decoder getDecoder() {
      return newDecoder();
    }
  }

  static abstract class SpecificTest<T extends SpecificRecordBase> extends BasicTest {
    protected final SpecificDatumReader<T> reader;
    protected final SpecificDatumWriter<T> writer;
    private Object[] sourceData;

    protected SpecificTest(String name, String writerSchema) throws IOException {
      super(name, writerSchema, 48);
      reader = newReader();
      writer = newWriter();
    }
    protected SpecificDatumReader<T> getReader() {
      return reader;
    }
    protected SpecificDatumWriter<T> getWriter() {
      return writer;
    }
    protected SpecificDatumReader<T> newReader() {
      return new SpecificDatumReader<T>(schema);
    }
    protected SpecificDatumWriter<T> newWriter() {
      return new SpecificDatumWriter<T>(schema);
    }
    @Override
    void genSourceData() {
      Random r = newRandom();
      sourceData = new Object[count];
      for (int i = 0; i < sourceData.length; i++) {
        sourceData[i] = genSingleRecord(r);
      }
    }

    protected abstract T genSingleRecord(Random r);

    @Override
    void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count; i++) {
        getReader().read(null, d);
      }
    }
    @Override
    void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length; i++) {
        @SuppressWarnings("unchecked")
        T rec = (T) sourceData[i];
        getWriter().write(rec, e);
      }
    }
    @Override
    void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class FooBarSpecificRecordTest extends
      SpecificTest<FooBarSpecificRecord> {
    public FooBarSpecificRecordTest() throws IOException {
      super("FooBarSpecificRecordTest", FooBarSpecificRecord.SCHEMA$.toString());
    }

    @Override
    protected FooBarSpecificRecord genSingleRecord(Random r) {
      TypeEnum[] typeEnums = TypeEnum.values();
      List<Integer> relatedIds = new ArrayList<Integer>(10);
      for (int i = 0; i < 10; i++) {
        relatedIds.add(r.nextInt());
      }

      try {
        return FooBarSpecificRecord.newBuilder().setId(r.nextInt())
            .setName(randomString(r))
            .setNicknames(Arrays.asList(randomString(r), randomString(r)))
            .setTypeEnum(typeEnums[r.nextInt(typeEnums.length)])
            .setRelatedids(relatedIds).build();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
  }

  static abstract class ReflectTest<T> extends BasicTest {
    T[] sourceData = null;
    ReflectDatumReader<T> reader;
    ReflectDatumWriter<T> writer;
    Class<T> clazz;

    @SuppressWarnings("unchecked")
    ReflectTest(String name, T sample, int factor) throws IOException {
      super(name, ReflectData.get().getSchema(sample.getClass()).toString(), factor);
      clazz = (Class<T>) sample.getClass();
      reader = new ReflectDatumReader<T>(schema);
      writer = new ReflectDatumWriter<T>(schema);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected final void genSourceData() {
      Random r = newRandom();
      sourceData = (T[]) Array.newInstance(clazz, count);
      for (int i = 0; i < sourceData.length; i++) {
        sourceData[i] = createDatum(r);
      }
    }

    protected abstract T createDatum(Random r);

    @Override
    protected final void readInternal(Decoder d) throws IOException {
      for (int i = 0; i < count; i++) {
        reader.read(null, d);
      }
    }

    @Override
    protected final void writeInternal(Encoder e) throws IOException {
      for (int i = 0; i < sourceData.length; i++) {
        writer.write(sourceData[i], e);
      }
    }

    @Override
    protected final void reset() {
      sourceData = null;
      data = null;
    }
  }

  static class ReflectRecordTest extends ReflectTest<Rec> {
    ReflectRecordTest() throws IOException {
      super("ReflectRecord", new Rec(), 12);
    }

    @Override
    protected Rec createDatum(Random r) {
      return new Rec(r);
    }
  }

  static class ReflectFloatTest extends ReflectTest<float[]> {
    ReflectFloatTest() throws IOException {
      super("ReflectFloat", new float[0], COUNT);
    }

    @Override
    protected float[] createDatum(Random r) {
      return populateFloatArray(r, COUNT / count);
    }
  }

  static class ReflectDoubleTest extends ReflectTest<double[]> {
    ReflectDoubleTest() throws IOException {
      super("ReflectDouble", new double[0], COUNT);
    }

    @Override
    protected double[] createDatum(Random r) {
      return populateDoubleArray(r, COUNT / count);
    }
  }

  static class ReflectFloatArrayTest extends ReflectTest<float[]> {
    ReflectFloatArrayTest() throws IOException {
      super("ReflectFloatArray", new float[0], 10);
    }

    @Override
    protected float[] createDatum(Random r) {
      return populateFloatArray(r, false);
    }
  }

  static class ReflectDoubleArrayTest extends ReflectTest<double[]> {
    ReflectDoubleArrayTest() throws IOException {
      super("ReflectDoubleArray", new double[0], 20);
    }

    @Override
    protected double[] createDatum(Random r) {
      return populateDoubleArray(r);
    }
  }

  static class ReflectIntArrayTest extends ReflectTest<int[]> {
    ReflectIntArrayTest() throws IOException {
      super("ReflectIntArray", new int[0], 12);
    }

    @Override
    protected int[] createDatum(Random r) {
      return populateIntArray(r);
    }
  }

  static class ReflectLongArrayTest extends ReflectTest<long[]> {
    ReflectLongArrayTest() throws IOException {
      super("ReflectLongArray", new long[0], 24);
    }

    @Override
    protected long[] createDatum(Random r) {
      return populateLongArray(r);
    }
  }


  static class ReflectNestedObjectArrayTest extends
      ReflectTest<ReflectNestedObjectArrayTest.Foo> {
    ReflectNestedObjectArrayTest() throws IOException {
      super("ReflectNestedObjectArray", new Foo(new Random()), 50);
    }

    @Override
    protected Foo createDatum(Random r) {
      return new Foo(r);
    }

    static public class Foo {
      Vals[] bar;

      Foo() {
      }

      Foo(Random r) {
        bar = new Vals[smallArraySize(r)];
        for (int i = 0; i < bar.length; i++) {
          bar[i] = new Vals(r);
        }
      }
    }

    static class Vals {
      float f1;
      float f2;
      float f3;
      float f4;

      Vals(){
      }

      Vals(Random r) {
        this.f1 = r.nextFloat();
        this.f2 = r.nextFloat();
        this.f3 = r.nextFloat();
        this.f4 = r.nextFloat();
      }
    }

  }

  static public class FloatFoo {
    float[] floatBar;

    FloatFoo() {
    }

    FloatFoo(Random r, boolean large) {
      floatBar = populateFloatArray(r, large);
    }
  }

  // average of 8, between 1 and 15
  private static int smallArraySize(Random r) {
    return r.nextInt(15) + 1;
  }

  // average of 64, between 16 and 112
  private static int largeArraySize(Random r) {
    return r.nextInt(97) + 16;
  }

  static float[] populateFloatArray(Random r, boolean large) {
    int size = large ? largeArraySize(r) : smallArraySize(r);
    return populateFloatArray(r, size);
  }

  static float[] populateFloatArray(Random r, int size) {
    float[] result = new float[size];
    for (int i = 0; i < result.length; i++) {
      result[i] = r.nextFloat();
    }
    return result;
  }

  static double[] populateDoubleArray(Random r) {
    return populateDoubleArray(r, smallArraySize(r));
  }

  static double[] populateDoubleArray(Random r, int size) {
    double[] result = new double[size];
    for (int i = 0; i < result.length; i++) {
      result[i] = r.nextDouble();
    }
    return result;
  }

  static int[] populateIntArray(Random r) {
    int size = smallArraySize(r);
    int[] result = new int[size];
    for (int i = 0; i < result.length; i++) {
      result[i] = r.nextInt();
    }
    return result;
  }

  static long[] populateLongArray(Random r) {
    int size = smallArraySize(r);
    long[] result = new long[size];
    for (int i = 0; i < result.length; i++) {
      result[i] = r.nextLong();
    }
    return result;
  }

  static class ReflectNestedFloatArrayTest extends ReflectTest<FloatFoo> {
    public ReflectNestedFloatArrayTest() throws IOException {
      super("ReflectNestedFloatArray", new FloatFoo(new Random(), false), 10);
    }

    @Override
    protected FloatFoo createDatum(Random r) {
      return new FloatFoo(r, false);
    }
  }

  static class ReflectNestedLargeFloatArrayTest extends ReflectTest<FloatFoo> {
    public ReflectNestedLargeFloatArrayTest() throws IOException {
      super("ReflectNestedLargeFloatArray", new FloatFoo(new Random(), true),
          60);
    }

    @Override
    protected FloatFoo createDatum(Random r) {
      return new FloatFoo(r, true);
    }

  }

  static class ReflectNestedLargeFloatArrayBlockedTest extends ReflectTest<FloatFoo> {
    public ReflectNestedLargeFloatArrayBlockedTest() throws IOException {
      super("ReflectNestedLargeFloatArrayBlocked", new FloatFoo(new Random(), true),
          60);
    }

    @Override
    protected FloatFoo createDatum(Random r) {
      return new FloatFoo(r, true);
    }

    @Override
    protected Encoder newEncoder(ByteArrayOutputStream out) throws IOException {
      return new EncoderFactory().configureBlockSize(254).blockingBinaryEncoder(out, null);
    }

  }

  @SuppressWarnings("unused")
  private static class Rec1 {
    double d1;
    double d11;
    float f2;
    float f22;
    int f3;
    int f33;
    long f4;
    long f44;
    byte f5;
    byte f55;
    short f6;
    short f66;

    Rec1() {
    }

    Rec1(Random r) {
      d1 = r.nextDouble();
      d11 = r.nextDouble();
      f2 = r.nextFloat();
      f22 = r.nextFloat();
      f3 = r.nextInt();
      f33 = r.nextInt();
      f4 = r.nextLong();
      f44 = r.nextLong();
      f5 = (byte) r.nextInt();
      f55 = (byte) r.nextInt();
      f6 = (short) r.nextInt();
      f66 = (short) r.nextInt();
    }
  }

  static class ReflectBigRecordTest extends ReflectTest<Rec1> {
    public ReflectBigRecordTest() throws IOException {
      super("ReflectBigRecord", new Rec1(new Random()), 20);
    }

    @Override
    protected Rec1 createDatum(Random r) {
      return new Rec1(r);
    }
  }
}
