You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2016/02/25 10:56:26 UTC

svn commit: r1732270 - in /jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak: api/jmx/ cache/ plugins/document/ plugins/document/persistentCache/

Author: thomasm
Date: Thu Feb 25 09:56:25 2016
New Revision: 1732270

URL: http://svn.apache.org/viewvc?rev=1732270&view=rev
Log:
OAK-3850 Collect and expose Persistent Cache stats

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/PersistentCacheStatsMBean.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCacheStats.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/ConsolidatedCacheStats.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/NodeCache.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCache.java

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/PersistentCacheStatsMBean.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/PersistentCacheStatsMBean.java?rev=1732270&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/PersistentCacheStatsMBean.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/PersistentCacheStatsMBean.java Thu Feb 25 09:56:25 2016
@@ -0,0 +1,51 @@
+package org.apache.jackrabbit.oak.api.jmx;
+/*
+ * 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.
+ */
+
+import aQute.bnd.annotation.ProviderType;
+
+import javax.management.openmbean.CompositeData;
+
+@ProviderType
+public interface PersistentCacheStatsMBean extends CacheStatsMBean {
+    String TYPE = "PersistentCacheStats";
+
+    CompositeData getRequestRateHistory();
+
+    CompositeData getHitRateHistory();
+
+    CompositeData getLoadRateHistory();
+
+    CompositeData getLoadExceptionRateHistory();
+
+    CompositeData getHitPercentageHistory();
+
+    CompositeData getPutRateHistory();
+
+    CompositeData getInvalidateOneRateHistory();
+
+    CompositeData getInvalidateAllRateHistory();
+
+    CompositeData getBroadcastRecvRateHistory();
+
+    CompositeData getUsedSpaceHistory();
+
+    String cacheInfoAsString();
+
+}
+
+

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/ConsolidatedCacheStats.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/ConsolidatedCacheStats.java?rev=1732270&r1=1732269&r2=1732270&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/ConsolidatedCacheStats.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/ConsolidatedCacheStats.java Thu Feb 25 09:56:25 2016
@@ -37,6 +37,7 @@ import org.apache.felix.scr.annotations.
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.api.jmx.ConsolidatedCacheStatsMBean;
+import org.apache.jackrabbit.oak.api.jmx.PersistentCacheStatsMBean;
 import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
 import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
 import org.apache.jackrabbit.oak.spi.whiteboard.Tracker;
@@ -51,6 +52,7 @@ import static org.apache.jackrabbit.oak.
 public class ConsolidatedCacheStats implements ConsolidatedCacheStatsMBean {
 
     private Tracker<CacheStatsMBean> cacheStats;
+    private Tracker<PersistentCacheStatsMBean> persistentCacheStats;
 
     private Registration mbeanReg;
 
@@ -64,6 +66,9 @@ public class ConsolidatedCacheStats impl
             for(CacheStatsMBean stats : cacheStats.getServices()){
                 tds.put(new CacheStatsData(stats).toCompositeData());
             }
+            for(CacheStatsMBean stats : persistentCacheStats.getServices()){
+                tds.put(new CacheStatsData(stats).toCompositeData());
+            }
         } catch (OpenDataException e) {
             throw new IllegalStateException(e);
         }
@@ -74,6 +79,7 @@ public class ConsolidatedCacheStats impl
     private void activate(BundleContext context){
         Whiteboard wb = new OsgiWhiteboard(context);
         cacheStats = wb.track(CacheStatsMBean.class);
+        persistentCacheStats = wb.track(PersistentCacheStatsMBean.class);
         mbeanReg = registerMBean(wb,
                 ConsolidatedCacheStatsMBean.class,
                 this,
@@ -90,6 +96,10 @@ public class ConsolidatedCacheStats impl
         if(cacheStats != null){
             cacheStats.stop();
         }
+
+        if(persistentCacheStats != null) {
+            persistentCacheStats.stop();
+        }
     }
 
     private static class CacheStatsData {

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java?rev=1732270&r1=1732269&r2=1732270&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java Thu Feb 25 09:56:25 2016
@@ -17,15 +17,12 @@
 package org.apache.jackrabbit.oak.plugins.document;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.cache.RemovalCause.COLLECTED;
-import static com.google.common.cache.RemovalCause.EXPIRED;
-import static com.google.common.cache.RemovalCause.SIZE;
 
 import java.io.InputStream;
-import java.lang.ref.Reference;
 import java.net.UnknownHostException;
-import java.util.EnumSet;
+import java.util.EnumMap;
 import java.util.Iterator;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.Executor;
@@ -71,6 +68,7 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.CacheType;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.EvictionListener;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.PersistentCache;
+import org.apache.jackrabbit.oak.plugins.document.persistentCache.PersistentCacheStats;
 import org.apache.jackrabbit.oak.plugins.document.rdb.RDBBlobStore;
 import org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore;
 import org.apache.jackrabbit.oak.plugins.document.rdb.RDBOptions;
@@ -534,6 +532,8 @@ public class DocumentMK {
         private BlobStoreStats blobStoreStats;
         private CacheStats blobStoreCacheStats;
         private DocumentStoreStatsCollector documentStoreStatsCollector;
+        private Map<CacheType, PersistentCacheStats> persistentCacheStats =
+                new EnumMap<CacheType, PersistentCacheStats>(CacheType.class);
 
         public Builder() {
         }
@@ -895,6 +895,9 @@ public class DocumentMK {
             return this;
         }
 
+        public StatisticsProvider getStatisticsProvider() {
+            return this.statisticsProvider;
+        }
         public DocumentStoreStatsCollector getDocumentStoreStatsCollector() {
             if (documentStoreStatsCollector == null) {
                 documentStoreStatsCollector = new DocumentStoreStats(statisticsProvider);
@@ -907,6 +910,11 @@ public class DocumentMK {
             return this;
         }
 
+        @Nonnull
+        public Map<CacheType, PersistentCacheStats> getPersistenceCacheStats() {
+            return persistentCacheStats;
+        }
+
         @CheckForNull
         public BlobStoreStats getBlobStoreStats() {
             return blobStoreStats;
@@ -1033,10 +1041,14 @@ public class DocumentMK {
                 if (docNodeStore != null) {
                     docNodeStore.setPersistentCache(p);
                 }
-                cache = p.wrap(docNodeStore, docStore, cache, cacheType);
+                cache = p.wrap(docNodeStore, docStore, cache, cacheType, statisticsProvider);
                 if (cache instanceof EvictionListener) {
                     listeners.add((EvictionListener<K, V>) cache);
                 }
+                PersistentCacheStats stats = PersistentCache.getPersistentCacheStats(cache);
+                if (stats != null) {
+                    persistentCacheStats.put(cacheType, stats);
+                }
             }
             return cache;
         }

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java?rev=1732270&r1=1732269&r2=1732270&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java Thu Feb 25 09:56:25 2016
@@ -60,6 +60,7 @@ import org.apache.jackrabbit.commons.Sim
 import org.apache.jackrabbit.oak.api.Descriptors;
 import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.api.jmx.CheckpointMBean;
+import org.apache.jackrabbit.oak.api.jmx.PersistentCacheStatsMBean;
 import org.apache.jackrabbit.oak.cache.CacheStats;
 import org.apache.jackrabbit.oak.commons.PropertiesUtil;
 import org.apache.jackrabbit.oak.osgi.ObserverTracker;
@@ -70,6 +71,8 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.blob.BlobStoreStats;
 import org.apache.jackrabbit.oak.plugins.blob.SharedDataStore;
 import org.apache.jackrabbit.oak.plugins.blob.datastore.SharedDataStoreUtils;
+import org.apache.jackrabbit.oak.plugins.document.persistentCache.CacheType;
+import org.apache.jackrabbit.oak.plugins.document.persistentCache.PersistentCacheStats;
 import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
 import org.apache.jackrabbit.oak.plugins.identifier.ClusterRepositoryInfo;
 import org.apache.jackrabbit.oak.spi.blob.BlobStore;
@@ -687,6 +690,19 @@ public class DocumentNodeStoreService {
             );
         }
 
+        // register persistent cache stats
+        Map<CacheType, PersistentCacheStats> persistenceCacheStats = mkBuilder.getPersistenceCacheStats();
+        for (PersistentCacheStats pcs: persistenceCacheStats.values()) {
+            registrations.add(
+                    registerMBean(whiteboard,
+                            PersistentCacheStatsMBean.class,
+                            pcs,
+                            PersistentCacheStatsMBean.TYPE,
+                            pcs.getName())
+            );
+        }
+
+
         final long versionGcMaxAgeInSecs = toLong(prop(PROP_VER_GC_MAX_AGE), DEFAULT_VER_GC_MAX_AGE);
         final long blobGcMaxAgeInSecs = toLong(prop(PROP_BLOB_GC_MAX_AGE), DEFAULT_BLOB_GC_MAX_AGE);
 

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/NodeCache.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/NodeCache.java?rev=1732270&r1=1732269&r2=1732270&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/NodeCache.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/NodeCache.java Thu Feb 25 09:56:25 2016
@@ -35,6 +35,8 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.PersistentCache.GenerationCache;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.async.CacheActionDispatcher;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.async.CacheWriteQueue;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.jackrabbit.oak.stats.TimerStats;
 import org.h2.mvstore.MVMap;
 import org.h2.mvstore.WriteBuffer;
 import org.h2.mvstore.type.DataType;
@@ -51,6 +53,7 @@ class NodeCache<K, V> implements Cache<K
     private static final Set<RemovalCause> EVICTION_CAUSES = ImmutableSet.of(COLLECTED, EXPIRED, SIZE);
 
     private final PersistentCache cache;
+    private final PersistentCacheStats stats;
     private final Cache<K, V> memCache;
     private final MultiGenerationMap<K, V> map;
     private final CacheType type;
@@ -64,7 +67,8 @@ class NodeCache<K, V> implements Cache<K
             DocumentNodeStore docNodeStore, 
             DocumentStore docStore,
             CacheType type,
-            CacheActionDispatcher dispatcher) {
+            CacheActionDispatcher dispatcher,
+            StatisticsProvider statisticsProvider) {
         this.cache = cache;
         this.memCache = memCache;
         this.type = type;
@@ -73,6 +77,7 @@ class NodeCache<K, V> implements Cache<K
         keyType = new KeyDataType(type);
         valueType = new ValueDataType(docNodeStore, docStore, type);
         this.writerQueue = new CacheWriteQueue<K, V>(dispatcher, cache, map);
+        this.stats = new PersistentCacheStats(type, statisticsProvider);
     }
     
     @Override
@@ -89,12 +94,14 @@ class NodeCache<K, V> implements Cache<K
         map.addReadMap(generation, m);
         if (!readOnly) {
             map.setWriteMap(m);
+            stats.addWriteGeneration(generation);
         }
     }
     
     @Override
     public void removeGeneration(int generation) {
         map.removeReadMap(generation);
+        stats.removeReadGeneration(generation);
     }
     
     private V readIfPresent(K key) {
@@ -102,11 +109,18 @@ class NodeCache<K, V> implements Cache<K
             return null;
         }
         cache.switchGenerationIfNeeded();
+        TimerStats.Context ctx = stats.startReadTimer();
         V v = map.get(key);
+        ctx.stop();
         return v;
     }
 
     private void broadcast(final K key, final V value) {
+        long memory = 0L;
+        memory += (key == null ? 0L: keyType.getMemory(key));
+        memory += (value == null ? 0L: valueType.getMemory(value));
+        stats.markBytesWritten(memory);
+
         cache.broadcast(type, new Function<WriteBuffer, Void>() {
             @Override
             @Nullable
@@ -131,9 +145,12 @@ class NodeCache<K, V> implements Cache<K
         if (value != null) {
             return value;
         }
+        stats.markRequest();
+
         value = readIfPresent((K) key);
         if (value != null) {
             memCache.put((K) key, value);
+            stats.markHit();
         }
         return value;
     }
@@ -142,13 +159,24 @@ class NodeCache<K, V> implements Cache<K
     public V get(K key,
             Callable<? extends V> valueLoader)
             throws ExecutionException {
+            
+        // Get stats covered in getIfPresent
         V value = getIfPresent(key);
         if (value != null) {
             return value;
         }
-        value = memCache.get(key, valueLoader);
-        broadcast(key, value);
-        return value;
+        
+        // Track entry load time
+        TimerStats.Context ctx = stats.startLoaderTimer();
+        try {
+            value = memCache.get(key, valueLoader);
+            ctx.stop();
+            broadcast(key, value);
+            return value;
+        } catch (ExecutionException e) {
+            stats.markException();
+            throw e;
+         }        
     }
 
     @Override
@@ -161,6 +189,7 @@ class NodeCache<K, V> implements Cache<K
     public void put(K key, V value) {
         memCache.put(key, value);
         broadcast(key, value);
+        stats.markPut();
     }
 
     @SuppressWarnings("unchecked")
@@ -169,6 +198,7 @@ class NodeCache<K, V> implements Cache<K
         memCache.invalidate(key);
         writerQueue.addInvalidate(singleton((K) key));
         broadcast((K) key, null);
+        stats.markInvalidateOne();
     }
 
     @Override
@@ -185,6 +215,7 @@ class NodeCache<K, V> implements Cache<K
     public void invalidateAll() {
         memCache.invalidateAll();
         map.clear();
+        stats.markInvalidateAll();
     }
 
     @Override
@@ -219,6 +250,7 @@ class NodeCache<K, V> implements Cache<K
             value = (V) valueType.read(buff);
             memCache.put(key, value);
         }
+        stats.markRecvBroadcast();
     }
 
     /**
@@ -232,4 +264,8 @@ class NodeCache<K, V> implements Cache<K
         }
     }
 
+    public PersistentCacheStats getPersistentCacheStats() {
+        return stats;
+    }
+
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCache.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCache.java?rev=1732270&r1=1732269&r2=1732270&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCache.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCache.java Thu Feb 25 09:56:25 2016
@@ -34,6 +34,7 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.broadcast.TCPBroadcaster;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.broadcast.UDPBroadcaster;
 import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
 import org.h2.mvstore.FileStore;
 import org.h2.mvstore.MVMap;
 import org.h2.mvstore.MVMap.Builder;
@@ -381,6 +382,14 @@ public class PersistentCache implements
             DocumentNodeStore docNodeStore, 
             DocumentStore docStore,
             Cache<K, V> base, CacheType type) {
+       return wrap(docNodeStore, docStore, base, type, StatisticsProvider.NOOP);
+    }
+
+    public synchronized <K, V> Cache<K, V> wrap(
+            DocumentNodeStore docNodeStore,
+            DocumentStore docStore,
+            Cache<K, V> base, CacheType type,
+            StatisticsProvider statisticsProvider) {
         boolean wrap;
         switch (type) {
         case NODE:
@@ -410,7 +419,8 @@ public class PersistentCache implements
         }
         if (wrap) {
             NodeCache<K, V> c = new NodeCache<K, V>(this, 
-                    base, docNodeStore, docStore, type, writeDispatcher);
+                    base, docNodeStore, docStore,
+                    type, writeDispatcher, statisticsProvider);
             initGenerationCache(c);
             return c;
         }
@@ -530,6 +540,15 @@ public class PersistentCache implements
         buff.position(end);
     }
     
+    public static PersistentCacheStats getPersistentCacheStats(Cache cache) {
+        if (cache instanceof NodeCache) {
+            return ((NodeCache) cache).getPersistentCacheStats();
+        }
+        else {
+            return null;
+        }
+    }
+
     private void receiveMessage(ByteBuffer buff) {
         CacheType type = CacheType.VALUES[buff.get()];
         GenerationCache cache = caches.get(type);

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCacheStats.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCacheStats.java?rev=1732270&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCacheStats.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/persistentCache/PersistentCacheStats.java Thu Feb 25 09:56:25 2016
@@ -0,0 +1,520 @@
+/*
+ * 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.persistentCache;
+
+import com.google.common.base.Objects;
+import org.apache.jackrabbit.api.stats.TimeSeries;
+import org.apache.jackrabbit.oak.api.jmx.PersistentCacheStatsMBean;
+import org.apache.jackrabbit.oak.commons.IOUtils;
+import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean;
+import org.apache.jackrabbit.oak.stats.CounterStats;
+import org.apache.jackrabbit.oak.stats.MeterStats;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.jackrabbit.oak.stats.StatsOptions;
+import org.apache.jackrabbit.oak.stats.TimerStats;
+import org.apache.jackrabbit.stats.TimeSeriesStatsUtil;
+
+import javax.management.openmbean.CompositeData;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Persistence Cache Statistics.
+ */
+public class PersistentCacheStats extends AnnotatedStandardMBean implements PersistentCacheStatsMBean {
+
+    private static final Boolean ENABLE_READ_TIMER;
+    private static final Boolean ENABLE_LOAD_TIMER;
+    static {
+        String enableReadTimer = System.getProperty("PersistentCacheStats.readTimer", "true");
+        String enableLoadTimer = System.getProperty("PersistentCacheStats.loadTimer", "false");
+        ENABLE_READ_TIMER = Boolean.parseBoolean(enableReadTimer);
+        ENABLE_LOAD_TIMER = Boolean.parseBoolean(enableLoadTimer);
+    }
+
+    private static final String HITS = "HITS";
+    private static final String REQUESTS = "REQUESTS";
+    private static final String LOAD_TIMER = "LOAD_TIMER";
+    private static final String LOAD_EXCEPTIONS = "LOAD_EXCEPTIONS";
+    private static final String PUT_ONE = "CACHE_PUT";
+    private static final String BROADCAST_RECV = "BROADCAST_RECV";
+    private static final String INVALIDATE_ONE = "INVALIDATE_ONE";
+    private static final String INVALIDATE_ALL = "INVALIDATE_ALL";
+    private static final String READ_TIMER = "READ_TIMER";
+    private static final String USED_DISK_SPACE = "USED_SPACE_BYTES";
+
+    private final StatisticsProvider statisticsProvider;
+    private final String cacheName;
+
+    private final MeterStats hitMeter;
+    private final TimeSeries hitRateHistory;
+
+    private final MeterStats requestMeter;
+    private final TimeSeries requestRateHistory;
+
+    private final MeterStats loadExceptionMeter;
+    private final TimeSeries loadExceptionRateHistory;
+
+    private final TimerStats loadTimer;
+    private final TimeSeries loadRateHistory;
+
+    private final MeterStats putMeter;
+    private final TimeSeries putRateHistory;
+
+    private final MeterStats broadcastRecvMeter;
+    private final TimeSeries broadcastRecvRateHistory;
+
+    private final MeterStats invalidateOneMeter;
+    private final TimeSeries invalidateOneRateHistory;
+
+    private final MeterStats invalidateAllMeter;
+    private final TimeSeries invalidateAllRateHistory;
+
+    private final TimerStats readTimer;
+
+    private final CounterStats usedSpaceByteCounter;
+    private final TimeSeries usedSpaceByteCounterHistory;
+
+    private final UsedSpaceTracker diskStats;
+
+    private final TimeSeries hitPercentageHistory;
+
+
+    public PersistentCacheStats(CacheType cacheType, StatisticsProvider provider) {
+        super(PersistentCacheStatsMBean.class);
+
+        if (provider == null) {
+            statisticsProvider = StatisticsProvider.NOOP;
+        } else {
+            statisticsProvider = provider;
+        }
+
+        // Configure cache name
+        cacheName = "PersistentCache.NodeCache." + cacheType.name().toLowerCase();
+
+        // Fetch stats and time series
+        String statName;
+
+        statName = getStatName(HITS, cacheName);
+        hitMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        hitRateHistory = getTimeSeries(statName);
+
+        statName = getStatName(REQUESTS, cacheName);
+        requestMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        requestRateHistory = getTimeSeries(statName);
+        hitPercentageHistory = new PercentageTimeSeries(hitRateHistory, requestRateHistory);
+
+        statName = getStatName(LOAD_TIMER, cacheName);
+        loadRateHistory = new DifferenceTimeSeries(requestRateHistory, hitRateHistory);
+        if (ENABLE_LOAD_TIMER) {
+            loadTimer = statisticsProvider.getTimer(statName, StatsOptions.METRICS_ONLY);
+        }
+        else {
+            loadTimer = StatisticsProvider.NOOP.getTimer(statName, StatsOptions.METRICS_ONLY);
+        }
+
+        statName = getStatName(LOAD_EXCEPTIONS, cacheName);
+        loadExceptionMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        loadExceptionRateHistory = getTimeSeries(statName);
+
+        statName = getStatName(PUT_ONE, cacheName);
+        putMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        putRateHistory = getTimeSeries(statName);
+
+        statName = getStatName(BROADCAST_RECV, cacheName);
+        broadcastRecvMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        broadcastRecvRateHistory = getTimeSeries(statName);
+
+        statName = getStatName(INVALIDATE_ONE, cacheName);
+        invalidateOneMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        invalidateOneRateHistory = getTimeSeries(statName);
+
+        statName = getStatName(INVALIDATE_ALL, cacheName);
+        invalidateAllMeter = statisticsProvider.getMeter(statName, StatsOptions.DEFAULT);
+        invalidateAllRateHistory = getTimeSeries(statName);
+
+        statName = getStatName(USED_DISK_SPACE, cacheName);
+        usedSpaceByteCounter = statisticsProvider.getCounterStats(statName, StatsOptions.DEFAULT);
+        usedSpaceByteCounterHistory = getTimeSeries(statName, false);
+
+        statName = getStatName(READ_TIMER, cacheName);
+        if (ENABLE_READ_TIMER) {
+            readTimer = statisticsProvider.getTimer(statName, StatsOptions.METRICS_ONLY);
+        }
+        else {
+            readTimer = StatisticsProvider.NOOP.getTimer(statName, StatsOptions.METRICS_ONLY);
+        }
+
+        diskStats = new UsedSpaceTracker(usedSpaceByteCounter);
+    }
+
+    //~--------------------------------------< stats update methods
+
+    public void markHit() {
+        hitMeter.mark();
+    }
+
+    public void markRequest() {
+        requestMeter.mark();
+    }
+
+    public void markException() {
+        loadExceptionMeter.mark();
+    }
+
+    public void markPut() {
+        putMeter.mark();
+    }
+
+    public void markRecvBroadcast() {
+        broadcastRecvMeter.mark();
+    }
+
+    public void markInvalidateOne() {
+        invalidateOneMeter.mark();
+    }
+
+    public void markInvalidateAll() {
+        invalidateAllMeter.mark();
+    }
+
+    public TimerStats.Context startReadTimer() {
+        return this.readTimer.time();
+    }
+
+    public TimerStats.Context startLoaderTimer() {
+        return this.loadTimer.time();
+    }
+
+    // Update disk space
+
+    public void addWriteGeneration(int generation) {
+        diskStats.addWriteGeneration(generation);
+    }
+
+    public void removeReadGeneration(int generation) {
+        diskStats.removeReadGeneration(generation);
+    }
+
+    public void markBytesWritten(long numBytes) {
+        diskStats.markBytesWritten(numBytes);
+    }
+
+    //~--------------------------------------< diskspace usage helper
+
+    static class UsedSpaceTracker {
+        private final CounterStats byteCounter;
+
+        private final Map<Integer, AtomicLong> generationByteCounters;
+
+        private AtomicLong currentGenCounter;
+
+        UsedSpaceTracker(CounterStats usageCounter) {
+            this.byteCounter = usageCounter;
+            this.generationByteCounters = new ConcurrentHashMap<Integer, AtomicLong>();
+            this.currentGenCounter = new AtomicLong();
+        }
+
+        void addWriteGeneration(int generation) {
+            currentGenCounter = new AtomicLong(0L);
+            generationByteCounters.put(generation, currentGenCounter);
+        }
+
+        void removeReadGeneration(int generation) {
+            AtomicLong genCounter = generationByteCounters.remove(generation);
+            byteCounter.dec(genCounter == null ? 0L : genCounter.get());
+        }
+
+        void markBytesWritten(long bytes) {
+            currentGenCounter.addAndGet(bytes);
+            byteCounter.inc(bytes);
+        }
+    }
+
+    //~--------------------------------------< CacheStatsMbean
+
+    @Override
+    public String getName() {
+        return cacheName;
+    }
+
+    @Override
+    public long getRequestCount() {
+        return requestMeter.getCount();
+    }
+
+    @Override
+    public long getHitCount() {
+        return hitMeter.getCount();
+    }
+
+    @Override
+    public double getHitRate() {
+        long hitCount = hitMeter.getCount();
+        long requestCount = requestMeter.getCount();
+        return (requestCount == 0L ? 0L : (double)hitCount/requestCount);
+    }
+
+    @Override
+    public long getMissCount() {
+        return requestMeter.getCount() - hitMeter.getCount();
+    }
+
+    @Override
+    public double getMissRate() {
+        long missCount = getMissCount();
+        long requestCount = requestMeter.getCount();
+        return (requestCount == 0L ? 0L : (double)missCount/requestCount);
+    }
+
+    @Override
+    public long getLoadCount() {
+        return getMissCount();
+    }
+
+    @Override
+    public long getLoadSuccessCount() {
+        return getLoadCount() - getLoadExceptionCount();
+    }
+
+    @Override
+    public long getLoadExceptionCount() {
+        return loadExceptionMeter.getCount();
+    }
+
+    @Override
+    public double getLoadExceptionRate() {
+        long exceptionCount = loadExceptionMeter.getCount();
+        long loadCount = loadTimer.getCount();
+        return (loadCount == 0L ? 0L : (double)exceptionCount/loadCount);
+    }
+
+    @Override
+    public long estimateCurrentWeight() {
+        return usedSpaceByteCounter.getCount();
+    }
+
+
+    @Override
+    public CompositeData getRequestRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(requestRateHistory, "Persistent cache requests");
+    }
+
+    @Override
+    public CompositeData getHitRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(hitRateHistory, "Persistent cache hits");
+    }
+
+    @Override
+    public CompositeData getLoadRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(loadRateHistory, "Persistent cache loads/misses");
+    }
+
+    @Override
+    public CompositeData getLoadExceptionRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(loadExceptionRateHistory, "Persistent cache load exceptions");
+    }
+
+    @Override
+    public CompositeData getHitPercentageHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(hitPercentageHistory, "Persistent cache hit percentage");
+    }
+
+    @Override
+    public CompositeData getPutRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(putRateHistory, "Persistent cache manual put entry");
+    }
+
+    @Override
+    public CompositeData getInvalidateOneRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(invalidateOneRateHistory, "Persistent cache invalidate one entry");
+    }
+
+    @Override
+    public CompositeData getInvalidateAllRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(invalidateAllRateHistory, "Persistent cache invalidate all entries");
+    }
+
+    @Override
+    public CompositeData getBroadcastRecvRateHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(broadcastRecvRateHistory, "Persistent cache entries received from broadcast");
+    }
+
+    @Override
+    public CompositeData getUsedSpaceHistory() {
+        return TimeSeriesStatsUtil.asCompositeData(usedSpaceByteCounterHistory, "Persistent cache estimated size (bytes)");
+    }
+
+    @Override
+    public String cacheInfoAsString() {
+        return Objects.toStringHelper("PersistentCacheStats")
+                .add("requestCount", getRequestCount())
+                .add("hitCount", getHitCount())
+                .add("hitRate", String.format("%1.2f", getHitRate()))
+                .add("missCount", getMissCount())
+                .add("missRate", String.format("%1.2f", getMissRate()))
+                .add("loadCount", getLoadCount())
+                .add("loadSuccessCount", getLoadSuccessCount())
+                .add("loadExceptionCount", getLoadExceptionCount())
+                .add("totalWeight", IOUtils.humanReadableByteCount(estimateCurrentWeight()))
+                .toString();
+    }
+
+    //~--------------------------------------< CacheStatsMbean - stats that are not (yet) available
+    @Override
+    public long getTotalLoadTime() {
+        return 0;
+    }
+
+    @Override
+    public double getAverageLoadPenalty() {
+        return 0;
+    }
+
+    @Override
+    public long getEvictionCount() {
+        return 0;
+    }
+
+    @Override
+    public long getElementCount() {
+        return 0;
+    }
+
+    @Override
+    public long getMaxTotalWeight() {
+        return Long.MAX_VALUE;
+    }
+
+    @Override
+    public void resetStats() {
+    }
+
+
+    //~--------------------------------------< private helpers
+
+    private static String getStatName(String meter, String cacheName) {
+        return cacheName + "." + meter;
+    }
+
+    private TimeSeries getTimeSeries(String name) {
+        return statisticsProvider.getStats().getTimeSeries(name, true);
+    }
+
+    private TimeSeries getTimeSeries(String name, boolean resetValues) {
+        return statisticsProvider.getStats().getTimeSeries(name, resetValues);
+    }
+
+
+    /**
+     * TimeSeries that computes the hit ratio in percentages. ( hit/total * 100 )
+     */
+    private static class PercentageTimeSeries implements TimeSeries {
+
+        private TimeSeries hit, total;
+
+        PercentageTimeSeries(TimeSeries hit, TimeSeries total) {
+            this.hit = hit;
+            this.total = total;
+        }
+
+        @Override
+        public long[] getValuePerSecond() {
+            return percentage(hit.getValuePerSecond(), total.getValuePerSecond());
+        }
+
+        @Override
+        public long[] getValuePerMinute() {
+            return percentage(hit.getValuePerMinute(), total.getValuePerMinute());
+        }
+
+        @Override
+        public long[] getValuePerHour() {
+            return percentage(hit.getValuePerHour(), total.getValuePerHour());
+        }
+
+        @Override
+        public long[] getValuePerWeek() {
+            return percentage(hit.getValuePerWeek(), total.getValuePerWeek());
+        }
+
+        @Override
+        public long getMissingValue() {
+            return 0;
+        }
+
+        private static long[] percentage(long[] a, long[] b) {
+            long[] result = new long[a.length];
+            for (int i=0; i<a.length; ++i) {
+                if (b[i] == 0) {
+                    result[i] = 0;
+                }
+                else {
+                    result[i] = (a[i] * 100) / b[i];
+                }
+            }
+            return result;
+        }
+    }
+
+    /**
+     * TimeSeries as a difference between two other TimeSeries
+     */
+    private static class DifferenceTimeSeries implements TimeSeries {
+
+        private TimeSeries tsA, tsB;
+
+        DifferenceTimeSeries(TimeSeries tsA, TimeSeries tsB) {
+            this.tsA = tsA;
+            this.tsB = tsB;
+        }
+
+        @Override
+        public long[] getValuePerSecond() {
+            return difference(tsA.getValuePerSecond(), tsB.getValuePerSecond());
+        }
+
+        @Override
+        public long[] getValuePerMinute() {
+            return difference(tsA.getValuePerMinute(), tsB.getValuePerMinute());
+        }
+
+        @Override
+        public long[] getValuePerHour() {
+            return difference(tsA.getValuePerHour(), tsB.getValuePerHour());
+        }
+
+        @Override
+        public long[] getValuePerWeek() {
+            return difference(tsA.getValuePerWeek(), tsB.getValuePerWeek());
+        }
+
+        @Override
+        public long getMissingValue() {
+            return 0;
+        }
+
+        private static long[] difference(long[] a, long[] b) {
+            long[] result = new long[a.length];
+            for (int i=0; i<a.length; ++i) {
+                result[i] = a[i] - b[i];
+            }
+            return result;
+        }
+    }
+}
\ No newline at end of file