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:
parent
133f943694
commit
deddec38ac
@ -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/trace.TraceState`'s performance. (#4722)
|
||||||
- Improve `go.opentelemetry.io/otel/propagation.TraceContext`'s performance. (#4721)
|
- Improve `go.opentelemetry.io/otel/propagation.TraceContext`'s performance. (#4721)
|
||||||
- Improve `go.opentelemetry.io/otel/baggage` performance. (#4743)
|
- 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)
|
- `Member.String` in `go.opentelemetry.io/otel/baggage` percent-encodes only when necessary. (#4775)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -279,52 +279,75 @@ func NewSetWithSortableFiltered(kvs []KeyValue, tmp *Sortable, filter Filter) (S
|
|||||||
position--
|
position--
|
||||||
kvs[offset], kvs[position] = kvs[position], kvs[offset]
|
kvs[offset], kvs[position] = kvs[position], kvs[offset]
|
||||||
}
|
}
|
||||||
|
kvs = kvs[position:]
|
||||||
|
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
return filterSet(kvs[position:], filter)
|
if div := filteredToFront(kvs, filter); div != 0 {
|
||||||
|
return Set{equivalent: computeDistinct(kvs[div:])}, kvs[:div]
|
||||||
}
|
}
|
||||||
return Set{
|
}
|
||||||
equivalent: computeDistinct(kvs[position:]),
|
return Set{equivalent: computeDistinct(kvs)}, nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterSet reorders kvs so that included keys are contiguous at the end of
|
// filteredToFront filters slice in-place using keep function. All KeyValues that need to
|
||||||
// the slice, while excluded keys precede the included keys.
|
// be removed are moved to the front. All KeyValues that need to be kept are
|
||||||
func filterSet(kvs []KeyValue, filter Filter) (Set, []KeyValue) {
|
// moved (in-order) to the back. The index for the first KeyValue to be kept is
|
||||||
var excluded []KeyValue
|
// returned.
|
||||||
|
func filteredToFront(slice []KeyValue, keep Filter) int {
|
||||||
// Move attributes that do not match the filter so they're adjacent before
|
n := len(slice)
|
||||||
// calling computeDistinct().
|
j := n
|
||||||
distinctPosition := len(kvs)
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
if keep(slice[i]) {
|
||||||
// Swap indistinct keys forward and distinct keys toward the
|
j--
|
||||||
// end of the slice.
|
slice[i], slice[j] = slice[j], slice[i]
|
||||||
offset := len(kvs) - 1
|
|
||||||
for ; offset >= 0; offset-- {
|
|
||||||
if filter(kvs[offset]) {
|
|
||||||
distinctPosition--
|
|
||||||
kvs[offset], kvs[distinctPosition] = kvs[distinctPosition], kvs[offset]
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
excluded = kvs[:distinctPosition]
|
return j
|
||||||
|
|
||||||
return Set{
|
|
||||||
equivalent: computeDistinct(kvs[distinctPosition:]),
|
|
||||||
}, excluded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter returns a filtered copy of this Set. See the documentation for
|
// Filter returns a filtered copy of this Set. See the documentation for
|
||||||
// NewSetWithSortableFiltered for more details.
|
// NewSetWithSortableFiltered for more details.
|
||||||
func (l *Set) Filter(re Filter) (Set, []KeyValue) {
|
func (l *Set) Filter(re Filter) (Set, []KeyValue) {
|
||||||
if re == nil {
|
if re == nil {
|
||||||
return Set{
|
return *l, nil
|
||||||
equivalent: l.equivalent,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: This could be refactored to avoid the temporary slice
|
// Iterate in reverse to the first attribute that will be filtered out.
|
||||||
// allocation, if it proves to be expensive.
|
n := l.Len()
|
||||||
return filterSet(l.ToSlice(), re)
|
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
|
// computeDistinct returns a Distinct using either the fixed- or
|
||||||
|
@ -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) {
|
func TestUniqueness(t *testing.T) {
|
||||||
short := []attribute.KeyValue{
|
short := []attribute.KeyValue{
|
||||||
attribute.String("A", "0"),
|
attribute.String("A", "0"),
|
||||||
@ -225,3 +325,45 @@ func args(m reflect.Method) []reflect.Value {
|
|||||||
}
|
}
|
||||||
return out
|
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 }))
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user