Merge pull request #1053 from adamretter/benchmark-java-comparator

Benchmark Java comparator vs C++ comparator
main
Adam Retter 9 years ago committed by Yueh-Hsuan Chiang
parent f2c43a4a27
commit 200654067a
  1. 1
      java/Makefile
  2. 49
      java/benchmark/src/main/java/org/rocksdb/benchmark/DbBenchmark.java
  3. 2
      java/src/main/java/org/rocksdb/AbstractSlice.java
  4. 91
      java/src/main/java/org/rocksdb/util/BytewiseComparator.java
  5. 88
      java/src/main/java/org/rocksdb/util/DirectBytewiseComparator.java
  6. 37
      java/src/main/java/org/rocksdb/util/ReverseBytewiseComparator.java
  7. 480
      java/src/test/java/org/rocksdb/util/BytewiseComparatorTest.java

@ -64,6 +64,7 @@ JAVA_TESTS = org.rocksdb.BackupableDBOptionsTest\
org.rocksdb.BackupEngineTest\
org.rocksdb.BackupableDBTest\
org.rocksdb.BlockBasedTableConfigTest\
org.rocksdb.util.BytewiseComparatorTest\
org.rocksdb.CheckPointTest\
org.rocksdb.ColumnFamilyOptionsTest\
org.rocksdb.ColumnFamilyTest\

@ -21,10 +21,14 @@
*/
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;
@ -601,6 +605,11 @@ public class DbBenchmark {
(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));
@ -774,6 +783,7 @@ public class DbBenchmark {
}
private void open(Options options) throws RocksDBException {
System.out.println("Using database directory: " + databaseDir_);
db_ = RocksDB.open(options, databaseDir_);
}
@ -1475,7 +1485,7 @@ public class DbBenchmark {
return Integer.parseInt(value);
}
},
db("/tmp/rocksdbjni-bench",
db(getTempDir("rocksdb-jni"),
"Use the db with the following name.") {
@Override public Object parseValue(String value) {
return value;
@ -1486,6 +1496,31 @@ public class DbBenchmark {
@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) {
@ -1516,6 +1551,18 @@ public class DbBenchmark {
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_;

@ -24,7 +24,7 @@ package org.rocksdb;
* C++ BaseComparatorJniCallback subclass, which in turn destroys the
* Java @see org.rocksdb.AbstractSlice subclass Objects.
*/
abstract class AbstractSlice<T> extends RocksMutableObject {
public abstract class AbstractSlice<T> extends RocksMutableObject {
protected AbstractSlice() {
super();

@ -0,0 +1,91 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
package org.rocksdb.util;
import org.rocksdb.*;
import java.nio.ByteBuffer;
/**
* This is a Java Native implementation of the C++
* equivalent BytewiseComparatorImpl using {@link Slice}
*
* The performance of Comparators implemented in Java is always
* less than their C++ counterparts due to the bridging overhead,
* as such you likely don't want to use this apart from benchmarking
* and you most likely instead wanted
* {@link org.rocksdb.BuiltinComparator#BYTEWISE_COMPARATOR}
*/
public class BytewiseComparator extends Comparator {
public BytewiseComparator(final ComparatorOptions copt) {
super(copt);
}
@Override
public String name() {
return "rocksdb.java.BytewiseComparator";
}
@Override
public int compare(final Slice a, final Slice b) {
return compare(a.data(), b.data());
}
@Override
public String findShortestSeparator(final String start,
final Slice limit) {
final byte[] startBytes = start.getBytes();
final byte[] limitBytes = limit.data();
// Find length of common prefix
final int min_length = Math.min(startBytes.length, limit.size());
int diff_index = 0;
while ((diff_index < min_length) &&
(startBytes[diff_index] == limitBytes[diff_index])) {
diff_index++;
}
if (diff_index >= min_length) {
// Do not shorten if one string is a prefix of the other
} else {
final byte diff_byte = startBytes[diff_index];
if(diff_byte < 0xff && diff_byte + 1 < limitBytes[diff_index]) {
final byte shortest[] = new byte[diff_index + 1];
System.arraycopy(startBytes, 0, shortest, 0, diff_index + 1);
shortest[diff_index]++;
assert(compare(shortest, limitBytes) < 0);
return new String(shortest);
}
}
return null;
}
private static int compare(final byte[] a, final byte[] b) {
return ByteBuffer.wrap(a).compareTo(ByteBuffer.wrap(b));
}
@Override
public String findShortSuccessor(final String key) {
final byte[] keyBytes = key.getBytes();
// Find first character that can be incremented
final int n = keyBytes.length;
for (int i = 0; i < n; i++) {
final byte byt = keyBytes[i];
if (byt != 0xff) {
final byte shortSuccessor[] = new byte[i + 1];
System.arraycopy(keyBytes, 0, shortSuccessor, 0, i + 1);
shortSuccessor[i]++;
return new String(shortSuccessor);
}
}
// *key is a run of 0xffs. Leave it alone.
return null;
}
}

@ -0,0 +1,88 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
package org.rocksdb.util;
import org.rocksdb.ComparatorOptions;
import org.rocksdb.DirectComparator;
import org.rocksdb.DirectSlice;
import java.nio.ByteBuffer;
/**
* This is a Java Native implementation of the C++
* equivalent BytewiseComparatorImpl using {@link DirectSlice}
*
* The performance of Comparators implemented in Java is always
* less than their C++ counterparts due to the bridging overhead,
* as such you likely don't want to use this apart from benchmarking
* and you most likely instead wanted
* {@link org.rocksdb.BuiltinComparator#BYTEWISE_COMPARATOR}
*/
public class DirectBytewiseComparator extends DirectComparator {
public DirectBytewiseComparator(final ComparatorOptions copt) {
super(copt);
}
@Override
public String name() {
return "rocksdb.java.DirectBytewiseComparator";
}
@Override
public int compare(final DirectSlice a, final DirectSlice b) {
return a.data().compareTo(b.data());
}
@Override
public String findShortestSeparator(final String start,
final DirectSlice limit) {
final byte[] startBytes = start.getBytes();
// Find length of common prefix
final int min_length = Math.min(startBytes.length, limit.size());
int diff_index = 0;
while ((diff_index < min_length) &&
(startBytes[diff_index] == limit.get(diff_index))) {
diff_index++;
}
if (diff_index >= min_length) {
// Do not shorten if one string is a prefix of the other
} else {
final byte diff_byte = startBytes[diff_index];
if(diff_byte < 0xff && diff_byte + 1 < limit.get(diff_index)) {
final byte shortest[] = new byte[diff_index + 1];
System.arraycopy(startBytes, 0, shortest, 0, diff_index + 1);
shortest[diff_index]++;
assert(ByteBuffer.wrap(shortest).compareTo(limit.data()) < 0);
return new String(shortest);
}
}
return null;
}
@Override
public String findShortSuccessor(final String key) {
final byte[] keyBytes = key.getBytes();
// Find first character that can be incremented
final int n = keyBytes.length;
for (int i = 0; i < n; i++) {
final byte byt = keyBytes[i];
if (byt != 0xff) {
final byte shortSuccessor[] = new byte[i + 1];
System.arraycopy(keyBytes, 0, shortSuccessor, 0, i + 1);
shortSuccessor[i]++;
return new String(shortSuccessor);
}
}
// *key is a run of 0xffs. Leave it alone.
return null;
}
}

@ -0,0 +1,37 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
package org.rocksdb.util;
import org.rocksdb.BuiltinComparator;
import org.rocksdb.ComparatorOptions;
import org.rocksdb.Slice;
/**
* This is a Java Native implementation of the C++
* equivalent ReverseBytewiseComparatorImpl using {@link Slice}
*
* The performance of Comparators implemented in Java is always
* less than their C++ counterparts due to the bridging overhead,
* as such you likely don't want to use this apart from benchmarking
* and you most likely instead wanted
* {@link BuiltinComparator#REVERSE_BYTEWISE_COMPARATOR}
*/
public class ReverseBytewiseComparator extends BytewiseComparator {
public ReverseBytewiseComparator(final ComparatorOptions copt) {
super(copt);
}
@Override
public String name() {
return "rocksdb.java.ReverseBytewiseComparator";
}
@Override
public int compare(final Slice a, final Slice b) {
return -super.compare(a, b);
}
}

@ -0,0 +1,480 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
package org.rocksdb.util;
import org.junit.Test;
import org.rocksdb.*;
import org.rocksdb.Comparator;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import static org.junit.Assert.*;
/**
* This is a direct port of various C++
* tests from db/comparator_db_test.cc
* and some code to adapt it to RocksJava
*/
public class BytewiseComparatorTest {
/**
* Open the database using the C++ BytewiseComparatorImpl
* and test the results against our Java BytewiseComparator
*/
@Test
public void java_vs_cpp_bytewiseComparator()
throws IOException, RocksDBException {
for(int rand_seed = 301; rand_seed < 306; rand_seed++) {
final Path dbDir = Files.createTempDirectory("comparator_db_test");
try(final RocksDB db = openDatabase(dbDir,
BuiltinComparator.BYTEWISE_COMPARATOR)) {
final Random rnd = new Random(rand_seed);
doRandomIterationTest(
db,
toJavaComparator(new BytewiseComparator(new ComparatorOptions())),
Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"),
rnd,
8, 100, 3
);
} finally {
removeData(dbDir);
}
}
}
/**
* Open the database using the Java BytewiseComparator
* and test the results against another Java BytewiseComparator
*/
@Test
public void java_vs_java_bytewiseComparator()
throws IOException, RocksDBException {
for(int rand_seed = 301; rand_seed < 306; rand_seed++) {
final Path dbDir = Files.createTempDirectory("comparator_db_test");
try(final RocksDB db = openDatabase(dbDir, new BytewiseComparator(
new ComparatorOptions()))) {
final Random rnd = new Random(rand_seed);
doRandomIterationTest(
db,
toJavaComparator(new BytewiseComparator(new ComparatorOptions())),
Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"),
rnd,
8, 100, 3
);
} finally {
removeData(dbDir);
}
}
}
/**
* Open the database using the C++ BytewiseComparatorImpl
* and test the results against our Java DirectBytewiseComparator
*/
@Test
public void java_vs_cpp_directBytewiseComparator()
throws IOException, RocksDBException {
for(int rand_seed = 301; rand_seed < 306; rand_seed++) {
final Path dbDir = Files.createTempDirectory("comparator_db_test");
try(final RocksDB db = openDatabase(dbDir,
BuiltinComparator.BYTEWISE_COMPARATOR)) {
final Random rnd = new Random(rand_seed);
doRandomIterationTest(
db,
toJavaComparator(new DirectBytewiseComparator(
new ComparatorOptions())
),
Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"),
rnd,
8, 100, 3
);
} finally {
removeData(dbDir);
}
}
}
/**
* Open the database using the Java DirectBytewiseComparator
* and test the results against another Java DirectBytewiseComparator
*/
@Test
public void java_vs_java_directBytewiseComparator()
throws IOException, RocksDBException {
for(int rand_seed = 301; rand_seed < 306; rand_seed++) {
final Path dbDir = Files.createTempDirectory("comparator_db_test");
try(final RocksDB db = openDatabase(dbDir, new DirectBytewiseComparator(
new ComparatorOptions()))) {
final Random rnd = new Random(rand_seed);
doRandomIterationTest(
db,
toJavaComparator(new DirectBytewiseComparator(
new ComparatorOptions())
),
Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"),
rnd,
8, 100, 3
);
} finally {
removeData(dbDir);
}
}
}
/**
* Open the database using the C++ ReverseBytewiseComparatorImpl
* and test the results against our Java ReverseBytewiseComparator
*/
@Test
public void java_vs_cpp_reverseBytewiseComparator()
throws IOException, RocksDBException {
for(int rand_seed = 301; rand_seed < 306; rand_seed++) {
final Path dbDir = Files.createTempDirectory("comparator_db_test");
try(final RocksDB db = openDatabase(dbDir,
BuiltinComparator.REVERSE_BYTEWISE_COMPARATOR)) {
final Random rnd = new Random(rand_seed);
doRandomIterationTest(
db,
toJavaComparator(
new ReverseBytewiseComparator(new ComparatorOptions())
),
Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"),
rnd,
8, 100, 3
);
} finally {
removeData(dbDir);
}
}
}
/**
* Open the database using the Java ReverseBytewiseComparator
* and test the results against another Java ReverseBytewiseComparator
*/
@Test
public void java_vs_java_reverseBytewiseComparator()
throws IOException, RocksDBException {
for(int rand_seed = 301; rand_seed < 306; rand_seed++) {
final Path dbDir = Files.createTempDirectory("comparator_db_test");
try(final RocksDB db = openDatabase(dbDir, new ReverseBytewiseComparator(
new ComparatorOptions()))) {
final Random rnd = new Random(rand_seed);
doRandomIterationTest(
db,
toJavaComparator(
new ReverseBytewiseComparator(new ComparatorOptions())
),
Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"),
rnd,
8, 100, 3
);
} finally {
removeData(dbDir);
}
}
}
private void doRandomIterationTest(
final RocksDB db, final java.util.Comparator<String> javaComparator,
final List<String> source_strings, final Random rnd,
final int num_writes, final int num_iter_ops,
final int num_trigger_flush) throws RocksDBException {
final TreeMap<String, String> map = new TreeMap<>(javaComparator);
for (int i = 0; i < num_writes; i++) {
if (num_trigger_flush > 0 && i != 0 && i % num_trigger_flush == 0) {
db.flush(new FlushOptions());
}
final int type = rnd.nextInt(2);
final int index = rnd.nextInt(source_strings.size());
final String key = source_strings.get(index);
switch (type) {
case 0:
// put
map.put(key, key);
db.put(new WriteOptions(), bytes(key), bytes(key));
break;
case 1:
// delete
if (map.containsKey(key)) {
map.remove(key);
}
db.remove(new WriteOptions(), bytes(key));
break;
default:
fail("Should not be able to generate random outside range 1..2");
}
}
try(final RocksIterator iter = db.newIterator(new ReadOptions())) {
final KVIter<String, String> result_iter = new KVIter(map);
boolean is_valid = false;
for (int i = 0; i < num_iter_ops; i++) {
// Random walk and make sure iter and result_iter returns the
// same key and value
final int type = rnd.nextInt(6);
iter.status();
switch (type) {
case 0:
// Seek to First
iter.seekToFirst();
result_iter.seekToFirst();
break;
case 1:
// Seek to last
iter.seekToLast();
result_iter.seekToLast();
break;
case 2: {
// Seek to random key
final int key_idx = rnd.nextInt(source_strings.size());
final String key = source_strings.get(key_idx);
iter.seek(bytes(key));
result_iter.seek(bytes(key));
break;
}
case 3:
// Next
if (is_valid) {
iter.next();
result_iter.next();
} else {
continue;
}
break;
case 4:
// Prev
if (is_valid) {
iter.prev();
result_iter.prev();
} else {
continue;
}
break;
default: {
assert (type == 5);
final int key_idx = rnd.nextInt(source_strings.size());
final String key = source_strings.get(key_idx);
final byte[] result = db.get(new ReadOptions(), bytes(key));
if (!map.containsKey(key)) {
assertNull(result);
} else {
assertArrayEquals(bytes(map.get(key)), result);
}
break;
}
}
assertEquals(result_iter.isValid(), iter.isValid());
is_valid = iter.isValid();
if (is_valid) {
assertArrayEquals(bytes(result_iter.key()), iter.key());
//note that calling value on a non-valid iterator from the Java API
//results in a SIGSEGV
assertArrayEquals(bytes(result_iter.value()), iter.value());
}
}
}
}
/**
* Open the database using a C++ Comparator
*/
private RocksDB openDatabase(
final Path dbDir, final BuiltinComparator cppComparator)
throws IOException, RocksDBException {
final Options options = new Options()
.setCreateIfMissing(true)
.setComparator(cppComparator);
return RocksDB.open(options, dbDir.toAbsolutePath().toString());
}
/**
* Open the database using a Java Comparator
*/
private RocksDB openDatabase(
final Path dbDir,
final AbstractComparator<? extends AbstractSlice<?>> javaComparator)
throws IOException, RocksDBException {
final Options options = new Options()
.setCreateIfMissing(true)
.setComparator(javaComparator);
return RocksDB.open(options, dbDir.toAbsolutePath().toString());
}
private void closeDatabase(final RocksDB db) {
db.close();
}
private void removeData(final Path dbDir) throws IOException {
Files.walkFileTree(dbDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(
final Path file, final BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(
final Path dir, final IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
private byte[] bytes(final String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
private java.util.Comparator<String> toJavaComparator(
final Comparator rocksComparator) {
return new java.util.Comparator<String>() {
@Override
public int compare(final String s1, final String s2) {
return rocksComparator.compare(new Slice(s1), new Slice(s2));
}
};
}
private java.util.Comparator<String> toJavaComparator(
final DirectComparator rocksComparator) {
return new java.util.Comparator<String>() {
@Override
public int compare(final String s1, final String s2) {
return rocksComparator.compare(new DirectSlice(s1),
new DirectSlice(s2));
}
};
}
private class KVIter<K, V> implements RocksIteratorInterface {
private final List<Map.Entry<K, V>> entries;
private final java.util.Comparator<? super K> comparator;
private int offset = -1;
private int lastPrefixMatchIdx = -1;
private int lastPrefixMatch = 0;
public KVIter(final TreeMap<K, V> map) {
this.entries = new ArrayList<>();
final Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();
while(iterator.hasNext()) {
entries.add(iterator.next());
}
this.comparator = map.comparator();
}
@Override
public boolean isValid() {
return offset > -1 && offset < entries.size();
}
@Override
public void seekToFirst() {
offset = 0;
}
@Override
public void seekToLast() {
offset = entries.size() - 1;
}
@Override
public void seek(final byte[] target) {
for(offset = 0; offset < entries.size(); offset++) {
if(comparator.compare(entries.get(offset).getKey(),
(K)new String(target, StandardCharsets.UTF_8)) >= 0) {
return;
}
}
}
/**
* Is `a` a prefix of `b`
*
* @return The length of the matching prefix, or 0 if it is not a prefix
*/
private int isPrefix(final byte[] a, final byte[] b) {
if(b.length >= a.length) {
for(int i = 0; i < a.length; i++) {
if(a[i] != b[i]) {
return i;
}
}
return a.length;
} else {
return 0;
}
}
@Override
public void next() {
if(offset < entries.size()) {
offset++;
}
}
@Override
public void prev() {
if(offset >= 0) {
offset--;
}
}
@Override
public void status() throws RocksDBException {
if(offset < 0 || offset >= entries.size()) {
throw new RocksDBException("Index out of bounds. Size is: " +
entries.size() + ", offset is: " + offset);
}
}
public K key() {
if(!isValid()) {
if(entries.isEmpty()) {
return (K)"";
} else if(offset == -1){
return entries.get(0).getKey();
} else if(offset == entries.size()) {
return entries.get(offset - 1).getKey();
} else {
return (K)"";
}
} else {
return entries.get(offset).getKey();
}
}
public V value() {
if(!isValid()) {
return (V)"";
} else {
return entries.get(offset).getValue();
}
}
}
}
Loading…
Cancel
Save