Skip to content

Commit 37e0a65

Browse files
jensneuseclaude
authored andcommitted
test(resolve): entity-caching benchmarks (copy budget, overhead ladder, analytics)
Add Go benchmarks (bench_test files only, no production changes) covering the caching engine: the StructuralCopy helper primitives (L1/L2 read/write, with vs without transform), the Copy-Budget pair (caching vs non-caching merge paths so per-request copy overhead is measurable 1:1), the overhead ladder (Disabled / ConfiguredButDisabled / L1Only / L1L2_Miss / L1L2_Hit), and the analytics micro-benches (disabled vs enabled, field hashing). Validates the design: ConfiguredButDisabled has identical allocs to Disabled (no guard leak), L1L2_Hit is cheaper than L1L2_Miss, and analytics adds zero allocations when off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e820d0b commit 37e0a65

5 files changed

Lines changed: 769 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package resolve
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/cespare/xxhash/v2"
8+
)
9+
10+
var cacheAnalyticsBenchSink uint64
11+
12+
func BenchmarkCacheAnalytics_Disabled(b *testing.B) {
13+
ctx := NewContext(context.Background())
14+
ctx.ExecutionOptions.Caching.EnableCacheAnalytics = false
15+
event := CacheKeyEvent{
16+
Key: `{"__typename":"User","key":{"id":"1"}}`,
17+
EntityType: "User",
18+
Hit: true,
19+
Bytes: 64,
20+
}
21+
22+
b.ReportAllocs()
23+
b.ResetTimer()
24+
for b.Loop() {
25+
ctx.cacheAnalytics().recordL1Read(event)
26+
}
27+
}
28+
29+
func BenchmarkCacheAnalytics_Enabled(b *testing.B) {
30+
ctx := NewContext(context.Background())
31+
ctx.ExecutionOptions.Caching.EnableCacheAnalytics = true
32+
collector := ctx.cacheAnalytics()
33+
collector.l1Reads = make([]CacheKeyEvent, 0, 1024)
34+
event := CacheKeyEvent{
35+
Key: `{"__typename":"User","key":{"id":"1"}}`,
36+
EntityType: "User",
37+
Hit: true,
38+
Bytes: 64,
39+
}
40+
41+
b.ReportAllocs()
42+
b.ResetTimer()
43+
for b.Loop() {
44+
if len(collector.l1Reads) == cap(collector.l1Reads) {
45+
collector.l1Reads = collector.l1Reads[:0]
46+
}
47+
collector.recordL1Read(event)
48+
}
49+
}
50+
51+
func BenchmarkCacheAnalytics_FieldHashing(b *testing.B) {
52+
ctx := NewContext(context.Background())
53+
ctx.ExecutionOptions.Caching.EnableCacheAnalytics = true
54+
collector := ctx.cacheAnalytics()
55+
collector.fieldHashes = make([]FieldHashEvent, 0, 1024)
56+
fieldPath := []byte("User.profile.displayName")
57+
58+
b.ReportAllocs()
59+
b.ResetTimer()
60+
for b.Loop() {
61+
if len(collector.fieldHashes) == cap(collector.fieldHashes) {
62+
collector.fieldHashes = collector.fieldHashes[:0]
63+
}
64+
hash := xxhash.Sum64(fieldPath)
65+
collector.recordFieldHash(FieldHashEvent{
66+
EntityType: "User",
67+
FieldPath: "profile.displayName",
68+
Hash: hash,
69+
})
70+
cacheAnalyticsBenchSink = hash
71+
}
72+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package resolve
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
)
8+
9+
var cachingOverheadBenchSink string
10+
11+
func BenchmarkCachingOverhead_Sequential(b *testing.B) {
12+
b.Run("Disabled", func(b *testing.B) {
13+
benchCachingOverheadSequential(b, &FetchCacheConfiguration{}, ResolverOptions{}, func(ctx *Context) {
14+
ctx.ExecutionOptions.Caching = CachingOptions{
15+
EnableL1Cache: false,
16+
EnableL2Cache: false,
17+
}
18+
})
19+
})
20+
b.Run("ConfiguredButDisabled", func(b *testing.B) {
21+
cache := batchUserNameCacheConfig(false)
22+
cache.UseL1Cache = true
23+
benchCachingOverheadSequential(b, cache, ResolverOptions{}, func(ctx *Context) {
24+
ctx.ExecutionOptions.Caching = CachingOptions{
25+
EnableL1Cache: false,
26+
EnableL2Cache: false,
27+
}
28+
})
29+
})
30+
b.Run("L1Only", func(b *testing.B) {
31+
cache := batchUserNameCacheConfig(false)
32+
cache.EnableL2Cache = false
33+
cache.UseL1Cache = true
34+
benchCachingOverheadSequential(b, cache, ResolverOptions{}, func(ctx *Context) {
35+
ctx.ExecutionOptions.Caching = CachingOptions{
36+
EnableL1Cache: true,
37+
EnableL2Cache: false,
38+
}
39+
})
40+
})
41+
b.Run("L1L2_Miss", func(b *testing.B) {
42+
cacheBackend := newBenchLoaderCache()
43+
cache := batchUserNameCacheConfig(false)
44+
cache.UseL1Cache = true
45+
benchCachingOverheadSequential(b, cache, ResolverOptions{
46+
Caches: map[string]LoaderCache{"default": cacheBackend},
47+
}, func(ctx *Context) {
48+
cacheBackend.Clear()
49+
ctx.ExecutionOptions.Caching = CachingOptions{
50+
EnableL1Cache: true,
51+
EnableL2Cache: true,
52+
}
53+
})
54+
})
55+
b.Run("L1L2_Hit", func(b *testing.B) {
56+
cacheBackend := newBenchLoaderCache()
57+
options := ResolverOptions{Caches: map[string]LoaderCache{"default": cacheBackend}}
58+
cache := batchUserNameCacheConfig(false)
59+
cache.UseL1Cache = true
60+
response, cancel := newCachingOverheadBenchResponse(cache)
61+
defer cancel()
62+
resolver, shutdown := newCachingOverheadBenchResolver(options)
63+
defer shutdown()
64+
benchResolveGraphQLResponse(b, resolver, response, func(ctx *Context) {
65+
ctx.ExecutionOptions.Caching = CachingOptions{
66+
EnableL1Cache: true,
67+
EnableL2Cache: true,
68+
}
69+
})
70+
b.ReportAllocs()
71+
b.ResetTimer()
72+
for b.Loop() {
73+
cachingOverheadBenchSink = benchResolveGraphQLResponse(b, resolver, response, func(ctx *Context) {
74+
ctx.ExecutionOptions.Caching = CachingOptions{
75+
EnableL1Cache: true,
76+
EnableL2Cache: true,
77+
}
78+
})
79+
}
80+
})
81+
}
82+
83+
func BenchmarkCachingOverhead_Analytics(b *testing.B) {
84+
cacheBackend := newBenchLoaderCache()
85+
options := ResolverOptions{Caches: map[string]LoaderCache{"default": cacheBackend}}
86+
cache := batchUserNameCacheConfig(false)
87+
cache.UseL1Cache = true
88+
response, cancel := newCachingOverheadBenchResponse(cache)
89+
defer cancel()
90+
resolver, shutdown := newCachingOverheadBenchResolver(options)
91+
defer shutdown()
92+
benchResolveGraphQLResponse(b, resolver, response, func(ctx *Context) {
93+
ctx.ExecutionOptions.Caching = CachingOptions{
94+
EnableL1Cache: true,
95+
EnableL2Cache: true,
96+
}
97+
})
98+
99+
b.Run("AnalyticsOff", func(b *testing.B) {
100+
b.ReportAllocs()
101+
b.ResetTimer()
102+
for b.Loop() {
103+
cachingOverheadBenchSink = benchResolveGraphQLResponse(b, resolver, response, func(ctx *Context) {
104+
ctx.ExecutionOptions.Caching = CachingOptions{
105+
EnableL1Cache: true,
106+
EnableL2Cache: true,
107+
}
108+
})
109+
}
110+
})
111+
b.Run("AnalyticsOn", func(b *testing.B) {
112+
b.ReportAllocs()
113+
b.ResetTimer()
114+
for b.Loop() {
115+
cachingOverheadBenchSink = benchResolveGraphQLResponse(b, resolver, response, func(ctx *Context) {
116+
ctx.ExecutionOptions.Caching = CachingOptions{
117+
EnableL1Cache: true,
118+
EnableL2Cache: true,
119+
EnableCacheAnalytics: true,
120+
}
121+
})
122+
}
123+
})
124+
}
125+
126+
func benchCachingOverheadSequential(b *testing.B, cache *FetchCacheConfiguration, options ResolverOptions, configure func(*Context)) {
127+
b.Helper()
128+
129+
response, cancel := newCachingOverheadBenchResponse(cache)
130+
defer cancel()
131+
resolver, shutdown := newCachingOverheadBenchResolver(options)
132+
defer shutdown()
133+
134+
benchResolveGraphQLResponse(b, resolver, response, configure)
135+
b.ReportAllocs()
136+
b.ResetTimer()
137+
for b.Loop() {
138+
cachingOverheadBenchSink = benchResolveGraphQLResponse(b, resolver, response, configure)
139+
}
140+
}
141+
142+
func newCachingOverheadBenchResponse(cache *FetchCacheConfiguration) (*GraphQLResponse, context.CancelFunc) {
143+
root := &countingCacheTestDataSource{
144+
responses: [][]byte{
145+
[]byte(`{"data":{"users":[{"__typename":"User","id":"1"},{"__typename":"User","id":"2"}]}}`),
146+
},
147+
}
148+
entities := &countingCacheTestDataSource{
149+
responses: [][]byte{
150+
[]byte(`{"data":{"_entities":[{"__typename":"User","id":"1","name":"Ada"},{"__typename":"User","id":"2","name":"Grace"}]}}`),
151+
},
152+
}
153+
return cacheTestBatchEntityResponse(root, entities, cache), func() {}
154+
}
155+
156+
func newCachingOverheadBenchResolver(options ResolverOptions) (*Resolver, context.CancelFunc) {
157+
resolverCtx, cancel := context.WithCancel(context.Background())
158+
resolver := New(resolverCtx, options)
159+
return resolver, cancel
160+
}
161+
162+
func benchResolveGraphQLResponse(b *testing.B, resolver *Resolver, response *GraphQLResponse, configure func(*Context)) string {
163+
b.Helper()
164+
165+
ctx := NewContext(context.Background())
166+
ctx.ExecutionOptions.DisableSubgraphRequestDeduplication = true
167+
if configure != nil {
168+
configure(ctx)
169+
}
170+
171+
var out bytes.Buffer
172+
_, err := resolver.ResolveGraphQLResponse(ctx, response, nil, &out)
173+
if err != nil {
174+
b.Fatal(err)
175+
}
176+
return out.String()
177+
}
178+
179+
type benchLoaderCache struct {
180+
entries map[string][]byte
181+
}
182+
183+
func newBenchLoaderCache() *benchLoaderCache {
184+
return &benchLoaderCache{entries: map[string][]byte{}}
185+
}
186+
187+
func (c *benchLoaderCache) Get(_ context.Context, keys []string) ([]*CacheEntry, error) {
188+
entries := make([]*CacheEntry, len(keys))
189+
for i, key := range keys {
190+
value, ok := c.entries[key]
191+
if !ok {
192+
continue
193+
}
194+
entries[i] = &CacheEntry{
195+
Key: key,
196+
Value: append([]byte(nil), value...),
197+
}
198+
}
199+
return entries, nil
200+
}
201+
202+
func (c *benchLoaderCache) Set(_ context.Context, entries []*CacheEntry) error {
203+
for _, entry := range entries {
204+
if entry == nil {
205+
continue
206+
}
207+
c.entries[entry.Key] = append([]byte(nil), entry.Value...)
208+
}
209+
return nil
210+
}
211+
212+
func (c *benchLoaderCache) Delete(_ context.Context, keys []string) error {
213+
for _, key := range keys {
214+
delete(c.entries, key)
215+
}
216+
return nil
217+
}
218+
219+
func (c *benchLoaderCache) Clear() {
220+
for key := range c.entries {
221+
delete(c.entries, key)
222+
}
223+
}

0 commit comments

Comments
 (0)