Browse Source

statistics performance improvements

Jason Rivard 4 years ago
parent
commit
f522dabd22
22 changed files with 353 additions and 284 deletions
  1. 1 2
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  2. 2 2
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStageProcessor.java
  3. 2 2
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  4. 6 2
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationOptionsBean.java
  5. 9 9
      server/src/main/java/password/pwm/ldap/UserInfoBean.java
  6. 5 4
      server/src/main/java/password/pwm/svc/cache/CacheService.java
  7. 10 1
      server/src/main/java/password/pwm/svc/cache/CacheStore.java
  8. 0 72
      server/src/main/java/password/pwm/svc/cache/CacheStoreInfo.java
  9. 9 9
      server/src/main/java/password/pwm/svc/cache/MemoryCacheStore.java
  10. 4 3
      server/src/main/java/password/pwm/svc/report/ReportService.java
  11. 97 100
      server/src/main/java/password/pwm/svc/report/ReportSummaryData.java
  12. 7 6
      server/src/main/java/password/pwm/svc/report/UserCacheService.java
  13. 3 3
      server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java
  14. 9 9
      server/src/main/java/password/pwm/svc/wordlist/WordlistStatistics.java
  15. 4 4
      server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java
  16. 141 0
      server/src/main/java/password/pwm/util/java/Memorizer.java
  17. 15 5
      server/src/main/java/password/pwm/util/java/StatisticCounterBundle.java
  18. 16 5
      server/src/main/java/password/pwm/util/localdb/LocalDBAdaptor.java
  19. 0 32
      server/src/main/java/password/pwm/util/localdb/LocalDBStatistics.java
  20. 7 7
      server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java
  21. 1 1
      server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java
  22. 5 6
      server/src/main/java/password/pwm/ws/server/RestAuthenticationProcessor.java

+ 1 - 2
server/src/main/java/password/pwm/health/LDAPHealthChecker.java

@@ -74,7 +74,6 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -705,7 +704,7 @@ public class LDAPHealthChecker implements HealthChecker
         }
 
         final ArrayList<HealthRecord> healthRecords = new ArrayList<>();
-        final Set<DirectoryVendor> discoveredVendors = EnumSet.copyOf( replicaVendorMap.values() );
+        final Set<DirectoryVendor> discoveredVendors = JavaHelper.copiedEnumSet( replicaVendorMap.values(), DirectoryVendor.class );
 
         if ( discoveredVendors.size() >= 2 )
         {

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStageProcessor.java

@@ -38,13 +38,13 @@ import password.pwm.http.bean.ForgottenPasswordBean;
 import password.pwm.http.bean.ForgottenPasswordStage;
 import password.pwm.ldap.UserInfo;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.password.PasswordUtility;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -235,7 +235,7 @@ class ForgottenPasswordStageProcessor
 
                     // for rest method, fail if any required methods are not supported
                     {
-                        final Set<IdentityVerificationMethod> tempSet = EnumSet.copyOf( remainingAvailableOptionalMethods );
+                        final Set<IdentityVerificationMethod> tempSet = JavaHelper.copiedEnumSet( remainingAvailableOptionalMethods, IdentityVerificationMethod.class );
                         tempSet.removeAll( ForgottenPasswordStateMachine.supportedVerificationMethods() );
                         if ( !tempSet.isEmpty() )
                         {

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -67,6 +67,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
@@ -77,7 +78,6 @@ import javax.servlet.ServletException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -859,7 +859,7 @@ public class ForgottenPasswordUtil
                     commonValues,
                     forgottenPasswordBean
             );
-            final Set<IdentityVerificationMethod> otherOptionalMethodChoices = EnumSet.copyOf( remainingAvailableOptionalMethods );
+            final Set<IdentityVerificationMethod> otherOptionalMethodChoices = JavaHelper.copiedEnumSet( remainingAvailableOptionalMethods, IdentityVerificationMethod.class );
             otherOptionalMethodChoices.remove( thisMethod );
 
             if ( !otherOptionalMethodChoices.isEmpty() )

+ 6 - 2
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationOptionsBean.java

@@ -175,12 +175,16 @@ public class HelpdeskVerificationOptionsBean implements Serializable
         {
             final Map<VerificationMethodValue.EnabledState, Collection<IdentityVerificationMethod>> returnMap = new HashMap<>();
             {
-                final Set<IdentityVerificationMethod> optionalMethods = EnumSet.copyOf( helpdeskProfile.readOptionalVerificationMethods() );
+                final Set<IdentityVerificationMethod> optionalMethods = JavaHelper.copiedEnumSet(
+                        helpdeskProfile.readOptionalVerificationMethods(),
+                        IdentityVerificationMethod.class );
                 optionalMethods.removeAll( unavailableMethods );
                 returnMap.put( VerificationMethodValue.EnabledState.optional, optionalMethods );
             }
             {
-                final Set<IdentityVerificationMethod> requiredMethods = EnumSet.copyOf( helpdeskProfile.readRequiredVerificationMethods() );
+                final Set<IdentityVerificationMethod> requiredMethods = JavaHelper.copiedEnumSet(
+                        helpdeskProfile.readRequiredVerificationMethods(),
+                        IdentityVerificationMethod.class );
                 requiredMethods.removeAll( unavailableMethods );
                 returnMap.put( VerificationMethodValue.EnabledState.required, requiredMethods );
             }

+ 9 - 9
server/src/main/java/password/pwm/ldap/UserInfoBean.java

@@ -23,6 +23,7 @@ package password.pwm.ldap;
 import com.novell.ldapchai.impl.edir.entry.EdirEntries;
 import lombok.Builder;
 import lombok.Getter;
+import lombok.Singular;
 import password.pwm.bean.PasswordStatus;
 import password.pwm.bean.ResponseInfoBean;
 import password.pwm.bean.UserIdentity;
@@ -35,7 +36,6 @@ import password.pwm.util.operations.otp.OTPUserRecord;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -59,14 +59,14 @@ public class UserInfoBean implements UserInfo
     /**
      * A listing of all readable attributes on the ldap user object.
      */
-    @Builder.Default
-    private final Map<String, String> cachedPasswordRuleAttributes = Collections.emptyMap();
+    @Singular
+    private final Map<String, String> cachedPasswordRuleAttributes;
 
-    @Builder.Default
-    private final Map<String, String> cachedAttributeValues = Collections.emptyMap();
+    @Singular
+    private final Map<String, String> cachedAttributeValues;
 
-    @Builder.Default
-    private final Map<ProfileDefinition, String> profileIDs = new HashMap<>();
+    @Singular
+    private final Map<ProfileDefinition, String> profileIDs;
 
     @Builder.Default
     private final PasswordStatus passwordStatus = PasswordStatus.builder().build();
@@ -97,8 +97,8 @@ public class UserInfoBean implements UserInfo
     private final boolean requiresInteraction;
     private final boolean withinPasswordMinimumLifetime;
 
-    @Builder.Default
-    private Map<String, String> attributes = Collections.emptyMap();
+    @Singular
+    private final Map<String, String> attributes;
 
     @Override
     public String readStringAttribute( final String attribute ) throws PwmUnrecoverableException

+ 5 - 4
server/src/main/java/password/pwm/svc/cache/CacheService.java

@@ -28,6 +28,7 @@ import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StatisticCounterBundle;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -102,7 +103,7 @@ public class CacheService implements PwmService
         final Map<String, String> debugInfo = new TreeMap<>( );
         debugInfo.put( "itemCount", String.valueOf( memoryCacheStore.itemCount() ) );
         debugInfo.put( "byteCount", String.valueOf( memoryCacheStore.byteCount() ) );
-        debugInfo.putAll( JsonUtil.deserializeStringMap( JsonUtil.serialize( memoryCacheStore.getCacheStoreInfo() ) ) );
+        debugInfo.putAll( JsonUtil.deserializeStringMap( JsonUtil.serializeMap( memoryCacheStore.getCacheStoreInfo().debugStats() ) ) );
         debugInfo.putAll( JsonUtil.deserializeStringMap( JsonUtil.serializeMap( memoryCacheStore.storedClassHistogram( "histogram." ) ) ) );
         return ServiceInfoBean.builder().debugProperties( debugInfo ).build();
     }
@@ -110,7 +111,7 @@ public class CacheService implements PwmService
     public Map<String, Serializable> debugInfo( )
     {
         final Map<String, Serializable> debugInfo = new LinkedHashMap<>( );
-        debugInfo.put( "memory-statistics", memoryCacheStore.getCacheStoreInfo() );
+        debugInfo.put( "memory-statistics", JsonUtil.serializeMap( memoryCacheStore.getCacheStoreInfo().debugStats() ) );
         debugInfo.put( "memory-items", new ArrayList<Serializable>( memoryCacheStore.getCacheDebugItems() ) );
         debugInfo.put( "memory-histogram", new HashMap<>( memoryCacheStore.storedClassHistogram( "" ) ) );
         return Collections.unmodifiableMap( debugInfo );
@@ -179,9 +180,9 @@ public class CacheService implements PwmService
         final StringBuilder traceOutput = new StringBuilder();
         if ( memoryCacheStore != null )
         {
-            final CacheStoreInfo info = memoryCacheStore.getCacheStoreInfo();
+            final StatisticCounterBundle<CacheStore.DebugKey> info = memoryCacheStore.getCacheStoreInfo();
             traceOutput.append( "memCache=" );
-            traceOutput.append( JsonUtil.serialize( info ) );
+            traceOutput.append( JsonUtil.serializeMap( info.debugStats() ) );
             traceOutput.append( ", histogram=" );
             traceOutput.append( JsonUtil.serializeMap( memoryCacheStore.storedClassHistogram( "" ) ) );
         }

+ 10 - 1
server/src/main/java/password/pwm/svc/cache/CacheStore.java

@@ -21,6 +21,7 @@
 package password.pwm.svc.cache;
 
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.StatisticCounterBundle;
 
 import java.io.Serializable;
 import java.time.Instant;
@@ -35,11 +36,19 @@ public interface CacheStore
 
     <T extends Serializable> T read( CacheKey cacheKey, Class<T> classOfT ) throws PwmUnrecoverableException;
 
-    CacheStoreInfo getCacheStoreInfo( );
+    StatisticCounterBundle<DebugKey> getCacheStoreInfo( );
 
     int itemCount( );
 
     List<CacheDebugItem> getCacheDebugItems( );
 
     long byteCount();
+
+    enum DebugKey
+    {
+        storeCount,
+        readCount,
+        hitCount,
+        missCount,
+    }
 }

+ 0 - 72
server/src/main/java/password/pwm/svc/cache/CacheStoreInfo.java

@@ -1,72 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2020 The PWM Project
- *
- * Licensed 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 password.pwm.svc.cache;
-
-import java.io.Serializable;
-import java.util.concurrent.atomic.AtomicLong;
-
-public class CacheStoreInfo implements Serializable
-{
-    private final AtomicLong storeCount = new AtomicLong();
-    private final AtomicLong readCount = new AtomicLong();
-    private final AtomicLong hitCount = new AtomicLong();
-    private final AtomicLong missCount = new AtomicLong();
-
-    public void incrementStoreCount( )
-    {
-        storeCount.incrementAndGet();
-    }
-
-    public void incrementReadCount( )
-    {
-        readCount.incrementAndGet();
-    }
-
-    public void incrementHitCount( )
-    {
-        hitCount.incrementAndGet();
-    }
-
-    public void incrementMissCount( )
-    {
-        missCount.incrementAndGet();
-    }
-
-    public long getStoreCount( )
-    {
-        return storeCount.get();
-    }
-
-    public long getReadCount( )
-    {
-        return readCount.get();
-    }
-
-    public long getHitCount( )
-    {
-        return hitCount.get();
-    }
-
-    public long getMissCount( )
-    {
-        return missCount.get();
-    }
-}

+ 9 - 9
server/src/main/java/password/pwm/svc/cache/MemoryCacheStore.java

@@ -26,6 +26,7 @@ import lombok.Value;
 import password.pwm.bean.UserIdentity;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StatisticCounterBundle;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
@@ -41,7 +42,7 @@ class MemoryCacheStore implements CacheStore
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( MemoryCacheStore.class );
     private final Cache<CacheKey, CacheValueWrapper> memoryStore;
-    private final CacheStoreInfo cacheStoreInfo = new CacheStoreInfo();
+    private final StatisticCounterBundle<DebugKey> cacheStoreInfo = new StatisticCounterBundle<>( DebugKey.class );
 
     MemoryCacheStore( final int maxItems )
     {
@@ -54,8 +55,7 @@ class MemoryCacheStore implements CacheStore
     public void store( final CacheKey cacheKey, final Instant expirationDate, final Serializable data )
             throws PwmUnrecoverableException
     {
-        cacheStoreInfo.incrementStoreCount();
-
+        cacheStoreInfo.increment( DebugKey.storeCount );
         final String jsonData = JsonUtil.serialize( data );
         memoryStore.put( cacheKey, new CacheValueWrapper( cacheKey, expirationDate, jsonData ) );
     }
@@ -64,7 +64,7 @@ class MemoryCacheStore implements CacheStore
     public <T extends Serializable> T readAndStore( final CacheKey cacheKey, final Instant expirationDate, final Class<T> classOfT, final CacheLoader<T> cacheLoader )
             throws PwmUnrecoverableException
     {
-        cacheStoreInfo.incrementReadCount();
+        cacheStoreInfo.increment( DebugKey.readCount );
         {
             final CacheValueWrapper valueWrapper = memoryStore.getIfPresent( cacheKey );
             final T extractedValue = extractValue( classOfT, valueWrapper, cacheKey );
@@ -76,7 +76,7 @@ class MemoryCacheStore implements CacheStore
 
         final T data = cacheLoader.read();
         final String jsonIfiedData = JsonUtil.serialize( data );
-        cacheStoreInfo.incrementMissCount();
+        cacheStoreInfo.increment( DebugKey.missCount );
         memoryStore.put( cacheKey, new CacheValueWrapper( cacheKey, expirationDate, jsonIfiedData ) );
         return data;
     }
@@ -89,7 +89,7 @@ class MemoryCacheStore implements CacheStore
             {
                 if ( valueWrapper.getExpirationDate().isAfter( Instant.now() ) )
                 {
-                    cacheStoreInfo.incrementHitCount();
+                    cacheStoreInfo.increment( DebugKey.hitCount );
                     final String jsonValue  = valueWrapper.getPayload();
                     return JsonUtil.deserialize( jsonValue, classOfT );
                 }
@@ -102,7 +102,7 @@ class MemoryCacheStore implements CacheStore
     @Override
     public <T extends Serializable> T read( final CacheKey cacheKey, final Class<T> classOfT )
     {
-        cacheStoreInfo.incrementReadCount();
+        cacheStoreInfo.increment( DebugKey.readCount );
         final CacheValueWrapper valueWrapper = memoryStore.getIfPresent( cacheKey );
         final T extractedValue = extractValue( classOfT, valueWrapper, cacheKey );
         if ( extractedValue != null )
@@ -111,12 +111,12 @@ class MemoryCacheStore implements CacheStore
         }
 
         memoryStore.invalidate( cacheKey );
-        cacheStoreInfo.incrementMissCount();
+        cacheStoreInfo.increment( DebugKey.missCount );
         return null;
     }
 
     @Override
-    public CacheStoreInfo getCacheStoreInfo( )
+    public StatisticCounterBundle<DebugKey> getCacheStoreInfo( )
     {
         return cacheStoreInfo;
     }

+ 4 - 3
server/src/main/java/password/pwm/svc/report/ReportService.java

@@ -59,6 +59,7 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Queue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicReference;
@@ -288,10 +289,10 @@ public class ReportService implements PwmService
                     while ( this.storageKeyIterator.hasNext() )
                     {
                         final UserCacheService.StorageKey key = this.storageKeyIterator.next();
-                        final UserCacheRecord returnBean = userCacheService.readStorageKey( key );
-                        if ( returnBean != null )
+                        final Optional<UserCacheRecord> returnBean = userCacheService.readStorageKey( key );
+                        if ( returnBean.isPresent() )
                         {
-                            return returnBean;
+                            return returnBean.get();
                         }
 
                     }

+ 97 - 100
server/src/main/java/password/pwm/svc/report/ReportSummaryData.java

@@ -32,51 +32,48 @@ import password.pwm.util.java.Percent;
 import password.pwm.util.java.PwmNumberFormat;
 import password.pwm.util.java.TimeDuration;
 
-import java.math.BigInteger;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.LongAdder;
 import java.util.stream.Collectors;
 
 @Value
 public class ReportSummaryData
 {
     private static final long MS_DAY = TimeDuration.DAY.asMillis();
-    private static final BigInteger TWO = new BigInteger( "2" );
-
-    private final AtomicInteger totalUsers = new AtomicInteger( 0 );
-    private final AtomicInteger hasResponses = new AtomicInteger( 0 );
-    private final AtomicInteger hasResponseSetTime = new AtomicInteger( 0 );
-    private final AtomicInteger hasHelpdeskResponses = new AtomicInteger( 0 );
-    private final AtomicInteger hasPasswordExpirationTime = new AtomicInteger( 0 );
-    private final AtomicInteger hasAccountExpirationTime = new AtomicInteger( 0 );
-    private final AtomicInteger hasLoginTime = new AtomicInteger( 0 );
-    private final AtomicInteger hasChangePwTime = new AtomicInteger( 0 );
-    private final AtomicInteger hasOtpSecret = new AtomicInteger( 0 );
-    private final AtomicInteger hasOtpSecretSetTime = new AtomicInteger( 0 );
-    private final AtomicInteger pwExpired = new AtomicInteger( 0 );
-    private final AtomicInteger pwPreExpired = new AtomicInteger( 0 );
-    private final AtomicInteger pwWarnPeriod = new AtomicInteger( 0 );
-    private final AtomicInteger hasReceivedPwExpireNotification = new AtomicInteger( 0 );
-
-    private final Map<DataStorageMethod, AtomicInteger> responseStorage = new ConcurrentHashMap<>();
-    private final Map<Answer.FormatType, AtomicInteger> responseFormatType = new ConcurrentHashMap<>();
-    private final Map<String, AtomicInteger> ldapProfile = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> pwExpireDays = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> accountExpireDays = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> changePwDays = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> responseSetDays = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> otpSetDays = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> loginDays = new ConcurrentHashMap<>();
-    private final Map<Integer, AtomicInteger> pwExpireNotificationDays = new ConcurrentHashMap<>();
+
+    private final LongAdder totalUsers = new LongAdder();
+    private final LongAdder hasResponses = new LongAdder();
+    private final LongAdder hasResponseSetTime = new LongAdder();
+    private final LongAdder hasHelpdeskResponses = new LongAdder();
+    private final LongAdder hasPasswordExpirationTime = new LongAdder();
+    private final LongAdder hasAccountExpirationTime = new LongAdder();
+    private final LongAdder hasLoginTime = new LongAdder();
+    private final LongAdder hasChangePwTime = new LongAdder();
+    private final LongAdder hasOtpSecret = new LongAdder();
+    private final LongAdder hasOtpSecretSetTime = new LongAdder();
+    private final LongAdder pwExpired = new LongAdder();
+    private final LongAdder pwPreExpired = new LongAdder();
+    private final LongAdder pwWarnPeriod = new LongAdder();
+    private final LongAdder hasReceivedPwExpireNotification = new LongAdder();
+
+    private final Map<DataStorageMethod, LongAdder> responseStorage = new ConcurrentHashMap<>();
+    private final Map<Answer.FormatType, LongAdder> responseFormatType = new ConcurrentHashMap<>();
+    private final Map<String, LongAdder> ldapProfile = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> pwExpireDays = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> accountExpireDays = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> changePwDays = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> responseSetDays = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> otpSetDays = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> loginDays = new ConcurrentHashMap<>();
+    private final Map<Integer, LongAdder> pwExpireNotificationDays = new ConcurrentHashMap<>();
 
     private ReportSummaryData( )
     {
@@ -90,82 +87,82 @@ public class ReportSummaryData
         {
             for ( final int day : trackedDays )
             {
-                reportSummaryData.pwExpireDays.put( day, new AtomicInteger( 0 ) );
-                reportSummaryData.accountExpireDays.put( day, new AtomicInteger( 0 ) );
-                reportSummaryData.changePwDays.put( day, new AtomicInteger( 0 ) );
-                reportSummaryData.responseSetDays.put( day, new AtomicInteger( 0 ) );
-                reportSummaryData.otpSetDays.put( day, new AtomicInteger( 0 ) );
-                reportSummaryData.loginDays.put( day, new AtomicInteger( 0 ) );
-                reportSummaryData.pwExpireNotificationDays.put( day, new AtomicInteger( 0 ) );
+                reportSummaryData.pwExpireDays.put( day, new LongAdder() );
+                reportSummaryData.accountExpireDays.put( day, new LongAdder() );
+                reportSummaryData.changePwDays.put( day, new LongAdder() );
+                reportSummaryData.responseSetDays.put( day, new LongAdder() );
+                reportSummaryData.otpSetDays.put( day, new LongAdder() );
+                reportSummaryData.loginDays.put( day, new LongAdder() );
+                reportSummaryData.pwExpireNotificationDays.put( day, new LongAdder() );
             }
         }
 
         return reportSummaryData;
     }
 
-    public Map<DataStorageMethod, Integer> getResponseStorage( )
+    public Map<DataStorageMethod, Long> getResponseStorage( )
     {
         return Collections.unmodifiableMap( responseStorage.entrySet()
                 .stream()
                 .collect( Collectors.toMap( Map.Entry::getKey,
-                        e -> e.getValue().get() ) ) );
+                        e -> e.getValue().sum() ) ) );
     }
 
-    public Map<Answer.FormatType, Integer> getResponseFormatType( )
+    public Map<Answer.FormatType, Long> getResponseFormatType( )
     {
         return Collections.unmodifiableMap( responseFormatType.entrySet()
                 .stream()
                 .collect( Collectors.toMap( Map.Entry::getKey,
-                        e -> e.getValue().get() ) ) );
+                        e -> e.getValue().sum() ) ) );
     }
 
     void update( final UserCacheRecord userCacheRecord )
     {
-        totalUsers.incrementAndGet();
+        totalUsers.increment();
 
         if ( userCacheRecord.isHasResponses() )
         {
-            hasResponses.incrementAndGet();
+            hasResponses.increment();
         }
 
         if ( userCacheRecord.isHasHelpdeskResponses() )
         {
-            hasHelpdeskResponses.incrementAndGet();
+            hasHelpdeskResponses.increment();
         }
 
         if ( userCacheRecord.getResponseSetTime() != null )
         {
-            hasResponseSetTime.incrementAndGet();
+            hasResponseSetTime.increment();
             incrementIfWithinTimeWindow( userCacheRecord, responseSetDays );
         }
 
         if ( userCacheRecord.getPasswordExpirationTime() != null )
         {
-            hasPasswordExpirationTime.incrementAndGet();
+            hasPasswordExpirationTime.increment();
             incrementIfWithinTimeWindow( userCacheRecord, pwExpireDays );
         }
 
         if ( userCacheRecord.getAccountExpirationTime() != null )
         {
-            hasAccountExpirationTime.incrementAndGet();
+            hasAccountExpirationTime.increment();
             incrementIfWithinTimeWindow( userCacheRecord, accountExpireDays );
         }
 
         if ( userCacheRecord.getLastLoginTime() != null )
         {
-            hasLoginTime.incrementAndGet();
+            hasLoginTime.increment();
             incrementIfWithinTimeWindow( userCacheRecord, loginDays );
         }
 
         if ( userCacheRecord.getPasswordChangeTime() != null )
         {
-            hasChangePwTime.incrementAndGet();
+            hasChangePwTime.increment();
             incrementIfWithinTimeWindow( userCacheRecord, changePwDays );
         }
 
         if ( userCacheRecord.getPasswordExpirationNoticeSendTime() != null )
         {
-            hasReceivedPwExpireNotification.incrementAndGet();
+            hasReceivedPwExpireNotification.increment();
             incrementIfWithinTimeWindow( userCacheRecord, pwExpireNotificationDays );
         }
 
@@ -173,23 +170,23 @@ public class ReportSummaryData
         {
             if ( userCacheRecord.getPasswordStatus().isExpired() )
             {
-                pwExpired.incrementAndGet();
+                pwExpired.increment();
             }
             if ( userCacheRecord.getPasswordStatus().isPreExpired() )
             {
-                pwPreExpired.incrementAndGet();
+                pwPreExpired.increment();
             }
             if ( userCacheRecord.getPasswordStatus().isWarnPeriod() )
             {
-                pwWarnPeriod.incrementAndGet();
+                pwWarnPeriod.increment();
             }
         }
 
         if ( userCacheRecord.getResponseStorageMethod() != null )
         {
             final DataStorageMethod method = userCacheRecord.getResponseStorageMethod();
-            responseStorage.putIfAbsent( method, new AtomicInteger( 0 ) );
-            responseStorage.get( method ).incrementAndGet();
+            responseStorage.computeIfAbsent( method, dataStorageMethod -> new LongAdder() );
+            responseStorage.get( method ).increment();
         }
 
         if ( userCacheRecord.getLdapProfile() != null )
@@ -197,41 +194,41 @@ public class ReportSummaryData
             final String userProfile = userCacheRecord.getLdapProfile();
             if ( !ldapProfile.containsKey( userProfile ) )
             {
-                ldapProfile.put( userProfile, new AtomicInteger( 0 ) );
+                ldapProfile.put( userProfile, new LongAdder() );
             }
-            ldapProfile.get( userProfile ).incrementAndGet();
+            ldapProfile.get( userProfile ).increment();
         }
 
         if ( userCacheRecord.getResponseFormatType() != null )
         {
             final Answer.FormatType type = userCacheRecord.getResponseFormatType();
-            responseFormatType.putIfAbsent( type, new AtomicInteger( 0 ) );
-            responseFormatType.get( type ).incrementAndGet();
+            responseFormatType.computeIfAbsent( type, formatType -> new LongAdder() );
+            responseFormatType.get( type ).increment();
         }
 
         if ( userCacheRecord.isHasOtpSecret() )
         {
-            hasOtpSecret.incrementAndGet();
+            hasOtpSecret.increment();
         }
 
         if ( userCacheRecord.getOtpSecretSetTime() != null )
         {
-            hasOtpSecretSetTime.incrementAndGet();
+            hasOtpSecretSetTime.increment();
             incrementIfWithinTimeWindow( userCacheRecord, otpSetDays );
         }
     }
 
     private void incrementIfWithinTimeWindow(
             final UserCacheRecord userCacheRecord,
-            final Map<Integer, AtomicInteger> map
+            final Map<Integer, LongAdder> map
     )
     {
-        for ( final Map.Entry<Integer, AtomicInteger> entry : map.entrySet() )
+        for ( final Map.Entry<Integer, LongAdder> entry : map.entrySet() )
         {
             final int day = entry.getKey();
             final Instant eventDate = userCacheRecord.getOtpSecretSetTime();
             final long timeWindow = MS_DAY * day;
-            final AtomicInteger number = entry.getValue();
+            final LongAdder number = entry.getValue();
 
             if ( eventDate != null )
             {
@@ -243,7 +240,7 @@ public class ReportSummaryData
                                 || ( timeWindow < 0 && eventDate.isBefore( Instant.now() ) && eventDifference.isShorterThan( timeBoundary ) )
                 )
                 {
-                    number.incrementAndGet();
+                    number.increment();
                 }
             }
         }
@@ -253,20 +250,20 @@ public class ReportSummaryData
     public List<PresentationRow> asPresentableCollection( final Configuration config, final Locale locale )
     {
         final ArrayList<PresentationRow> returnCollection = new ArrayList<>();
-        final PresentationRowBuilder builder = new PresentationRowBuilder( config, this.totalUsers.get(), locale );
+        final PresentationRowBuilder builder = new PresentationRowBuilder( config, this.totalUsers.sum(), locale );
 
-        returnCollection.add( builder.makeNoPctRow( "Field_Report_Sum_Total", this.totalUsers.get(), null ) );
-        if ( totalUsers.get() == 0 )
+        returnCollection.add( builder.makeNoPctRow( "Field_Report_Sum_Total", this.totalUsers.sum(), null ) );
+        if ( totalUsers.sum() == 0 )
         {
             return returnCollection;
         }
 
         if ( config.getLdapProfiles().keySet().size() > 1 )
         {
-            for ( final Map.Entry<String, AtomicInteger> entry : new TreeMap<>( ldapProfile ).entrySet() )
+            for ( final Map.Entry<String, LongAdder> entry : new TreeMap<>( ldapProfile ).entrySet() )
             {
                 final String userProfile = entry.getKey();
-                final int count = entry.getValue().get();
+                final long count = entry.getValue().sum();
                 final String displayName = config.getLdapProfiles().containsKey( userProfile )
                         ? config.getLdapProfiles().get( userProfile ).getDisplayName( locale )
                         : userProfile;
@@ -275,83 +272,83 @@ public class ReportSummaryData
             }
         }
 
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveLoginTime", this.hasLoginTime.get() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveLoginTime", this.hasLoginTime.sum() ) );
         for ( final Integer day : new TreeSet<>( loginDays.keySet() ) )
         {
             if ( day < 0 )
             {
-                returnCollection.add( builder.makeRow( "Field_Report_Sum_LoginTimePrevious", this.loginDays.get( day ).get(), String.valueOf( Math.abs( day ) ) ) );
+                returnCollection.add( builder.makeRow( "Field_Report_Sum_LoginTimePrevious", this.loginDays.get( day ).sum(), String.valueOf( Math.abs( day ) ) ) );
             }
         }
 
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveAccountExpirationTime", this.hasAccountExpirationTime.get() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveAccountExpirationTime", this.hasAccountExpirationTime.sum() ) );
         for ( final Integer day : new TreeSet<>( accountExpireDays.keySet() ) )
         {
             final String key = day < 0 ? "Field_Report_Sum_AccountExpirationPrevious" : "Field_Report_Sum_AccountExpirationNext";
-            returnCollection.add( builder.makeRow( key, this.accountExpireDays.get( day ).get(), String.valueOf( Math.abs( day ) ) ) );
+            returnCollection.add( builder.makeRow( key, this.accountExpireDays.get( day ).sum(), String.valueOf( Math.abs( day ) ) ) );
         }
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HavePwExpirationTime", this.hasPasswordExpirationTime.get() ) );
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveExpiredPw", this.pwExpired.get() ) );
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HavePreExpiredPw", this.pwPreExpired.get() ) );
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveExpiredPwWarn", this.pwWarnPeriod.get() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HavePwExpirationTime", this.hasPasswordExpirationTime.sum() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveExpiredPw", this.pwExpired.sum() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HavePreExpiredPw", this.pwPreExpired.sum() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveExpiredPwWarn", this.pwWarnPeriod.sum() ) );
         for ( final Integer day : new TreeSet<>( pwExpireDays.keySet() ) )
         {
             final String key = day < 0 ? "Field_Report_Sum_PwExpirationPrevious" : "Field_Report_Sum_PwExpirationNext";
-            returnCollection.add( builder.makeRow( key, this.pwExpireDays.get( day ).get(), String.valueOf( Math.abs( day ) ) ) );
+            returnCollection.add( builder.makeRow( key, this.pwExpireDays.get( day ).sum(), String.valueOf( Math.abs( day ) ) ) );
         }
 
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveChgPw", this.hasChangePwTime.get() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveChgPw", this.hasChangePwTime.sum() ) );
         for ( final Integer day : new TreeSet<>( changePwDays.keySet() ) )
         {
             if ( day < 0 )
             {
-                returnCollection.add( builder.makeRow( "Field_Report_Sum_ChgPwPrevious", this.changePwDays.get( day ).get(), String.valueOf( Math.abs( day ) ) ) );
+                returnCollection.add( builder.makeRow( "Field_Report_Sum_ChgPwPrevious", this.changePwDays.get( day ).sum(), String.valueOf( Math.abs( day ) ) ) );
             }
         }
 
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveResponses", this.hasResponses.get() ) );
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveHelpdeskResponses", this.hasHelpdeskResponses.get() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveResponses", this.hasResponses.sum() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveHelpdeskResponses", this.hasHelpdeskResponses.sum() ) );
         for ( final DataStorageMethod storageMethod : JavaHelper.copiedEnumSet( this.getResponseStorage().keySet(), DataStorageMethod.class ) )
         {
-            final int count = this.getResponseStorage().get( storageMethod );
+            final long count = this.getResponseStorage().get( storageMethod );
             returnCollection.add( builder.makeRow( "Field_Report_Sum_StorageMethod", count, storageMethod.toString() ) );
         }
-        for ( final Answer.FormatType formatType : EnumSet.copyOf( this.getResponseFormatType().keySet() ) )
+        for ( final Answer.FormatType formatType : JavaHelper.copiedEnumSet( this.getResponseFormatType().keySet(), Answer.FormatType.class ) )
         {
-            final int count = this.getResponseFormatType().get( formatType );
+            final long count = this.getResponseFormatType().get( formatType );
             returnCollection.add( builder.makeRow( "Field_Report_Sum_ResponseFormatType", count, formatType.toString() ) );
         }
 
-        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveResponseTime", this.hasResponseSetTime.get() ) );
+        returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveResponseTime", this.hasResponseSetTime.sum() ) );
         for ( final Integer day : new TreeSet<>( responseSetDays.keySet() ) )
         {
             if ( day < 0 )
             {
-                returnCollection.add( builder.makeRow( "Field_Report_Sum_ResponseTimePrevious", this.responseSetDays.get( day ).get(), String.valueOf( Math.abs( day ) ) ) );
+                returnCollection.add( builder.makeRow( "Field_Report_Sum_ResponseTimePrevious", this.responseSetDays.get( day ).sum(), String.valueOf( Math.abs( day ) ) ) );
             }
         }
 
-        if ( this.hasOtpSecret.get() > 0 )
+        if ( this.hasOtpSecret.sum() > 0 )
         {
-            returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveOtpSecret", this.hasOtpSecret.get() ) );
-            returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveOtpSecretSetTime", this.hasOtpSecretSetTime.get() ) );
+            returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveOtpSecret", this.hasOtpSecret.sum() ) );
+            returnCollection.add( builder.makeRow( "Field_Report_Sum_HaveOtpSecretSetTime", this.hasOtpSecretSetTime.sum() ) );
             for ( final Integer day : new TreeSet<>( otpSetDays.keySet() ) )
             {
                 if ( day < 0 )
                 {
-                    returnCollection.add( builder.makeRow( "Field_Report_Sum_OtpSecretTimePrevious", this.otpSetDays.get( day ).get(), String.valueOf( Math.abs( day ) ) ) );
+                    returnCollection.add( builder.makeRow( "Field_Report_Sum_OtpSecretTimePrevious", this.otpSetDays.get( day ).sum(), String.valueOf( Math.abs( day ) ) ) );
                 }
             }
         }
 
-        if ( this.hasReceivedPwExpireNotification.get() > 0 )
+        if ( this.hasReceivedPwExpireNotification.sum() > 0 )
         {
-            returnCollection.add( new PresentationRow( "Has Received PwExpiry Notice", Integer.toString( this.hasReceivedPwExpireNotification.get() ), null ) );
+            returnCollection.add( new PresentationRow( "Has Received PwExpiry Notice", Long.toString( this.hasReceivedPwExpireNotification.sum() ), null ) );
             for ( final Integer day : new TreeSet<>( pwExpireNotificationDays.keySet() ) )
             {
                 if ( day < 0 )
                 {
-                    returnCollection.add( new PresentationRow( "PwExpireNotice " + day, Integer.toString( this.pwExpireNotificationDays.get( day ).get() ), null ) );
+                    returnCollection.add( new PresentationRow( "PwExpireNotice " + day, Long.toString( this.pwExpireNotificationDays.get( day ).sum() ), null ) );
                 }
             }
         }
@@ -373,25 +370,25 @@ public class ReportSummaryData
     public static class PresentationRowBuilder
     {
         private final Configuration config;
-        private final int totalUsers;
+        private final long totalUsers;
         private final Locale locale;
 
-        PresentationRow makeRow( final String labelKey, final int valueCount )
+        PresentationRow makeRow( final String labelKey, final long valueCount )
         {
             return makeRow( labelKey, valueCount, null );
         }
 
-        PresentationRow makeRow( final String labelKey, final int valueCount, final String replacement )
+        PresentationRow makeRow( final String labelKey, final long valueCount, final String replacement )
         {
             return makeRowImpl( labelKey, valueCount, replacement );
         }
 
-        PresentationRow makeNoPctRow( final String labelKey, final int valueCount, final String replacement )
+        PresentationRow makeNoPctRow( final String labelKey, final long valueCount, final String replacement )
         {
             return makeRowImpl( labelKey, valueCount, replacement ).toBuilder().pct( null ).build();
         }
 
-        private PresentationRow makeRowImpl( final String labelKey, final int valueCount, final String replacement )
+        private PresentationRow makeRowImpl( final String labelKey, final long valueCount, final String replacement )
         {
             final String display = replacement == null
                     ? LocaleHelper.getLocalizedMessage( locale, labelKey, config, Admin.class )

+ 7 - 6
server/src/main/java/password/pwm/svc/report/UserCacheService.java

@@ -41,6 +41,7 @@ import password.pwm.util.secure.SecureService;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 public class UserCacheService implements PwmService
 {
@@ -81,7 +82,7 @@ public class UserCacheService implements PwmService
         return null;
     }
 
-    UserCacheRecord readStorageKey( final StorageKey storageKey ) throws LocalDBException
+    Optional<UserCacheRecord> readStorageKey( final StorageKey storageKey ) throws LocalDBException
     {
         return cacheStore.read( storageKey );
     }
@@ -115,7 +116,7 @@ public class UserCacheService implements PwmService
     public class UserStatusCacheBeanIterator<K extends StorageKey> implements ClosableIterator
     {
 
-        private LocalDB.LocalDBIterator<Map.Entry<String, String>> innerIterator;
+        private final LocalDB.LocalDBIterator<Map.Entry<String, String>> innerIterator;
 
         private UserStatusCacheBeanIterator( ) throws LocalDBException
         {
@@ -173,7 +174,7 @@ public class UserCacheService implements PwmService
 
     public static class StorageKey
     {
-        private String key;
+        private final String key;
 
         private StorageKey( final String key )
         {
@@ -229,7 +230,7 @@ public class UserCacheService implements PwmService
             localDB.put( DB, key.getKey(), jsonValue );
         }
 
-        private UserCacheRecord read( final StorageKey key )
+        private Optional<UserCacheRecord> read( final StorageKey key )
                 throws LocalDBException
         {
             final String jsonValue = localDB.get( DB, key.getKey() );
@@ -237,7 +238,7 @@ public class UserCacheService implements PwmService
             {
                 try
                 {
-                    return JsonUtil.deserialize( jsonValue, UserCacheRecord.class );
+                    return Optional.of( JsonUtil.deserialize( jsonValue, UserCacheRecord.class ) );
                 }
                 catch ( final JsonSyntaxException e )
                 {
@@ -245,7 +246,7 @@ public class UserCacheService implements PwmService
                     localDB.remove( DB, key.getKey() );
                 }
             }
-            return null;
+            return Optional.empty();
         }
 
         private boolean remove( final StorageKey key )

+ 3 - 3
server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java

@@ -195,7 +195,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService
     private boolean realBucketCheck( final String word, final WordType wordType )
             throws PwmUnrecoverableException
     {
-        getStatistics().getWordChecks().next();
+        getStatistics().getWordChecks().increment();
 
         final Instant startTime = Instant.now();
         final boolean isContainsWord = wordlistBucket.containsWord( word );
@@ -210,11 +210,11 @@ abstract class AbstractWordlist implements Wordlist, PwmService
 
         if ( isContainsWord )
         {
-            getStatistics().getWordTypeHits().get( wordType ).next();
+            getStatistics().getWordTypeHits().get( wordType ).increment();
         }
         else
         {
-            getStatistics().getMisses().next();
+            getStatistics().getMisses().increment();
         }
 
         return isContainsWord;

+ 9 - 9
server/src/main/java/password/pwm/svc/wordlist/WordlistStatistics.java

@@ -21,7 +21,6 @@
 package password.pwm.svc.wordlist;
 
 import lombok.Value;
-import password.pwm.util.java.AtomicLoopLongIncrementer;
 import password.pwm.util.java.MovingAverage;
 import password.pwm.util.java.TimeDuration;
 
@@ -29,21 +28,22 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.LongAdder;
 
 @Value
 class WordlistStatistics
 {
     private MovingAverage wordCheckTimeMS = new MovingAverage( TimeDuration.of( 5, TimeDuration.Unit.MINUTES ) );
     private MovingAverage chunksPerWordCheck = new MovingAverage( TimeDuration.of( 1, TimeDuration.Unit.DAYS ) );
-    private AtomicLoopLongIncrementer wordChecks = new AtomicLoopLongIncrementer();
-    private Map<WordType, AtomicLoopLongIncrementer> wordTypeHits = new HashMap<>(  );
-    private AtomicLoopLongIncrementer misses = new AtomicLoopLongIncrementer();
+    private LongAdder wordChecks = new LongAdder();
+    private Map<WordType, LongAdder> wordTypeHits = new HashMap<>(  );
+    private LongAdder misses = new LongAdder();
 
     WordlistStatistics()
     {
         for ( final WordType wordType : WordType.values() )
         {
-            wordTypeHits.put( wordType, new AtomicLoopLongIncrementer() );
+            wordTypeHits.put( wordType, new LongAdder() );
         }
     }
 
@@ -52,11 +52,11 @@ class WordlistStatistics
         final Map<String, String> outputMap = new TreeMap<>(  );
         outputMap.put( "AvgLocalDBWordCheckTimeMS", Double.toString( wordCheckTimeMS.getAverage() ) );
         outputMap.put( "ChunksPerCheck", Double.toString( chunksPerWordCheck.getAverage() ) );
-        outputMap.put( "LocalDBWordChecks", Long.toString( wordChecks.get() ) );
-        outputMap.put( "Misses", Long.toString( misses.get() ) );
-        for ( final Map.Entry<WordType, AtomicLoopLongIncrementer> entry : wordTypeHits.entrySet() )
+        outputMap.put( "LocalDBWordChecks", Long.toString( wordChecks.sum() ) );
+        outputMap.put( "Misses", Long.toString( misses.sum() ) );
+        for ( final Map.Entry<WordType, LongAdder> entry : wordTypeHits.entrySet() )
         {
-            outputMap.put( "Hits-" + entry.getKey().name(), Long.toString( entry.getValue().get() ) );
+            outputMap.put( "Hits-" + entry.getKey().name(), Long.toString( entry.getValue().sum() ) );
         }
         return Collections.unmodifiableMap( outputMap );
     }

+ 4 - 4
server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java

@@ -32,7 +32,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.Objects;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
@@ -45,7 +45,7 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
     private final ZipInputStream zipStream;
     private final CountingInputStream countingInputStream;
-    private final AtomicLong lineCounter = new AtomicLong( 0 );
+    private final LongAdder lineCounter = new LongAdder();
 
     private BufferedReader reader;
     private ZipEntry zipEntry;
@@ -138,7 +138,7 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
         if ( line != null )
         {
-            lineCounter.incrementAndGet();
+            lineCounter.increment();
         }
 
         return line;
@@ -146,7 +146,7 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
     long getLineCount()
     {
-        return lineCounter.get();
+        return lineCounter.sum();
     }
 
     long getByteCount()

+ 141 - 0
server/src/main/java/password/pwm/util/java/Memorizer.java

@@ -0,0 +1,141 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 The PWM Project
+ *
+ * Licensed 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 password.pwm.util.java;
+
+import lombok.Value;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Memorizer implements InvocationHandler
+{
+    private final StatisticCounterBundle<DebugStats> stats = new StatisticCounterBundle<>( DebugStats.class );
+
+    enum DebugStats
+    {
+        hits,
+        misses,
+    }
+
+    private final Object memorizedObject;
+
+    private final Map<MethodAndArgsKey, ValueWrapper> valueCache = new HashMap<>();
+
+    public static Object forObject( final Object memorizedObject )
+    {
+        if ( memorizedObject instanceof Memorizer )
+        {
+            return memorizedObject;
+        }
+
+        return Proxy.newProxyInstance(
+                memorizedObject.getClass().getClassLoader(),
+                memorizedObject.getClass().getInterfaces(),
+                new Memorizer( memorizedObject ) );
+    }
+
+    private Memorizer( final Object memorizedObject )
+    {
+        this.memorizedObject = memorizedObject;
+    }
+
+    @Override
+    public Object invoke( final Object object, final Method method, final Object[] args )
+            throws Throwable
+    {
+        if ( method.getReturnType().equals( Void.TYPE ) )
+        {
+            // Don't cache void methods
+            return realInvoke( method, args );
+        }
+        else
+        {
+            final MethodAndArgsKey key = new MethodAndArgsKey( method, args == null ? null : Arrays.asList( args ) );
+
+            ValueWrapper valueWrapper = valueCache.get( key );
+
+            // value is not in cache, so invoke method normally
+            if ( valueWrapper == null )
+            {
+                stats.increment( DebugStats.misses );
+                try
+                {
+                    final Object realValue = realInvoke( method, args );
+                    valueWrapper = new ValueWrapper( realValue );
+                    valueCache.put( key, valueWrapper );
+                }
+                catch ( final Exception e )
+                {
+                    throw e.getCause();
+                }
+            }
+            else
+            {
+                stats.increment( DebugStats.hits );
+            }
+
+            return valueWrapper.getValue();
+        }
+    }
+
+    public void outputStats()
+    {
+        System.out.println( stats.debugString() );
+    }
+
+    private Object realInvoke( final Method method, final Object[] args )
+    throws Throwable
+    {
+        try
+        {
+            return method.invoke( memorizedObject, args );
+        }
+        catch ( final IllegalAccessException | InvocationTargetException e )
+        {
+            throw e.getCause();
+        }
+        catch ( final Throwable t )
+        {
+            throw t;
+        }
+    }
+
+
+    @Value
+    private static class ValueWrapper
+    {
+        private final Object value;
+    }
+
+    @Value
+    private static class MethodAndArgsKey
+    {
+        private final Method method;
+        private final List<Object> args;
+    }
+
+}

+ 15 - 5
server/src/main/java/password/pwm/util/java/StatisticCounterBundle.java

@@ -26,13 +26,13 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.Map;
-import java.util.concurrent.atomic.LongAdder;
+import java.util.concurrent.atomic.LongAccumulator;
 import java.util.stream.Collectors;
 
 public class StatisticCounterBundle<K extends Enum<K>>
 {
     private final Class<K> keyType;
-    private final Map<K, LongAdder> statMap;
+    private final Map<K, LongAccumulator> statMap;
 
     public StatisticCounterBundle( final Class<K> keyType )
     {
@@ -42,13 +42,18 @@ public class StatisticCounterBundle<K extends Enum<K>>
 
     public void increment( final K stat )
     {
-        statMap.computeIfAbsent( stat, k -> new LongAdder() ).increment();
+        increment( stat, 1 );
+    }
+
+    public void increment( final K stat, final long amount )
+    {
+        statMap.computeIfAbsent( stat, k -> makeLongAccumulator() ).accumulate( amount );
     }
 
     public long get( final K stat )
     {
-        final LongAdder longAdder = statMap.get( stat );
-        return longAdder == null ? 0 : longAdder.sum();
+        final LongAccumulator longAdder = statMap.get( stat );
+        return longAdder == null ? 0 : longAdder.longValue();
     }
 
     public Map<String, String> debugStats()
@@ -65,4 +70,9 @@ public class StatisticCounterBundle<K extends Enum<K>>
         return StringHelper.stringMapToString( debugStats(), null );
     }
 
+    private static LongAccumulator makeLongAccumulator()
+    {
+        return new LongAccumulator( Long::sum, 0L );
+    }
+
 }

+ 16 - 5
server/src/main/java/password/pwm/util/localdb/LocalDBAdaptor.java

@@ -22,17 +22,26 @@ package password.pwm.util.localdb;
 
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
+import password.pwm.util.java.StatisticCounterBundle;
 
 import java.io.File;
 import java.io.Serializable;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 
 public class LocalDBAdaptor implements LocalDB
 {
     private final LocalDBProvider innerDB;
-    private final LocalDBStatistics localDBStatistics = new LocalDBStatistics();
+    private final StatisticCounterBundle<DebugKey> stats = new StatisticCounterBundle<>( DebugKey.class );
+
+    enum DebugKey
+    {
+        readOperations,
+        writeOperations,
+    }
 
     LocalDBAdaptor( final LocalDBProvider innerDB )
     {
@@ -86,7 +95,9 @@ public class LocalDBAdaptor implements LocalDB
 
     public Map<String, Serializable> debugInfo( )
     {
-        return innerDB.debugInfo();
+        final Map<String, Serializable> debugValues = new LinkedHashMap<>( innerDB.debugInfo() );
+        debugValues.putAll( stats.debugStats() );
+        return Collections.unmodifiableMap( debugValues );
     }
 
     @WriteOperation
@@ -198,7 +209,7 @@ public class LocalDBAdaptor implements LocalDB
     public void truncate( final DB db ) throws LocalDBException
     {
         ParameterValidator.validateDBValue( db );
-            innerDB.truncate( db );
+        innerDB.truncate( db );
     }
 
     public Status status( )
@@ -251,11 +262,11 @@ public class LocalDBAdaptor implements LocalDB
 
     private void markRead()
     {
-       this.localDBStatistics.getReadOperations().incrementAndGet();
+        stats.increment( DebugKey.readOperations );
     }
 
     private void markWrite( final int events )
     {
-        this.localDBStatistics.getWriteOperations().addAndGet( events );
+        stats.increment( DebugKey.writeOperations, events );
     }
 }

+ 0 - 32
server/src/main/java/password/pwm/util/localdb/LocalDBStatistics.java

@@ -1,32 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2020 The PWM Project
- *
- * Licensed 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 password.pwm.util.localdb;
-
-import lombok.Value;
-
-import java.util.concurrent.atomic.AtomicLong;
-
-@Value
-public class LocalDBStatistics
-{
-    private final AtomicLong readOperations = new AtomicLong( 0 );
-    private final AtomicLong writeOperations = new AtomicLong( 0 );
-}

+ 7 - 7
server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java

@@ -54,7 +54,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.TreeMap;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 import java.util.zip.ZipEntry;
@@ -96,7 +96,7 @@ public class LocalDBUtility
             throws PwmOperationalException
     {
         Objects.requireNonNull( outputStream );
-        final AtomicLong exportLineCounter = new AtomicLong( 0 );
+        final LongAdder exportLineCounter = new LongAdder();
 
         final long totalLines = countBackupableRecords( debugOutput );
 
@@ -106,7 +106,7 @@ public class LocalDBUtility
 
         final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
         final ConditionalTaskExecutor debugOutputter = ConditionalTaskExecutor.forPeriodicTask( () ->
-                        outputExportDebugStats( totalLines, exportLineCounter.get(), eventRateMeter, startTime, debugOutput ),
+                        outputExportDebugStats( totalLines, exportLineCounter.sum(), eventRateMeter, startTime, debugOutput ),
                 TimeDuration.MINUTE );
 
         try ( CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( new GZIPOutputStream( outputStream, GZIP_BUFFER_SIZE ) ) )
@@ -125,7 +125,7 @@ public class LocalDBUtility
                             final String key = entry.getKey();
                             final String value = entry.getValue();
                             csvPrinter.printRecord( loopDB.toString(), key, value );
-                            exportLineCounter.incrementAndGet();
+                            exportLineCounter.increment();
                             eventRateMeter.markEvents( 1 );
                             debugOutputter.conditionallyExecuteTask();
                         }
@@ -150,7 +150,7 @@ public class LocalDBUtility
 
         final long totalLines = localDB.size( LocalDB.DB.WORDLIST_WORDS );
 
-        final AtomicLong exportLineCounter = new AtomicLong( 0 );
+        final LongAdder exportLineCounter = new LongAdder();
 
         writeStringToOut( debugOutput, "Wordlist ZIP export beginning of "
                 + StringUtil.formatDiskSize( totalLines ) + " records" );
@@ -158,7 +158,7 @@ public class LocalDBUtility
 
         final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
         final ConditionalTaskExecutor debugOutputter = ConditionalTaskExecutor.forPeriodicTask( () ->
-                        outputExportDebugStats( totalLines, exportLineCounter.get(), eventRateMeter, startTime, debugOutput ),
+                        outputExportDebugStats( totalLines, exportLineCounter.sum(), eventRateMeter, startTime, debugOutput ),
                 TimeDuration.MINUTE );
 
         try ( ZipOutputStream zipOutputStream = new ZipOutputStream( outputStream, PwmConstants.DEFAULT_CHARSET ) )
@@ -172,7 +172,7 @@ public class LocalDBUtility
                     final String key = entry.getKey();
                     zipOutputStream.write( key.getBytes( PwmConstants.DEFAULT_CHARSET ) );
                     zipOutputStream.write( '\n' );
-                    exportLineCounter.incrementAndGet();
+                    exportLineCounter.increment();
                     eventRateMeter.markEvents( 1 );
                     debugOutputter.conditionallyExecuteTask();
                 }

+ 1 - 1
server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java

@@ -525,7 +525,7 @@ public class XodusLocalDB implements LocalDBProvider
                 final StatisticsItem item = statistics.getStatisticsItem( name );
                 if ( item != null )
                 {
-                    outputStats.put( name, String.valueOf( item.getTotal() ) );
+                    outputStats.put( "xodus." + name, String.valueOf( item.getTotal() ) );
                 }
             }
         }

+ 5 - 6
server/src/main/java/password/pwm/ws/server/RestAuthenticationProcessor.java

@@ -43,7 +43,6 @@ import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -75,10 +74,10 @@ public class RestAuthenticationProcessor
             if ( namedSecretName != null )
             {
                 LOGGER.trace( sessionLabel, () -> "authenticating with named secret '" + namedSecretName + "'" );
-                final Set<WebServiceUsage> usages = EnumSet.copyOf( JavaHelper.readEnumSetFromStringCollection(
+                final Set<WebServiceUsage> usages = JavaHelper.copiedEnumSet( JavaHelper.readEnumSetFromStringCollection(
                         WebServiceUsage.class,
                         pwmApplication.getConfig().readSettingAsNamedPasswords( PwmSetting.WEBSERVICES_EXTERNAL_SECRET ).get( namedSecretName ).getUsage()
-                ) );
+                ), WebServiceUsage.class );
                 return new RestAuthentication(
                         RestAuthenticationType.NAMED_SECRET,
                         namedSecretName,
@@ -120,7 +119,7 @@ public class RestAuthenticationProcessor
                         RestAuthenticationType.LDAP,
                         null,
                         userIdentity,
-                        Collections.unmodifiableSet( EnumSet.copyOf( WebServiceUsage.forType( RestAuthenticationType.LDAP ) ) ),
+                        Collections.unmodifiableSet( JavaHelper.copiedEnumSet( WebServiceUsage.forType( RestAuthenticationType.LDAP ), WebServiceUsage.class ) ),
                         thirdParty,
                         chaiProvider
                 );
@@ -128,8 +127,8 @@ public class RestAuthenticationProcessor
         }
 
         final Set<WebServiceUsage> publicUsages = WebServiceUsage.forType( RestAuthenticationType.PUBLIC );
-        final Set<WebServiceUsage> enabledUsages = EnumSet.copyOf(
-                pwmApplication.getConfig().readSettingAsOptionList( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, WebServiceUsage.class ) );
+        final Set<WebServiceUsage> enabledUsages = JavaHelper.copiedEnumSet(
+                pwmApplication.getConfig().readSettingAsOptionList( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, WebServiceUsage.class ), WebServiceUsage.class );
         enabledUsages.retainAll( publicUsages );
 
         return new RestAuthentication(