Author: mduerig
Date: Tue Feb 11 15:06:03 2014
New Revision: 1567170

URL: http://svn.apache.org/r1567170
Log:
OAK-1413: Add scalability tests for large operations
Initial tests for asserting scalability of large commits, copy of large trees, 
move of large trees, many siblings, many pending observation events. All 
currently @Ignored

Added:
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/LargeOperationIT.java
Modified:
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/NodeStoreFixture.java

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java?rev=1567170&r1=1567169&r2=1567170&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AbstractRepositoryTest.java
 Tue Feb 11 15:06:03 2014
@@ -57,11 +57,11 @@ public abstract class AbstractRepository
     @Parameterized.Parameters
     public static Collection<Object[]> fixtures() {
         Object[][] fixtures = new Object[][] {
-                {NodeStoreFixture.MK_IMPL},
-                {NodeStoreFixture.MONGO_MK},
-                {NodeStoreFixture.MONGO_NS},
+//                {NodeStoreFixture.MK_IMPL},
+//                {NodeStoreFixture.MONGO_MK},
+//                {NodeStoreFixture.MONGO_NS},
                 {NodeStoreFixture.SEGMENT_MK},
-                {NodeStoreFixture.MONGO_JDBC},
+//                {NodeStoreFixture.MONGO_JDBC},
         };
         return Arrays.asList(fixtures);
     }

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/LargeOperationIT.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/LargeOperationIT.java?rev=1567170&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/LargeOperationIT.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/LargeOperationIT.java
 Tue Feb 11 15:06:03 2014
@@ -0,0 +1,443 @@
+/*
+ * 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.oak.jcr;
+
+import static javax.jcr.observation.Event.NODE_ADDED;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.api.JackrabbitRepository;
+import org.apache.jackrabbit.oak.jcr.NodeStoreFixture.DocumentFixture;
+import org.apache.jackrabbit.oak.jcr.NodeStoreFixture.SegmentFixture;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentStore;
+import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Scalability test asserting certain operations scale linearly in the
+ * size of their input.
+ */
+@Ignore("WIP OAK-1413")
+@RunWith(Parameterized.class)
+public class LargeOperationIT {
+    private static final Logger LOG = 
LoggerFactory.getLogger(LargeOperationIT.class);
+
+    /** Maximum quotient of non linearity allowed */
+    private static final double MAX_QUOTIENT = 1.2;
+
+    /** Scales for running the tests against */
+    private static final int[] SEGMENT_SCALES = new int[] {
+            1024, 4096, 16384, 65536, 262144, 1048576};
+    private static final int[] MONGO_SCALES = new int[] {
+            128, 512, 2048, 8192, 32768, 131072};
+
+    private final NodeStoreFixture fixture;
+    private final int[] scales;
+
+    private NodeStore nodeStore;
+    private Repository repository;
+    private Session session;
+
+    public LargeOperationIT(NodeStoreFixture fixture, int[] scales) {
+        this.fixture = fixture;
+        this.scales = scales;
+    }
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> fixtures() throws IOException {
+        File file = new File(new File("target"), "tar." + System.nanoTime());
+        SegmentStore segmentStore = new FileStore(file, 266, true);
+
+        List<Object[]> fixtures = Lists.newArrayList();
+        SegmentFixture segmentFixture = new SegmentFixture(segmentStore);
+        if (segmentFixture.isAvailable()) {
+            fixtures.add(new Object[]{segmentFixture, SEGMENT_SCALES});
+        }
+        DocumentFixture documentFixture = new DocumentFixture();
+        if (documentFixture.isAvailable()) {
+            fixtures.add(new Object[]{documentFixture, MONGO_SCALES});
+        }
+        return fixtures;
+    }
+
+    private Session createAdminSession() throws RepositoryException {
+        return repository.login(new SimpleCredentials("admin", 
"admin".toCharArray()));
+    }
+
+    @Before
+    public void setup() throws RepositoryException {
+        nodeStore = fixture.createNodeStore();
+        repository  = new Jcr(nodeStore).createRepository();
+        session = createAdminSession();
+    }
+
+    @After
+    public void tearDown() {
+        session.logout();
+        if (repository instanceof JackrabbitRepository) {
+            ((JackrabbitRepository) repository).shutdown();
+        }
+        fixture.dispose(nodeStore);
+    }
+
+    private static List<Double> quotients(Iterable<Long> sequence) {
+        Double prev = null;
+        List<Double> quotients = Lists.newArrayList();
+        for (long current : sequence) {
+            if (prev != null) {
+                quotients.add(current / prev);
+            }
+            prev = (double) current;
+        }
+        return quotients;
+    }
+
+    private static void assertMaxQuotient(String message, Iterable<Double> 
quotients) {
+        for (double value : quotients) {
+            if (value >= MAX_QUOTIENT) {
+                fail(message + " The quotients between subsequent operations " 
+
+                        "should all be below " + MAX_QUOTIENT + ". Quotients: 
" + quotients);
+            }
+        }
+    }
+
+    /**
+     * Assert that large commits scale linearly wrt. to the number of changed 
items.
+     * @throws RepositoryException
+     * @throws InterruptedException
+     */
+    @Test
+    public void largeCommit() throws RepositoryException, InterruptedException 
{
+        final Node n = session.getRootNode().addNode("large-commit", 
"oak:Unstructured");
+        final ContentGenerator contentGenerator = new ContentGenerator();
+
+        ArrayList<Long> executionTimes = Lists.newArrayList();
+        for (int scale : scales) {
+            ScalabilityTest test = new ScalabilityTest(scale) {
+                @Override
+                void before(int scale) throws RepositoryException {
+                    contentGenerator.addNodes(n, scale);
+                }
+
+                @Override
+                void run(int scale) throws RepositoryException {
+                    session.save();
+                }
+            };
+            long t = test.run();
+            executionTimes.add(t);
+            LOG.info("Committing {} node took {} ns/node", scale, t);
+        }
+        List<Double> quotients = quotients(executionTimes);
+        LOG.info("Scaling quotients: {}", quotients);
+        assertMaxQuotient("Commit does not scale linearly.", quotients);
+    }
+
+    /**
+     * Assert copy scales linearly with the number of items copied
+     * @throws RepositoryException
+     * @throws InterruptedException
+     */
+    @Test
+    public void largeCopy() throws RepositoryException, InterruptedException {
+        final Node n = session.getRootNode().addNode("large-copy", 
"oak:Unstructured");
+        final ContentGenerator contentGenerator = new ContentGenerator(1000);
+
+        ArrayList<Long> executionTimes = Lists.newArrayList();
+        for (int scale : scales) {
+            ScalabilityTest test = new ScalabilityTest(scale) {
+                @Override
+                void before(int scale) throws RepositoryException {
+                    Node s = n.addNode("s" + scale);
+                    contentGenerator.addNodes(s, scale);
+                }
+
+                @Override
+                void run(int scale) throws RepositoryException {
+                    session.getWorkspace().copy("/large-copy/s" + scale, 
"/large-copy/t" + scale);
+                }
+            };
+            long t = test.run();
+            executionTimes.add(t);
+            LOG.info("Copying {} node took {} ns/node", scale, t);
+        }
+        List<Double> quotients = quotients(executionTimes);
+        LOG.info("Scaling quotients: {}", quotients);
+        assertMaxQuotient("Copy does not scale linearly.", quotients);
+    }
+
+    /**
+     * Assert move scales linearly with the number of items copied
+     * @throws RepositoryException
+     * @throws InterruptedException
+     */
+    @Test
+    public void largeMove() throws RepositoryException, InterruptedException {
+        final Node n = session.getRootNode().addNode("large-move", 
"oak:Unstructured");
+        final ContentGenerator contentGenerator = new ContentGenerator(1000);
+
+        ArrayList<Long> executionTimes = Lists.newArrayList();
+        for (int scale : scales) {
+            ScalabilityTest test = new ScalabilityTest(scale) {
+                @Override
+                void before(int scale) throws RepositoryException {
+                    Node s = n.addNode("s" + scale);
+                    contentGenerator.addNodes(s, scale);
+                }
+
+                @Override
+                void run(int scale) throws RepositoryException {
+                    session.getWorkspace().move("/large-move/s" + scale, 
"/large-move/t" + scale);
+                }
+            };
+            long t = test.run();
+            executionTimes.add(t);
+            LOG.info("Moving {} node took {} ns/node", scale, t);
+        }
+        List<Double> quotients = quotients(executionTimes);
+        LOG.info("Scaling quotients: {}", quotients);
+        assertMaxQuotient("Move does not scale linearly.", quotients);
+    }
+
+    /**
+     * Assert adding many siblings scales linearly with the number of added 
siblings.
+     * @throws RepositoryException
+     * @throws InterruptedException
+     */
+    @Test
+    public void manySiblings() throws RepositoryException, 
InterruptedException {
+        final Node n = session.getRootNode().addNode("many-siblings", 
"oak:Unstructured");
+
+        ArrayList<Long> executionTimes = Lists.newArrayList();
+        for (int scale : scales) {
+            ScalabilityTest test = new ScalabilityTest(scale) {
+                @Override
+                void before(int scale) throws RepositoryException {
+                    n.addNode("s" + scale);
+                }
+
+                @Override
+                void run(int scale) throws RepositoryException {
+                    Node s = n.getNode("s" + scale);
+                    for (int k = 0; k < scale; k++) {
+                        s.addNode("s" + k);
+                    }
+                    session.save();
+                }
+            };
+            long t = test.run();
+            executionTimes.add(t);
+            LOG.info("Adding {} siblings took {} ns/node", scale, t);
+        }
+        List<Double> quotients = quotients(executionTimes);
+        LOG.info("Scaling quotients: {}", quotients);
+        assertMaxQuotient("Adding siblings does not scale linearly.", 
quotients);
+    }
+
+    /**
+     * Assert processing of pending observation events scales linearly with the
+     * number of pending events.
+     * @throws RepositoryException
+     * @throws InterruptedException
+     */
+    @Test
+    public void largeNumberOfPendingEvents() throws RepositoryException, 
InterruptedException {
+        final Node n = session.getRootNode().addNode("pending-events", 
"oak:Unstructured");
+        final ContentGenerator contentGenerator = new ContentGenerator(1000);
+
+        ArrayList<Long> executionTimes = Lists.newArrayList();
+        for (int scale : scales) {
+            final Observer observer = new Observer(scale, 100);
+            try {
+                ScalabilityTest test = new ScalabilityTest(scale) {
+                    @Override
+                    void before(int scale) throws RepositoryException {
+                        contentGenerator.addNodes(n, scale);
+                    }
+
+                    @Override
+                    void run(int scale) throws InterruptedException {
+                        observer.waitForEvents(scale);
+                    }
+                };
+                long t = test.run();
+                executionTimes.add(t);
+                LOG.info("{} pending events took {} ns/event to process", 
scale, t);
+            } finally {
+                observer.dispose();
+            }
+        }
+        List<Double> quotients = quotients(executionTimes);
+        LOG.info("Scaling quotients: {}", quotients);
+        assertMaxQuotient("Processing pending events does not scale 
linearly.", quotients);
+    }
+
+    //------------------------------------------------------------< 
ContentGenerator >---
+
+    private static class ContentGenerator {
+        private static final int FAN_OUT = 10;
+        private final int saveInterval;
+
+        private int count;
+
+        public ContentGenerator(int saveInterval) {
+            this.saveInterval = saveInterval;
+        }
+
+        public ContentGenerator() {
+            this(Integer.MAX_VALUE);
+        }
+
+        public void addNodes(Node node, int count) throws RepositoryException {
+            LOG.info("Adding {} nodes to {}", count, node.getPath());
+            this.count = count;
+            while (createContent(node));
+            if (saveInterval < Integer.MAX_VALUE) {
+                node.getSession().save();
+            }
+        }
+
+        private boolean createContent(Node node) throws RepositoryException {
+            NodeIterator nodes = node.getNodes();
+            if (nodes.hasNext()) {
+                while (nodes.hasNext()) {
+                    if (!createContent(nodes.nextNode())) {
+                        return false;
+                    }
+                }
+                return true;
+            } else {
+                boolean result = true;
+                for (int c = 0; c < FAN_OUT && (result = addNode(node)); c++);
+                return result;
+            }
+        }
+
+        boolean addNode(Node node) throws RepositoryException {
+            node.addNode("n" + count--);
+            if (count % saveInterval == 0) {
+                node.getSession().save();
+            }
+            if (count % 1000 == 0) {
+                LOG.debug("add {}", node.getPath());
+            }
+            return !isDone();
+        }
+
+        boolean isDone() {
+            return count == 0;
+        }
+    }
+
+    //------------------------------------------------------------< 
ScalabilityTest >---
+
+    private abstract static class ScalabilityTest {
+        private final int scale;
+
+        protected ScalabilityTest(int scale) {
+            this.scale = scale;
+        }
+
+        void before(int scale) throws RepositoryException {}
+        abstract void run(int scale) throws RepositoryException, 
InterruptedException;
+        void after(int scale) {}
+
+        public long run() throws RepositoryException, InterruptedException {
+            before(scale);
+            long t0 = System.nanoTime();
+            run(scale);
+            long dt = System.nanoTime() - t0;
+            after(scale);
+            return dt / scale;
+        }
+    }
+
+    //------------------------------------------------------------< Observer 
>---
+
+    private class Observer implements EventListener {
+        private final CountDownLatch start = new CountDownLatch(1);
+        private final int eventCount;
+        private final int listenerCount;
+        private final Session[] sessions;
+
+        private CountDownLatch done;
+
+        public Observer(int eventCount, int listenerCount) throws 
RepositoryException {
+            this.eventCount = eventCount;
+            this.listenerCount = listenerCount;
+            this.sessions = new Session[listenerCount];
+            for (int k = 0; k < sessions.length; k++) {
+                sessions[k] = createAdminSession();
+                
sessions[k].getWorkspace().getObservationManager().addEventListener(
+                        this, NODE_ADDED, "/", true, null, null, false);
+            }
+        }
+
+        public void waitForEvents(int scale) throws InterruptedException {
+            done = new CountDownLatch(scale);
+            start.countDown();
+            done.await();
+        }
+
+        public void dispose() {
+            for (Session session : sessions) {
+                session.logout();
+            }
+        }
+
+        @Override
+        public void onEvent(EventIterator events) {
+            try {
+                start.await();
+                while (events.hasNext()) {
+                    events.nextEvent().getPath();
+                    done.countDown();
+                }
+            } catch (Exception e) {
+                LOG.error(e.getMessage(), e);
+            }
+        }
+    }
+
+}

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/NodeStoreFixture.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/NodeStoreFixture.java?rev=1567170&r1=1567169&r2=1567170&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/NodeStoreFixture.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/NodeStoreFixture.java
 Tue Feb 11 15:06:03 2014
@@ -22,32 +22,23 @@ import java.io.Closeable;
 import java.io.IOException;
 import java.util.UUID;
 
+import com.mongodb.DB;
 import org.apache.jackrabbit.mk.core.MicroKernelImpl;
-import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
 import org.apache.jackrabbit.oak.kernel.KernelNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
 import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
 import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
 import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentStore;
 import org.apache.jackrabbit.oak.plugins.segment.memory.MemoryStore;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 
-import com.mongodb.DB;
-
 /**
  * NodeStore fixture for parametrized tests.
  */
 public abstract class NodeStoreFixture {
 
-    public static final NodeStoreFixture SEGMENT_MK = new NodeStoreFixture() {
-        @Override
-        public NodeStore createNodeStore() {
-            return new SegmentNodeStore(new MemoryStore());
-        }
-
-        @Override
-        public void dispose(NodeStore nodeStore) {
-        }
-    };
+    public static final NodeStoreFixture SEGMENT_MK = new SegmentFixture();
 
     public static final NodeStoreFixture MONGO_MK = new NodeStoreFixture() {
         @Override
@@ -119,32 +110,7 @@ public abstract class NodeStoreFixture {
     };
     
     public static NodeStoreFixture createMongoFixture(final String uri) {
-        return new NodeStoreFixture() {
-            @Override
-            public NodeStore createNodeStore() {
-                return new DocumentMK.Builder().getNodeStore();
-            }
-            
-            @Override
-            public NodeStore createNodeStore(int clusterNodeId) {
-                MongoConnection connection;
-                try {
-                    connection = new MongoConnection(uri);
-                    DB mongoDB = connection.getDB();
-                    return new DocumentMK.Builder()
-                                    .setMongoDB(mongoDB).getNodeStore();
-                } catch (Exception e) {
-                    return null;
-                }
-            }
-
-            @Override
-            public void dispose(NodeStore nodeStore) {
-                if (nodeStore instanceof DocumentNodeStore) {
-                    ((DocumentNodeStore) nodeStore).dispose();
-                }
-            }
-        };
+        return new DocumentFixture(uri);
     }
 
     /**
@@ -168,6 +134,10 @@ public abstract class NodeStoreFixture {
 
     public abstract void dispose(NodeStore nodeStore);
 
+    public boolean isAvailable() {
+        return true;
+    }
+
     private static class CloseableNodeStore
             extends KernelNodeStore implements Closeable {
 
@@ -183,4 +153,90 @@ public abstract class NodeStoreFixture {
             kernel.dispose();
         }
     }
+
+    public static class SegmentFixture extends NodeStoreFixture {
+        private final SegmentStore store;
+
+        public SegmentFixture() {
+            this(null);
+        }
+
+        public SegmentFixture(SegmentStore store) {
+            this.store = store;
+        }
+
+        @Override
+        public NodeStore createNodeStore() {
+            return new SegmentNodeStore(store == null ? new MemoryStore() : 
store);
+        }
+
+        @Override
+        public void dispose(NodeStore nodeStore) {
+        }
+    }
+
+    public static class DocumentFixture extends NodeStoreFixture {
+        public static final String DEFAULT_URI = 
"mongodb://localhost:27017/oak";
+
+        private final String uri;
+        private final boolean inMemory;
+
+        public DocumentFixture(String uri, boolean inMemory) {
+            this.uri = uri;
+            this.inMemory = inMemory;
+        }
+
+        public DocumentFixture(String uri) {
+            this(uri, true);
+        }
+
+        public DocumentFixture() {
+            this(DEFAULT_URI, false);
+        }
+
+        private static NodeStore createNodeStore(String uri) {
+            MongoConnection connection;
+            try {
+                connection = new MongoConnection(uri);
+                DB mongoDB = connection.getDB();
+                return new DocumentMK.Builder()
+                        .setMongoDB(mongoDB).getNodeStore();
+            } catch (Exception e) {
+                return null;
+            }
+        }
+
+        @Override
+        public NodeStore createNodeStore() {
+            if (inMemory) {
+                return new DocumentMK.Builder().getNodeStore();
+            } else {
+                return createNodeStore(uri);
+            }
+        }
+
+        @Override
+        public NodeStore createNodeStore(int clusterNodeId) {
+            return createNodeStore(uri);
+        }
+
+        @Override
+        public boolean isAvailable() {
+            // FIXME is there a better way to check whether MongoDB is 
available?
+            NodeStore nodeStore = createNodeStore(uri);
+            if (nodeStore == null) {
+                return false;
+            } else {
+                dispose(nodeStore);
+                return true;
+            }
+        }
+
+        @Override
+        public void dispose(NodeStore nodeStore) {
+            if (nodeStore instanceof DocumentNodeStore) {
+                ((DocumentNodeStore) nodeStore).dispose();
+            }
+        }
+    }
 }


Reply via email to