Author: mreutegg Date: Tue Jul 21 10:25:04 2015 New Revision: 1692079 URL: http://svn.apache.org/r1692079 Log: OAK-3112: Performance degradation of UnsavedModifications on MapDB
Added: jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/HybridMapFactory.java (with props) Modified: jackrabbit/oak/branches/1.0/RELEASE-NOTES.txt jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactory.java jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapFactory.java jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UnsavedModificationsTest.java jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactoryTest.java Modified: jackrabbit/oak/branches/1.0/RELEASE-NOTES.txt URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/RELEASE-NOTES.txt?rev=1692079&r1=1692078&r2=1692079&view=diff ============================================================================== --- jackrabbit/oak/branches/1.0/RELEASE-NOTES.txt (original) +++ jackrabbit/oak/branches/1.0/RELEASE-NOTES.txt Tue Jul 21 10:25:04 2015 @@ -21,6 +21,10 @@ New configuration options in Oak 1.0.18 Support for pre extracting text to speed up reindexing has been added in OAK-2892 For more details refer to http://jackrabbit.apache.org/oak/docs/query/lucene.html#text-extraction +For DocumentNodeStore based deployments a new MapFactory implementation has +been introduced and can be enabled with -Doak.useHybridMapFactory=true +See OAK-3112 for details. + Changes in Oak 1.0.18 --------------------- Added: jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/HybridMapFactory.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/HybridMapFactory.java?rev=1692079&view=auto ============================================================================== --- jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/HybridMapFactory.java (added) +++ jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/HybridMapFactory.java Tue Jul 21 10:25:04 2015 @@ -0,0 +1,345 @@ +/* + * 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.plugins.document.util; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.ForwardingConcurrentMap; +import com.google.common.collect.Maps; + +import org.apache.jackrabbit.oak.plugins.document.Revision; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A hybrid MapFactory implementation switching from an in-memory map to a + * MapDB backed map when the size of the map reaches a threshold. Once the + * map shrinks again, the implementation switches back to an in-memory map. + * + * This factory implementation keeps track of maps created by {@link #create()} + * and {@link #create(Comparator)} with weak references and disposes the + * underlying {@link MapDBMapFactory} when a map created by that factory is not + * referenceable and not in use anymore. + */ +public class HybridMapFactory extends MapFactory { + + private static final Logger LOG = LoggerFactory.getLogger(HybridMapFactory.class); + + /** + * Keep at most this number of entries in memory, otherwise use a map + * implementation backed by MapDB. + */ + static final int IN_MEMORY_SIZE_LIMIT = 100000; + + /** + * Switch to an in-memory map when there are less than 10'000 entries + * in the map. + */ + static final int IN_MEMORY_SIZE_LIMIT_LOW = 10000; + + /** + * Reference queue for maps backed by MapDB. + */ + private final ReferenceQueue<Object> queue = new ReferenceQueue<Object>(); + + /** + * Maps {@link MapReference}s to their corresponding MapFactory. + */ + private final Map<Reference, MapFactory> factories = Maps.newIdentityHashMap(); + + @Override + public ConcurrentMap<String, Revision> create() { + return create(null); + } + + @Override + public ConcurrentMap<String, Revision> create(Comparator<String> comparator) { + return new MapImpl(comparator); + } + + @Override + public void dispose() { + for (MapFactory f : factories.values()) { + dispose(f); + } + factories.clear(); + } + + //------------------------------< internal >-------------------------------- + + private synchronized void pollReferenceQueue() { + Reference ref; + while ((ref = queue.poll()) != null) { + dispose(factories.remove(ref)); + } + } + + private void dispose(MapFactory factory) { + try { + if (factory != null) { + Stopwatch sw = Stopwatch.createStarted(); + factory.dispose(); + sw.stop(); + LOG.debug("Disposed MapDB map in {}", sw); + } + } catch (Exception e) { + LOG.warn("Failed to dispose MapFactory", e); + } + } + + /** + * A map implementation, which forwards calls to either an in-memory or + * MapDB backed map implementation. Methods, which modify the underlying map + * are synchronized because of OAK-2888. + */ + private final class MapImpl extends + ForwardingConcurrentMap<String, Revision> { + + /** + * A comparator, if keys of the map are sorted, otherwise {@code null}. + */ + private final Comparator<String> comparator; + + /** + * The current map. This is either in-memory or a map backed by MapDB. + */ + private volatile ConcurrentMap<String, Revision> map; + + /** + * Maintain size of {@link #map} because the in-memory MapFactory + * variant uses a ConcurrentSkipListMap with a non-constant size() cost. + */ + private long size; + + /** + * Whether the current {@link #map} is in memory. + */ + private boolean mapInMemory; + + MapImpl(@Nullable Comparator<String> comparator) { + this.comparator = comparator; + this.map = createMap(true); + this.mapInMemory = true; + } + + @Override + protected ConcurrentMap<String, Revision> delegate() { + return map; + } + + @Override + public synchronized Revision putIfAbsent(@Nonnull String key, + @Nonnull Revision value) { + Revision r = map.putIfAbsent(key, checkNotNull(value)); + if (r == null) { + size++; + maybeSwitchToMapDB(); + } + return r; + } + + @Override + public synchronized Revision put(@Nonnull String key, @Nonnull Revision value) { + Revision r = map.put(key, checkNotNull(value)); + if (r == null) { + size++; + maybeSwitchToMapDB(); + } + return r; + } + + @Override + public synchronized void putAll(@Nonnull Map<? extends String, ? extends Revision> map) { + for (Map.Entry<? extends String, ? extends Revision> entry : map.entrySet()) { + if (this.map.put(entry.getKey(), checkNotNull(entry.getValue())) == null) { + size++; + maybeSwitchToMapDB(); + } + } + } + + @Override + public synchronized boolean remove(@Nonnull Object key, + @Nonnull Object value) { + boolean remove = map.remove(key, value); + if (remove) { + size--; + maybeSwitchToInMemoryMap(); + } + return remove; + } + + @Override + public synchronized Revision remove(@Nonnull Object object) { + Revision r = map.remove(object); + if (r != null) { + size--; + maybeSwitchToInMemoryMap(); + } + return r; + } + + @Override + public synchronized Revision replace(@Nonnull String key, + @Nonnull Revision value) { + return map.replace(key, checkNotNull(value)); + } + + @Override + public synchronized boolean replace(@Nonnull String key, + @Nonnull Revision oldValue, + @Nonnull Revision newValue) { + return map.replace(key, checkNotNull(oldValue), checkNotNull(newValue)); + } + + @Override + public synchronized void clear() { + map.clear(); + size = 0; + } + + @Nonnull + @Override + public Collection<Revision> values() { + return Collections.unmodifiableCollection(map.values()); + } + + @Nonnull + @Override + public Set<Entry<String, Revision>> entrySet() { + return Collections.unmodifiableSet(map.entrySet()); + } + + @Nonnull + @Override + public Set<String> keySet() { + return Collections.unmodifiableSet(map.keySet()); + } + + private void maybeSwitchToMapDB() { + // switch map if + // - map is currently in-memory + // - size limit is reached + if (mapInMemory + && size >= IN_MEMORY_SIZE_LIMIT) { + Stopwatch sw = Stopwatch.createStarted(); + ConcurrentMap<String, Revision> tmp = createMap(false); + tmp.putAll(map); + map = tmp; + mapInMemory = false; + sw.stop(); + LOG.debug("Switched to MapDB map in {}", sw); + pollReferenceQueue(); + } + } + + private void maybeSwitchToInMemoryMap() { + // only switch to in memory if not already in memory + // and size is less than lower limit + if (!mapInMemory && size < IN_MEMORY_SIZE_LIMIT_LOW) { + Stopwatch sw = Stopwatch.createStarted(); + ConcurrentMap<String, Revision> tmp = createMap(true); + tmp.putAll(map); + map = tmp; + mapInMemory = true; + sw.stop(); + LOG.debug("Switched to in-memory map in {}", sw); + pollReferenceQueue(); + } + } + + private ConcurrentMap<String, Revision> createMap(boolean inMemory) { + MapFactory f; + if (inMemory) { + f = MapFactory.DEFAULT; + } else { + f = new MapDBMapFactory(); + } + ConcurrentMap<String, Revision> map; + if (comparator != null) { + map = f.create(comparator); + } else { + map = f.create(); + } + if (!inMemory) { + map = new MapDBWrapper(map); + factories.put(new MapReference(map, queue), f); + } + return map; + } + } + + /** + * A map wrapper implementation forwarding calls to a MapDB backed map + * implementation. Instances of this class are tracked with weak references + * and the corresponding {@link MapDBMapFactory} is disposed when there + * are no more references to a MapDBWrapper. This also includes collections + * obtained from this map, e.g. {@link #entrySet()}, {@link #values()} or + * {@link #keySet()}. + * + */ + private static final class MapDBWrapper extends ForwardingConcurrentMap<String, Revision> { + + private final ConcurrentMap<String, Revision> map; + + protected MapDBWrapper(@Nonnull ConcurrentMap<String, Revision> map) { + this.map = checkNotNull(map); + } + + @Override + protected ConcurrentMap<String, Revision> delegate() { + return map; + } + + @Override + public Set<Entry<String, Revision>> entrySet() { + return Collections.unmodifiableSet(map.entrySet()); + } + + @Override + public Collection<Revision> values() { + return Collections.unmodifiableCollection(map.values()); + } + + @Override + public Set<String> keySet() { + return Collections.unmodifiableSet(map.keySet()); + } + } + + private static final class MapReference extends WeakReference<Object> { + + public MapReference(@Nonnull Object referent, + @Nonnull ReferenceQueue<Object> queue) { + super(checkNotNull(referent), checkNotNull(queue)); + } + } +} Propchange: jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/HybridMapFactory.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactory.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactory.java?rev=1692079&r1=1692078&r2=1692079&view=diff ============================================================================== --- jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactory.java (original) +++ jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactory.java Tue Jul 21 10:25:04 2015 @@ -36,19 +36,22 @@ import org.mapdb.Serializer; public class MapDBMapFactory extends MapFactory { private final AtomicInteger counter = new AtomicInteger(); - private final DB db; + private DB db; - public MapDBMapFactory() { - this.db = DBMaker.newTempFileDB() - .deleteFilesAfterClose() - .transactionDisable() - .asyncWriteEnable() - .make(); + private synchronized DB getDB() { + if (db == null) { + this.db = DBMaker.newTempFileDB() + .deleteFilesAfterClose() + .transactionDisable() + .asyncWriteEnable() + .make(); + } + return db; } @Override public BTreeMap<String, Revision> create() { - return db.createTreeMap(String.valueOf(counter.incrementAndGet())) + return getDB().createTreeMap(String.valueOf(counter.incrementAndGet())) .valueSerializer(new RevisionSerializer()) .counterEnable() .makeStringMap(); @@ -57,7 +60,7 @@ public class MapDBMapFactory extends Map @Override public synchronized BTreeMap<String, Revision> create( Comparator<String> comparator) { - return db.createTreeMap(String.valueOf(counter.incrementAndGet())) + return getDB().createTreeMap(String.valueOf(counter.incrementAndGet())) .valueSerializer(new RevisionSerializer()) .keySerializer(new CustomKeySerializer(comparator)) .counterEnable() @@ -65,8 +68,11 @@ public class MapDBMapFactory extends Map } @Override - public void dispose() { - db.close(); + public synchronized void dispose() { + if (db != null) { + db.close(); + db = null; + } } private static class CustomKeySerializer extends BTreeKeySerializer<String> Modified: jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapFactory.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapFactory.java?rev=1692079&r1=1692078&r2=1692079&view=diff ============================================================================== --- jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapFactory.java (original) +++ jackrabbit/oak/branches/1.0/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/MapFactory.java Tue Jul 21 10:25:04 2015 @@ -32,10 +32,15 @@ import org.apache.jackrabbit.oak.plugins */ public abstract class MapFactory { - private static final boolean USE_MEMORY_MAP_FACTORY - = Boolean.getBoolean("oak.useMemoryMapFactory"); + private static boolean useMemoryMapFactory() { + return Boolean.getBoolean("oak.useMemoryMapFactory"); + } + + private static boolean useHybridMapFactory() { + return Boolean.getBoolean("oak.useHybridMapFactory"); + } - private static MapFactory DEFAULT = new MapFactory() { + public static final MapFactory DEFAULT = new MapFactory() { @Override public ConcurrentMap<String, Revision> create() { return new ConcurrentHashMap<String, Revision>(); @@ -69,8 +74,10 @@ public abstract class MapFactory { } public static MapFactory createFactory() { - if (USE_MEMORY_MAP_FACTORY) { + if (useMemoryMapFactory()) { return MapFactory.getInstance(); + } else if (useHybridMapFactory()) { + return new HybridMapFactory(); } else { return new MapDBMapFactory(); } Modified: jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UnsavedModificationsTest.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UnsavedModificationsTest.java?rev=1692079&r1=1692078&r2=1692079&view=diff ============================================================================== --- jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UnsavedModificationsTest.java (original) +++ jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UnsavedModificationsTest.java Tue Jul 21 10:25:04 2015 @@ -18,6 +18,7 @@ package org.apache.jackrabbit.oak.plugin import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; @@ -28,11 +29,14 @@ import com.google.common.collect.Sets; import org.apache.jackrabbit.oak.commons.PathUtils; import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore; +import org.apache.jackrabbit.oak.plugins.document.util.HybridMapFactory; import org.apache.jackrabbit.oak.plugins.document.util.MapDBMapFactory; import org.apache.jackrabbit.oak.plugins.document.util.MapFactory; import org.junit.Ignore; import org.junit.Test; +import static org.junit.Assert.assertEquals; + public class UnsavedModificationsTest { private static final String CHARS = "abcdefghijklmnopqrstuvwxyz"; @@ -123,7 +127,7 @@ public class UnsavedModificationsTest { } // OAK-3112 - @Ignore + @Ignore("Long running performance test") @Test public void performance() throws Exception { DocumentStore store = new MemoryDocumentStore() { @@ -137,8 +141,7 @@ public class UnsavedModificationsTest { final DocumentNodeStore ns = new DocumentMK.Builder().setDocumentStore(store) .getNodeStore(); - // MapFactory factory = MapFactory.getInstance(); - MapFactory factory = new MapDBMapFactory(); + MapFactory factory = new HybridMapFactory(); final UnsavedModifications pending = new UnsavedModifications(factory); Thread t = new Thread(new Runnable() { @@ -165,7 +168,7 @@ public class UnsavedModificationsTest { } paths.clear(); } - if (num % 1000 == 0) { + if (random.nextFloat() < 0.00005) { pending.persist(ns, new ReentrantLock()); } } @@ -189,6 +192,105 @@ public class UnsavedModificationsTest { ns.dispose(); } + @Test + public void getPathsAfterPersist() throws Exception { + DocumentStore store = new MemoryDocumentStore() { + @Override + public <T extends Document> void update(Collection<T> collection, + List<String> keys, + UpdateOp updateOp) { + // ignore call + } + }; + final DocumentNodeStore ns = new DocumentMK.Builder().setDocumentStore(store) + .getNodeStore(); + + MapFactory factory = new HybridMapFactory(); + UnsavedModifications pending = new UnsavedModifications(factory); + + Revision r = new Revision(1, 0, 1); + for (int i = 0; i <= UnsavedModifications.IN_MEMORY_SIZE_LIMIT; i++) { + pending.put("/node-" + i, r); + } + + // start iterating over paths + Iterator<String> paths = pending.getPaths().iterator(); + for (int i = 0; i < 1000; i++) { + paths.next(); + } + + // drain pending, this will force it back to in-memory + pending.persist(ns, new ReentrantLock()); + + // loop over remaining paths + while (paths.hasNext()) { + paths.next(); + } + pending.close(); + } + + @Test + public void getPathsWithRevisionAfterPersist() throws Exception { + DocumentStore store = new MemoryDocumentStore() { + @Override + public <T extends Document> void update(Collection<T> collection, + List<String> keys, + UpdateOp updateOp) { + // ignore call + } + }; + final DocumentNodeStore ns = new DocumentMK.Builder().setDocumentStore(store) + .getNodeStore(); + + MapFactory factory = new HybridMapFactory(); + UnsavedModifications pending = new UnsavedModifications(factory); + + Revision r = new Revision(1, 0, 1); + for (int i = 0; i <= UnsavedModifications.IN_MEMORY_SIZE_LIMIT; i++) { + pending.put("/node-" + i, r); + } + + // start iterating over paths + Iterator<String> paths = pending.getPaths(r).iterator(); + for (int i = 0; i < 1000; i++) { + paths.next(); + } + + // drain pending, this will force it back to in-memory + pending.persist(ns, new ReentrantLock()); + + // loop over remaining paths + while (paths.hasNext()) { + paths.next(); + } + pending.close(); + } + + @Test + public void defaultMapFactoryCreate() { + MapFactory factory = MapFactory.createFactory(); + assertEquals(MapDBMapFactory.class, factory.getClass()); + factory.dispose(); + } + + @Test + public void useHybridMapFactory() { + System.setProperty("oak.useHybridMapFactory", "true"); + MapFactory factory = MapFactory.createFactory(); + assertEquals(HybridMapFactory.class, factory.getClass()); + factory.dispose(); + System.clearProperty("oak.useHybridMapFactory"); + } + + @Test + public void useMemoryMapFactory() { + System.setProperty("oak.useMemoryMapFactory", "true"); + MapFactory factory = MapFactory.createFactory(); + assertEquals(MapFactory.DEFAULT.getClass(), factory.getClass()); + factory.dispose(); + System.clearProperty("oak.useMemoryMapFactory"); + } + private static String randomName(Random r, int len) { StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { Modified: jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactoryTest.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactoryTest.java?rev=1692079&r1=1692078&r2=1692079&view=diff ============================================================================== --- jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactoryTest.java (original) +++ jackrabbit/oak/branches/1.0/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/MapDBMapFactoryTest.java Tue Jul 21 10:25:04 2015 @@ -16,12 +16,17 @@ */ package org.apache.jackrabbit.oak.plugins.document.util; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import org.apache.jackrabbit.oak.plugins.document.PathComparator; import org.apache.jackrabbit.oak.plugins.document.Revision; +import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import com.google.common.collect.Lists; @@ -30,12 +35,32 @@ import static org.junit.Assert.assertEqu /** * <code>MapDBMapFactoryTest</code>... */ +@RunWith(Parameterized.class) public class MapDBMapFactoryTest { + private MapFactory factory; + + @Parameterized.Parameters + public static Collection<Object[]> factories() { + Object[][] factories = new Object[][] { + {new MapDBMapFactory()}, + {new HybridMapFactory()}, + {MapFactory.DEFAULT} + }; + return Arrays.asList(factories); + } + + public MapDBMapFactoryTest(MapFactory factory) { + this.factory = factory; + } + + @After + public void dispose() { + factory.dispose(); + } + @Test public void comparator() { - MapFactory factory = new MapDBMapFactory(); - Revision r = new Revision(1, 0, 1); Map<String, Revision> map = factory.create(PathComparator.INSTANCE); @@ -58,7 +83,5 @@ public class MapDBMapFactoryTest { List<String> actual = Lists.newArrayList(map.keySet()); assertEquals(expected, actual); - - factory.dispose(); } }