0
# Statistics
1
2
Caffeine provides comprehensive cache performance statistics through the `CacheStats` class and configurable `StatsCounter` implementations. Statistics enable monitoring cache effectiveness, identifying performance bottlenecks, and making informed tuning decisions.
3
4
## CacheStats Class
5
6
The `CacheStats` class provides an immutable snapshot of cache performance metrics.
7
8
```java { .api }
9
public final class CacheStats {
10
// Request metrics
11
public long requestCount();
12
public long hitCount();
13
public double hitRate();
14
public long missCount();
15
public double missRate();
16
17
// Load metrics
18
public long loadCount();
19
public long loadSuccessCount();
20
public long loadFailureCount();
21
public double loadFailureRate();
22
public double averageLoadPenalty();
23
public long totalLoadTime();
24
25
// Eviction metrics
26
public long evictionCount();
27
public long evictionWeight();
28
29
// Statistics operations
30
public CacheStats minus(CacheStats other);
31
public CacheStats plus(CacheStats other);
32
33
// Factory methods
34
public static CacheStats empty();
35
public static CacheStats of(long hitCount, long missCount, long loadSuccessCount,
36
long loadFailureCount, long totalLoadTime,
37
long evictionCount, long evictionWeight);
38
}
39
```
40
41
### Basic Statistics Usage
42
43
```java
44
Cache<String, String> statsCache = Caffeine.newBuilder()
45
.maximumSize(1000)
46
.recordStats()
47
.build();
48
49
// Perform cache operations
50
statsCache.put("key1", "value1");
51
statsCache.put("key2", "value2");
52
statsCache.getIfPresent("key1"); // hit
53
statsCache.getIfPresent("key3"); // miss
54
55
// Get statistics snapshot
56
CacheStats stats = statsCache.stats();
57
58
// Request statistics
59
System.out.println("Total requests: " + stats.requestCount()); // 2
60
System.out.println("Hits: " + stats.hitCount()); // 1
61
System.out.println("Hit rate: " + stats.hitRate()); // 0.5
62
System.out.println("Misses: " + stats.missCount()); // 1
63
System.out.println("Miss rate: " + stats.missRate()); // 0.5
64
```
65
66
### Load Statistics
67
68
```java
69
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
70
.maximumSize(1000)
71
.recordStats()
72
.build(key -> {
73
if (key.equals("error")) {
74
throw new RuntimeException("Load error");
75
}
76
Thread.sleep(100); // Simulate load time
77
return "loaded_" + key;
78
});
79
80
// Perform operations that trigger loading
81
loadingCache.get("key1"); // successful load
82
loadingCache.get("key2"); // successful load
83
try {
84
loadingCache.get("error"); // failed load
85
} catch (Exception e) {
86
// Handle load failure
87
}
88
89
CacheStats loadStats = loadingCache.stats();
90
91
// Load statistics
92
System.out.println("Load count: " + loadStats.loadCount()); // 3
93
System.out.println("Load successes: " + loadStats.loadSuccessCount()); // 2
94
System.out.println("Load failures: " + loadStats.loadFailureCount()); // 1
95
System.out.println("Load failure rate: " + loadStats.loadFailureRate()); // 0.33
96
System.out.println("Average load time (ns): " + loadStats.averageLoadPenalty()); // ~100ms in ns
97
System.out.println("Total load time (ns): " + loadStats.totalLoadTime());
98
```
99
100
### Eviction Statistics
101
102
```java
103
Cache<String, String> evictionCache = Caffeine.newBuilder()
104
.maximumSize(3)
105
.recordStats()
106
.build();
107
108
// Fill cache beyond capacity to trigger evictions
109
evictionCache.put("key1", "value1");
110
evictionCache.put("key2", "value2");
111
evictionCache.put("key3", "value3");
112
evictionCache.put("key4", "value4"); // Triggers eviction
113
evictionCache.put("key5", "value5"); // Triggers eviction
114
115
CacheStats evictionStats = evictionCache.stats();
116
117
System.out.println("Evictions: " + evictionStats.evictionCount()); // 2
118
System.out.println("Eviction weight: " + evictionStats.evictionWeight()); // 2 (default weight 1 per entry)
119
```
120
121
### Weight-Based Eviction Statistics
122
123
```java
124
Cache<String, String> weightedCache = Caffeine.newBuilder()
125
.maximumWeight(10)
126
.weigher((key, value) -> key.length() + value.length())
127
.recordStats()
128
.build();
129
130
// Add entries with different weights
131
weightedCache.put("a", "1"); // weight: 2
132
weightedCache.put("bb", "22"); // weight: 4
133
weightedCache.put("ccc", "333"); // weight: 6
134
weightedCache.put("dddd", "4444"); // weight: 8, may trigger eviction
135
136
CacheStats weightStats = weightedCache.stats();
137
138
System.out.println("Evictions: " + weightStats.evictionCount());
139
System.out.println("Total evicted weight: " + weightStats.evictionWeight());
140
```
141
142
## Statistics Operations
143
144
### Combining Statistics
145
146
```java
147
Cache<String, String> cache1 = Caffeine.newBuilder()
148
.maximumSize(100)
149
.recordStats()
150
.build();
151
152
Cache<String, String> cache2 = Caffeine.newBuilder()
153
.maximumSize(100)
154
.recordStats()
155
.build();
156
157
// Perform operations on both caches
158
cache1.put("key1", "value1");
159
cache1.getIfPresent("key1"); // hit
160
161
cache2.put("key2", "value2");
162
cache2.getIfPresent("key3"); // miss
163
164
// Combine statistics
165
CacheStats stats1 = cache1.stats();
166
CacheStats stats2 = cache2.stats();
167
CacheStats combined = stats1.plus(stats2);
168
169
System.out.println("Combined hits: " + combined.hitCount()); // 1
170
System.out.println("Combined misses: " + combined.missCount()); // 1
171
System.out.println("Combined requests: " + combined.requestCount()); // 2
172
```
173
174
### Statistics Differences
175
176
```java
177
Cache<String, String> monitoredCache = Caffeine.newBuilder()
178
.maximumSize(1000)
179
.recordStats()
180
.build();
181
182
// Take baseline snapshot
183
CacheStats baseline = monitoredCache.stats();
184
185
// Perform operations
186
monitoredCache.put("key1", "value1");
187
monitoredCache.put("key2", "value2");
188
monitoredCache.getIfPresent("key1"); // hit
189
monitoredCache.getIfPresent("key3"); // miss
190
191
// Calculate delta
192
CacheStats current = monitoredCache.stats();
193
CacheStats delta = current.minus(baseline);
194
195
System.out.println("New hits: " + delta.hitCount()); // 1
196
System.out.println("New misses: " + delta.missCount()); // 1
197
System.out.println("New requests: " + delta.requestCount()); // 2
198
```
199
200
## StatsCounter Interface
201
202
The `StatsCounter` interface allows custom statistics collection and reporting.
203
204
```java { .api }
205
public interface StatsCounter {
206
// Recording methods
207
void recordHits(int count);
208
void recordMisses(int count);
209
void recordLoadSuccess(long loadTime);
210
void recordLoadFailure(long loadTime);
211
void recordEviction(int weight, RemovalCause cause);
212
213
// Snapshot method
214
CacheStats snapshot();
215
}
216
```
217
218
### Built-in StatsCounter Implementations
219
220
#### ConcurrentStatsCounter
221
222
```java
223
// Default thread-safe implementation using LongAdder
224
Cache<String, String> concurrentStatsCache = Caffeine.newBuilder()
225
.maximumSize(1000)
226
.recordStats(ConcurrentStatsCounter::new)
227
.build();
228
229
// High-concurrency statistics collection
230
ExecutorService executor = Executors.newFixedThreadPool(10);
231
for (int i = 0; i < 100; i++) {
232
final int threadId = i;
233
executor.submit(() -> {
234
concurrentStatsCache.put("key_" + threadId, "value_" + threadId);
235
concurrentStatsCache.getIfPresent("key_" + threadId);
236
});
237
}
238
239
// Statistics remain accurate under high concurrency
240
CacheStats concurrentStats = concurrentStatsCache.stats();
241
System.out.println("Concurrent hit rate: " + concurrentStats.hitRate());
242
```
243
244
#### Custom StatsCounter Implementation
245
246
```java
247
public class CustomStatsCounter implements StatsCounter {
248
private final AtomicLong hits = new AtomicLong();
249
private final AtomicLong misses = new AtomicLong();
250
private final AtomicLong loadSuccesses = new AtomicLong();
251
private final AtomicLong loadFailures = new AtomicLong();
252
private final AtomicLong totalLoadTime = new AtomicLong();
253
private final AtomicLong evictions = new AtomicLong();
254
private final AtomicLong evictionWeight = new AtomicLong();
255
256
@Override
257
public void recordHits(int count) {
258
hits.addAndGet(count);
259
// Custom logging or external metrics reporting
260
logger.info("Cache hits recorded: {}", count);
261
}
262
263
@Override
264
public void recordMisses(int count) {
265
misses.addAndGet(count);
266
// Alert on high miss rates
267
long totalRequests = hits.get() + misses.get();
268
if (totalRequests > 100 && (double) misses.get() / totalRequests > 0.9) {
269
alertingService.sendAlert("High cache miss rate detected");
270
}
271
}
272
273
@Override
274
public void recordLoadSuccess(long loadTime) {
275
loadSuccesses.incrementAndGet();
276
totalLoadTime.addAndGet(loadTime);
277
// Track slow loads
278
if (loadTime > TimeUnit.SECONDS.toNanos(1)) {
279
logger.warn("Slow cache load detected: {}ms", TimeUnit.NANOSECONDS.toMillis(loadTime));
280
}
281
}
282
283
@Override
284
public void recordLoadFailure(long loadTime) {
285
loadFailures.incrementAndGet();
286
totalLoadTime.addAndGet(loadTime);
287
logger.error("Cache load failure after {}ms", TimeUnit.NANOSECONDS.toMillis(loadTime));
288
}
289
290
@Override
291
public void recordEviction(int weight, RemovalCause cause) {
292
evictions.incrementAndGet();
293
evictionWeight.addAndGet(weight);
294
metricsRegistry.counter("cache.evictions", "cause", cause.name()).increment();
295
}
296
297
@Override
298
public CacheStats snapshot() {
299
return CacheStats.of(
300
hits.get(),
301
misses.get(),
302
loadSuccesses.get(),
303
loadFailures.get(),
304
totalLoadTime.get(),
305
evictions.get(),
306
evictionWeight.get()
307
);
308
}
309
}
310
311
// Use custom stats counter
312
Cache<String, String> customStatsCache = Caffeine.newBuilder()
313
.maximumSize(1000)
314
.recordStats(CustomStatsCounter::new)
315
.build();
316
```
317
318
## Statistics Monitoring and Alerting
319
320
### Periodic Statistics Collection
321
322
```java
323
Cache<String, String> monitoredCache = Caffeine.newBuilder()
324
.maximumSize(10000)
325
.expireAfterAccess(Duration.ofMinutes(30))
326
.recordStats()
327
.build();
328
329
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
330
331
scheduler.scheduleAtFixedRate(() -> {
332
CacheStats stats = monitoredCache.stats();
333
334
// Log key metrics
335
System.out.printf("Cache Stats - Requests: %d, Hit Rate: %.2f%%, " +
336
"Avg Load Time: %.2fms, Evictions: %d%n",
337
stats.requestCount(),
338
stats.hitRate() * 100,
339
stats.averageLoadPenalty() / 1_000_000.0,
340
stats.evictionCount());
341
342
// Send metrics to monitoring system
343
metricsCollector.gauge("cache.hit_rate", stats.hitRate());
344
metricsCollector.gauge("cache.miss_rate", stats.missRate());
345
metricsCollector.gauge("cache.avg_load_time_ms", stats.averageLoadPenalty() / 1_000_000.0);
346
metricsCollector.counter("cache.evictions").increment(stats.evictionCount());
347
348
}, 0, 30, TimeUnit.SECONDS);
349
```
350
351
### Performance Analysis
352
353
```java
354
public class CachePerformanceAnalyzer {
355
public void analyzePerformance(Cache<?, ?> cache) {
356
CacheStats stats = cache.stats();
357
358
// Hit rate analysis
359
double hitRate = stats.hitRate();
360
if (hitRate < 0.8) {
361
System.out.println("WARNING: Low hit rate (" + (hitRate * 100) + "%)");
362
System.out.println("Consider: increasing cache size, adjusting expiration, or reviewing access patterns");
363
}
364
365
// Load time analysis
366
double avgLoadTimeMs = stats.averageLoadPenalty() / 1_000_000.0;
367
if (avgLoadTimeMs > 100) {
368
System.out.println("WARNING: High average load time (" + avgLoadTimeMs + "ms)");
369
System.out.println("Consider: optimizing data loading, adding async loading, or caching strategies");
370
}
371
372
// Load failure analysis
373
double loadFailureRate = stats.loadFailureRate();
374
if (loadFailureRate > 0.05) {
375
System.out.println("WARNING: High load failure rate (" + (loadFailureRate * 100) + "%)");
376
System.out.println("Consider: improving error handling, adding retry logic, or fallback values");
377
}
378
379
// Eviction analysis
380
long evictionCount = stats.evictionCount();
381
long requestCount = stats.requestCount();
382
if (requestCount > 0 && evictionCount > requestCount * 0.1) {
383
System.out.println("WARNING: High eviction rate (" + evictionCount + " evictions)");
384
System.out.println("Consider: increasing cache size, adjusting expiration, or using weight-based eviction");
385
}
386
387
// Overall health summary
388
System.out.printf("Cache Health Summary:%n" +
389
" Requests: %d%n" +
390
" Hit Rate: %.1f%%%n" +
391
" Avg Load Time: %.1fms%n" +
392
" Load Failures: %d (%.1f%%)%n" +
393
" Evictions: %d%n",
394
requestCount,
395
hitRate * 100,
396
avgLoadTimeMs,
397
stats.loadFailureCount(),
398
loadFailureRate * 100,
399
evictionCount);
400
}
401
}
402
```
403
404
## Statistics Best Practices
405
406
### When to Enable Statistics
407
408
```java
409
// Enable for production monitoring
410
Cache<String, String> productionCache = Caffeine.newBuilder()
411
.maximumSize(10000)
412
.recordStats() // Small overhead, valuable insights
413
.build();
414
415
// Disable for high-frequency, low-value caches
416
Cache<String, String> highFreqCache = Caffeine.newBuilder()
417
.maximumSize(1000)
418
// No recordStats() - avoid overhead for simple caches
419
.build();
420
421
// Use custom counter for specialized monitoring
422
Cache<String, String> specializedCache = Caffeine.newBuilder()
423
.maximumSize(5000)
424
.recordStats(() -> new CustomMetricsStatsCounter(metricsRegistry))
425
.build();
426
```
427
428
### Performance Considerations
429
430
- **Overhead**: Statistics collection adds minimal overhead (typically <2% performance impact)
431
- **Memory**: Statistics counters use additional memory (typically 8-16 bytes per cache)
432
- **Concurrency**: `ConcurrentStatsCounter` provides excellent concurrent performance using `LongAdder`
433
- **Custom Counters**: Expensive operations in custom counters can impact cache performance