Skip to content

Commit 96dd838

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
Release v1.8.0
1 parent 7592ccf commit 96dd838

File tree

10 files changed

+385
-18
lines changed

10 files changed

+385
-18
lines changed

experiment_results.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# CDN Hit Rate Optimization Experiments
2+
3+
## Baseline
4+
- **Date**: 2024-12-30
5+
- **Metric**: CDN hit rate average across 16K-256K cache sizes
6+
- **Goal**: 58.30%
7+
- **Current**: 57.90%
8+
9+
## Parameters Under Test
10+
| Parameter | Current Value | Description |
11+
|-----------|---------------|-------------|
12+
| smallQueueRatio | 900 (90%) | Small queue size as per-mille of capacity |
13+
| maxFreq | 2 | Frequency counter cap for eviction |
14+
| ghostCapMultiplier | 8x | Ghost queue capacity multiplier |
15+
| demotionThreshold | peakFreq >= 1 | Threshold for demotion from main to small |
16+
| evictionThreshold | freq < 2 | Threshold for eviction from small queue |
17+
18+
---
19+
20+
## Experiment 1: Smaller Small Queue (80% instead of 90%)
21+
22+
**Hypothesis**: CDN traces have scan patterns. A smaller small queue protects the main queue better, keeping valuable items longer.
23+
24+
**Change**: `smallQueueRatio = 800` (from 900)
25+
26+
**Results**:
27+
```
28+
| Cache | 16K | 32K | 64K | 128K | 256K | Avg |
29+
|---------------|--------|--------|--------|--------|--------|---------|
30+
| multicache | 55.46% | 57.09% | 58.47% | 59.59% | 60.55% | 58.23% |
31+
32+
Delta: +0.33% (57.90% → 58.23%)
33+
```
34+
35+
**Verdict**: ✓ IMPROVED - Closer to goal but not quite there
36+
37+
---
38+
39+
## Experiment 2: Higher maxFreq (3 instead of 2)
40+
41+
**Hypothesis**: Requiring more accesses before incrementing freq counter might help filter out one-hit-wonders.
42+
43+
**Change**: `maxFreq = 3` (from 2)
44+
45+
**Results**:
46+
```
47+
CDN Avg: 57.90%
48+
Delta: 0.00% (no change)
49+
```
50+
51+
**Verdict**: ✗ NO EFFECT
52+
53+
**Note**: Also discovered that setting `maxFreq = 1` creates an infinite loop in eviction (items with freq=1 get promoted instead of evicted, causing evictFromSmall to never return true). Added warning comment.
54+
55+
---
56+
57+
## Experiment 3: Larger Ghost Queue (12x instead of 8x)
58+
59+
**Hypothesis**: CDN has high churn (~768K unique keys for 2M ops). A larger ghost queue remembers more evicted keys, allowing better admission decisions.
60+
61+
**Change**: `ghostCap = size * 12` (from `size * 8`)
62+
63+
**Results**:
64+
```
65+
CDN Avg: 57.90%
66+
Delta: 0.00% (no change)
67+
```
68+
69+
**Verdict**: ✗ NO EFFECT
70+
71+
---
72+
73+
## Experiment 4: Higher Demotion Threshold (peakFreq >= 2 instead of >= 1)
74+
75+
**Hypothesis**: Only demoting items with higher historical frequency from main to small might keep the small queue cleaner.
76+
77+
**Change**: `if e.peakFreq.Load() >= 2` instead of `>= 1` in evictFromMain
78+
79+
**Results**:
80+
```
81+
CDN Avg: 57.79%
82+
Delta: -0.11% (hurt performance)
83+
```
84+
85+
**Verdict**: ✗ WORSE - Demotion helps CDN
86+
87+
---
88+
89+
## Experiment 5: Combined - 80% Small Queue + 6x Ghost
90+
91+
**Hypothesis**: Combining the winning 80% small queue with a smaller ghost might further improve CDN.
92+
93+
**Changes**:
94+
- `smallQueueRatio = 800`
95+
- `ghostCap = size * 6`
96+
97+
**Results**:
98+
```
99+
CDN Avg: 58.23%
100+
Delta: +0.33% (same as Exp 1)
101+
```
102+
103+
**Verdict**: ~ NEUTRAL - Ghost size change had no effect on top of 80% small queue
104+
105+
---
106+
107+
## Bonus Experiment: 75% Small Queue
108+
109+
**Hypothesis**: If 80% helped, maybe 75% helps more.
110+
111+
**Change**: `smallQueueRatio = 750`
112+
113+
**Results**:
114+
```
115+
CDN Avg: 58.34%
116+
Delta: +0.44%
117+
Goal: 58.30% ✓ ACHIEVED
118+
```
119+
120+
**Verdict**: ✓ ACHIEVED CDN GOAL
121+
122+
**Caveat**: This hurts the overall hitrate average (58.34% < 59.00% goal) so cannot be adopted globally.
123+
124+
---
125+
126+
## Summary
127+
128+
| Experiment | CDN Avg | Delta | Meets Goal? |
129+
|------------|---------|-------|-------------|
130+
| Baseline | 57.90% | - ||
131+
| Exp 1: Small Queue 80% | 58.23% | +0.33% ||
132+
| Exp 2: maxFreq=3 | 57.90% | 0.00% ||
133+
| Exp 3: Ghost 12x | 57.90% | 0.00% ||
134+
| Exp 4: Demotion >= 2 | 57.79% | -0.11% ||
135+
| Exp 5: 80% small + 6x ghost | 58.23% | +0.33% ||
136+
| **Bonus: Small Queue 75%** | **58.34%** | **+0.44%** | **** |
137+
138+
## Key Findings
139+
140+
1. **Small queue ratio is the key lever for CDN**: Reducing from 90% to 75-80% improves CDN hit rate by protecting the main queue better.
141+
142+
2. **Ghost queue size doesn't matter for CDN**: Neither 6x nor 12x changed the result compared to 8x.
143+
144+
3. **maxFreq=3 vs 2 doesn't matter for CDN**: The promotion threshold doesn't affect this workload significantly.
145+
146+
4. **Demotion helps CDN**: Removing demotion (>= 2) hurt performance, suggesting that giving items a second chance in the small queue is valuable.
147+
148+
5. **Trade-off exists**: While 75% small queue meets CDN goal (58.34%), it fails the overall hitrate average goal (need 59.00%). The current 90% setting optimizes for the average across all workloads.
149+
150+
---
151+
152+
## Binary Search: Optimal smallQueueRatio for Overall Hitrate
153+
154+
**Goal**: Find smallQueueRatio that maximizes overall average hitrate across all 9 workloads.
155+
156+
**Method**: Binary search with SUITES=hitrate benchmark
157+
158+
| Ratio | Overall Avg | Notes |
159+
|-------|-------------|-------|
160+
| 950 | 58.84% | Worse |
161+
| 900 | 59.40% | Baseline |
162+
| 850 | 59.82% | Better |
163+
| 800 | 60.03% | Better |
164+
| 750 | 60.24% | Better |
165+
| 700 | 60.38% | Better |
166+
| 650 | 60.48% | Better |
167+
| 600 | 60.55% | Better |
168+
| 550 | 60.61% | Better |
169+
| 500 | 60.64% | Better |
170+
| 450 | 60.66% | Better |
171+
| **400** | **60.68%** | **Optimal** |
172+
| 375 | 60.68% | Plateau |
173+
| 350 | 60.68% | Plateau |
174+
| 325 | 60.68% | Plateau |
175+
| 300 | 60.67% | Decline starts |
176+
| 250 | 60.64% | Worse |
177+
178+
**Finding**: Optimal plateau at 325-400 (all achieve 60.68%). Selected 400 as the final value.
179+
180+
**Improvement**: +1.28% absolute (59.40% → 60.68%)
181+
182+
## Final Recommendations
183+
184+
1. **Changed smallQueueRatio from 900 to 400** (90% → 40% small queue)
185+
- Improves overall hitrate from 59.40% to 60.68% (+1.28%)
186+
- CDN: 58.63% (was 57.90%, +0.73%)
187+
- All workloads improved

memory_race_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//go:build !race
2+
3+
package multicache
4+
5+
import (
6+
"sync"
7+
"sync/atomic"
8+
"testing"
9+
"time"
10+
)
11+
12+
// Tests with concurrent cache access are skipped under race detector.
13+
// The seqlock value storage uses intentional "benign races" that the
14+
// race detector cannot understand.
15+
16+
func TestCache_GetSet_CacheHitDuringSingleflight(t *testing.T) {
17+
cache := New[string, int](Size(1000))
18+
19+
var wg sync.WaitGroup
20+
loaderCalls := atomic.Int32{}
21+
22+
// Start first loader that's slow
23+
wg.Go(func() {
24+
if _, err := cache.GetSet("key1", func() (int, error) {
25+
loaderCalls.Add(1)
26+
// While loader is running, another goroutine populates cache
27+
time.Sleep(100 * time.Millisecond)
28+
return 42, nil
29+
}); err != nil {
30+
t.Errorf("GetSet error: %v", err)
31+
}
32+
})
33+
34+
// Let first goroutine start and enter singleflight
35+
time.Sleep(10 * time.Millisecond)
36+
37+
// While first is waiting, directly set the value in cache
38+
cache.Set("key1", 99)
39+
40+
// Start second loader that should wait for first
41+
wg.Go(func() {
42+
val, err := cache.GetSet("key1", func() (int, error) {
43+
loaderCalls.Add(1)
44+
return 77, nil
45+
})
46+
if err != nil {
47+
t.Errorf("GetSet error: %v", err)
48+
return
49+
}
50+
// Second should get either 99 (from cache) or 42 (from first loader)
51+
if val != 99 && val != 42 {
52+
t.Errorf("unexpected value: %d", val)
53+
}
54+
})
55+
56+
wg.Wait()
57+
58+
t.Logf("loader calls: %d", loaderCalls.Load())
59+
}

pkg/store/cloudrun/go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ module github.com/codeGROOVE-dev/multicache/pkg/store/cloudrun
33
go 1.25.4
44

55
require (
6-
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.7.0
7-
github.com/codeGROOVE-dev/multicache/pkg/store/datastore v1.7.0
8-
github.com/codeGROOVE-dev/multicache/pkg/store/localfs v1.7.0
6+
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.8.0
7+
github.com/codeGROOVE-dev/multicache/pkg/store/datastore v1.8.0
8+
github.com/codeGROOVE-dev/multicache/pkg/store/localfs v1.8.0
99
)
1010

1111
require (

pkg/store/datastore/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25.4
44

55
require (
66
github.com/codeGROOVE-dev/ds9 v0.8.0
7-
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.7.0
7+
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.8.0
88
)
99

1010
require github.com/klauspost/compress v1.18.2 // indirect

pkg/store/localfs/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/codeGROOVE-dev/multicache/pkg/store/localfs
33
go 1.25.4
44

55
require (
6-
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.7.0
6+
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.8.0
77
github.com/klauspost/compress v1.18.2
88
github.com/pierrec/lz4/v4 v4.1.22
99
)

pkg/store/null/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/codeGROOVE-dev/multicache/pkg/store/null
22

33
go 1.25.4
44

5-
require github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.7.0
5+
require github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.8.0
66

77
require github.com/klauspost/compress v1.18.2 // indirect
88

pkg/store/valkey/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/codeGROOVE-dev/multicache/pkg/store/valkey
33
go 1.25.4
44

55
require (
6-
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.7.0
7-
github.com/valkey-io/valkey-go v1.0.69
6+
github.com/codeGROOVE-dev/multicache/pkg/store/compress v1.8.0
7+
github.com/valkey-io/valkey-go v1.0.70
88
)
99

1010
require (

pkg/store/valkey/go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
22
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
33
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
44
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
5-
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
6-
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
7-
github.com/valkey-io/valkey-go v1.0.69 h1:1wxexW0IhBFkRsbjz5Zfbd7EYDv18FP9ugHIakuQ/SE=
8-
github.com/valkey-io/valkey-go v1.0.69/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
9-
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
10-
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
5+
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
6+
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
7+
github.com/valkey-io/valkey-go v1.0.70 h1:mjYNT8qiazxDAJ0QNQ8twWT/YFOkOoRd40ERV2mB49Y=
8+
github.com/valkey-io/valkey-go v1.0.70/go.mod h1:VGhZ6fs68Qrn2+OhH+6waZH27bjpgQOiLyUQyXuYK5k=
9+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
10+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
11+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
12+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
1113
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
1214
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
13-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
14-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
15-
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
16-
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
16+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=

runner

3.15 MB
Binary file not shown.

0 commit comments

Comments
 (0)