1// Copyright (c) 2019 The Jaeger Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package jaeger
16
17import (
18	"fmt"
19	"testing"
20	"time"
21
22	"github.com/uber/jaeger-client-go/thrift-gen/sampling"
23
24	"github.com/opentracing/opentracing-go"
25)
26
27type benchSampler struct {
28	name    string
29	sampler Sampler
30}
31
32func (b benchSampler) String() string {
33	return b.name
34}
35
36func benchAdaptiveSampler(lateBinding bool, prob float64) Sampler {
37	ops := makeOps(5)
38	samplingRates := make([]*sampling.OperationSamplingStrategy, 5)
39	for i, op := range ops {
40		samplingRates[i] = &sampling.OperationSamplingStrategy{
41			Operation:             op,
42			ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: prob},
43		}
44	}
45	strategies := &sampling.PerOperationSamplingStrategies{
46		DefaultSamplingProbability:       prob,
47		DefaultLowerBoundTracesPerSecond: 0,
48		PerOperationStrategies:           samplingRates,
49	}
50	return NewPerOperationSampler(PerOperationSamplerParams{
51		MaxOperations:            7,
52		OperationNameLateBinding: lateBinding,
53		Strategies:               strategies,
54	})
55	// Change to below when running on <=2.19
56	//return newAdaptiveSampler(strategies, 7)
57}
58
59func BenchmarkTracer(b *testing.B) {
60	axes := []axis{
61		{
62			name: "sampler",
63			values: []interface{}{
64				benchSampler{name: "NeverSample", sampler: NewConstSampler(false)},
65				benchSampler{name: "AlwaysSample", sampler: NewConstSampler(true)},
66				benchSampler{name: "AdaptiveNeverSampleNoLateBinding", sampler: benchAdaptiveSampler(false, 0)},
67				benchSampler{name: "AdaptiveAlwaysSampleNoLateBinding", sampler: benchAdaptiveSampler(false, 1)},
68				benchSampler{name: "AdaptiveNeverSampleWithLateBinding", sampler: benchAdaptiveSampler(true, 0)},
69				benchSampler{name: "AdaptiveAlwaysSampleWithLateBinding", sampler: benchAdaptiveSampler(true, 1)},
70			},
71		},
72		// adding remote sampler on top of others did not show significant impact, keeping it off for now.
73		{name: "remoteSampler", values: []interface{}{false}},
74		{name: "children", values: []interface{}{false, true}},
75		// tags and ops dimensions did not show significant impact, so keeping to one value only for now.
76		{name: "tags", values: []interface{}{5}},
77		{name: "ops", values: []interface{}{10}},
78	}
79	for _, entry := range combinations(axes) {
80		b.Run(entry.encode(axes)+"cpus", func(b *testing.B) {
81			sampler := entry["sampler"].(benchSampler).sampler
82			if entry["remoteSampler"].(bool) {
83				sampler = NewRemotelyControlledSampler("service",
84					SamplerOptions.InitialSampler(sampler),
85					SamplerOptions.SamplingRefreshInterval(time.Minute),
86				)
87			}
88			options := benchOptions{
89				tags:     entry["tags"].(int),
90				ops:      entry["ops"].(int),
91				sampler:  sampler,
92				children: entry["children"].(bool),
93			}
94			benchmarkTracer(b, options)
95		})
96	}
97}
98
99type benchOptions struct {
100	tags     int
101	ops      int
102	sampler  Sampler
103	children bool
104}
105
106func benchmarkTracer(b *testing.B, options benchOptions) {
107	tags := makeStrings("tag", options.tags)
108	ops := makeOps(options.ops)
109
110	sampler := options.sampler
111	tracer, closer := NewTracer("service", sampler, NewNullReporter())
112	defer closer.Close()
113
114	b.ResetTimer()
115
116	b.RunParallel(func(pb *testing.PB) {
117		var parent opentracing.SpanContext
118		var op int
119		for pb.Next() {
120			span := tracer.StartSpan(ops[op], opentracing.ChildOf(parent))
121			for i := range tags {
122				span.SetTag(tags[i], tags[i])
123			}
124			if options.children {
125				parent = span.Context()
126			}
127			span.Finish()
128			op = (op + 1) % len(ops)
129		}
130	})
131}
132
133func makeOps(num int) []string {
134	return makeStrings("span", num)
135}
136
137func makeStrings(prefix string, num int) []string {
138	values := make([]string, num)
139	for i := 0; i < num; i++ {
140		values[i] = fmt.Sprintf("%s%02d", prefix, i)
141	}
142	return values
143}
144
145// combinations takes a list axes and their values and
146// returns a collection of entries which contain all combinations of each axis
147// value with every other axis' values.
148func combinations(axes []axis) []permutation {
149	if len(axes) == 0 {
150		return nil
151	}
152
153	if len(axes) == 1 {
154		return axes[0].entries()
155	}
156
157	var entries []permutation
158	// combos := combinations(axes[1:])
159	last := len(axes) - 1
160	combos := combinations(axes[:last])
161	for _, remaining := range combos {
162		for _, entry := range axes[last].entries() {
163			for k, v := range remaining {
164				entry[k] = v
165			}
166			entries = append(entries, entry)
167		}
168	}
169
170	return entries
171}
172
173type axis struct {
174	name   string
175	values []interface{}
176}
177
178func (x axis) entries() []permutation {
179	items := make([]permutation, len(x.values))
180	for i, value := range x.values {
181		items[i] = permutation{x.name: value}
182	}
183	return items
184}
185
186type permutation map[string]interface{}
187
188// encode converts a permutation into a string "k=v/k=v/...".
189// The axes argument is used to provide a definitive order of keys.
190func (p permutation) encode(axes []axis) string {
191	name := ""
192	for _, axis := range axes {
193		k := axis.name
194		v := p[k]
195		name = name + fmt.Sprintf("%s=%v", k, v) + "/"
196	}
197	return name
198}
199