diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
index b2e0fa4..386cb09 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
@@ -48,6 +48,8 @@ import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+import org.apache.jackrabbit.oak.util.PerfLogger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
@@ -63,6 +65,10 @@ import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE
  */
 public class DocumentNodeState extends AbstractNodeState implements CacheValue {
 
+    private static final PerfLogger perfLogger = new PerfLogger(
+            LoggerFactory.getLogger(DocumentNodeState.class.getName()
+                    + ".perf"));
+
     public static final Children NO_CHILDREN = new Children();
 
     /**
@@ -257,7 +263,19 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
                         return true;
                     } else {
                         // use DocumentNodeStore compare
-                        return dispatch(store.diffChildren(this, mBase), mBase, diff);
+                        final long start = perfLogger.start();
+                        try {
+                            return dispatch(store.diffChildren(this, mBase),
+                                    mBase, diff);
+                        } finally {
+                            perfLogger
+                                    .end(start,
+                                            1,
+                                            "compareAgainstBaseState, path={}, rev={}, lastRevision={}, base.path={}, base.rev={}, base.lastRevision={}",
+                                            path, rev, lastRevision,
+                                            mBase.path, mBase.rev,
+                                            mBase.lastRevision);
+                        }
                     }
                 }
             }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
index 4107e10..4a0f17e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
@@ -95,6 +95,7 @@ import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.apache.jackrabbit.oak.stats.Clock;
+import org.apache.jackrabbit.oak.util.PerfLogger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -106,6 +107,10 @@ public final class DocumentNodeStore
 
     private static final Logger LOG = LoggerFactory.getLogger(DocumentNodeStore.class);
 
+    private static final PerfLogger PERFLOG = new PerfLogger(
+            LoggerFactory.getLogger(DocumentNodeStore.class.getName()
+                    + ".perf"));
+
     /**
      * Do not cache more than this number of children for a document.
      */
@@ -697,6 +702,7 @@ public final class DocumentNodeStore
     @CheckForNull
     DocumentNodeState getNode(@Nonnull final String path, @Nonnull final Revision rev) {
         checkRevisionAge(checkNotNull(rev), checkNotNull(path));
+        final long start = PERFLOG.start();
         try {
             PathRev key = new PathRev(path, rev);
             DocumentNodeState node = nodeCache.get(key, new Callable<DocumentNodeState>() {
@@ -709,7 +715,9 @@ public final class DocumentNodeStore
                     return n;
                 }
             });
-            return node == missing || node.equals(missing) ? null : node;
+            final DocumentNodeState result = node == missing || node.equals(missing) ? null : node;
+            PERFLOG.end(start, 1, "getNode, path={}, rev={}", path, rev);
+            return result;
         } catch (ExecutionException e) {
             throw DocumentStoreException.convert(e.getCause());
         }
@@ -919,13 +927,20 @@ public final class DocumentNodeStore
 
     @CheckForNull
     DocumentNodeState readNode(String path, Revision readRevision) {
+        final long start = PERFLOG.start();
         String id = Utils.getIdFromPath(path);
         Revision lastRevision = getPendingModifications().get(path);
         NodeDocument doc = store.find(Collection.NODES, id);
         if (doc == null) {
+            PERFLOG.end(start, 1,
+                    "readNode: (document not found) path={}, readRevision={}",
+                    path, readRevision);
             return null;
         }
-        return doc.getNodeAtRevision(this, readRevision, lastRevision);
+        final DocumentNodeState result = doc.getNodeAtRevision(this, readRevision, lastRevision);
+        PERFLOG.end(start, 1, "readNode: path={}, readRevision={}", path,
+                readRevision);
+        return result;
     }
 
     /**
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
index 7038170..b73093b 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
@@ -62,6 +62,7 @@ import org.apache.jackrabbit.oak.plugins.document.cache.OffHeapCache;
 import org.apache.jackrabbit.oak.plugins.document.util.StringValue;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.stats.Clock;
+import org.apache.jackrabbit.oak.util.PerfLogger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -89,8 +90,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
 public class MongoDocumentStore implements DocumentStore {
 
     private static final Logger LOG = LoggerFactory.getLogger(MongoDocumentStore.class);
-
-    private static final boolean LOG_TIME = false;
+    private static final PerfLogger PERFLOG = new PerfLogger(
+            LoggerFactory.getLogger(MongoDocumentStore.class.getName()
+                    + ".perf"));
 
     private static final DBObject BY_ID_ASC = new BasicDBObject(Document.ID, 1);
 
@@ -237,20 +239,6 @@ public class MongoDocumentStore implements DocumentStore {
         return new NodeDocOffHeapCache(primaryCache, listener, builder, this);
     }
 
-    private static long start() {
-        return LOG_TIME ? System.currentTimeMillis() : 0;
-    }
-
-    private void end(String message, long start) {
-        if (LOG_TIME) {
-            long t = System.currentTimeMillis() - start;
-            if (t > 0) {
-                LOG.debug(message + ": " + t);
-            }
-            timeSum += t;
-        }
-    }
-
     @Override
     public void finalize() throws Throwable {
         super.finalize();
@@ -287,14 +275,20 @@ public class MongoDocumentStore implements DocumentStore {
 
     @Override
     public <T extends Document> T find(Collection<T> collection, String key) {
-        return find(collection, key, true, -1);
+        final long start = PERFLOG.start();
+        final T result = find(collection, key, true, -1);
+        PERFLOG.end(start, 1, "find: preferCached=true, key={}", key);
+        return result;
     }
 
     @Override
     public <T extends Document> T find(final Collection<T> collection,
                                        final String key,
                                        int maxCacheAge) {
-        return find(collection, key, false, maxCacheAge);
+        final long start = PERFLOG.start();
+        final T result = find(collection, key, false, maxCacheAge);
+        PERFLOG.end(start, 1, "find: preferCached=false, key={}", key);
+        return result;
     }
 
     @SuppressWarnings("unchecked")
@@ -365,12 +359,14 @@ public class MongoDocumentStore implements DocumentStore {
     private <T extends Document> T findUncached(Collection<T> collection, String key, DocumentReadPreference docReadPref) {
         log("findUncached", key, docReadPref);
         DBCollection dbCollection = getDBCollection(collection);
-        long start = start();
+        final long start = PERFLOG.start();
+        boolean isSlaveOk = false;
         try {
             ReadPreference readPreference = getMongoReadPreference(collection, Utils.getParentId(key), docReadPref);
 
             if(readPreference.isSlaveOk()){
                 LOG.trace("Routing call to secondary for fetching [{}]", key);
+                isSlaveOk = true;
             }
 
             DBObject obj = dbCollection.findOne(getByKeyQuery(key).get(), null, null, readPreference);
@@ -395,7 +391,8 @@ public class MongoDocumentStore implements DocumentStore {
             }
             return doc;
         } finally {
-            end("findUncached", start);
+            PERFLOG.end(start, 1, "findUncached on key={}, isSlaveOk={}", key,
+                    isSlaveOk);
         }
     }
 
@@ -446,7 +443,7 @@ public class MongoDocumentStore implements DocumentStore {
         DBObject query = queryBuilder.get();
         String parentId = Utils.getParentIdFromLowerLimit(fromKey);
         TreeLock lock = acquireExclusive(parentId != null ? parentId : "");
-        long start = start();
+        final long start = PERFLOG.start();
         try {
             DBCursor cursor = dbCollection.find(query).sort(BY_ID_ASC).hint(hint);
             ReadPreference readPreference =
@@ -494,7 +491,7 @@ public class MongoDocumentStore implements DocumentStore {
             return list;
         } finally {
             lock.unlock();
-            end("query", start);
+            PERFLOG.end(start, 1, "query for children from [{}] to [{}]", fromKey, toKey);
         }
     }
 
@@ -509,7 +506,7 @@ public class MongoDocumentStore implements DocumentStore {
     public <T extends Document> void remove(Collection<T> collection, String key) {
         log("remove", key);
         DBCollection dbCollection = getDBCollection(collection);
-        long start = start();
+        long start = PERFLOG.start();
         try {
             WriteResult writeResult = dbCollection.remove(getByKeyQuery(key).get());
             invalidateCache(collection, key);
@@ -517,7 +514,7 @@ public class MongoDocumentStore implements DocumentStore {
                 throw new DocumentStoreException("Remove failed: " + writeResult.getError());
             }
         } finally {
-            end("remove", start);
+            PERFLOG.end(start, 1, "remove key={}", key);
         }
     }
 
@@ -547,7 +544,7 @@ public class MongoDocumentStore implements DocumentStore {
         DBObject update = createUpdate(updateOp);
 
         TreeLock lock = acquire(updateOp.getId());
-        long start = start();
+        final long start = PERFLOG.start();
         try {
             // get modCount of cached document
             Number modCount = null;
@@ -600,7 +597,7 @@ public class MongoDocumentStore implements DocumentStore {
             throw DocumentStoreException.convert(e);
         } finally {
             lock.unlock();
-            end("findAndModify", start);
+            PERFLOG.end(start, 1, "findAndModify");
         }
     }
 
@@ -670,7 +667,7 @@ public class MongoDocumentStore implements DocumentStore {
         }
 
         DBCollection dbCollection = getDBCollection(collection);
-        long start = start();
+        final long start = PERFLOG.start();
         try {
             try {
                 WriteResult writeResult = dbCollection.insert(inserts);
@@ -692,7 +689,7 @@ public class MongoDocumentStore implements DocumentStore {
                 return false;
             }
         } finally {
-            end("create", start);
+            PERFLOG.end(start, 1, "create");
         }
     }
 
@@ -706,7 +703,7 @@ public class MongoDocumentStore implements DocumentStore {
         // make sure we don't modify the original updateOp
         updateOp = updateOp.copy();
         DBObject update = createUpdate(updateOp);
-        long start = start();
+        final long start = PERFLOG.start();
         try {
             Map<String, NodeDocument> cachedDocs = Collections.emptyMap();
             if (collection == Collection.NODES) {
@@ -742,7 +739,7 @@ public class MongoDocumentStore implements DocumentStore {
                 throw DocumentStoreException.convert(e);
             }
         } finally {
-            end("update", start);
+            PERFLOG.end(start, 1, "update");
         }
     }
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventGenerator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventGenerator.java
index 1683bef..c350171 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventGenerator.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/EventGenerator.java
@@ -36,6 +36,7 @@ import javax.annotation.Nonnull;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+import org.apache.jackrabbit.oak.util.PerfLogger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -57,6 +58,10 @@ import org.slf4j.LoggerFactory;
 public class EventGenerator {
     private static final Logger log = LoggerFactory.getLogger(EventGenerator.class);
 
+    private static final PerfLogger perfLogger = new PerfLogger(
+            LoggerFactory.getLogger(EventGenerator.class.getName()
+                    + ".perf"));
+
     /**
      * Maximum number of content changes to process during the
      * execution of a single diff continuation.
@@ -109,14 +114,11 @@ public class EventGenerator {
     public void generate() {
         if (!continuations.isEmpty()) {
             final Continuation c = continuations.removeFirst();
-            if (log.isDebugEnabled()) {
-                log.debug("Starting event generation ...");
-                long start = System.currentTimeMillis();
-                c.run();
-                log.debug("Generated {} events in {} ms", c.counter, (System.currentTimeMillis() - start));
-            } else {
-                c.run();
-            }
+            final long start = perfLogger
+                    .start("generate: Starting event generation");
+            c.run();
+            perfLogger.end(start, 1, "generate: Generated {} events in {} ms",
+                    c.counter);
         }
     }
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/util/PerfLogger.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/PerfLogger.java
new file mode 100644
index 0000000..d8db7cb
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/PerfLogger.java
@@ -0,0 +1,199 @@
+/*
+ * 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.util;
+
+import org.slf4j.Logger;
+
+/**
+ * PerfLogger is a simpler wrapper around a slf4j Logger which 
+ * comes with the capability to issue log statements containing
+ * the measurement between start() and end() methods.
+ * <p>
+ * Usage:
+ * <ul>
+ * <li>final long start = perflogger.start();</li>
+ * <li>.. some code ..
+ * <li>perflogger.end(start, 1, "myMethodName: param1={}", param1);</li>
+ * </ul>
+ * <p>
+ * The above will do nothing if the log level for the logger passed
+ * to PerfLogger at construction time is not DEBUG or TRACE - otherwise
+ * start() will return the current time in milliseconds and end will
+ * issue a log statement if the time between start and end was bigger
+ * than 1 ms, and it will pass the parameters to the log statement.
+ * The idea is to keep up performance at max possible if the log 
+ * level is INFO or higher - but to allow some meaningful logging
+ * if at DEBUG or TRACe. The difference between DEBUG and TRACE is
+ * that TRACE will log start too (if a log message is passed to start)
+ * and it will always log the end - whereas in case of DEBUG the start
+ * will never be logged and the end will only be logged if the time
+ * is bigger than what's passed to end.
+ */
+public final class PerfLogger {
+
+    /** The logger to which the log statements are emitted **/
+    private final Logger delegate;
+
+    /** Create a new PerfLogger that shall use the given Logger object for logging **/
+    public PerfLogger(Logger delegate) {
+        if (delegate == null) {
+            throw new IllegalArgumentException("delegate must not be null");
+        }
+        this.delegate = delegate;
+    }
+
+    /**
+     * Returns quickly if log level is not DEBUG or TRACE - otherwise just
+     * returns the current time in millis.
+     * 
+     * @return the current time if level is DEBUG or TRACE, -1 otherwise
+     */
+    public final long start() {
+        if (!delegate.isDebugEnabled()) {
+            // if log level is not at least DEBUG, then return fast, no-op
+            return -1;
+        }
+        return start(null);
+    }
+
+    /**
+     * Returns quickly if log level is not DEBUG or TRACE - if it is DEBUG, then
+     * just returns the current time in millis, if it is TRACE, then log the
+     * given message and also return the current time in millis.
+     * 
+     * @param traceMsgOrNull
+     *            the message to log if log level is TRACE - or null if no
+     *            message should be logged (even on TRACE level)
+     * @return the current time if level is DEBUG or TRACE, -1 otherwise
+     */
+    public final long start(String traceMsgOrNull) {
+        if (!delegate.isDebugEnabled()) {
+            // if log level is not at least DEBUG, then return fast, no-op
+            return -1;
+        }
+        if (traceMsgOrNull != null && delegate.isTraceEnabled()) {
+            delegate.trace(traceMsgOrNull);
+        }
+        return System.currentTimeMillis();
+    }
+
+    /**
+     * Returns quickly if log level is not DEBUG or TRACE - if it is DEBUG
+     * and the difference between now and the provided 'start' is bigger (slower)
+     * than 'logAtDebugIfSlowerThanMs', then a log.debug is emitted, if at
+     * TRACE then a log.trace is always emitted (independent of time measured).
+     * <p>
+     * Note that this method exists for performance optimization only (compared
+     * to the other end() method with a vararg.
+     * @param start the start time with which 'now' should be compared
+     * @param logAtDebugIfSlowerThanMs the number of milliseconds that must
+     * be surpassed to issue a log.debug (if log level is DEBUG)
+     * @param logMessagePrefix the log message 'prefix' - to which the given
+     * argument will be passed, plus the measured time difference in the format
+     * '[took x ms']
+     * @param arg1 the argument which is to be passed to the log statement
+     */
+    public final void end(long start, long logAtDebugIfSlowerThanMs,
+            String logMessagePrefix, Object arg1) {
+        if (!delegate.isTraceEnabled()) {
+            return;
+        }
+        end(start, logAtDebugIfSlowerThanMs, logMessagePrefix,
+                new Object[] { arg1 });
+    }
+
+    /**
+     * Returns quickly if log level is not DEBUG or TRACE - if it is DEBUG
+     * and the difference between now and the provided 'start' is bigger (slower)
+     * than 'logAtDebugIfSlowerThanMs', then a log.debug is emitted, if at
+     * TRACE then a log.trace is always emitted (independent of time measured).
+     * <p>
+     * Note that this method exists for performance optimization only (compared
+     * to the other end() method with a vararg.
+     * @param start the start time with which 'now' should be compared
+     * @param logAtDebugIfSlowerThanMs the number of milliseconds that must
+     * be surpassed to issue a log.debug (if log level is DEBUG)
+     * @param logMessagePrefix the log message 'prefix' - to which the given
+     * arguments will be passed, plus the measured time difference in the format
+     * '[took x ms']
+     * @param arg1 the first argument which is to be passed to the log statement
+     * @param arg2 the second argument which is to be passed to the log statement
+     */
+    public final void end(long start, long logAtDebugIfSlowerThanMs,
+            String logMessagePrefix, Object arg1, Object arg2) {
+        if (!delegate.isTraceEnabled()) {
+            return;
+        }
+        end(start, logAtDebugIfSlowerThanMs, logMessagePrefix, new Object[] {
+                arg1, arg2 });
+    }
+
+    /**
+     * Returns quickly if log level is not DEBUG or TRACE - if it is DEBUG
+     * and the difference between now and the provided 'start' is bigger (slower)
+     * than 'logAtDebugIfSlowerThanMs', then a log.debug is emitted, if at
+     * TRACE then a log.trace is always emitted (independent of time measured).
+     * @param start the start time with which 'now' should be compared
+     * @param logAtDebugIfSlowerThanMs the number of milliseconds that must
+     * be surpassed to issue a log.debug (if log level is DEBUG)
+     * @param logMessagePrefix the log message 'prefix' - to which the given
+     * arguments will be passed, plus the measured time difference in the format
+     * '[took x ms']
+     * @param arguments the arguments which is to be passed to the log statement
+     */
+    public void end(long start, long logAtDebugIfSlowerThanMs,
+            String logMessagePrefix, Object... arguments) {
+        if (!delegate.isDebugEnabled()) {
+            // if log level is not at least DEBUG, then return fast, no-op
+            return;
+        }
+
+        if (start == -1) {
+            // start was never set
+            // -> then log at trace as we have no diff available
+            delegate.trace(logMessagePrefix + " [start not set]",
+                    (Object[]) arguments);
+        } else {
+            final long diff = System.currentTimeMillis() - start;
+            if (delegate.isTraceEnabled()) {
+                // if log level is TRACE, then always log - and do that on TRACE
+                // then:
+                delegate.trace(logMessagePrefix + " [took " + diff + "ms]",
+                        (Object[]) arguments);
+            } else if ((logAtDebugIfSlowerThanMs < 0)
+                    || (diff > logAtDebugIfSlowerThanMs)) {
+                // otherwise (log level is DEBUG) only log if
+                // logDebugIfSlowerThanMs is set to -1 (or negative)
+                // OR the measured diff is larger than logDebugIfSlowerThanMs -
+                // and then do that on DEBUG:
+                delegate.debug(logMessagePrefix + " [took " + diff + "ms]",
+                        (Object[]) arguments);
+            }
+        }
+    }
+
+    /** Whether or not the delegate has log level DEBUG configured **/
+    public final boolean isDebugEnabled() {
+        return delegate.isDebugEnabled();
+    }
+
+    /** Whether or not the delegate has log level TRACE configured **/
+    public final boolean isTraceEnabled() {
+        return delegate.isTraceEnabled();
+    }
+
+}
