Files
a47746c5a7 Switch the dictionaries of dictionary tables only once (#14709)
Right now, when merging profiles, we switch dictionary entries when they
are used.

For example, for each sample, we do:

* Run `switchDictionary` for that sample.
* Switch all the sample indices dictionaries.
* Run `switchDictionary` for the stack associated with that sample.
* Update the sample's stack ID with the one provided by the new
dictionary.

This has a pretty big issue, as when stacks are associated to multiple
samples (which is very common in production workloads, but was missed in
tests), we will try switching the stack's dictionary multiple times.

* Switch the stack's dictionary, works fine.
* Switch it again. Now, we try assigning indices from the source
dictionary when they have meaning in the destination one.

Instead, we now do the following:

* Run `switchDictionary` for the stacks.
* Loop through samples, and run `switchDictionary` for each of them.
* Retrieve the stack. Because we already switched its dictionary, we get
a stack already pointing at the new dictionary.
* Update the sample's stack ID with the one provided by the new
dictionary.

<sub>This investigation and PR were proudly done **without** the use of
any LLM.</sub>

---------

Signed-off-by: Florian Lehner <[email protected]>
Co-authored-by: Florian Lehner <[email protected]>
2026-03-10 09:50:52 +00:00

339 lines
7.8 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package pprofile
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/internal/testutil"
"go.opentelemetry.io/collector/pdata/pcommon"
)
func TestSampleSwitchDictionary(t *testing.T) {
for _, tt := range []struct {
name string
sample Sample
src ProfilesDictionary
dst ProfilesDictionary
wantSample Sample
wantDictionary ProfilesDictionary
wantErr error
}{
{
name: "with an empty sample",
sample: NewSample(),
src: NewProfilesDictionary(),
dst: NewProfilesDictionary(),
wantSample: NewSample(),
wantDictionary: NewProfilesDictionary(),
},
{
name: "with an existing attribute",
sample: func() Sample {
s := NewSample()
s.AttributeIndices().Append(1)
return s
}(),
src: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.StringTable().Append("", "test")
d.AttributeTable().AppendEmpty()
a := d.AttributeTable().AppendEmpty()
a.SetKeyStrindex(1)
return d
}(),
dst: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.StringTable().Append("", "test")
d.AttributeTable().AppendEmpty()
d.AttributeTable().AppendEmpty()
a := d.AttributeTable().AppendEmpty()
a.SetKeyStrindex(1)
return d
}(),
wantSample: func() Sample {
s := NewSample()
s.AttributeIndices().Append(2)
return s
}(),
wantDictionary: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.StringTable().Append("", "test")
d.AttributeTable().AppendEmpty()
d.AttributeTable().AppendEmpty()
a := d.AttributeTable().AppendEmpty()
a.SetKeyStrindex(1)
return d
}(),
},
{
name: "with an attribute index that does not match anything",
sample: func() Sample {
s := NewSample()
s.AttributeIndices().Append(1)
return s
}(),
src: NewProfilesDictionary(),
dst: NewProfilesDictionary(),
wantSample: func() Sample {
s := NewSample()
s.AttributeIndices().Append(1)
return s
}(),
wantDictionary: NewProfilesDictionary(),
wantErr: errors.New("invalid attribute index 1"),
},
{
name: "with an attribute index equal to the source table length (boundary condition)",
sample: func() Sample {
s := NewSample()
s.AttributeIndices().Append(2)
return s
}(),
src: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.AttributeTable().AppendEmpty()
d.AttributeTable().AppendEmpty()
return d
}(),
dst: NewProfilesDictionary(),
wantSample: func() Sample {
s := NewSample()
s.AttributeIndices().Append(2)
return s
}(),
wantDictionary: NewProfilesDictionary(),
wantErr: errors.New("invalid attribute index 2"),
},
{
name: "with an existing link",
sample: func() Sample {
s := NewSample()
s.SetLinkIndex(1)
return s
}(),
src: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LinkTable().AppendEmpty()
l := d.LinkTable().AppendEmpty()
l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}))
return d
}(),
dst: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LinkTable().AppendEmpty()
d.LinkTable().AppendEmpty()
return d
}(),
wantSample: func() Sample {
s := NewSample()
s.SetLinkIndex(2)
return s
}(),
wantDictionary: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LinkTable().AppendEmpty()
d.LinkTable().AppendEmpty()
l := d.LinkTable().AppendEmpty()
l.SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}))
return d
}(),
},
{
name: "with a link index that does not match anything",
sample: func() Sample {
s := NewSample()
s.SetLinkIndex(1)
return s
}(),
src: NewProfilesDictionary(),
dst: NewProfilesDictionary(),
wantSample: func() Sample {
s := NewSample()
s.SetLinkIndex(1)
return s
}(),
wantDictionary: NewProfilesDictionary(),
wantErr: errors.New("invalid link index 1"),
},
{
name: "with a link index equal to the source table length (boundary condition)",
sample: func() Sample {
s := NewSample()
s.SetLinkIndex(2)
return s
}(),
src: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LinkTable().AppendEmpty()
d.LinkTable().AppendEmpty()
return d
}(),
dst: NewProfilesDictionary(),
wantSample: func() Sample {
s := NewSample()
s.SetLinkIndex(2)
return s
}(),
wantDictionary: NewProfilesDictionary(),
wantErr: errors.New("invalid link index 2"),
},
{
name: "with an existing stack",
sample: func() Sample {
s := NewSample()
s.SetStackIndex(1)
return s
}(),
src: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LocationTable().AppendEmpty().SetAddress(1)
d.LocationTable().AppendEmpty().SetAddress(2)
d.StackTable().AppendEmpty()
s := d.StackTable().AppendEmpty()
s.LocationIndices().Append(1)
return d
}(),
dst: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LocationTable().AppendEmpty()
d.LocationTable().AppendEmpty().SetAddress(2)
d.StackTable().AppendEmpty()
d.StackTable().AppendEmpty()
s := d.StackTable().AppendEmpty()
s.LocationIndices().Append(1)
return d
}(),
wantSample: func() Sample {
s := NewSample()
s.SetStackIndex(2)
return s
}(),
wantDictionary: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.LocationTable().AppendEmpty()
d.LocationTable().AppendEmpty().SetAddress(2)
d.StackTable().AppendEmpty()
d.StackTable().AppendEmpty()
s := d.StackTable().AppendEmpty()
s.LocationIndices().Append(1)
return d
}(),
},
{
name: "with a stack index that does not match anything",
sample: func() Sample {
s := NewSample()
s.SetStackIndex(1)
return s
}(),
src: NewProfilesDictionary(),
dst: NewProfilesDictionary(),
wantSample: func() Sample {
s := NewSample()
s.SetStackIndex(1)
return s
}(),
wantDictionary: NewProfilesDictionary(),
wantErr: errors.New("invalid stack index 1"),
},
{
name: "with a stack index equal to the source table length (boundary condition)",
sample: func() Sample {
s := NewSample()
s.SetStackIndex(2)
return s
}(),
src: func() ProfilesDictionary {
d := NewProfilesDictionary()
d.StackTable().AppendEmpty()
d.StackTable().AppendEmpty()
return d
}(),
dst: NewProfilesDictionary(),
wantSample: func() Sample {
s := NewSample()
s.SetStackIndex(2)
return s
}(),
wantDictionary: NewProfilesDictionary(),
wantErr: errors.New("invalid stack index 2"),
},
} {
t.Run(tt.name, func(t *testing.T) {
sample := tt.sample
dst := tt.dst
err := sample.switchDictionary(tt.src, dst)
if tt.wantErr == nil {
require.NoError(t, err)
} else {
require.Equal(t, tt.wantErr, err)
}
assert.Equal(t, tt.wantSample, sample)
assert.Equal(t, tt.wantDictionary, dst)
})
}
}
func BenchmarkSampleSwitchDictionary(b *testing.B) {
testutil.SkipMemoryBench(b)
s := NewSample()
s.SetLinkIndex(1)
s.SetStackIndex(1)
src := NewProfilesDictionary()
src.LocationTable().AppendEmpty()
src.LocationTable().AppendEmpty().SetAddress(2)
src.LinkTable().AppendEmpty()
src.LinkTable().AppendEmpty().SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}))
src.StackTable().AppendEmpty()
src.StackTable().AppendEmpty().LocationIndices().Append(1)
dst := NewProfilesDictionary()
src.LinkTable().AppendEmpty()
src.LinkTable().AppendEmpty().SetSpanID(pcommon.SpanID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}))
b.ReportAllocs()
for b.Loop() {
_ = s.switchDictionary(src, dst)
}
}