Skip to content

Commit 2d913e5

Browse files
committed
Add W3C tracestate benchmark tests
1 parent e35e714 commit 2d913e5

File tree

1 file changed

+356
-0
lines changed

1 file changed

+356
-0
lines changed
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package sampling
5+
6+
import (
7+
"strconv"
8+
"strings"
9+
"sync/atomic"
10+
"testing"
11+
)
12+
13+
func BenchmarkNewW3CTraceState(b *testing.B) {
14+
benchmarks := []struct {
15+
name string
16+
input string
17+
}{
18+
{
19+
name: "Empty",
20+
input: "",
21+
},
22+
{
23+
name: "OTelThresholdOnly",
24+
input: "ot=th:c",
25+
},
26+
{
27+
name: "OTelWithRValue",
28+
input: "ot=th:100;rv:abcdabcdabcdff",
29+
},
30+
{
31+
name: "OTelWithExtraFields",
32+
input: "ot=th:c;rv:d29d6a7215ced0;pn:abc",
33+
},
34+
{
35+
name: "SingleVendor",
36+
input: "zz=vendorcontent",
37+
},
38+
{
39+
name: "OTelPlusOneVendor",
40+
input: "ot=th:c,zz=vendorcontent",
41+
},
42+
{
43+
name: "OTelPlusMultipleVendors",
44+
input: "ot=th:c;rv:d29d6a7215ced0;pn:abc,zz=vendorcontent,aa=value1,bb=value2",
45+
},
46+
{
47+
name: "MultipleVendorsNoOTel",
48+
input: "vendor1=value1,vendor2=value2,vendor3=value3",
49+
},
50+
{
51+
name: "WithWhitespace",
52+
input: " ot=th:1 , other=value ",
53+
},
54+
{
55+
name: "MultiTenantKey",
56+
input: "tenant@system=value,ot=th:5",
57+
},
58+
{
59+
name: "LongValue",
60+
input: "ot=th:c,vendor=" + strings.Repeat("x", 200),
61+
},
62+
{
63+
name: "ManyPairs",
64+
input: generateManyPairs(20),
65+
},
66+
{
67+
name: "MaxPairs",
68+
input: generateManyPairs(32),
69+
},
70+
}
71+
72+
for _, bm := range benchmarks {
73+
b.Run(bm.name, func(b *testing.B) {
74+
b.ReportAllocs()
75+
for i := 0; i < b.N; i++ {
76+
_, _ = NewW3CTraceState(bm.input)
77+
}
78+
})
79+
}
80+
}
81+
82+
// generateManyPairs creates a tracestate string with the specified number of key-value pairs.
83+
func generateManyPairs(n int) string {
84+
var sb strings.Builder
85+
sb.WriteString("ot=th:c")
86+
for i := 1; i < n; i++ {
87+
sb.WriteString(",v")
88+
sb.WriteString(strings.Repeat("x", i%10))
89+
sb.WriteString("=val")
90+
}
91+
return sb.String()
92+
}
93+
94+
func BenchmarkNewW3CTraceStateParallel(b *testing.B) {
95+
// Benchmark parallel execution with a typical real-world input
96+
input := "ot=th:c;rv:d29d6a7215ced0;pn:abc,zz=vendorcontent"
97+
98+
b.ReportAllocs()
99+
b.RunParallel(func(pb *testing.PB) {
100+
for pb.Next() {
101+
_, _ = NewW3CTraceState(input)
102+
}
103+
})
104+
}
105+
106+
func BenchmarkW3CTracestateRegex(b *testing.B) {
107+
benchmarks := []struct {
108+
name string
109+
input string
110+
}{
111+
{
112+
name: "Empty",
113+
input: "",
114+
},
115+
{
116+
name: "OTelThresholdOnly",
117+
input: "ot=th:c",
118+
},
119+
{
120+
name: "OTelWithRValue",
121+
input: "ot=th:100;rv:abcdabcdabcdff",
122+
},
123+
{
124+
name: "OTelWithExtraFields",
125+
input: "ot=th:c;rv:d29d6a7215ced0;pn:abc",
126+
},
127+
{
128+
name: "SingleVendor",
129+
input: "zz=vendorcontent",
130+
},
131+
{
132+
name: "OTelPlusOneVendor",
133+
input: "ot=th:c,zz=vendorcontent",
134+
},
135+
{
136+
name: "OTelPlusMultipleVendors",
137+
input: "ot=th:c;rv:d29d6a7215ced0;pn:abc,zz=vendorcontent,aa=value1,bb=value2",
138+
},
139+
{
140+
name: "MultipleVendorsNoOTel",
141+
input: "vendor1=value1,vendor2=value2,vendor3=value3",
142+
},
143+
{
144+
name: "WithWhitespace",
145+
input: " ot=th:1 , other=value ",
146+
},
147+
{
148+
name: "MultiTenantKey",
149+
input: "tenant@system=value,ot=th:5",
150+
},
151+
{
152+
name: "LongValue",
153+
input: "ot=th:c,vendor=" + strings.Repeat("x", 200),
154+
},
155+
{
156+
name: "ManyPairs",
157+
input: generateManyPairs(20),
158+
},
159+
{
160+
name: "MaxPairs",
161+
input: generateManyPairs(32),
162+
},
163+
}
164+
165+
for _, bm := range benchmarks {
166+
b.Run(bm.name, func(b *testing.B) {
167+
b.ReportAllocs()
168+
for i := 0; i < b.N; i++ {
169+
_ = w3cTracestateRe.MatchString(bm.input)
170+
}
171+
})
172+
}
173+
}
174+
175+
// =============================================================================
176+
// Application-level Optimization Strategy: Lock-Free Last-Value Cache
177+
// =============================================================================
178+
179+
// w3cTraceStateCacheEntry holds an immutable cache entry.
180+
// Using an immutable struct allows lock-free atomic swaps.
181+
type w3cTraceStateCacheEntry struct {
182+
input string
183+
result W3CTraceState
184+
err error
185+
}
186+
187+
// w3cTraceStateCache is a lock-free single-entry cache for consecutive identical inputs.
188+
// This works well when spans from the same trace share the same tracestate.
189+
// Uses atomic.Pointer for lock-free reads and writes - no mutex contention.
190+
type w3cTraceStateCache struct {
191+
entry atomic.Pointer[w3cTraceStateCacheEntry]
192+
}
193+
194+
func (c *w3cTraceStateCache) parse(input string) (W3CTraceState, error) {
195+
// Lock-free read
196+
if entry := c.entry.Load(); entry != nil && entry.input == input {
197+
return entry.result, entry.err
198+
}
199+
200+
// Cache miss - parse and store
201+
result, err := NewW3CTraceState(input)
202+
203+
// Lock-free write (atomic swap)
204+
c.entry.Store(&w3cTraceStateCacheEntry{
205+
input: input,
206+
result: result,
207+
err: err,
208+
})
209+
210+
return result, err
211+
}
212+
213+
func BenchmarkW3CTraceStateCached(b *testing.B) {
214+
cache := &w3cTraceStateCache{}
215+
input := "ot=th:c;rv:d29d6a7215ced0;pn:abc,zz=vendorcontent"
216+
217+
b.Run("CacheHit", func(b *testing.B) {
218+
// Prime the cache
219+
_, _ = cache.parse(input)
220+
221+
b.ReportAllocs()
222+
b.ResetTimer()
223+
for i := 0; i < b.N; i++ {
224+
_, _ = cache.parse(input)
225+
}
226+
})
227+
228+
b.Run("CacheMiss", func(b *testing.B) {
229+
b.ReportAllocs()
230+
for i := 0; i < b.N; i++ {
231+
// Each iteration has a different input = cache miss
232+
_, _ = cache.parse(input + strconv.Itoa(i%100))
233+
}
234+
})
235+
236+
b.Run("AlternatingInputs", func(b *testing.B) {
237+
// Simulate alternating between two traces
238+
input1 := "ot=th:c;rv:d29d6a7215ced0"
239+
input2 := "ot=th:5;rv:abcdef12345678"
240+
241+
b.ReportAllocs()
242+
for i := 0; i < b.N; i++ {
243+
if i%2 == 0 {
244+
_, _ = cache.parse(input1)
245+
} else {
246+
_, _ = cache.parse(input2)
247+
}
248+
}
249+
})
250+
251+
// Parallel benchmark - demonstrates no mutex contention
252+
b.Run("ParallelCacheHit", func(b *testing.B) {
253+
// Prime the cache
254+
_, _ = cache.parse(input)
255+
256+
b.ReportAllocs()
257+
b.ResetTimer()
258+
b.RunParallel(func(pb *testing.PB) {
259+
for pb.Next() {
260+
_, _ = cache.parse(input)
261+
}
262+
})
263+
})
264+
}
265+
266+
// =============================================================================
267+
// Benchmark: Hand-written Validator vs Regex
268+
// =============================================================================
269+
270+
func BenchmarkW3CTracestateValidation(b *testing.B) {
271+
benchmarks := []struct {
272+
name string
273+
input string
274+
}{
275+
{"OTelThresholdOnly", "ot=th:c"},
276+
{"OTelWithRValue", "ot=th:100;rv:abcdabcdabcdff"},
277+
{"OTelPlusMultipleVendors", "ot=th:c;rv:d29d6a7215ced0;pn:abc,zz=vendorcontent,aa=value1,bb=value2"},
278+
{"LongValue", "ot=th:c,vendor=" + strings.Repeat("x", 200)},
279+
{"ManyPairs", generateManyPairs(20)},
280+
{"MaxPairs", generateManyPairs(32)},
281+
}
282+
283+
for _, bm := range benchmarks {
284+
b.Run("Regex/"+bm.name, func(b *testing.B) {
285+
b.ReportAllocs()
286+
for i := 0; i < b.N; i++ {
287+
_ = w3cTracestateRe.MatchString(bm.input)
288+
}
289+
})
290+
291+
b.Run("NoRegex/"+bm.name, func(b *testing.B) {
292+
b.ReportAllocs()
293+
for i := 0; i < b.N; i++ {
294+
_ = W3CTraceStateIsValid(bm.input)
295+
}
296+
})
297+
}
298+
}
299+
300+
// =============================================================================
301+
// Simulating Real-World Trace Patterns
302+
// =============================================================================
303+
304+
func BenchmarkW3CTraceStateRealWorldPatterns(b *testing.B) {
305+
// Simulate a batch of spans from the same trace (common in collectors)
306+
sameTraceInputs := make([]string, 100)
307+
tracestate := "ot=th:c;rv:d29d6a7215ced0;pn:abc,zz=vendorcontent"
308+
for i := range sameTraceInputs {
309+
sameTraceInputs[i] = tracestate
310+
}
311+
312+
b.Run("SameTrace_NoCache", func(b *testing.B) {
313+
b.ReportAllocs()
314+
for i := 0; i < b.N; i++ {
315+
for _, input := range sameTraceInputs {
316+
_, _ = NewW3CTraceState(input)
317+
}
318+
}
319+
})
320+
321+
cache := &w3cTraceStateCache{}
322+
b.Run("SameTrace_WithCache", func(b *testing.B) {
323+
b.ReportAllocs()
324+
for i := 0; i < b.N; i++ {
325+
for _, input := range sameTraceInputs {
326+
_, _ = cache.parse(input)
327+
}
328+
}
329+
})
330+
331+
// Simulate mixed traces (10 different traces, 10 spans each)
332+
mixedInputs := make([]string, 100)
333+
for i := range mixedInputs {
334+
traceIdx := i / 10 // 10 spans per trace
335+
mixedInputs[i] = "ot=th:" + strconv.Itoa(traceIdx) + ";rv:d29d6a7215ced0"
336+
}
337+
338+
b.Run("MixedTraces_NoCache", func(b *testing.B) {
339+
b.ReportAllocs()
340+
for i := 0; i < b.N; i++ {
341+
for _, input := range mixedInputs {
342+
_, _ = NewW3CTraceState(input)
343+
}
344+
}
345+
})
346+
347+
cache2 := &w3cTraceStateCache{}
348+
b.Run("MixedTraces_WithCache", func(b *testing.B) {
349+
b.ReportAllocs()
350+
for i := 0; i < b.N; i++ {
351+
for _, input := range mixedInputs {
352+
_, _ = cache2.parse(input)
353+
}
354+
}
355+
})
356+
}

0 commit comments

Comments
 (0)