DATAMONGO-439 - Created performance tests.

Added a performance tests that tests two things. Write performance in a variety of WriteConcern configurations as well as write-read performance against WriteConcern.SAFE. The tests can be executed by running the Maven build with the performance-test profile. Fixed some performance hotspots discovered in the course of profiling the test case.
This commit is contained in:
Oliver Gierke
2012-05-03 17:35:03 +02:00
parent fed31c74e1
commit 71e639402c
5 changed files with 642 additions and 9 deletions

View File

@@ -187,6 +187,9 @@
<includes>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/PerformanceTests.java</exclude>
</excludes>
<junitArtifactName>junit:junit</junitArtifactName>
</configuration>
</plugin>

View File

@@ -148,6 +148,29 @@
</dependencies>
<profiles>
<profile>
<id>performance-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.8</version>
<configuration>
<includes>
<include>**/PerformanceTests.java</include>
</includes>
<excludes>
<exclude>none</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>

View File

@@ -720,20 +720,24 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(targetType);
if (sourceValue.isEmpty()) {
return Collections.emptySet();
}
Class<?> collectionType = targetType.getType();
collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class;
Collection<Object> items = targetType.getType().isArray() ? new ArrayList<Object>() : CollectionFactory
.createCollection(collectionType, sourceValue.size());
TypeInformation<?> componentType = targetType.getComponentType();
for (int i = 0; i < sourceValue.size(); i++) {
Object dbObjItem = sourceValue.get(i);
if (dbObjItem instanceof DBRef) {
items.add(read(targetType.getComponentType(), ((DBRef) dbObjItem).fetch(), parent));
items.add(read(componentType, ((DBRef) dbObjItem).fetch(), parent));
} else if (dbObjItem instanceof DBObject) {
items.add(read(targetType.getComponentType(), (DBObject) dbObjItem, parent));
items.add(read(componentType, (DBObject) dbObjItem, parent));
} else {
TypeInformation<?> componentType = targetType.getComponentType();
items.add(getPotentiallyConvertedSimpleRead(dbObjItem, componentType == null ? null : componentType.getType()));
}
}
@@ -923,24 +927,25 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
public <T> T getPropertyValue(MongoPersistentProperty property) {
String expression = property.getSpelExpression();
TypeInformation<?> type = property.getTypeInformation();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
if (value == null) {
return null;
}
if (conversions.hasCustomReadTarget(value.getClass(), type.getType())) {
return (T) conversionService.convert(value, type.getType());
TypeInformation<?> type = property.getTypeInformation();
Class<?> rawType = type.getType();
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
return (T) conversionService.convert(value, rawType);
} else if (value instanceof DBRef) {
return (T) read(type, ((DBRef) value).fetch(), parent);
} else if (value instanceof BasicDBList) {
return (T) getPotentiallyConvertedSimpleRead(readCollectionOrArray(type, (BasicDBList) value, parent),
type.getType());
return (T) getPotentiallyConvertedSimpleRead(readCollectionOrArray(type, (BasicDBList) value, parent), rawType);
} else if (value instanceof DBObject) {
return (T) read(type, (DBObject) value, parent);
} else {
return (T) getPotentiallyConvertedSimpleRead(value, type.getType());
return (T) getPotentiallyConvertedSimpleRead(value, rawType);
}
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty {
private Boolean isIdProperty;
private Boolean isAssociation;
private String fieldName;
/**
@@ -57,6 +58,18 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty
return this.isIdProperty;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#isAssociation()
*/
@Override
public boolean isAssociation() {
if (this.isAssociation == null) {
this.isAssociation = super.isAssociation();
}
return this.isAssociation;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty#getFieldName()

View File

@@ -0,0 +1,589 @@
/*
* Copyright 2012 the original author or authors.
*
* 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.springframework.data.mongodb.performance;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;
import org.bson.types.ObjectId;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.Constants;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
import org.springframework.util.Assert;
import org.springframework.util.StopWatch;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.WriteConcern;
/**
* Test class to execute performance tests for plain MongoDB driver usage, {@link MongoTemplate} and the repositories
* abstraction.
*
* @author Oliver Gierke
*/
public class PerformanceTests {
private static final String DATABASE_NAME = "performance";
private static final int NUMBER_OF_PERSONS = 30000;
private static final StopWatch watch = new StopWatch();
private static final Collection<String> IGNORED_WRITE_CONCERNS = Arrays.asList("MAJORITY", "REPLICAS_SAFE",
"FSYNC_SAFE", "JOURNAL_SAFE");
private static final int COLLECTION_SIZE = 1024 * 1024 * 256; // 256 MB
private static final Collection<String> COLLECTION_NAMES = Arrays.asList("template", "driver", "person");
Mongo mongo;
MongoTemplate operations;
PersonRepository repository;
@Before
public void setUp() throws Exception {
this.mongo = new Mongo();
this.operations = new MongoTemplate(new SimpleMongoDbFactory(this.mongo, DATABASE_NAME));
MongoRepositoryFactoryBean<PersonRepository, Person, ObjectId> factory = new MongoRepositoryFactoryBean<PersonRepository, Person, ObjectId>();
factory.setMongoOperations(operations);
factory.setRepositoryInterface(PersonRepository.class);
factory.afterPropertiesSet();
repository = factory.getObject();
}
@Test
public void writeWithWriteConcerns() {
executeWithWriteConcerns(new WriteConcernCallback() {
public void doWithWriteConcern(String constantName, WriteConcern concern) {
writeHeadline("WriteConcern: " + constantName);
writingObjectsUsingPlainDriver("Writing %s objects using plain driver");
writingObjectsUsingMongoTemplate("Writing %s objects using template");
writingObjectsUsingRepositories("Writing %s objects using repository");
writeFooter();
}
});
}
@Test
public void writeAndRead() {
mongo.setWriteConcern(WriteConcern.SAFE);
for (int i = 3; i > 0; i--) {
setupCollections();
writeHeadline("Plain driver");
writingObjectsUsingPlainDriver("Writing %s objects using plain driver");
readingUsingPlainDriver("Reading all objects using plain driver");
queryUsingPlainDriver("Executing query using plain driver");
writeFooter();
writeHeadline("Template");
writingObjectsUsingMongoTemplate("Writing %s objects using template");
readingUsingTemplate("Reading all objects using template");
queryUsingTemplate("Executing query using template");
writeFooter();
writeHeadline("Repositories");
writingObjectsUsingRepositories("Writing %s objects using repository");
readingUsingRepository("Reading all objects using repository");
queryUsingRepository("Executing query using repository");
writeFooter();
writeFooter();
}
}
private void writeHeadline(String headline) {
System.out.println(headline);
System.out.println("---------------------------------".substring(0, headline.length()));
}
private void writeFooter() {
System.out.println();
}
private void queryUsingTemplate(String template) {
executeWatchedWithTimeAndResultSize(template, new WatchCallback<List<Person>>() {
public List<Person> doInWatch() {
Query query = query(where("addresses.zipCode").regex(".*1.*"));
return operations.find(query, Person.class, "template");
}
});
}
private void queryUsingRepository(String template) {
executeWatchedWithTimeAndResultSize(template, new WatchCallback<List<Person>>() {
public List<Person> doInWatch() {
return repository.findByAddressesZipCodeContaining("1");
}
});
}
private void executeWithWriteConcerns(WriteConcernCallback callback) {
Constants constants = new Constants(WriteConcern.class);
for (String constantName : constants.getNames(null)) {
if (IGNORED_WRITE_CONCERNS.contains(constantName)) {
continue;
}
WriteConcern writeConcern = (WriteConcern) constants.asObject(constantName);
mongo.setWriteConcern(writeConcern);
setupCollections();
callback.doWithWriteConcern(constantName, writeConcern);
}
}
private void setupCollections() {
DB db = this.mongo.getDB(DATABASE_NAME);
for (String collectionName : COLLECTION_NAMES) {
DBCollection collection = db.getCollection(collectionName);
collection.drop();
db.command(getCreateCollectionCommand(collectionName));
collection.ensureIndex(new BasicDBObject("firstname", -1));
collection.ensureIndex(new BasicDBObject("lastname", -1));
}
}
private DBObject getCreateCollectionCommand(String name) {
BasicDBObject dbObject = new BasicDBObject();
dbObject.put("createCollection", name);
dbObject.put("capped", false);
dbObject.put("size", COLLECTION_SIZE);
return dbObject;
}
private void writingObjectsUsingPlainDriver(String template) {
final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver");
final List<DBObject> persons = getPersonDBObjects();
executeWatchedWithTime(template, new WatchCallback<Void>() {
public Void doInWatch() {
for (DBObject person : persons) {
collection.save(person);
}
return null;
}
});
}
private void writingObjectsUsingRepositories(String template) {
final List<Person> persons = getPersonObjects();
executeWatchedWithTime(template, new WatchCallback<Void>() {
public Void doInWatch() {
repository.save(persons);
return null;
}
});
}
private void writingObjectsUsingMongoTemplate(String template) {
final List<Person> persons = getPersonObjects();
executeWatchedWithTime(template, new WatchCallback<Void>() {
public Void doInWatch() {
for (Person person : persons) {
operations.save(person, "template");
}
return null;
}
});
}
private void readingUsingPlainDriver(String template) {
final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver");
executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback<List<Person>>() {
public List<Person> doInWatch() {
return toPersons(collection.find());
}
});
}
private void readingUsingTemplate(String template) {
executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback<List<Person>>() {
public List<Person> doInWatch() {
return operations.findAll(Person.class, "template");
}
});
}
private void readingUsingRepository(String template) {
executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback<List<Person>>() {
public List<Person> doInWatch() {
return repository.findAll();
}
});
}
private void queryUsingPlainDriver(String template) {
final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver");
executeWatchedWithTimeAndResultSize(template, new WatchCallback<List<Person>>() {
public List<Person> doInWatch() {
DBObject regex = new BasicDBObject("$regex", Pattern.compile(".*1.*"));
DBObject query = new BasicDBObject("addresses.zipCode", regex);
return toPersons(collection.find(query));
}
});
}
private List<DBObject> getPersonDBObjects() {
List<DBObject> result = new ArrayList<DBObject>(NUMBER_OF_PERSONS);
for (Person person : getPersonObjects()) {
result.add(person.toDBObject());
}
return result;
}
private List<Person> getPersonObjects() {
List<Person> result = new ArrayList<Person>(NUMBER_OF_PERSONS);
watch.start("Created " + NUMBER_OF_PERSONS + " Persons");
for (int i = 0; i < NUMBER_OF_PERSONS; i++) {
Address address = new Address("zip" + i, "city" + i);
Person person = new Person("Firstname" + i, "Lastname" + i, Arrays.asList(address));
person.orders.add(new Order(LineItem.generate()));
person.orders.add(new Order(LineItem.generate()));
result.add(person);
}
watch.stop();
return result;
}
private <T> T executeWatched(String template, WatchCallback<T> callback) {
watch.start(String.format(template, NUMBER_OF_PERSONS));
try {
return callback.doInWatch();
} finally {
watch.stop();
}
}
private <T> void executeWatchedWithTime(String template, WatchCallback<?> callback) {
executeWatched(template, callback);
printStatistics(null);
}
private <T> void executeWatchedWithTimeAndResultSize(String template, WatchCallback<List<T>> callback) {
printStatistics(executeWatched(template, callback));
}
private void printStatistics(Collection<?> result) {
long time = watch.getLastTaskTimeMillis();
StringBuilder builder = new StringBuilder(watch.getLastTaskName());
if (result != null) {
builder.append(" returned ").append(result.size()).append(" results and");
}
builder.append(" took ").append(time).append(" milliseconds");
System.out.println(builder);
}
private static List<Person> toPersons(DBCursor cursor) {
List<Person> persons = new ArrayList<Person>();
while (cursor.hasNext()) {
persons.add(Person.from(cursor.next()));
}
return persons;
}
static class Person {
ObjectId id;
@Indexed
final String firstname, lastname;
final List<Address> addresses;
final Set<Order> orders;
public Person(String firstname, String lastname, List<Address> addresses) {
this.firstname = firstname;
this.lastname = lastname;
this.addresses = addresses;
this.orders = new HashSet<Order>();
}
public static Person from(DBObject source) {
BasicDBList addressesSource = (BasicDBList) source.get("addresses");
List<Address> addresses = new ArrayList<Address>(addressesSource.size());
for (Object addressSource : addressesSource) {
addresses.add(Address.from((DBObject) addressSource));
}
BasicDBList ordersSource = (BasicDBList) source.get("orders");
Set<Order> orders = new HashSet<Order>(ordersSource.size());
for (Object orderSource : ordersSource) {
orders.add(Order.from((DBObject) orderSource));
}
Person person = new Person((String) source.get("firstname"), (String) source.get("lastname"), addresses);
person.orders.addAll(orders);
return person;
}
public DBObject toDBObject() {
DBObject dbObject = new BasicDBObject();
dbObject.put("firstname", firstname);
dbObject.put("lastname", lastname);
dbObject.put("addresses", writeAll(addresses));
dbObject.put("orders", writeAll(orders));
return dbObject;
}
}
static class Address implements Convertible {
final String zipCode;
final String city;
final Set<AddressType> types;
public Address(String zipCode, String city) {
this(zipCode, city, new HashSet<AddressType>(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values()))));
}
@PersistenceConstructor
public Address(String zipCode, String city, Set<AddressType> types) {
this.zipCode = zipCode;
this.city = city;
this.types = types;
}
public static Address from(DBObject source) {
String zipCode = (String) source.get("zipCode");
String city = (String) source.get("city");
BasicDBList types = (BasicDBList) source.get("types");
return new Address(zipCode, city, new HashSet<AddressType>(readFromBasicDBList(types, AddressType.class)));
}
public DBObject toDBObject() {
BasicDBObject dbObject = new BasicDBObject();
dbObject.put("zipCode", zipCode);
dbObject.put("city", city);
dbObject.put("types", toBasicDBList(types));
return dbObject;
}
}
private static <T extends Enum<T>> List<T> readFromBasicDBList(BasicDBList source, Class<T> type) {
List<T> result = new ArrayList<T>(source.size());
for (Object object : source) {
result.add(Enum.valueOf(type, object.toString()));
}
return result;
}
private static <T extends Enum<T>> BasicDBList toBasicDBList(Collection<T> enums) {
BasicDBList result = new BasicDBList();
for (T element : enums) {
result.add(element.toString());
}
return result;
}
static class Order implements Convertible {
static enum Status {
ORDERED, PAYED, SHIPPED;
}
Date createdAt;
List<LineItem> lineItems;
Status status;
public Order(List<LineItem> lineItems, Date createdAt) {
this.lineItems = lineItems;
this.createdAt = createdAt;
this.status = Status.ORDERED;
}
@PersistenceConstructor
public Order(List<LineItem> lineItems, Date createdAt, Status status) {
this.lineItems = lineItems;
this.createdAt = createdAt;
this.status = status;
}
public static Order from(DBObject source) {
BasicDBList lineItemsSource = (BasicDBList) source.get("lineItems");
List<LineItem> lineItems = new ArrayList<PerformanceTests.LineItem>(lineItemsSource.size());
for (Object lineItemSource : lineItemsSource) {
lineItems.add(LineItem.from((DBObject) lineItemSource));
}
Date date = (Date) source.get("createdAt");
Status status = Status.valueOf((String) source.get("status"));
return new Order(lineItems, date, status);
}
public Order(List<LineItem> lineItems) {
this(lineItems, new Date());
}
public DBObject toDBObject() {
DBObject result = new BasicDBObject();
result.put("createdAt", createdAt);
result.put("lineItems", writeAll(lineItems));
result.put("status", status.toString());
return result;
}
}
static class LineItem implements Convertible {
String description;
double price;
int amount;
public LineItem(String description, int amount, double price) {
this.description = description;
this.amount = amount;
this.price = price;
}
public static List<LineItem> generate() {
LineItem iPad = new LineItem("iPad", 1, 649);
LineItem iPhone = new LineItem("iPhone", 1, 499);
LineItem macBook = new LineItem("MacBook", 2, 1299);
return pickRandomNumerOfItemsFrom(Arrays.asList(iPad, iPhone, macBook));
}
public static LineItem from(DBObject source) {
String description = (String) source.get("description");
double price = (Double) source.get("price");
int amount = (Integer) source.get("amount");
return new LineItem(description, amount, price);
}
public DBObject toDBObject() {
BasicDBObject dbObject = new BasicDBObject();
dbObject.put("description", description);
dbObject.put("price", price);
dbObject.put("amount", amount);
return dbObject;
}
}
private static <T> List<T> pickRandomNumerOfItemsFrom(List<T> source) {
Assert.isTrue(!source.isEmpty());
Random random = new Random();
int numberOfItems = random.nextInt(source.size());
numberOfItems = numberOfItems == 0 ? 1 : numberOfItems;
List<T> result = new ArrayList<T>(numberOfItems);
while (result.size() < numberOfItems) {
int index = random.nextInt(source.size());
T candidate = source.get(index);
if (!result.contains(candidate)) {
result.add(candidate);
}
}
return result;
}
static enum AddressType {
SHIPPING, BILLING;
}
private interface WriteConcernCallback {
void doWithWriteConcern(String constantName, WriteConcern concern);
}
private interface WatchCallback<T> {
T doInWatch();
}
private interface PersonRepository extends MongoRepository<Person, ObjectId> {
List<Person> findByAddressesZipCodeContaining(String parameter);
}
private interface Convertible {
DBObject toDBObject();
}
private static List<DBObject> writeAll(Collection<? extends Convertible> convertibles) {
List<DBObject> result = new ArrayList<DBObject>();
for (Convertible convertible : convertibles) {
result.add(convertible.toDBObject());
}
return result;
}
}