From be0092d3f518c056bc6665e8aa7e75c131c32f92 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 25 Apr 2013 12:34:13 +0200 Subject: [PATCH] DATAMONGO-682 - Performance improvement for mapping hotspots. This commit includes the MongoDB specific parts for the mapping subsystem performance improvements. Reworked PerformanceTest to output more reasonable numbers. Heavily inspired by Patryk Wasik's contribution at https://github.com/SpringSource/spring-data-mongodb/pull/37. GitHub PR: #37 --- .../core/convert/CustomConversions.java | 58 +- .../core/convert/MappingMongoConverter.java | 5 +- .../mongodb/core/mapping/MappingTests.java | 6 +- .../mongodb/performance/PerformanceTests.java | 530 ++++++++++++++---- 4 files changed, 487 insertions(+), 112 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java index af11de4e4..6b2d76431 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java @@ -17,9 +17,11 @@ package org.springframework.data.mongodb.core.convert; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import org.slf4j.Logger; @@ -63,6 +65,7 @@ public class CustomConversions { private final Set writingPairs; private final Set> customSimpleTypes; private final SimpleTypeHolder simpleTypeHolder; + private final Map, HashMap, CacheValue>> cache; private final List converters; @@ -85,6 +88,7 @@ public class CustomConversions { this.readingPairs = new HashSet(); this.writingPairs = new HashSet(); this.customSimpleTypes = new HashSet>(); + this.cache = new HashMap, HashMap, CacheValue>>(); this.converters = new ArrayList(); this.converters.add(CustomToStringConverter.INSTANCE); @@ -268,9 +272,11 @@ public class CustomConversions { * @return */ public boolean hasCustomReadTarget(Class source, Class expectedTargetType) { + Assert.notNull(source); Assert.notNull(expectedTargetType); - return getCustomTarget(source, expectedTargetType, readingPairs) != null; + + return getCustomReadTarget(source, expectedTargetType) != null; } /** @@ -299,8 +305,32 @@ public class CustomConversions { return null; } + private Class getCustomReadTarget(Class source, Class expectedTargetType) { + + Class type = expectedTargetType == null ? PlaceholderType.class : expectedTargetType; + + Map, CacheValue> map; + CacheValue toReturn; + + if ((map = cache.get(source)) == null || (toReturn = map.get(type)) == null) { + + Class target = getCustomTarget(source, type, readingPairs); + + if (cache.get(source) == null) { + cache.put(source, new HashMap, CacheValue>()); + } + + Map, CacheValue> value = cache.get(source); + toReturn = target == null ? CacheValue.NULL : new CacheValue(target); + value.put(type, toReturn); + } + + return toReturn.clazz; + } + @WritingConverter private enum CustomToStringConverter implements GenericConverter { + INSTANCE; public Set getConvertibleTypes() { @@ -313,4 +343,30 @@ public class CustomConversions { return source.toString(); } } + + /** + * Placeholder type to allow registering not-found values in the converter cache. + * + * @author Patryk Wasik + * @author Oliver Gierke + */ + private static class PlaceholderType { + + } + + /** + * Wrapper to safely store {@literal null} values in the type cache. + * + * @author Patryk Wasik + * @author Oliver Gierke + */ + private static class CacheValue { + + public static final CacheValue NULL = new CacheValue(null); + private final Class clazz; + + public CacheValue(Class clazz) { + this.clazz = clazz; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 845546524..bbbaecb2d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -239,10 +239,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App entity.doWithProperties(new PropertyHandler() { public void doWithPersistentProperty(MongoPersistentProperty prop) { - boolean isConstructorProperty = entity.isConstructorArgument(prop); - boolean hasValueForProperty = dbo.containsField(prop.getFieldName()); - - if (!hasValueForProperty || isConstructorProperty) { + if (!dbo.containsField(prop.getFieldName()) || entity.isConstructorArgument(prop)) { return; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MappingTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MappingTests.java index c25754c41..c7f6f0cc0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MappingTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MappingTests.java @@ -38,13 +38,13 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.MongoCollectionUtils; import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoDbUtils; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.DB; import com.mongodb.DBCollection; @@ -79,7 +79,7 @@ public class MappingTests { ApplicationContext applicationContext; Mongo mongo; MongoTemplate template; - MongoMappingContext mappingContext; + MappingContext, MongoPersistentProperty> mappingContext; @Before public void setUp() throws Exception { @@ -90,7 +90,7 @@ public class MappingTests { } applicationContext = new ClassPathXmlApplicationContext("/mapping.xml"); template = applicationContext.getBean(MongoTemplate.class); - mappingContext = (MongoMappingContext) ReflectionTestUtils.getField(template, "mappingContext"); + mappingContext = template.getConverter().getMappingContext(); } @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java index 28804dd7e..96d540c5b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java @@ -18,12 +18,16 @@ 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.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; import java.util.regex.Pattern; @@ -35,12 +39,15 @@ 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.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; 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 org.springframework.util.StringUtils; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; @@ -60,29 +67,40 @@ import com.mongodb.WriteConcern; public class PerformanceTests { private static final String DATABASE_NAME = "performance"; - private static final int NUMBER_OF_PERSONS = 30000; + private static final int NUMBER_OF_PERSONS = 300; + private static final int ITERATIONS = 50; private static final StopWatch watch = new StopWatch(); private static final Collection IGNORED_WRITE_CONCERNS = Arrays.asList("MAJORITY", "REPLICAS_SAFE", - "FSYNC_SAFE", "JOURNAL_SAFE"); + "FSYNC_SAFE", "FSYNCED", "JOURNAL_SAFE", "JOURNALED", "REPLICA_ACKNOWLEDGED"); private static final int COLLECTION_SIZE = 1024 * 1024 * 256; // 256 MB private static final Collection COLLECTION_NAMES = Arrays.asList("template", "driver", "person"); Mongo mongo; MongoTemplate operations; PersonRepository repository; + MongoConverter converter; @Before public void setUp() throws Exception { this.mongo = new Mongo(); - this.operations = new MongoTemplate(new SimpleMongoDbFactory(this.mongo, DATABASE_NAME)); + + SimpleMongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(this.mongo, DATABASE_NAME); + + MongoMappingContext context = new MongoMappingContext(); + context.setInitialEntitySet(Collections.singleton(Person.class)); + context.afterPropertiesSet(); + + this.converter = new MappingMongoConverter(mongoDbFactory, context); + this.operations = new MongoTemplate(new SimpleMongoDbFactory(this.mongo, DATABASE_NAME), converter); MongoRepositoryFactoryBean factory = new MongoRepositoryFactoryBean(); factory.setMongoOperations(operations); factory.setRepositoryInterface(PersonRepository.class); factory.afterPropertiesSet(); - repository = factory.getObject(); + this.repository = factory.getObject(); + } @Test @@ -90,69 +108,137 @@ public class PerformanceTests { 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"); + System.out.println(String.format("Writing %s objects using plain driver took %sms", NUMBER_OF_PERSONS, + writingObjectsUsingPlainDriver(NUMBER_OF_PERSONS))); + System.out.println(String.format("Writing %s objects using template took %sms", NUMBER_OF_PERSONS, + writingObjectsUsingMongoTemplate(NUMBER_OF_PERSONS))); + System.out.println(String.format("Writing %s objects using repository took %sms", NUMBER_OF_PERSONS, + writingObjectsUsingRepositories(NUMBER_OF_PERSONS))); writeFooter(); } }); } @Test - public void writeAndRead() { + public void plainConversion() throws InterruptedException { + + Statistics statistics = new Statistics("Plain conversion of " + NUMBER_OF_PERSONS * 100 + + " persons - After %s iterations"); + + List dbObjects = getPersonDBObjects(NUMBER_OF_PERSONS * 100); + + for (int i = 0; i < ITERATIONS; i++) { + statistics.registerTime(Api.DIRECT, Mode.READ, convertDirectly(dbObjects)); + statistics.registerTime(Api.CONVERTER, Mode.READ, convertUsingConverter(dbObjects)); + } + + statistics.printResults(ITERATIONS); + } + + private long convertDirectly(final List dbObjects) { + + executeWatched(new WatchCallback>() { + + @Override + public List doInWatch() { + + List persons = new ArrayList(); + + for (DBObject dbObject : dbObjects) { + persons.add(Person.from(dbObject)); + } + + return persons; + } + }); + + return watch.getLastTaskTimeMillis(); + } + + private long convertUsingConverter(final List dbObjects) { + + executeWatched(new WatchCallback>() { + + @Override + public List doInWatch() { + + List persons = new ArrayList(); + + for (DBObject dbObject : dbObjects) { + persons.add(converter.read(Person.class, dbObject)); + } + + return persons; + } + }); + + return watch.getLastTaskTimeMillis(); + } + + @Test + public void writeAndRead() throws Exception { mongo.setWriteConcern(WriteConcern.SAFE); - for (int i = 3; i > 0; i--) { + readsAndWrites(NUMBER_OF_PERSONS, ITERATIONS); + } + + private void readsAndWrites(int numberOfPersons, int iterations) { + + Statistics statistics = new Statistics("Reading " + numberOfPersons + " - After %s iterations"); + + for (int i = 0; i < iterations; 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(); + statistics.registerTime(Api.DRIVER, Mode.WRITE, writingObjectsUsingPlainDriver(numberOfPersons)); + statistics.registerTime(Api.TEMPLATE, Mode.WRITE, writingObjectsUsingMongoTemplate(numberOfPersons)); + statistics.registerTime(Api.REPOSITORY, Mode.WRITE, writingObjectsUsingRepositories(numberOfPersons)); - writeHeadline("Template"); - writingObjectsUsingMongoTemplate("Writing %s objects using template"); - readingUsingTemplate("Reading all objects using template"); - queryUsingTemplate("Executing query using template"); - writeFooter(); + statistics.registerTime(Api.DRIVER, Mode.READ, readingUsingPlainDriver()); + statistics.registerTime(Api.TEMPLATE, Mode.READ, readingUsingTemplate()); + statistics.registerTime(Api.REPOSITORY, Mode.READ, readingUsingRepository()); - writeHeadline("Repositories"); - writingObjectsUsingRepositories("Writing %s objects using repository"); - readingUsingRepository("Reading all objects using repository"); - queryUsingRepository("Executing query using repository"); - writeFooter(); + statistics.registerTime(Api.DRIVER, Mode.QUERY, queryUsingPlainDriver()); + statistics.registerTime(Api.TEMPLATE, Mode.QUERY, queryUsingTemplate()); + statistics.registerTime(Api.REPOSITORY, Mode.QUERY, queryUsingRepository()); - writeFooter(); + if (i > 0 && i % (iterations / 10) == 0) { + statistics.printResults(i); + } } + + statistics.printResults(iterations); } private void writeHeadline(String headline) { System.out.println(headline); - System.out.println("---------------------------------".substring(0, headline.length())); + System.out.println(createUnderline(headline)); } private void writeFooter() { System.out.println(); } - private void queryUsingTemplate(String template) { - executeWatchedWithTimeAndResultSize(template, new WatchCallback>() { + private long queryUsingTemplate() { + executeWatched(new WatchCallback>() { public List doInWatch() { Query query = query(where("addresses.zipCode").regex(".*1.*")); return operations.find(query, Person.class, "template"); } }); + + return watch.getLastTaskTimeMillis(); } - private void queryUsingRepository(String template) { - executeWatchedWithTimeAndResultSize(template, new WatchCallback>() { + private long queryUsingRepository() { + executeWatched(new WatchCallback>() { public List doInWatch() { return repository.findByAddressesZipCodeContaining("1"); } }); + + return watch.getLastTaskTimeMillis(); } private void executeWithWriteConcerns(WriteConcernCallback callback) { @@ -181,7 +267,7 @@ public class PerformanceTests { for (String collectionName : COLLECTION_NAMES) { DBCollection collection = db.getCollection(collectionName); collection.drop(); - db.command(getCreateCollectionCommand(collectionName)); + collection.getDB().command(getCreateCollectionCommand(collectionName)); collection.ensureIndex(new BasicDBObject("firstname", -1)); collection.ensureIndex(new BasicDBObject("lastname", -1)); } @@ -195,38 +281,42 @@ public class PerformanceTests { return dbObject; } - private void writingObjectsUsingPlainDriver(String template) { + private long writingObjectsUsingPlainDriver(int numberOfPersons) { final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); - final List persons = getPersonDBObjects(); + final List persons = getPersonObjects(numberOfPersons); - executeWatchedWithTime(template, new WatchCallback() { + executeWatched(new WatchCallback() { public Void doInWatch() { - for (DBObject person : persons) { - collection.save(person); + for (Person person : persons) { + collection.save(person.toDBObject()); } return null; } }); + + return watch.getLastTaskTimeMillis(); } - private void writingObjectsUsingRepositories(String template) { + private long writingObjectsUsingRepositories(int numberOfPersons) { - final List persons = getPersonObjects(); + final List persons = getPersonObjects(numberOfPersons); - executeWatchedWithTime(template, new WatchCallback() { + executeWatched(new WatchCallback() { public Void doInWatch() { repository.save(persons); return null; } }); + + return watch.getLastTaskTimeMillis(); } - private void writingObjectsUsingMongoTemplate(String template) { + private long writingObjectsUsingMongoTemplate(int numberOfPersons) { - final List persons = getPersonObjects(); + final List persons = getPersonObjects(numberOfPersons); - executeWatchedWithTime(template, new WatchCallback() { + executeWatched(new WatchCallback() { public Void doInWatch() { for (Person person : persons) { operations.save(person, "template"); @@ -234,82 +324,95 @@ public class PerformanceTests { return null; } }); + + return watch.getLastTaskTimeMillis(); } - private void readingUsingPlainDriver(String template) { + private long readingUsingPlainDriver() { - final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); - - executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback>() { + executeWatched(new WatchCallback>() { public List doInWatch() { - return toPersons(collection.find()); + return toPersons(mongo.getDB(DATABASE_NAME).getCollection("driver").find()); } }); + + return watch.getLastTaskTimeMillis(); } - private void readingUsingTemplate(String template) { - executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback>() { + private long readingUsingTemplate() { + executeWatched(new WatchCallback>() { public List doInWatch() { return operations.findAll(Person.class, "template"); } }); + + return watch.getLastTaskTimeMillis(); } - private void readingUsingRepository(String template) { - executeWatchedWithTimeAndResultSize(String.format(template, NUMBER_OF_PERSONS), new WatchCallback>() { + private long readingUsingRepository() { + executeWatched(new WatchCallback>() { public List doInWatch() { return repository.findAll(); } }); + + return watch.getLastTaskTimeMillis(); } - private void queryUsingPlainDriver(String template) { + private long queryUsingPlainDriver() { - final DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); - - executeWatchedWithTimeAndResultSize(template, new WatchCallback>() { + executeWatched(new WatchCallback>() { public List doInWatch() { + + DBCollection collection = mongo.getDB(DATABASE_NAME).getCollection("driver"); + DBObject regex = new BasicDBObject("$regex", Pattern.compile(".*1.*")); DBObject query = new BasicDBObject("addresses.zipCode", regex); return toPersons(collection.find(query)); } }); + + return watch.getLastTaskTimeMillis(); } - private List getPersonDBObjects() { + private List getPersonObjects(int numberOfPersons) { - List result = new ArrayList(NUMBER_OF_PERSONS); + List result = new ArrayList(); - for (Person person : getPersonObjects()) { - result.add(person.toDBObject()); - } + for (int i = 0; i < numberOfPersons; i++) { - return result; - } + List
addresses = new ArrayList
(); - private List getPersonObjects() { + for (int a = 0; a < 5; a++) { + addresses.add(new Address("zip" + a, "city" + a)); + } - List result = new ArrayList(NUMBER_OF_PERSONS); + Person person = new Person("Firstname" + i, "Lastname" + i, addresses); - watch.start("Created " + NUMBER_OF_PERSONS + " Persons"); + for (int o = 0; o < 10; o++) { + person.orders.add(new Order(LineItem.generate())); + } - 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 executeWatched(String template, WatchCallback callback) { + private List getPersonDBObjects(int numberOfPersons) { - watch.start(String.format(template, NUMBER_OF_PERSONS)); + List dbObjects = new ArrayList(numberOfPersons); + + for (Person person : getPersonObjects(numberOfPersons)) { + dbObjects.add(person.toDBObject()); + } + + return dbObjects; + } + + private T executeWatched(WatchCallback callback) { + + watch.start(); try { return callback.doInWatch(); @@ -318,28 +421,6 @@ public class PerformanceTests { } } - private void executeWatchedWithTime(String template, WatchCallback callback) { - executeWatched(template, callback); - printStatistics(null); - } - - private void executeWatchedWithTimeAndResultSize(String template, WatchCallback> 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 toPersons(DBCursor cursor) { List persons = new ArrayList(); @@ -354,10 +435,9 @@ public class PerformanceTests { static class Person { ObjectId id; - @Indexed - final String firstname, lastname; - final List
addresses; - final Set orders; + String firstname, lastname; + List
addresses; + Set orders; public Person(String firstname, String lastname, List
addresses) { this.firstname = firstname; @@ -579,11 +659,253 @@ public class PerformanceTests { DBObject toDBObject(); } - private static List writeAll(Collection convertibles) { - List result = new ArrayList(); + private static BasicDBList writeAll(Collection convertibles) { + BasicDBList result = new BasicDBList(); for (Convertible convertible : convertibles) { result.add(convertible.toDBObject()); } return result; } + + static enum Api { + DRIVER, TEMPLATE, REPOSITORY, DIRECT, CONVERTER; + } + + static enum Mode { + WRITE, READ, QUERY; + } + + private static class Statistics { + + private final String headline; + private final Map times; + + public Statistics(String headline) { + + this.headline = headline; + this.times = new HashMap(); + + for (Mode mode : Mode.values()) { + times.put(mode, new ModeTimes(mode)); + } + } + + public void registerTime(Api api, Mode mode, double time) { + times.get(mode).add(api, time); + } + + public void printResults(int iterations) { + + String title = String.format(headline, iterations); + + System.out.println(title); + System.out.println(createUnderline(title)); + + StringBuilder builder = new StringBuilder(); + for (Mode mode : Mode.values()) { + String print = times.get(mode).print(); + if (!print.isEmpty()) { + builder.append(print).append('\n'); + } + } + + System.out.println(builder.toString()); + } + + @Override + public String toString() { + + StringBuilder builder = new StringBuilder(times.size()); + + for (ModeTimes times : this.times.values()) { + builder.append(times.toString()); + } + + return builder.toString(); + } + } + + private static String createUnderline(String input) { + + StringBuilder builder = new StringBuilder(input.length()); + + for (int i = 0; i < input.length(); i++) { + builder.append("-"); + } + + return builder.toString(); + } + + static class ApiTimes { + + private static final String TIME_TEMPLATE = "%s %s time -\tAverage: %sms%s,%sMedian: %sms%s"; + + private static final DecimalFormat TIME_FORMAT; + private static final DecimalFormat DEVIATION_FORMAT; + + static { + + TIME_FORMAT = new DecimalFormat("0.00"); + + DEVIATION_FORMAT = new DecimalFormat("0.00"); + DEVIATION_FORMAT.setPositivePrefix("+"); + } + + private final Api api; + private final Mode mode; + private final List times; + + public ApiTimes(Api api, Mode mode) { + this.api = api; + this.mode = mode; + this.times = new ArrayList(); + } + + public void add(double time) { + this.times.add(time); + } + + public boolean hasTimes() { + return !times.isEmpty(); + } + + public double getAverage() { + + double result = 0; + + for (Double time : times) { + result += time; + } + + return result == 0.0 ? 0.0 : result / times.size(); + } + + public double getMedian() { + + if (times.isEmpty()) { + return 0.0; + } + + ArrayList list = new ArrayList(times); + Collections.sort(list); + + int size = list.size(); + + if (size % 2 == 0) { + return (list.get(size / 2 - 1) + list.get(size / 2)) / 2; + } else { + return list.get(size / 2); + } + } + + private double getDeviationFrom(double otherAverage) { + + double average = getAverage(); + return average * 100 / otherAverage - 100; + } + + private double getMediaDeviationFrom(double otherMedian) { + double median = getMedian(); + return median * 100 / otherMedian - 100; + } + + public String print() { + + if (times.isEmpty()) { + return ""; + } + + return basicPrint("", "\t\t", "") + '\n'; + } + + private String basicPrint(String extension, String middle, String foo) { + return String.format(TIME_TEMPLATE, api, mode, TIME_FORMAT.format(getAverage()), extension, middle, + TIME_FORMAT.format(getMedian()), foo); + } + + public String print(double referenceAverage, double referenceMedian) { + + if (times.isEmpty()) { + return ""; + } + + return basicPrint(String.format(" %s%%", DEVIATION_FORMAT.format(getDeviationFrom(referenceAverage))), "\t", + String.format(" %s%%", DEVIATION_FORMAT.format(getMediaDeviationFrom(referenceMedian)))) + '\n'; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return times.isEmpty() ? "" : String.format("%s, %s: %s", api, mode, + StringUtils.collectionToCommaDelimitedString(times)) + '\n'; + } + } + + static class ModeTimes { + + private final Map times; + + public ModeTimes(Mode mode) { + + this.times = new HashMap(); + + for (Api api : Api.values()) { + this.times.put(api, new ApiTimes(api, mode)); + } + } + + public void add(Api api, double time) { + times.get(api).add(time); + } + + @SuppressWarnings("null") + public String print() { + + if (times.isEmpty()) { + return ""; + } + + Double previousTime = null; + Double previousMedian = null; + StringBuilder builder = new StringBuilder(); + + for (Api api : Api.values()) { + + ApiTimes apiTimes = times.get(api); + + if (!apiTimes.hasTimes()) { + continue; + } + + if (previousTime == null) { + builder.append(apiTimes.print()); + previousTime = apiTimes.getAverage(); + previousMedian = apiTimes.getMedian(); + } else { + builder.append(apiTimes.print(previousTime, previousMedian)); + } + } + + return builder.toString(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + StringBuilder builder = new StringBuilder(times.size()); + + for (ApiTimes times : this.times.values()) { + builder.append(times.toString()); + } + + return builder.toString(); + } + } }