1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-05-13 21:56:48 +02:00

Optimize (attribute.Set).Filter for no filtered case (#4774)

* Optimize Set.Filter for no filtered case

When all elements of the Set are kept during a call to Filter, do not
allocate a new Set and the dropped attributes slice. Instead, return the
immutable Set and nil.

To achieve this the functionality of filterSet is broken down into a
more generic filteredToFront function.

* Apply suggestions from code review

Co-authored-by: Robert Pająk <pellared@hotmail.com>

* Rename run to benchFn based on review feedback

---------

Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
Tyler Yahn 2024-01-08 07:49:45 -08:00 committed by GitHub
parent 133f943694
commit deddec38ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 200 additions and 34 deletions

View File

@ -29,6 +29,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Improve `go.opentelemetry.io/otel/trace.TraceState`'s performance. (#4722)
- Improve `go.opentelemetry.io/otel/propagation.TraceContext`'s performance. (#4721)
- Improve `go.opentelemetry.io/otel/baggage` performance. (#4743)
- Improve performance of the `(*Set).Filter` method in `go.opentelemetry.io/otel/attribute` when the passed filter does not filter out any attributes from the set. (#4774)
- `Member.String` in `go.opentelemetry.io/otel/baggage` percent-encodes only when necessary. (#4775)
### Fixed

View File

@ -279,52 +279,75 @@ func NewSetWithSortableFiltered(kvs []KeyValue, tmp *Sortable, filter Filter) (S
position--
kvs[offset], kvs[position] = kvs[position], kvs[offset]
}
kvs = kvs[position:]
if filter != nil {
return filterSet(kvs[position:], filter)
}
return Set{
equivalent: computeDistinct(kvs[position:]),
}, nil
}
// filterSet reorders kvs so that included keys are contiguous at the end of
// the slice, while excluded keys precede the included keys.
func filterSet(kvs []KeyValue, filter Filter) (Set, []KeyValue) {
var excluded []KeyValue
// Move attributes that do not match the filter so they're adjacent before
// calling computeDistinct().
distinctPosition := len(kvs)
// Swap indistinct keys forward and distinct keys toward the
// end of the slice.
offset := len(kvs) - 1
for ; offset >= 0; offset-- {
if filter(kvs[offset]) {
distinctPosition--
kvs[offset], kvs[distinctPosition] = kvs[distinctPosition], kvs[offset]
continue
if div := filteredToFront(kvs, filter); div != 0 {
return Set{equivalent: computeDistinct(kvs[div:])}, kvs[:div]
}
}
excluded = kvs[:distinctPosition]
return Set{equivalent: computeDistinct(kvs)}, nil
}
return Set{
equivalent: computeDistinct(kvs[distinctPosition:]),
}, excluded
// filteredToFront filters slice in-place using keep function. All KeyValues that need to
// be removed are moved to the front. All KeyValues that need to be kept are
// moved (in-order) to the back. The index for the first KeyValue to be kept is
// returned.
func filteredToFront(slice []KeyValue, keep Filter) int {
n := len(slice)
j := n
for i := n - 1; i >= 0; i-- {
if keep(slice[i]) {
j--
slice[i], slice[j] = slice[j], slice[i]
}
}
return j
}
// Filter returns a filtered copy of this Set. See the documentation for
// NewSetWithSortableFiltered for more details.
func (l *Set) Filter(re Filter) (Set, []KeyValue) {
if re == nil {
return Set{
equivalent: l.equivalent,
}, nil
return *l, nil
}
// Note: This could be refactored to avoid the temporary slice
// allocation, if it proves to be expensive.
return filterSet(l.ToSlice(), re)
// Iterate in reverse to the first attribute that will be filtered out.
n := l.Len()
first := n - 1
for ; first >= 0; first-- {
kv, _ := l.Get(first)
if !re(kv) {
break
}
}
// No attributes will be dropped, return the immutable Set l and nil.
if first < 0 {
return *l, nil
}
// Copy now that we know we need to return a modified set.
//
// Do not do this in-place on the underlying storage of *Set l. Sets are
// immutable and filtering should not change this.
slice := l.ToSlice()
// Don't re-iterate the slice if only slice[0] is filtered.
if first == 0 {
// It is safe to assume len(slice) >= 1 given we found at least one
// attribute above that needs to be filtered out.
return Set{equivalent: computeDistinct(slice[1:])}, slice[:1]
}
// Move the filtered slice[first] to the front (preserving order).
kv := slice[first]
copy(slice[1:first+1], slice[:first])
slice[0] = kv
// Do not re-evaluate re(slice[first+1:]).
div := filteredToFront(slice[1:first+1], re) + 1
return Set{equivalent: computeDistinct(slice[div:])}, slice[:div]
}
// computeDistinct returns a Distinct using either the fixed- or

View File

@ -130,6 +130,106 @@ func TestSetDedup(t *testing.T) {
}
}
func TestFiltering(t *testing.T) {
a := attribute.String("A", "a")
b := attribute.String("B", "b")
c := attribute.String("C", "c")
tests := []struct {
name string
in []attribute.KeyValue
filter attribute.Filter
kept, drop []attribute.KeyValue
}{
{
name: "A",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool { return kv.Key == "A" },
kept: []attribute.KeyValue{a},
drop: []attribute.KeyValue{b, c},
},
{
name: "B",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool { return kv.Key == "B" },
kept: []attribute.KeyValue{b},
drop: []attribute.KeyValue{a, c},
},
{
name: "C",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool { return kv.Key == "C" },
kept: []attribute.KeyValue{c},
drop: []attribute.KeyValue{a, b},
},
{
name: "A||B",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool {
return kv.Key == "A" || kv.Key == "B"
},
kept: []attribute.KeyValue{a, b},
drop: []attribute.KeyValue{c},
},
{
name: "B||C",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool {
return kv.Key == "B" || kv.Key == "C"
},
kept: []attribute.KeyValue{b, c},
drop: []attribute.KeyValue{a},
},
{
name: "A||C",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool {
return kv.Key == "A" || kv.Key == "C"
},
kept: []attribute.KeyValue{a, c},
drop: []attribute.KeyValue{b},
},
{
name: "None",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool { return false },
kept: nil,
drop: []attribute.KeyValue{a, b, c},
},
{
name: "All",
in: []attribute.KeyValue{a, b, c},
filter: func(kv attribute.KeyValue) bool { return true },
kept: []attribute.KeyValue{a, b, c},
drop: nil,
},
{
name: "Empty",
in: []attribute.KeyValue{},
filter: func(kv attribute.KeyValue) bool { return true },
kept: nil,
drop: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Run("NewSetWithFiltered", func(t *testing.T) {
fltr, drop := attribute.NewSetWithFiltered(test.in, test.filter)
assert.Equal(t, test.kept, fltr.ToSlice(), "filtered")
assert.ElementsMatch(t, test.drop, drop, "dropped")
})
t.Run("Set.Filter", func(t *testing.T) {
s := attribute.NewSet(test.in...)
fltr, drop := s.Filter(test.filter)
assert.Equal(t, test.kept, fltr.ToSlice(), "filtered")
assert.ElementsMatch(t, test.drop, drop, "dropped")
})
})
}
}
func TestUniqueness(t *testing.T) {
short := []attribute.KeyValue{
attribute.String("A", "0"),
@ -225,3 +325,45 @@ func args(m reflect.Method) []reflect.Value {
}
return out
}
func BenchmarkFiltering(b *testing.B) {
var kvs [26]attribute.KeyValue
buf := [1]byte{'A' - 1}
for i := range kvs {
buf[0]++ // A, B, C ... Z
kvs[i] = attribute.String(string(buf[:]), "")
}
var result struct {
set attribute.Set
dropped []attribute.KeyValue
}
benchFn := func(fltr attribute.Filter) func(*testing.B) {
return func(b *testing.B) {
b.Helper()
b.Run("Set.Filter", func(b *testing.B) {
s := attribute.NewSet(kvs[:]...)
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
result.set, result.dropped = s.Filter(fltr)
}
})
b.Run("NewSetWithFiltered", func(b *testing.B) {
attrs := kvs[:]
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
result.set, result.dropped = attribute.NewSetWithFiltered(attrs, fltr)
}
})
}
}
b.Run("NoFilter", benchFn(nil))
b.Run("NoFiltered", benchFn(func(attribute.KeyValue) bool { return true }))
b.Run("Filtered", benchFn(func(kv attribute.KeyValue) bool { return kv.Key == "A" }))
b.Run("AllDropped", benchFn(func(attribute.KeyValue) bool { return false }))
}