/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.jcr2spi.benchmark;
import static org.apache.jackrabbit.spi.commons.iterator.Iterators.filterIterator;
import static org.apache.jackrabbit.spi.commons.iterator.Iterators.iteratorChain;
import static org.apache.jackrabbit.spi.commons.iterator.Iterators.singleton;
import org.apache.jackrabbit.jcr2spi.AbstractJCR2SPITest;
import org.apache.jackrabbit.spi.ChildInfo;
import org.apache.jackrabbit.spi.ItemInfo;
import org.apache.jackrabbit.spi.ItemInfoCache;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.NodeInfo;
import org.apache.jackrabbit.spi.PropertyId;
import org.apache.jackrabbit.spi.PropertyInfo;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.RepositoryService;
import org.apache.jackrabbit.spi.SessionInfo;
import org.apache.jackrabbit.spi.commons.ItemInfoBuilder.NodeInfoBuilder;
import org.apache.jackrabbit.spi.commons.ItemInfoBuilder.PropertyInfoBuilder;
import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl;
import org.apache.jackrabbit.spi.commons.iterator.Predicate;
import javax.jcr.Item;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Callable;
/**
* Utility for testing jcr2spi read performance
*/
public class ReadPerformanceTest extends AbstractJCR2SPITest {
/**
* Depth of the content tree
*/
private static int TREE_DEPTH = 3;
/**
* Number of child nodes of each internal node
*/
private static int NODE_COUNT = 6;
/**
* Number of properties of each node
*/
private static int PROPERTY_COUNT = 60;
/**
* Number of JCR operations to perform per run
*/
private static int OP_COUNT = 500;
/**
* Size of the item info cache.
* @see ItemInfoCache
*/
private static int ITEM_INFO_CACHE_SIZE = 50000;
/**
* Ratios of the number of items in the whole content tree compared to the number of items
* in a batch of a {@link RepositoryService#getItemInfos(SessionInfo, NodeId)} call.
* The array contains one ratio per run
*/
private static int[] BATCH_RATIOS = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384};
/**
* Valid paths of nodes in the mock repository
*/
private final List<String> nodePaths = new ArrayList<String>();
/**
* Valid paths of properties in the mock repository
*/
private final List<String> propertyPaths = new ArrayList<String>();
private final Random rnd = new Random(12345);
/**
* This implementation overrides the default cache size with the value of
* {@value #ITEM_INFO_CACHE_SIZE}
*/
@Override
public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException {
return new ItemInfoCacheImpl(ITEM_INFO_CACHE_SIZE);
}
/**
* This implementation adds a tree of nodes and properties up to certain {@link #TREE_DEPTH}.
* Each node has {@link #NODE_COUNT} child nodes and {@link #PROPERTY_COUNT} properties.
* {@inheritDoc}
*/
@Override
protected void initInfosStore(NodeInfoBuilder builder) throws RepositoryException {
addNodes(builder, "");
}
private void addNodes(NodeInfoBuilder builder, String name) throws RepositoryException {
if (name.length() >= TREE_DEPTH) {
builder.build();
nodePaths.add(toJCRPath(builder.getNodeInfo().getPath()));
return;
}
for (int k = 0; k <= NODE_COUNT; k++) {
String n = name + k;
addNodes(addProperties(builder.createNodeInfo(n), PROPERTY_COUNT), n);
}
builder.build();
}
private NodeInfoBuilder addProperties(NodeInfoBuilder builder, int count) throws RepositoryException {
for (int k = 0; k < count; k++) {
PropertyInfoBuilder pBuilder = builder.createPropertyInfo("property_" + k, "Just some string value " + k);
pBuilder.build();
propertyPaths.add(toJCRPath(pBuilder.getPropertyInfo().getPath()));
}
return builder;
}
/**
* Create <code>count</number> JCR operations for a <code>session</code>
* @param session
* @param count
* @return
*/
protected Iterable<Callable<Long>> getOperations(final Session session, int count) {
ArrayList<Callable<Long>> callables = new ArrayList<Callable<Long>>();
final List<Item> items = new ArrayList<Item>();
for (int k = 0; k < count; k ++) {
switch (rnd.nextInt(4)) {
case 0: { // getItem
callables.add(new Callable<Long>() {
public Long call() throws Exception {
int i = rnd.nextInt(nodePaths.size() + propertyPaths.size());
String path = i < nodePaths.size()
? nodePaths.get(i)
: propertyPaths.get(i - nodePaths.size());
long t1 = System.currentTimeMillis();
Item item = session.getItem(path);
long t2 = System.currentTimeMillis();
items.add(item);
return t2 - t1;
}
@Override
public String toString() {
return "getItem";
}
});
break;
}
case 1: { // getNode
callables.add(new Callable<Long>() {
public Long call() throws Exception {
String path = nodePaths.get(rnd.nextInt(nodePaths.size()));
long t1 = System.currentTimeMillis();
Node node = session.getNode(path);
long t2 = System.currentTimeMillis();
items.add(node);
return t2 - t1;
}
@Override
public String toString() {
return "getNode";
}
});
break;
}
case 2: { // getProperty
callables.add(new Callable<Long>() {
public Long call() throws Exception {
String path = propertyPaths.get(rnd.nextInt(propertyPaths.size()));
long t1 = System.currentTimeMillis();
Property property = session.getProperty(path);
long t2 = System.currentTimeMillis();
items.add(property);
return t2 - t1;
}
@Override
public String toString() {
return "getProperty";
}
});
break;
}
case 3: { // refresh
callables.add(new Callable<Long>() {
public Long call() throws Exception {
if (items.isEmpty()) {
return 0L;
}
Item item = items.get(rnd.nextInt(items.size()));
long t1 = System.currentTimeMillis();
item.refresh(rnd.nextBoolean());
long t2 = System.currentTimeMillis();
return t2 - t1;
}
@Override
public String toString() {
return "refresh";
}
});
break;
}
default:
fail("Invalid case in switch");
}
}
return callables;
}
private int roundTripCount;
private int batchRatio;
/**
* Perform {@link #OP_COUNT} JCR operations on a fresh session once for each batch ratio value
* given in {@link #BATCH_RATIOS}.
* @throws Exception
*/
public void testReadOperations() throws Exception {
for (int ratio : BATCH_RATIOS) {
testReadOperations(OP_COUNT, ratio);
}
}
private void testReadOperations(int opCount, int batchRatio) throws Exception {
this.batchRatio = batchRatio;
this.roundTripCount = 0;
Map<String, Integer> opCounts = new HashMap<String, Integer>();
Map<String, Long> opTimes = new HashMap<String, Long>();
Session session = repository.login();
Iterable<Callable<Long>> operations = getOperations(session, opCount);
for (Callable<Long> operation : operations) {
String opName = operation.toString();
Long t = operation.call();
if (opCounts.containsKey(opName)) {
opCounts.put(opName, opCounts.get(opName) + 1);
opTimes.put(opName, opTimes.get(opName) + t);
}
else {
opCounts.put(opName, 1);
opTimes.put(opName, t);
}
}
System.out.println("Batch ratio: " + batchRatio);
System.out.println("Round trips: " + roundTripCount);
int count = 0;
long time = 0L;
for (String opName : opCounts.keySet()) {
int c = opCounts.get(opName);
count += c;
System.out.println(opName + " count: " + c);
long t = opTimes.get(opName);
time += t;
System.out.println(opName + " time: " + t);
}
System.out.println("Total count: " + count);
System.out.println("Total time: " + time);
session.logout();
}
private Iterator<ItemInfo> getBatch() {
return filterIterator(itemInfoStore.getItemInfos(), new Predicate<ItemInfo>() {
public boolean evaluate(ItemInfo value) {
return rnd.nextInt(batchRatio) == 0;
}
});
}
// -----------------------------------------------------< RepositoryService >---
@Override
protected QNodeDefinition createRootNodeDefinition() {
fail("not implemented: createRootNodeDefinition");
return null;
}
@Override
public Iterator<? extends ItemInfo> getItemInfos(SessionInfo sessionInfo, NodeId nodeId)
throws ItemNotFoundException, RepositoryException {
roundTripCount++;
NodeInfo nodeInfo = itemInfoStore.getNodeInfo(nodeId);
return iteratorChain(singleton(nodeInfo), getBatch());
}
@Override
public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException {
roundTripCount++;
return itemInfoStore.getNodeInfo(nodeId);
}
@Override
public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId)
throws ItemNotFoundException {
roundTripCount++;
return itemInfoStore.getPropertyInfo(propertyId);
}
@Override
public Iterator<ChildInfo> getChildInfos(SessionInfo sessionInfo, NodeId parentId)
throws ItemNotFoundException, RepositoryException {
roundTripCount++;
return itemInfoStore.getChildInfos(parentId);
}
}