/*
* Copyright 2016-2017 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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
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.Optional;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bson.Document;
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.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DbRefProxyHandler;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DbRefResolverCallback;
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.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory;
import org.springframework.util.Assert;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBRef;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
/**
* Test class to execute performance tests for plain Reactive Streams MongoDB driver usage,
* {@link ReactiveMongoOperations} and the repositories abstraction.
*
* @author Mark Paluch
*/
public class ReactivePerformanceTests {
private static final String DATABASE_NAME = "performance";
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<String> IGNORED_WRITE_CONCERNS = Arrays.asList("MAJORITY", "REPLICAS_SAFE",
"FSYNC_SAFE", "FSYNCED", "JOURNAL_SAFE", "JOURNALED", "REPLICA_ACKNOWLEDGED");
private static final int COLLECTION_SIZE = 1024 * 1024 * 256; // 256 MB
private static final Collection<String> COLLECTION_NAMES = Arrays.asList("template", "driver", "person");
MongoClient mongo;
ReactiveMongoTemplate operations;
ReactivePersonRepository repository;
MongoConverter converter;
@Before
public void setUp() throws Exception {
mongo = MongoClients.create();
SimpleReactiveMongoDatabaseFactory mongoDbFactory = new SimpleReactiveMongoDatabaseFactory(this.mongo,
DATABASE_NAME);
MongoMappingContext context = new MongoMappingContext();
context.setInitialEntitySet(Collections.singleton(Person.class));
context.afterPropertiesSet();
converter = new MappingMongoConverter(new DbRefResolver() {
@Override
public Optional<Object> resolveDbRef(MongoPersistentProperty property, DBRef dbref,
DbRefResolverCallback callback, DbRefProxyHandler proxyHandler) {
return Optional.empty();
}
@Override
public DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation,
MongoPersistentEntity<?> entity, Object id) {
return null;
}
@Override
public Document fetch(DBRef dbRef) {
return null;
}
@Override
public List<Document> bulkFetch(List<DBRef> dbRefs) {
return null;
}
}, context);
operations = new ReactiveMongoTemplate(mongoDbFactory, converter);
ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(operations);
repository = factory.getRepository(ReactivePersonRepository.class);
}
@Test // DATAMONGO-1444
public void writeWithWriteConcerns() {
executeWithWriteConcerns((constantName, concern) -> {
writeHeadline("WriteConcern: " + constantName);
System.out.println(String.format("Writing %s objects using plain driver took %sms", NUMBER_OF_PERSONS,
writingObjectsUsingPlainDriver(NUMBER_OF_PERSONS, concern)));
System.out.println(String.format("Writing %s objects using template took %sms", NUMBER_OF_PERSONS,
writingObjectsUsingMongoTemplate(NUMBER_OF_PERSONS, concern)));
System.out.println(String.format("Writing %s objects using repository took %sms", NUMBER_OF_PERSONS,
writingObjectsUsingRepositories(NUMBER_OF_PERSONS, concern)));
System.out.println(String.format("Writing %s objects async using plain driver took %sms", NUMBER_OF_PERSONS,
writingAsyncObjectsUsingPlainDriver(NUMBER_OF_PERSONS, concern)));
System.out.println(String.format("Writing %s objects async using template took %sms", NUMBER_OF_PERSONS,
writingAsyncObjectsUsingMongoTemplate(NUMBER_OF_PERSONS, concern)));
System.out.println(String.format("Writing %s objects async using repository took %sms", NUMBER_OF_PERSONS,
writingAsyncObjectsUsingRepositories(NUMBER_OF_PERSONS, concern)));
writeFooter();
});
}
@Test
public void plainConversion() throws InterruptedException {
Statistics statistics = new Statistics(
"Plain conversion of " + NUMBER_OF_PERSONS * 100 + " persons - After %s iterations");
List<Document> dbObjects = getPersonDocuments(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<Document> dbObjects) {
executeWatched(() -> {
List<Person> persons = new ArrayList<Person>();
for (Document dbObject : dbObjects) {
persons.add(Person.from(new Document(dbObject)));
}
return persons;
});
return watch.getLastTaskTimeMillis();
}
private long convertUsingConverter(final List<Document> dbObjects) {
executeWatched(() -> {
List<Person> persons = new ArrayList<Person>();
for (Document dbObject : dbObjects) {
persons.add(converter.read(Person.class, dbObject));
}
return persons;
});
return watch.getLastTaskTimeMillis();
}
@Test // DATAMONGO-1444
public void writeAndRead() throws Exception {
readsAndWrites(NUMBER_OF_PERSONS, ITERATIONS, WriteConcern.SAFE);
}
private void readsAndWrites(int numberOfPersons, int iterations, WriteConcern concern) {
Statistics statistics = new Statistics("Reading " + numberOfPersons + " - After %s iterations");
for (int i = 0; i < iterations; i++) {
setupCollections();
statistics.registerTime(Api.DRIVER, Mode.WRITE, writingObjectsUsingPlainDriver(numberOfPersons, concern));
statistics.registerTime(Api.TEMPLATE, Mode.WRITE, writingObjectsUsingMongoTemplate(numberOfPersons, concern));
statistics.registerTime(Api.REPOSITORY, Mode.WRITE, writingObjectsUsingRepositories(numberOfPersons, concern));
statistics.registerTime(Api.DRIVER, Mode.WRITE_ASYNC,
writingAsyncObjectsUsingPlainDriver(numberOfPersons, concern));
statistics.registerTime(Api.TEMPLATE, Mode.WRITE_ASYNC,
writingAsyncObjectsUsingMongoTemplate(numberOfPersons, concern));
statistics.registerTime(Api.REPOSITORY, Mode.WRITE_ASYNC,
writingAsyncObjectsUsingRepositories(numberOfPersons, concern));
statistics.registerTime(Api.DRIVER, Mode.READ, readingUsingPlainDriver());
statistics.registerTime(Api.TEMPLATE, Mode.READ, readingUsingTemplate());
statistics.registerTime(Api.REPOSITORY, Mode.READ, readingUsingRepository());
statistics.registerTime(Api.DRIVER, Mode.QUERY, queryUsingPlainDriver());
statistics.registerTime(Api.TEMPLATE, Mode.QUERY, queryUsingTemplate());
statistics.registerTime(Api.REPOSITORY, Mode.QUERY, queryUsingRepository());
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(createUnderline(headline));
}
private void writeFooter() {
System.out.println();
}
private long queryUsingTemplate() {
executeWatched(() -> {
Query query = query(where("addresses.zipCode").regex(".*1.*"));
return operations.find(query, Person.class, "template").collectList().block();
});
return watch.getLastTaskTimeMillis();
}
private long queryUsingRepository() {
executeWatched(() -> repository.findByAddressesZipCodeContaining("1").collectList().block());
return watch.getLastTaskTimeMillis();
}
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);
setupCollections();
callback.doWithWriteConcern(constantName, writeConcern);
}
}
private void setupCollections() {
MongoDatabase db = this.mongo.getDatabase(DATABASE_NAME);
for (String collectionName : COLLECTION_NAMES) {
MongoCollection<Document> collection = db.getCollection(collectionName);
Mono.from(collection.drop()).block();
Mono.from(db.createCollection(collectionName, getCreateCollectionOptions())).block();
collection.createIndex(new BasicDBObject("firstname", -1));
collection.createIndex(new BasicDBObject("lastname", -1));
}
}
private CreateCollectionOptions getCreateCollectionOptions() {
CreateCollectionOptions options = new CreateCollectionOptions();
return options.sizeInBytes(COLLECTION_SIZE).capped(false);
}
private long writingObjectsUsingPlainDriver(int numberOfPersons, WriteConcern concern) {
MongoCollection<Document> collection = mongo.getDatabase(DATABASE_NAME).getCollection("driver")
.withWriteConcern(concern);
List<Person> persons = getPersonObjects(numberOfPersons);
executeWatched(
() -> persons.stream().map(it -> Mono.from(collection.insertOne(new Document(it.toDocument()))).block()));
return watch.getLastTaskTimeMillis();
}
private long writingObjectsUsingRepositories(int numberOfPersons, WriteConcern concern) {
final List<Person> persons = getPersonObjects(numberOfPersons);
operations.setWriteConcern(concern);
executeWatched(() -> persons.stream().map(it -> repository.save(it).block()));
return watch.getLastTaskTimeMillis();
}
private long writingObjectsUsingMongoTemplate(int numberOfPersons, WriteConcern concern) {
final List<Person> persons = getPersonObjects(numberOfPersons);
executeWatched(() -> {
operations.setWriteConcern(concern);
return persons.stream().map(it -> operations.save(it, "template").block());
});
return watch.getLastTaskTimeMillis();
}
private long writingAsyncObjectsUsingPlainDriver(int numberOfPersons, WriteConcern concern) {
MongoCollection<Document> collection = mongo.getDatabase(DATABASE_NAME).getCollection("driver")
.withWriteConcern(concern);
List<Person> persons = getPersonObjects(numberOfPersons);
executeWatched(() -> Flux
.from(collection
.insertMany(persons.stream().map(person -> new Document(person.toDocument())).collect(Collectors.toList())))
.then().block());
return watch.getLastTaskTimeMillis();
}
private long writingAsyncObjectsUsingRepositories(int numberOfPersons, WriteConcern concern) {
List<Person> persons = getPersonObjects(numberOfPersons);
operations.setWriteConcern(concern);
executeWatched(() -> repository.saveAll(persons).then().block());
return watch.getLastTaskTimeMillis();
}
private long writingAsyncObjectsUsingMongoTemplate(int numberOfPersons, WriteConcern concern) {
List<Person> persons = getPersonObjects(numberOfPersons);
executeWatched(() -> {
operations.setWriteConcern(concern);
return Flux.from(operations.insertAll(persons)).then().block();
});
return watch.getLastTaskTimeMillis();
}
private long readingUsingPlainDriver() {
executeWatched(() -> Flux.from(mongo.getDatabase(DATABASE_NAME).getCollection("driver").find()).map(Person::from)
.collectList().block());
return watch.getLastTaskTimeMillis();
}
private long readingUsingTemplate() {
executeWatched(() -> operations.findAll(Person.class, "template").collectList().block());
return watch.getLastTaskTimeMillis();
}
private long readingUsingRepository() {
executeWatched(() -> repository.findAll().collectList().block());
return watch.getLastTaskTimeMillis();
}
private long queryUsingPlainDriver() {
executeWatched(() -> {
MongoCollection<Document> collection = mongo.getDatabase(DATABASE_NAME).getCollection("driver");
Document regex = new Document("$regex", Pattern.compile(".*1.*"));
Document query = new Document("addresses.zipCode", regex);
return Flux.from(collection.find(query)).map(Person::from).collectList().block();
});
return watch.getLastTaskTimeMillis();
}
private List<Person> getPersonObjects(int numberOfPersons) {
List<Person> result = new ArrayList<Person>();
for (int i = 0; i < numberOfPersons; i++) {
List<Address> addresses = new ArrayList<Address>();
for (int a = 0; a < 5; a++) {
addresses.add(new Address("zip" + a, "city" + a));
}
Person person = new Person("Firstname" + i, "Lastname" + i, addresses);
for (int o = 0; o < 10; o++) {
person.orders.add(new Order(LineItem.generate()));
}
result.add(person);
}
return result;
}
private List<Document> getPersonDocuments(int numberOfPersons) {
List<Document> dbObjects = new ArrayList<Document>(numberOfPersons);
for (Person person : getPersonObjects(numberOfPersons)) {
dbObjects.add(person.toDocument());
}
return dbObjects;
}
private <T> T executeWatched(WatchCallback<T> callback) {
watch.start();
try {
return callback.doInWatch();
} finally {
watch.stop();
}
}
static class Person {
ObjectId id;
String firstname, lastname;
List<Address> addresses;
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(Document source) {
List<Document> addressesSource = (List<Document>) source.get("addresses");
List<Address> addresses = new ArrayList<Address>(addressesSource.size());
for (Object addressSource : addressesSource) {
addresses.add(Address.from((Document) addressSource));
}
List<Document> ordersSource = (List<Document>) source.get("orders");
Set<Order> orders = new HashSet<Order>(ordersSource.size());
for (Object orderSource : ordersSource) {
orders.add(Order.from((Document) orderSource));
}
Person person = new Person((String) source.get("firstname"), (String) source.get("lastname"), addresses);
person.orders.addAll(orders);
return person;
}
public Document toDocument() {
Document dbObject = new Document();
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(Document source) {
String zipCode = (String) source.get("zipCode");
String city = (String) source.get("city");
List types = (List) source.get("types");
return new Address(zipCode, city, new HashSet<AddressType>(fromList(types, AddressType.class)));
}
public Document toDocument() {
Document dbObject = new Document();
dbObject.put("zipCode", zipCode);
dbObject.put("city", city);
dbObject.put("types", toList(types));
return dbObject;
}
}
private static <T extends Enum<T>> List<T> fromList(List 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>> List toList(Collection<T> enums) {
List<String> result = new ArrayList<>();
for (T element : enums) {
result.add(element.toString());
}
return result;
}
static class Order implements Convertible {
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(Document source) {
List lineItemsSource = (List) source.get("lineItems");
List<LineItem> lineItems = new ArrayList<ReactivePerformanceTests.LineItem>(lineItemsSource.size());
for (Object lineItemSource : lineItemsSource) {
lineItems.add(LineItem.from((Document) 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 Document toDocument() {
Document result = new Document();
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(Document 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 Document toDocument() {
Document dbObject = new Document();
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(), "Source must not be empty!");
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;
}
enum AddressType {
SHIPPING, BILLING
}
private interface WriteConcernCallback {
void doWithWriteConcern(String constantName, WriteConcern concern);
}
private interface WatchCallback<T> {
T doInWatch();
}
private interface ReactivePersonRepository extends ReactiveMongoRepository<Person, ObjectId> {
Flux<Person> findByAddressesZipCodeContaining(String parameter);
}
private interface Convertible {
Document toDocument();
}
private static BasicDBList writeAll(Collection<? extends Convertible> convertibles) {
BasicDBList result = new BasicDBList();
for (Convertible convertible : convertibles) {
result.add(convertible.toDocument());
}
return result;
}
enum Api {
DRIVER, TEMPLATE, REPOSITORY, DIRECT, CONVERTER
}
enum Mode {
WRITE, READ, QUERY, WRITE_ASYNC
}
private static class Statistics {
private final String headline;
private final Map<Mode, ModeTimes> times;
public Statistics(String headline) {
this.headline = headline;
this.times = new HashMap<Mode, ModeTimes>();
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<Double> times;
public ApiTimes(Api api, Mode mode) {
this.api = api;
this.mode = mode;
this.times = new ArrayList<Double>();
}
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<Double> list = new ArrayList<Double>(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<Api, ApiTimes> times;
public ModeTimes(Mode mode) {
this.times = new HashMap<Api, ApiTimes>();
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();
}
}
}