// Copyright (c) 2011-present, Facebook, Inc.  All rights reserved.
//  This source code is licensed under both the GPLv2 (found in the
//  COPYING file in the root directory) and Apache 2.0 License
//  (found in the LICENSE.Apache file in the root directory).
/**
 * Copyright (C) 2011 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.rocksdb.benchmark;

import java.io.IOException;
import java.lang.Runnable;
import java.lang.Math;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Date;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.rocksdb.*;
import org.rocksdb.RocksMemEnv;
import org.rocksdb.util.SizeUnit;

class Stats {
  int id_;
  long start_;
  long finish_;
  double seconds_;
  long done_;
  long found_;
  long lastOpTime_;
  long nextReport_;
  long bytes_;
  StringBuilder message_;
  boolean excludeFromMerge_;

  // TODO(yhchiang): use the following arguments:
  //   (Long)Flag.stats_interval
  //   (Integer)Flag.stats_per_interval

  Stats(int id) {
    id_ = id;
    nextReport_ = 100;
    done_ = 0;
    bytes_ = 0;
    seconds_ = 0;
    start_ = System.nanoTime();
    lastOpTime_ = start_;
    finish_ = start_;
    found_ = 0;
    message_ = new StringBuilder("");
    excludeFromMerge_ = false;
  }

  void merge(final Stats other) {
    if (other.excludeFromMerge_) {
      return;
    }

    done_ += other.done_;
    found_ += other.found_;
    bytes_ += other.bytes_;
    seconds_ += other.seconds_;
    if (other.start_ < start_) start_ = other.start_;
    if (other.finish_ > finish_) finish_ = other.finish_;

    // Just keep the messages from one thread
    if (message_.length() == 0) {
      message_ = other.message_;
    }
  }

  void stop() {
    finish_ = System.nanoTime();
    seconds_ = (double) (finish_ - start_) * 1e-9;
  }

  void addMessage(String msg) {
    if (message_.length() > 0) {
      message_.append(" ");
    }
    message_.append(msg);
  }

  void setId(int id) { id_ = id; }
  void setExcludeFromMerge() { excludeFromMerge_ = true; }

  void finishedSingleOp(int bytes) {
    done_++;
    lastOpTime_ = System.nanoTime();
    bytes_ += bytes;
    if (done_ >= nextReport_) {
      if (nextReport_ < 1000) {
        nextReport_ += 100;
      } else if (nextReport_ < 5000) {
        nextReport_ += 500;
      } else if (nextReport_ < 10000) {
        nextReport_ += 1000;
      } else if (nextReport_ < 50000) {
        nextReport_ += 5000;
      } else if (nextReport_ < 100000) {
        nextReport_ += 10000;
      } else if (nextReport_ < 500000) {
        nextReport_ += 50000;
      } else {
        nextReport_ += 100000;
      }
      System.err.printf("... Task %s finished %d ops%30s\r", id_, done_, "");
    }
  }

  void report(String name) {
    // Pretend at least one op was done in case we are running a benchmark
    // that does not call FinishedSingleOp().
    if (done_ < 1) done_ = 1;

    StringBuilder extra = new StringBuilder("");
    if (bytes_ > 0) {
      // Rate is computed on actual elapsed time, not the sum of per-thread
      // elapsed times.
      double elapsed = (finish_ - start_) * 1e-9;
      extra.append(String.format("%6.1f MB/s", (bytes_ / 1048576.0) / elapsed));
    }
    extra.append(message_.toString());
    double elapsed = (finish_ - start_);
    double throughput = (double) done_ / (elapsed * 1e-9);

    System.out.format("%-12s : %11.3f micros/op %d ops/sec;%s%s\n",
            name, (elapsed * 1e-6) / done_,
            (long) throughput, (extra.length() == 0 ? "" : " "), extra.toString());
  }
}

public class DbBenchmark {
  enum Order {
    SEQUENTIAL,
    RANDOM
  }

  enum DBState {
    FRESH,
    EXISTING
  }

  static {
    RocksDB.loadLibrary();
  }

  abstract class BenchmarkTask implements Callable<Stats> {
    // TODO(yhchiang): use (Integer)Flag.perf_level.
    public BenchmarkTask(
        int tid, long randSeed, long numEntries, long keyRange) {
      tid_ = tid;
      rand_ = new Random(randSeed + tid * 1000);
      numEntries_ = numEntries;
      keyRange_ = keyRange;
      stats_ = new Stats(tid);
    }

    @Override public Stats call() throws RocksDBException {
      stats_.start_ = System.nanoTime();
      runTask();
      stats_.finish_ = System.nanoTime();
      return stats_;
    }

    abstract protected void runTask() throws RocksDBException;

    protected int tid_;
    protected Random rand_;
    protected long numEntries_;
    protected long keyRange_;
    protected Stats stats_;

    protected void getFixedKey(byte[] key, long sn) {
      generateKeyFromLong(key, sn);
    }

    protected void getRandomKey(byte[] key, long range) {
      generateKeyFromLong(key, Math.abs(rand_.nextLong() % range));
    }
  }

  abstract class WriteTask extends BenchmarkTask {
    public WriteTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch) {
      super(tid, randSeed, numEntries, keyRange);
      writeOpt_ = writeOpt;
      entriesPerBatch_ = entriesPerBatch;
      maxWritesPerSecond_ = -1;
    }

    public WriteTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch, long maxWritesPerSecond) {
      super(tid, randSeed, numEntries, keyRange);
      writeOpt_ = writeOpt;
      entriesPerBatch_ = entriesPerBatch;
      maxWritesPerSecond_ = maxWritesPerSecond;
    }

    @Override public void runTask() throws RocksDBException {
      if (numEntries_ != DbBenchmark.this.num_) {
        stats_.message_.append(String.format(" (%d ops)", numEntries_));
      }
      byte[] key = new byte[keySize_];
      byte[] value = new byte[valueSize_];

      try {
        if (entriesPerBatch_ == 1) {
          for (long i = 0; i < numEntries_; ++i) {
            getKey(key, i, keyRange_);
            DbBenchmark.this.gen_.generate(value);
            db_.put(writeOpt_, key, value);
            stats_.finishedSingleOp(keySize_ + valueSize_);
            writeRateControl(i);
            if (isFinished()) {
              return;
            }
          }
        } else {
          for (long i = 0; i < numEntries_; i += entriesPerBatch_) {
            WriteBatch batch = new WriteBatch();
            for (long j = 0; j < entriesPerBatch_; j++) {
              getKey(key, i + j, keyRange_);
              DbBenchmark.this.gen_.generate(value);
              batch.put(key, value);
              stats_.finishedSingleOp(keySize_ + valueSize_);
            }
            db_.write(writeOpt_, batch);
            batch.dispose();
            writeRateControl(i);
            if (isFinished()) {
              return;
            }
          }
        }
      } catch (InterruptedException e) {
        // thread has been terminated.
      }
    }

    protected void writeRateControl(long writeCount)
        throws InterruptedException {
      if (maxWritesPerSecond_ <= 0) return;
      long minInterval =
          writeCount * TimeUnit.SECONDS.toNanos(1) / maxWritesPerSecond_;
      long interval = System.nanoTime() - stats_.start_;
      if (minInterval - interval > TimeUnit.MILLISECONDS.toNanos(1)) {
        TimeUnit.NANOSECONDS.sleep(minInterval - interval);
      }
    }

    abstract protected void getKey(byte[] key, long id, long range);
    protected WriteOptions writeOpt_;
    protected long entriesPerBatch_;
    protected long maxWritesPerSecond_;
  }

  class WriteSequentialTask extends WriteTask {
    public WriteSequentialTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch) {
      super(tid, randSeed, numEntries, keyRange,
            writeOpt, entriesPerBatch);
    }
    public WriteSequentialTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch,
        long maxWritesPerSecond) {
      super(tid, randSeed, numEntries, keyRange,
            writeOpt, entriesPerBatch,
            maxWritesPerSecond);
    }
    @Override protected void getKey(byte[] key, long id, long range) {
      getFixedKey(key, id);
    }
  }

  class WriteRandomTask extends WriteTask {
    public WriteRandomTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch) {
      super(tid, randSeed, numEntries, keyRange,
            writeOpt, entriesPerBatch);
    }
    public WriteRandomTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch,
        long maxWritesPerSecond) {
      super(tid, randSeed, numEntries, keyRange,
            writeOpt, entriesPerBatch,
            maxWritesPerSecond);
    }
    @Override protected void getKey(byte[] key, long id, long range) {
      getRandomKey(key, range);
    }
  }

  class WriteUniqueRandomTask extends WriteTask {
    static final int MAX_BUFFER_SIZE = 10000000;
    public WriteUniqueRandomTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch) {
      super(tid, randSeed, numEntries, keyRange,
            writeOpt, entriesPerBatch);
      initRandomKeySequence();
    }
    public WriteUniqueRandomTask(
        int tid, long randSeed, long numEntries, long keyRange,
        WriteOptions writeOpt, long entriesPerBatch,
        long maxWritesPerSecond) {
      super(tid, randSeed, numEntries, keyRange,
            writeOpt, entriesPerBatch,
            maxWritesPerSecond);
      initRandomKeySequence();
    }
    @Override protected void getKey(byte[] key, long id, long range) {
      generateKeyFromLong(key, nextUniqueRandom());
    }

    protected void initRandomKeySequence() {
      bufferSize_ = MAX_BUFFER_SIZE;
      if (bufferSize_ > keyRange_) {
        bufferSize_ = (int) keyRange_;
      }
      currentKeyCount_ = bufferSize_;
      keyBuffer_ = new long[MAX_BUFFER_SIZE];
      for (int k = 0; k < bufferSize_; ++k) {
        keyBuffer_[k] = k;
      }
    }

    /**
     * Semi-randomly return the next unique key.  It is guaranteed to be
     * fully random if keyRange_ <= MAX_BUFFER_SIZE.
     */
    long nextUniqueRandom() {
      if (bufferSize_ == 0) {
        System.err.println("bufferSize_ == 0.");
        return 0;
      }
      int r = rand_.nextInt(bufferSize_);
      // randomly pick one from the keyBuffer
      long randKey = keyBuffer_[r];
      if (currentKeyCount_ < keyRange_) {
        // if we have not yet inserted all keys, insert next new key to [r].
        keyBuffer_[r] = currentKeyCount_++;
      } else {
        // move the last element to [r] and decrease the size by 1.
        keyBuffer_[r] = keyBuffer_[--bufferSize_];
      }
      return randKey;
    }

    int bufferSize_;
    long currentKeyCount_;
    long[] keyBuffer_;
  }

  class ReadRandomTask extends BenchmarkTask {
    public ReadRandomTask(
        int tid, long randSeed, long numEntries, long keyRange) {
      super(tid, randSeed, numEntries, keyRange);
    }
    @Override public void runTask() throws RocksDBException {
      byte[] key = new byte[keySize_];
      byte[] value = new byte[valueSize_];
      for (long i = 0; i < numEntries_; i++) {
        getRandomKey(key, keyRange_);
        int len = db_.get(key, value);
        if (len != RocksDB.NOT_FOUND) {
          stats_.found_++;
          stats_.finishedSingleOp(keySize_ + valueSize_);
        } else {
          stats_.finishedSingleOp(keySize_);
        }
        if (isFinished()) {
          return;
        }
      }
    }
  }

  class ReadSequentialTask extends BenchmarkTask {
    public ReadSequentialTask(
        int tid, long randSeed, long numEntries, long keyRange) {
      super(tid, randSeed, numEntries, keyRange);
    }
    @Override public void runTask() throws RocksDBException {
      RocksIterator iter = db_.newIterator();
      long i;
      for (iter.seekToFirst(), i = 0;
           iter.isValid() && i < numEntries_;
           iter.next(), ++i) {
        stats_.found_++;
        stats_.finishedSingleOp(iter.key().length + iter.value().length);
        if (isFinished()) {
          iter.dispose();
          return;
        }
      }
      iter.dispose();
    }
  }

  public DbBenchmark(Map<Flag, Object> flags) throws Exception {
    benchmarks_ = (List<String>) flags.get(Flag.benchmarks);
    num_ = (Integer) flags.get(Flag.num);
    threadNum_ = (Integer) flags.get(Flag.threads);
    reads_ = (Integer) (flags.get(Flag.reads) == null ?
        flags.get(Flag.num) : flags.get(Flag.reads));
    keySize_ = (Integer) flags.get(Flag.key_size);
    valueSize_ = (Integer) flags.get(Flag.value_size);
    compressionRatio_ = (Double) flags.get(Flag.compression_ratio);
    useExisting_ = (Boolean) flags.get(Flag.use_existing_db);
    randSeed_ = (Long) flags.get(Flag.seed);
    databaseDir_ = (String) flags.get(Flag.db);
    writesPerSeconds_ = (Integer) flags.get(Flag.writes_per_second);
    memtable_ = (String) flags.get(Flag.memtablerep);
    maxWriteBufferNumber_ = (Integer) flags.get(Flag.max_write_buffer_number);
    prefixSize_ = (Integer) flags.get(Flag.prefix_size);
    keysPerPrefix_ = (Integer) flags.get(Flag.keys_per_prefix);
    hashBucketCount_ = (Long) flags.get(Flag.hash_bucket_count);
    usePlainTable_ = (Boolean) flags.get(Flag.use_plain_table);
    useMemenv_ = (Boolean) flags.get(Flag.use_mem_env);
    flags_ = flags;
    finishLock_ = new Object();
    // options.setPrefixSize((Integer)flags_.get(Flag.prefix_size));
    // options.setKeysPerPrefix((Long)flags_.get(Flag.keys_per_prefix));
    compressionType_ = (String) flags.get(Flag.compression_type);
    compression_ = CompressionType.NO_COMPRESSION;
    try {
      if (compressionType_!=null) {
          final CompressionType compressionType =
              CompressionType.getCompressionType(compressionType_);
          if (compressionType != null &&
              compressionType != CompressionType.NO_COMPRESSION) {
            System.loadLibrary(compressionType.getLibraryName());
          }

      }
    } catch (UnsatisfiedLinkError e) {
      System.err.format("Unable to load %s library:%s%n" +
                        "No compression is used.%n",
          compressionType_, e.toString());
      compressionType_ = "none";
    }
    gen_ = new RandomGenerator(randSeed_, compressionRatio_);
  }

  private void prepareReadOptions(ReadOptions options) {
    options.setVerifyChecksums((Boolean)flags_.get(Flag.verify_checksum));
    options.setTailing((Boolean)flags_.get(Flag.use_tailing_iterator));
  }

  private void prepareWriteOptions(WriteOptions options) {
    options.setSync((Boolean)flags_.get(Flag.sync));
    options.setDisableWAL((Boolean)flags_.get(Flag.disable_wal));
  }

  private void prepareOptions(Options options) throws RocksDBException {
    if (!useExisting_) {
      options.setCreateIfMissing(true);
    } else {
      options.setCreateIfMissing(false);
    }
    if (useMemenv_) {
      options.setEnv(new RocksMemEnv(Env.getDefault()));
    }
    switch (memtable_) {
      case "skip_list":
        options.setMemTableConfig(new SkipListMemTableConfig());
        break;
      case "vector":
        options.setMemTableConfig(new VectorMemTableConfig());
        break;
      case "hash_linkedlist":
        options.setMemTableConfig(
            new HashLinkedListMemTableConfig()
                .setBucketCount(hashBucketCount_));
        options.useFixedLengthPrefixExtractor(prefixSize_);
        break;
      case "hash_skiplist":
      case "prefix_hash":
        options.setMemTableConfig(
            new HashSkipListMemTableConfig()
                .setBucketCount(hashBucketCount_));
        options.useFixedLengthPrefixExtractor(prefixSize_);
        break;
      default:
        System.err.format(
            "unable to detect the specified memtable, " +
                "use the default memtable factory %s%n",
            options.memTableFactoryName());
        break;
    }
    if (usePlainTable_) {
      options.setTableFormatConfig(
          new PlainTableConfig().setKeySize(keySize_));
    } else {
      BlockBasedTableConfig table_options = new BlockBasedTableConfig();
      table_options.setBlockSize((Long)flags_.get(Flag.block_size))
                   .setBlockCacheSize((Long)flags_.get(Flag.cache_size))
                   .setCacheNumShardBits(
                      (Integer)flags_.get(Flag.cache_numshardbits));
      options.setTableFormatConfig(table_options);
    }
    options.setWriteBufferSize(
        (Long)flags_.get(Flag.write_buffer_size));
    options.setMaxWriteBufferNumber(
        (Integer)flags_.get(Flag.max_write_buffer_number));
    options.setMaxBackgroundCompactions(
        (Integer)flags_.get(Flag.max_background_compactions));
    options.getEnv().setBackgroundThreads(
        (Integer)flags_.get(Flag.max_background_compactions));
    options.setMaxBackgroundFlushes(
        (Integer)flags_.get(Flag.max_background_flushes));
    options.setMaxBackgroundJobs((Integer) flags_.get(Flag.max_background_jobs));
    options.setMaxOpenFiles(
        (Integer)flags_.get(Flag.open_files));
    options.setUseFsync(
        (Boolean)flags_.get(Flag.use_fsync));
    options.setWalDir(
        (String)flags_.get(Flag.wal_dir));
    options.setDeleteObsoleteFilesPeriodMicros(
        (Integer)flags_.get(Flag.delete_obsolete_files_period_micros));
    options.setTableCacheNumshardbits(
        (Integer)flags_.get(Flag.table_cache_numshardbits));
    options.setAllowMmapReads(
        (Boolean)flags_.get(Flag.mmap_read));
    options.setAllowMmapWrites(
        (Boolean)flags_.get(Flag.mmap_write));
    options.setAdviseRandomOnOpen(
        (Boolean)flags_.get(Flag.advise_random_on_open));
    options.setUseAdaptiveMutex(
        (Boolean)flags_.get(Flag.use_adaptive_mutex));
    options.setBytesPerSync(
        (Long)flags_.get(Flag.bytes_per_sync));
    options.setBloomLocality(
        (Integer)flags_.get(Flag.bloom_locality));
    options.setMinWriteBufferNumberToMerge(
        (Integer)flags_.get(Flag.min_write_buffer_number_to_merge));
    options.setMemtablePrefixBloomSizeRatio((Double) flags_.get(Flag.memtable_bloom_size_ratio));
    options.setMemtableWholeKeyFiltering((Boolean) flags_.get(Flag.memtable_whole_key_filtering));
    options.setNumLevels(
        (Integer)flags_.get(Flag.num_levels));
    options.setTargetFileSizeBase(
        (Integer)flags_.get(Flag.target_file_size_base));
    options.setTargetFileSizeMultiplier((Integer)flags_.get(Flag.target_file_size_multiplier));
    options.setMaxBytesForLevelBase(
        (Integer)flags_.get(Flag.max_bytes_for_level_base));
    options.setMaxBytesForLevelMultiplier((Double) flags_.get(Flag.max_bytes_for_level_multiplier));
    options.setLevelZeroStopWritesTrigger(
        (Integer)flags_.get(Flag.level0_stop_writes_trigger));
    options.setLevelZeroSlowdownWritesTrigger(
        (Integer)flags_.get(Flag.level0_slowdown_writes_trigger));
    options.setLevelZeroFileNumCompactionTrigger(
        (Integer)flags_.get(Flag.level0_file_num_compaction_trigger));
    options.setMaxCompactionBytes(
        (Long) flags_.get(Flag.max_compaction_bytes));
    options.setDisableAutoCompactions(
        (Boolean)flags_.get(Flag.disable_auto_compactions));
    options.setMaxSuccessiveMerges(
        (Integer)flags_.get(Flag.max_successive_merges));
    options.setWalTtlSeconds((Long)flags_.get(Flag.wal_ttl_seconds));
    options.setWalSizeLimitMB((Long)flags_.get(Flag.wal_size_limit_MB));
    if(flags_.get(Flag.java_comparator) != null) {
      options.setComparator(
          (AbstractComparator)flags_.get(Flag.java_comparator));
    }

    /* TODO(yhchiang): enable the following parameters
    options.setCompressionType((String)flags_.get(Flag.compression_type));
    options.setCompressionLevel((Integer)flags_.get(Flag.compression_level));
    options.setMinLevelToCompress((Integer)flags_.get(Flag.min_level_to_compress));
    options.setStatistics((Boolean)flags_.get(Flag.statistics));
    options.setUniversalSizeRatio(
        (Integer)flags_.get(Flag.universal_size_ratio));
    options.setUniversalMinMergeWidth(
        (Integer)flags_.get(Flag.universal_min_merge_width));
    options.setUniversalMaxMergeWidth(
        (Integer)flags_.get(Flag.universal_max_merge_width));
    options.setUniversalMaxSizeAmplificationPercent(
        (Integer)flags_.get(Flag.universal_max_size_amplification_percent));
    options.setUniversalCompressionSizePercent(
        (Integer)flags_.get(Flag.universal_compression_size_percent));
    // TODO(yhchiang): add RocksDB.openForReadOnly() to enable Flag.readonly
    // TODO(yhchiang): enable Flag.merge_operator by switch
    */
  }

  private void run() throws RocksDBException {
    if (!useExisting_) {
      destroyDb();
    }
    Options options = new Options();
    prepareOptions(options);
    open(options);

    printHeader(options);

    for (String benchmark : benchmarks_) {
      List<Callable<Stats>> tasks = new ArrayList<Callable<Stats>>();
      List<Callable<Stats>> bgTasks = new ArrayList<Callable<Stats>>();
      WriteOptions writeOpt = new WriteOptions();
      prepareWriteOptions(writeOpt);
      ReadOptions readOpt = new ReadOptions();
      prepareReadOptions(readOpt);
      int currentTaskId = 0;
      boolean known = true;

      switch (benchmark) {
        case "fillseq":
          tasks.add(new WriteSequentialTask(
              currentTaskId++, randSeed_, num_, num_, writeOpt, 1));
          break;
        case "fillbatch":
          tasks.add(
              new WriteSequentialTask(currentTaskId++, randSeed_, num_, num_, writeOpt, 1000));
          break;
        case "fillrandom":
          tasks.add(new WriteRandomTask(
              currentTaskId++, randSeed_, num_, num_, writeOpt, 1));
          break;
        case "filluniquerandom":
          tasks.add(new WriteUniqueRandomTask(
              currentTaskId++, randSeed_, num_, num_, writeOpt, 1));
          break;
        case "fillsync":
          writeOpt.setSync(true);
          tasks.add(new WriteRandomTask(
              currentTaskId++, randSeed_, num_ / 1000, num_ / 1000,
              writeOpt, 1));
          break;
        case "readseq":
          for (int t = 0; t < threadNum_; ++t) {
            tasks.add(new ReadSequentialTask(
                currentTaskId++, randSeed_, reads_ / threadNum_, num_));
          }
          break;
        case "readrandom":
          for (int t = 0; t < threadNum_; ++t) {
            tasks.add(new ReadRandomTask(
                currentTaskId++, randSeed_, reads_ / threadNum_, num_));
          }
          break;
        case "readwhilewriting":
          WriteTask writeTask = new WriteRandomTask(
              -1, randSeed_, Long.MAX_VALUE, num_, writeOpt, 1, writesPerSeconds_);
          writeTask.stats_.setExcludeFromMerge();
          bgTasks.add(writeTask);
          for (int t = 0; t < threadNum_; ++t) {
            tasks.add(new ReadRandomTask(
                currentTaskId++, randSeed_, reads_ / threadNum_, num_));
          }
          break;
        case "readhot":
          for (int t = 0; t < threadNum_; ++t) {
            tasks.add(new ReadRandomTask(
                currentTaskId++, randSeed_, reads_ / threadNum_, num_ / 100));
          }
          break;
        case "delete":
          destroyDb();
          open(options);
          break;
        default:
          known = false;
          System.err.println("Unknown benchmark: " + benchmark);
          break;
      }
      if (known) {
        ExecutorService executor = Executors.newCachedThreadPool();
        ExecutorService bgExecutor = Executors.newCachedThreadPool();
        try {
          // measure only the main executor time
          List<Future<Stats>> bgResults = new ArrayList<Future<Stats>>();
          for (Callable bgTask : bgTasks) {
            bgResults.add(bgExecutor.submit(bgTask));
          }
          start();
          List<Future<Stats>> results = executor.invokeAll(tasks);
          executor.shutdown();
          boolean finished = executor.awaitTermination(10, TimeUnit.SECONDS);
          if (!finished) {
            System.out.format(
                "Benchmark %s was not finished before timeout.",
                benchmark);
            executor.shutdownNow();
          }
          setFinished(true);
          bgExecutor.shutdown();
          finished = bgExecutor.awaitTermination(10, TimeUnit.SECONDS);
          if (!finished) {
            System.out.format(
                "Benchmark %s was not finished before timeout.",
                benchmark);
            bgExecutor.shutdownNow();
          }

          stop(benchmark, results, currentTaskId);
        } catch (InterruptedException e) {
          System.err.println(e);
        }
      }
      writeOpt.dispose();
      readOpt.dispose();
    }
    options.dispose();
    db_.close();
  }

  private void printHeader(Options options) {
    int kKeySize = 16;
    System.out.printf("Keys:     %d bytes each\n", kKeySize);
    System.out.printf("Values:   %d bytes each (%d bytes after compression)\n",
        valueSize_,
        (int) (valueSize_ * compressionRatio_ + 0.5));
    System.out.printf("Entries:  %d\n", num_);
    System.out.printf("RawSize:  %.1f MB (estimated)\n",
        ((double)(kKeySize + valueSize_) * num_) / SizeUnit.MB);
    System.out.printf("FileSize:   %.1f MB (estimated)\n",
        (((kKeySize + valueSize_ * compressionRatio_) * num_) / SizeUnit.MB));
    System.out.format("Memtable Factory: %s%n", options.memTableFactoryName());
    System.out.format("Prefix:   %d bytes%n", prefixSize_);
    System.out.format("Compression: %s%n", compressionType_);
    printWarnings();
    System.out.printf("------------------------------------------------\n");
  }

  void printWarnings() {
    boolean assertsEnabled = false;
    assert assertsEnabled = true; // Intentional side effect!!!
    if (assertsEnabled) {
      System.out.printf(
          "WARNING: Assertions are enabled; benchmarks unnecessarily slow\n");
    }
  }

  private void open(Options options) throws RocksDBException {
    System.out.println("Using database directory: " + databaseDir_);
    db_ = RocksDB.open(options, databaseDir_);
  }

  private void start() {
    setFinished(false);
    startTime_ = System.nanoTime();
  }

  private void stop(
      String benchmark, List<Future<Stats>> results, int concurrentThreads) {
    long endTime = System.nanoTime();
    double elapsedSeconds =
        1.0d * (endTime - startTime_) / TimeUnit.SECONDS.toNanos(1);

    Stats stats = new Stats(-1);
    int taskFinishedCount = 0;
    for (Future<Stats> result : results) {
      if (result.isDone()) {
        try {
          Stats taskStats = result.get(3, TimeUnit.SECONDS);
          if (!result.isCancelled()) {
            taskFinishedCount++;
          }
          stats.merge(taskStats);
        } catch (Exception e) {
          // then it's not successful, the output will indicate this
        }
      }
    }
    String extra = "";
    if (benchmark.indexOf("read") >= 0) {
      extra = String.format(" %d / %d found; ", stats.found_, stats.done_);
    } else {
      extra = String.format(" %d ops done; ", stats.done_);
    }

    System.out.printf(
        "%-16s : %11.5f micros/op; %6.1f MB/s;%s %d / %d task(s) finished.\n",
        benchmark, elapsedSeconds / stats.done_ * 1e6,
        (stats.bytes_ / 1048576.0) / elapsedSeconds, extra,
        taskFinishedCount, concurrentThreads);
  }

  public void generateKeyFromLong(byte[] slice, long n) {
    assert(n >= 0);
    int startPos = 0;

    if (keysPerPrefix_ > 0) {
      long numPrefix = (num_ + keysPerPrefix_ - 1) / keysPerPrefix_;
      long prefix = n % numPrefix;
      int bytesToFill = Math.min(prefixSize_, 8);
      for (int i = 0; i < bytesToFill; ++i) {
        slice[i] = (byte) (prefix % 256);
        prefix /= 256;
      }
      for (int i = 8; i < bytesToFill; ++i) {
        slice[i] = '0';
      }
      startPos = bytesToFill;
    }

    for (int i = slice.length - 1; i >= startPos; --i) {
      slice[i] = (byte) ('0' + (n % 10));
      n /= 10;
    }
  }

  private void destroyDb() {
    if (db_ != null) {
      db_.close();
    }
    // TODO(yhchiang): develop our own FileUtil
    // FileUtil.deleteDir(databaseDir_);
  }

  private void printStats() {
  }

  static void printHelp() {
    System.out.println("usage:");
    for (Flag flag : Flag.values()) {
      System.out.format("  --%s%n\t%s%n",
          flag.name(),
          flag.desc());
      if (flag.getDefaultValue() != null) {
        System.out.format("\tDEFAULT: %s%n",
            flag.getDefaultValue().toString());
      }
    }
  }

  public static void main(String[] args) throws Exception {
    Map<Flag, Object> flags = new EnumMap<Flag, Object>(Flag.class);
    for (Flag flag : Flag.values()) {
      if (flag.getDefaultValue() != null) {
        flags.put(flag, flag.getDefaultValue());
      }
    }
    for (String arg : args) {
      boolean valid = false;
      if (arg.equals("--help") || arg.equals("-h")) {
        printHelp();
        System.exit(0);
      }
      if (arg.startsWith("--")) {
        try {
          String[] parts = arg.substring(2).split("=");
          if (parts.length >= 1) {
            Flag key = Flag.valueOf(parts[0]);
            if (key != null) {
              Object value = null;
              if (parts.length >= 2) {
                value = key.parseValue(parts[1]);
              }
              flags.put(key, value);
              valid = true;
            }
          }
        }
        catch (Exception e) {
        }
      }
      if (!valid) {
        System.err.println("Invalid argument " + arg);
        System.exit(1);
      }
    }
    new DbBenchmark(flags).run();
  }

  private enum Flag {
    benchmarks(Arrays.asList("fillseq", "readrandom", "fillrandom"),
        "Comma-separated list of operations to run in the specified order\n"
            + "\tActual benchmarks:\n"
            + "\t\tfillseq          -- write N values in sequential key order in async mode.\n"
            + "\t\tfillrandom       -- write N values in random key order in async mode.\n"
            + "\t\tfillbatch        -- write N/1000 batch where each batch has 1000 values\n"
            + "\t\t                   in sequential key order in sync mode.\n"
            + "\t\tfillsync         -- write N/100 values in random key order in sync mode.\n"
            + "\t\tfill100K         -- write N/1000 100K values in random order in async mode.\n"
            + "\t\treadseq          -- read N times sequentially.\n"
            + "\t\treadrandom       -- read N times in random order.\n"
            + "\t\treadhot          -- read N times in random order from 1% section of DB.\n"
            + "\t\treadwhilewriting -- measure the read performance of multiple readers\n"
            + "\t\t                   with a bg single writer.  The write rate of the bg\n"
            + "\t\t                   is capped by --writes_per_second.\n"
            + "\tMeta Operations:\n"
            + "\t\tdelete            -- delete DB") {
      @Override public Object parseValue(String value) {
        return new ArrayList<String>(Arrays.asList(value.split(",")));
      }
    },
    compression_ratio(0.5d,
        "Arrange to generate values that shrink to this fraction of\n" +
        "\ttheir original size after compression.") {
      @Override public Object parseValue(String value) {
        return Double.parseDouble(value);
      }
    },
    use_existing_db(false,
        "If true, do not destroy the existing database.  If you set this\n" +
        "\tflag and also specify a benchmark that wants a fresh database,\n" +
        "\tthat benchmark will fail.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    num(1000000,
        "Number of key/values to place in database.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    threads(1,
        "Number of concurrent threads to run.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    reads(null,
        "Number of read operations to do.  If negative, do --nums reads.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    key_size(16,
        "The size of each key in bytes.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    value_size(100,
        "The size of each value in bytes.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    write_buffer_size(4L * SizeUnit.MB,
        "Number of bytes to buffer in memtable before compacting\n" +
        "\t(initialized to default value by 'main'.)") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    max_write_buffer_number(2,
             "The number of in-memory memtables. Each memtable is of size\n" +
             "\twrite_buffer_size.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    prefix_size(0, "Controls the prefix size for HashSkipList, HashLinkedList,\n" +
                   "\tand plain table.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    keys_per_prefix(0, "Controls the average number of keys generated\n" +
             "\tper prefix, 0 means no special handling of the prefix,\n" +
             "\ti.e. use the prefix comes with the generated random number.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    memtablerep("skip_list",
        "The memtable format.  Available options are\n" +
        "\tskip_list,\n" +
        "\tvector,\n" +
        "\thash_linkedlist,\n" +
        "\thash_skiplist (prefix_hash.)") {
      @Override public Object parseValue(String value) {
        return value;
      }
    },
    hash_bucket_count(SizeUnit.MB,
        "The number of hash buckets used in the hash-bucket-based\n" +
        "\tmemtables.  Memtables that currently support this argument are\n" +
        "\thash_linkedlist and hash_skiplist.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    writes_per_second(10000,
        "The write-rate of the background writer used in the\n" +
        "\t`readwhilewriting` benchmark.  Non-positive number indicates\n" +
        "\tusing an unbounded write-rate in `readwhilewriting` benchmark.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    use_plain_table(false,
        "Use plain-table sst format.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    cache_size(-1L,
        "Number of bytes to use as a cache of uncompressed data.\n" +
        "\tNegative means use default settings.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    seed(0L,
        "Seed base for random number generators.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    num_levels(7,
        "The total number of levels.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    numdistinct(1000L,
        "Number of distinct keys to use. Used in RandomWithVerify to\n" +
        "\tread/write on fewer keys so that gets are more likely to find the\n" +
        "\tkey and puts are more likely to update the same key.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    merge_keys(-1L,
        "Number of distinct keys to use for MergeRandom and\n" +
        "\tReadRandomMergeRandom.\n" +
        "\tIf negative, there will be FLAGS_num keys.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    bloom_locality(0,"Control bloom filter probes locality.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    duration(0,"Time in seconds for the random-ops tests to run.\n" +
        "\tWhen 0 then num & reads determine the test duration.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    num_multi_db(0,
        "Number of DBs used in the benchmark. 0 means single DB.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    histogram(false,"Print histogram of operation timings.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    min_write_buffer_number_to_merge(
        defaultOptions_.minWriteBufferNumberToMerge(),
        "The minimum number of write buffers that will be merged together\n" +
        "\tbefore writing to storage. This is cheap because it is an\n" +
        "\tin-memory merge. If this feature is not enabled, then all these\n" +
        "\twrite buffers are flushed to L0 as separate files and this\n" +
        "\tincreases read amplification because a get request has to check\n" +
        "\tin all of these files. Also, an in-memory merge may result in\n" +
        "\twriting less data to storage if there are duplicate records\n" +
        "\tin each of these individual write buffers.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    max_background_compactions(
        defaultOptions_.maxBackgroundCompactions(),
        "The maximum number of concurrent background compactions\n" +
        "\tthat can occur in parallel.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    max_background_flushes(
        defaultOptions_.maxBackgroundFlushes(),
        "The maximum number of concurrent background flushes\n" +
        "\tthat can occur in parallel.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    max_background_jobs(defaultOptions_.maxBackgroundJobs(),
        "The maximum number of concurrent background jobs\n"
            + "\tthat can occur in parallel.") {
      @Override
      public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    /* TODO(yhchiang): enable the following
    compaction_style((int32_t) defaultOptions_.compactionStyle(),
        "style of compaction: level-based vs universal.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },*/
    universal_size_ratio(0,
        "Percentage flexibility while comparing file size\n" +
        "\t(for universal compaction only).") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    universal_min_merge_width(0,"The minimum number of files in a\n" +
        "\tsingle compaction run (for universal compaction only).") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    universal_max_merge_width(0,"The max number of files to compact\n" +
        "\tin universal style compaction.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    universal_max_size_amplification_percent(0,
        "The max size amplification for universal style compaction.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    universal_compression_size_percent(-1,
        "The percentage of the database to compress for universal\n" +
        "\tcompaction. -1 means compress everything.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    block_size(defaultBlockBasedTableOptions_.blockSize(),
        "Number of bytes in a block.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    compressed_cache_size(-1L,
        "Number of bytes to use as a cache of compressed data.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    open_files(defaultOptions_.maxOpenFiles(),
        "Maximum number of files to keep open at the same time\n" +
        "\t(use default if == 0)") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    bloom_bits(-1,"Bloom filter bits per key. Negative means\n" +
        "\tuse default settings.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    memtable_bloom_size_ratio(0.0d, "Ratio of memtable used by the bloom filter.\n"
            + "\t0 means no bloom filter.") {
      @Override public Object parseValue(String value) {
        return Double.parseDouble(value);
      }
    },
    memtable_whole_key_filtering(false, "Enable whole key bloom filter in memtable.") {
      @Override
      public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    cache_numshardbits(-1,"Number of shards for the block cache\n" +
        "\tis 2 ** cache_numshardbits. Negative means use default settings.\n" +
        "\tThis is applied only if FLAGS_cache_size is non-negative.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    verify_checksum(false,"Verify checksum for every block read\n" +
        "\tfrom storage.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    statistics(false,"Database statistics.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    writes(-1L, "Number of write operations to do. If negative, do\n" +
        "\t--num reads.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    sync(false,"Sync all writes to disk.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    use_fsync(false,"If true, issue fsync instead of fdatasync.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    disable_wal(false,"If true, do not write WAL for write.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    wal_dir("", "If not empty, use the given dir for WAL.") {
      @Override public Object parseValue(String value) {
        return value;
      }
    },
    target_file_size_base(2 * 1048576,"Target file size at level-1") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    target_file_size_multiplier(1,
        "A multiplier to compute target level-N file size (N >= 2)") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    max_bytes_for_level_base(10 * 1048576,
      "Max bytes for level-1") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    max_bytes_for_level_multiplier(10.0d,
        "A multiplier to compute max bytes for level-N (N >= 2)") {
      @Override public Object parseValue(String value) {
        return Double.parseDouble(value);
      }
    },
    level0_stop_writes_trigger(12,"Number of files in level-0\n" +
        "\tthat will trigger put stop.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    level0_slowdown_writes_trigger(8,"Number of files in level-0\n" +
        "\tthat will slow down writes.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    level0_file_num_compaction_trigger(4,"Number of files in level-0\n" +
        "\twhen compactions start.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    readwritepercent(90,"Ratio of reads to reads/writes (expressed\n" +
        "\tas percentage) for the ReadRandomWriteRandom workload. The\n" +
        "\tdefault value 90 means 90% operations out of all reads and writes\n" +
        "\toperations are reads. In other words, 9 gets for every 1 put.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    mergereadpercent(70,"Ratio of merges to merges&reads (expressed\n" +
        "\tas percentage) for the ReadRandomMergeRandom workload. The\n" +
        "\tdefault value 70 means 70% out of all read and merge operations\n" +
        "\tare merges. In other words, 7 merges for every 3 gets.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    deletepercent(2,"Percentage of deletes out of reads/writes/\n" +
        "\tdeletes (used in RandomWithVerify only). RandomWithVerify\n" +
        "\tcalculates writepercent as (100 - FLAGS_readwritepercent -\n" +
        "\tdeletepercent), so deletepercent must be smaller than (100 -\n" +
        "\tFLAGS_readwritepercent)") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    delete_obsolete_files_period_micros(0,"Option to delete\n" +
        "\tobsolete files periodically. 0 means that obsolete files are\n" +
        "\tdeleted after every compaction run.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    compression_type("snappy",
        "Algorithm used to compress the database.") {
      @Override public Object parseValue(String value) {
        return value;
      }
    },
    compression_level(-1,
        "Compression level. For zlib this should be -1 for the\n" +
        "\tdefault level, or between 0 and 9.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    min_level_to_compress(-1,"If non-negative, compression starts\n" +
        "\tfrom this level. Levels with number < min_level_to_compress are\n" +
        "\tnot compressed. Otherwise, apply compression_type to\n" +
        "\tall levels.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    table_cache_numshardbits(4,"") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    stats_interval(0L, "Stats are reported every N operations when\n" +
        "\tthis is greater than zero. When 0 the interval grows over time.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    stats_per_interval(0,"Reports additional stats per interval when\n" +
        "\tthis is greater than 0.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    perf_level(0,"Level of perf collection.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    max_compaction_bytes(0L, "Limit number of bytes in one compaction to be lower than this\n" +
            "\threshold. But it's not guaranteed.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    readonly(false,"Run read only benchmarks.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    disable_auto_compactions(false,"Do not auto trigger compactions.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    wal_ttl_seconds(0L,"Set the TTL for the WAL Files in seconds.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    wal_size_limit_MB(0L,"Set the size limit for the WAL Files\n" +
        "\tin MB.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    /* TODO(yhchiang): enable the following
    direct_reads(rocksdb::EnvOptions().use_direct_reads,
        "Allow direct I/O reads.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
      },
    direct_writes(rocksdb::EnvOptions().use_direct_reads,
      "Allow direct I/O reads.") {
      @Override public Object parseValue(String value) {
      return parseBoolean(value);
      }
      },
    */
    mmap_read(false,
        "Allow reads to occur via mmap-ing files.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    mmap_write(false,
        "Allow writes to occur via mmap-ing files.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    advise_random_on_open(defaultOptions_.adviseRandomOnOpen(),
        "Advise random access on table file open.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    compaction_fadvice("NORMAL",
      "Access pattern advice when a file is compacted.") {
      @Override public Object parseValue(String value) {
        return value;
      }
    },
    use_tailing_iterator(false,
        "Use tailing iterator to access a series of keys instead of get.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    use_adaptive_mutex(defaultOptions_.useAdaptiveMutex(),
        "Use adaptive mutex.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    bytes_per_sync(defaultOptions_.bytesPerSync(),
        "Allows OS to incrementally sync files to disk while they are\n" +
        "\tbeing written, in the background. Issue one request for every\n" +
        "\tbytes_per_sync written. 0 turns it off.") {
      @Override public Object parseValue(String value) {
        return Long.parseLong(value);
      }
    },
    filter_deletes(false," On true, deletes use bloom-filter and drop\n" +
        "\tthe delete if key not present.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    max_successive_merges(0,"Maximum number of successive merge\n" +
        "\toperations on a key in the memtable.") {
      @Override public Object parseValue(String value) {
        return Integer.parseInt(value);
      }
    },
    db(getTempDir("rocksdb-jni"),
       "Use the db with the following name.") {
      @Override public Object parseValue(String value) {
        return value;
      }
    },
    use_mem_env(false, "Use RocksMemEnv instead of default filesystem based\n" +
        "environment.") {
      @Override public Object parseValue(String value) {
        return parseBoolean(value);
      }
    },
    java_comparator(null, "Class name of a Java Comparator to use instead\n" +
        "\tof the default C++ ByteWiseComparatorImpl. Must be available on\n" +
        "\tthe classpath") {
      @Override
      protected Object parseValue(final String value) {
        try {
          final ComparatorOptions copt = new ComparatorOptions();
          final Class<AbstractComparator> clsComparator =
              (Class<AbstractComparator>)Class.forName(value);
          final Constructor cstr =
              clsComparator.getConstructor(ComparatorOptions.class);
          return cstr.newInstance(copt);
        } catch(final ClassNotFoundException cnfe) {
          throw new IllegalArgumentException("Java Comparator '" + value + "'" +
              " not found on the classpath", cnfe);
        } catch(final NoSuchMethodException nsme) {
          throw new IllegalArgumentException("Java Comparator '" + value + "'" +
              " does not have a public ComparatorOptions constructor", nsme);
        } catch(final IllegalAccessException | InstantiationException
            | InvocationTargetException ie) {
          throw new IllegalArgumentException("Unable to construct Java" +
              " Comparator '" + value + "'", ie);
        }
      }
    };

    private Flag(Object defaultValue, String desc) {
      defaultValue_ = defaultValue;
      desc_ = desc;
    }

    public Object getDefaultValue() {
      return defaultValue_;
    }

    public String desc() {
      return desc_;
    }

    public boolean parseBoolean(String value) {
      if (value.equals("1")) {
        return true;
      } else if (value.equals("0")) {
        return false;
      }
      return Boolean.parseBoolean(value);
    }

    protected abstract Object parseValue(String value);

    private final Object defaultValue_;
    private final String desc_;
  }

  private final static String DEFAULT_TEMP_DIR = "/tmp";

  private static String getTempDir(final String dirName) {
    try {
      return Files.createTempDirectory(dirName).toAbsolutePath().toString();
    } catch(final IOException ioe) {
      System.err.println("Unable to create temp directory, defaulting to: " +
          DEFAULT_TEMP_DIR);
      return DEFAULT_TEMP_DIR + File.pathSeparator + dirName;
    }
  }

  private static class RandomGenerator {
    private final byte[] data_;
    private int dataLength_;
    private int position_;
    private double compressionRatio_;
    Random rand_;

    private RandomGenerator(long seed, double compressionRatio) {
      // We use a limited amount of data over and over again and ensure
      // that it is larger than the compression window (32KB), and also
      byte[] value = new byte[100];
      // large enough to serve all typical value sizes we want to write.
      rand_ = new Random(seed);
      dataLength_ = value.length * 10000;
      data_ = new byte[dataLength_];
      compressionRatio_ = compressionRatio;
      int pos = 0;
      while (pos < dataLength_) {
        compressibleBytes(value);
        System.arraycopy(value, 0, data_, pos,
                         Math.min(value.length, dataLength_ - pos));
        pos += value.length;
      }
    }

    private void compressibleBytes(byte[] value) {
      int baseLength = value.length;
      if (compressionRatio_ < 1.0d) {
        baseLength = (int) (compressionRatio_ * value.length + 0.5);
      }
      if (baseLength <= 0) {
        baseLength = 1;
      }
      int pos;
      for (pos = 0; pos < baseLength; ++pos) {
        value[pos] = (byte) (' ' + rand_.nextInt(95));  // ' ' .. '~'
      }
      while (pos < value.length) {
        System.arraycopy(value, 0, value, pos,
                         Math.min(baseLength, value.length - pos));
        pos += baseLength;
      }
    }

    private void generate(byte[] value) {
      if (position_ + value.length > data_.length) {
        position_ = 0;
        assert(value.length <= data_.length);
      }
      position_ += value.length;
      System.arraycopy(data_, position_ - value.length,
                       value, 0, value.length);
    }
  }

  boolean isFinished() {
    synchronized(finishLock_) {
      return isFinished_;
    }
  }

  void setFinished(boolean flag) {
    synchronized(finishLock_) {
      isFinished_ = flag;
    }
  }

  RocksDB db_;
  final List<String> benchmarks_;
  final int num_;
  final int reads_;
  final int keySize_;
  final int valueSize_;
  final int threadNum_;
  final int writesPerSeconds_;
  final long randSeed_;
  final boolean useExisting_;
  final String databaseDir_;
  double compressionRatio_;
  RandomGenerator gen_;
  long startTime_;

  // env
  boolean useMemenv_;

  // memtable related
  final int maxWriteBufferNumber_;
  final int prefixSize_;
  final int keysPerPrefix_;
  final String memtable_;
  final long hashBucketCount_;

  // sst format related
  boolean usePlainTable_;

  Object finishLock_;
  boolean isFinished_;
  Map<Flag, Object> flags_;
  // as the scope of a static member equals to the scope of the problem,
  // we let its c++ pointer to be disposed in its finalizer.
  static Options defaultOptions_ = new Options();
  static BlockBasedTableConfig defaultBlockBasedTableOptions_ =
    new BlockBasedTableConfig();
  String compressionType_;
  CompressionType compression_;
}
