Introduce SetMapping for profiles (#13197)

<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

This is part of
https://github.com/open-telemetry/opentelemetry-collector/issues/13106
It introduces a new `SetMapping` method, which allows setting a
location's mapping based on a specific dictionary.

I named the method `SetMapping` and not `PutMapping`, as it overrides
the `MappingIndex` value, it doesn't append to a slice.

Contrarily to Locations or Attributes, as location has a single mapping.
So we don't need a `FromMappingIndices` method, we can use
`MappingSlice.At()` directly.

---------

Co-authored-by: Tim Rühsen <tim.ruehsen@gmx.de>
This commit is contained in:
Damien Mathieu 2025-06-16 17:56:18 +02:00 committed by GitHub
parent 3ef58fda95
commit 0387e8323b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 345 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: pdata/pprofile
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Introduce `Equal` method on the `Mapping` type
# One or more tracking issues or pull requests related to the change
issues: [13197]
# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]

View File

@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: pdata/pprofile
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add new helper method `SetMapping` to set a new mapping on a location.
# One or more tracking issues or pull requests related to the change
issues: [13197]
# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]

17
pdata/pprofile/mapping.go Normal file
View File

@ -0,0 +1,17 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile"
// Equal checks equality with another Mapping
func (m Mapping) Equal(val Mapping) bool {
return m.MemoryStart() == val.MemoryStart() &&
m.MemoryLimit() == val.MemoryLimit() &&
m.FileOffset() == val.FileOffset() &&
m.FilenameStrindex() == val.FilenameStrindex() &&
m.AttributeIndices().Equal(val.AttributeIndices()) &&
m.HasFunctions() == val.HasFunctions() &&
m.HasFilenames() == val.HasFilenames() &&
m.HasLineNumbers() == val.HasLineNumbers() &&
m.HasInlineFrames() == val.HasInlineFrames()
}

View File

@ -0,0 +1,108 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package pprofile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMappingEqual(t *testing.T) {
for _, tt := range []struct {
name string
orig Mapping
dest Mapping
want bool
}{
{
name: "empty mappings",
orig: NewMapping(),
dest: NewMapping(),
want: true,
},
{
name: "non-empty identical mappings",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
want: true,
},
{
name: "with different MemoryStart",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(2, 2, 3, 4, []int32{1, 2}, true, true, true, true),
want: false,
},
{
name: "with different MemoryLimit",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 3, 3, 4, []int32{1, 2}, true, true, true, true),
want: false,
},
{
name: "with different FileOffset",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 4, 4, []int32{1, 2}, true, true, true, true),
want: false,
},
{
name: "with different FilenameStrindex",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 5, []int32{1, 2}, true, true, true, true),
want: false,
},
{
name: "with different AttributeIndices",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 4, []int32{1, 3}, true, true, true, true),
want: false,
},
{
name: "with different HasFunctions",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 4, []int32{1, 2}, false, true, true, true),
want: false,
},
{
name: "with different HasFilenames",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, false, true, true),
want: false,
},
{
name: "with different HasLineNumbers",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, false, true),
want: false,
},
{
name: "with different HasInlineFrames",
orig: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, true),
dest: buildMapping(1, 2, 3, 4, []int32{1, 2}, true, true, true, false),
want: false,
},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.want {
assert.True(t, tt.orig.Equal(tt.dest))
} else {
assert.False(t, tt.orig.Equal(tt.dest))
}
})
}
}
func buildMapping(memStart, memLimit, fileOffset uint64, filenameIdx int32, attrIdxs []int32, hasFn, hasFilenames, hasLnNumber, hasInlFrames bool) Mapping {
m := NewMapping()
m.SetMemoryStart(memStart)
m.SetMemoryLimit(memLimit)
m.SetFileOffset(fileOffset)
m.SetFilenameStrindex(filenameIdx)
m.AttributeIndices().FromRaw(attrIdxs)
m.SetHasFunctions(hasFn)
m.SetHasFilenames(hasFilenames)
m.SetHasLineNumbers(hasLnNumber)
m.SetHasInlineFrames(hasInlFrames)
return m
}

View File

@ -0,0 +1,47 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile"
import (
"errors"
"fmt"
"math"
)
var errTooManyMappingTableEntries = errors.New("too many entries in MappingTable")
// SetMapping updates a MappingTable and a Location's MappingIndex to
// add or update a mapping.
func SetMapping(table MappingSlice, record Location, ma Mapping) error {
idx := int(record.MappingIndex())
if idx > 0 {
if idx >= table.Len() {
return fmt.Errorf("index value %d out of range for MappingIndex", idx)
}
mapAt := table.At(idx)
if mapAt.Equal(ma) {
// Mapping already exists, nothing to do.
return nil
}
}
for j, m := range table.All() {
if m.Equal(ma) {
if j > math.MaxInt32 {
return errTooManyMappingTableEntries
}
// Add the index of the existing mapping to the indices.
record.SetMappingIndex(int32(j)) //nolint:gosec // G115 overflow checked
return nil
}
}
if table.Len() >= math.MaxInt32 {
return errTooManyMappingTableEntries
}
ma.CopyTo(table.AppendEmpty())
record.SetMappingIndex(int32(table.Len() - 1)) //nolint:gosec // G115 overflow checked
return nil
}

View File

@ -0,0 +1,123 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package pprofile
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSetMapping(t *testing.T) {
table := NewMappingSlice()
m := NewMapping()
m.SetMemoryLimit(1)
m2 := NewMapping()
m2.SetMemoryLimit(2)
loc := NewLocation()
// Put a first mapping
require.NoError(t, SetMapping(table, loc, m))
assert.Equal(t, 1, table.Len())
assert.Equal(t, int32(0), loc.MappingIndex())
// Put the same mapping
// This should be a no-op.
require.NoError(t, SetMapping(table, loc, m))
assert.Equal(t, 1, table.Len())
assert.Equal(t, int32(0), loc.MappingIndex())
// Set a new mapping
// This sets the index and adds to the table.
require.NoError(t, SetMapping(table, loc, m2))
assert.Equal(t, 2, table.Len())
assert.Equal(t, int32(table.Len()-1), loc.MappingIndex()) //nolint:gosec // G115
// Set an existing mapping
require.NoError(t, SetMapping(table, loc, m))
assert.Equal(t, 2, table.Len())
assert.Equal(t, int32(0), loc.MappingIndex())
// Set another existing mapping
require.NoError(t, SetMapping(table, loc, m2))
assert.Equal(t, 2, table.Len())
assert.Equal(t, int32(table.Len()-1), loc.MappingIndex()) //nolint:gosec // G115
}
func TestSetMappingCurrentTooHigh(t *testing.T) {
table := NewMappingSlice()
loc := NewLocation()
loc.SetMappingIndex(42)
err := SetMapping(table, loc, NewMapping())
require.Error(t, err)
assert.Equal(t, 0, table.Len())
assert.Equal(t, int32(42), loc.MappingIndex())
}
func BenchmarkSetMapping(b *testing.B) {
for _, bb := range []struct {
name string
mapping Mapping
runBefore func(*testing.B, MappingSlice, Location)
}{
{
name: "with a new mapping",
mapping: NewMapping(),
},
{
name: "with an existing mapping",
mapping: func() Mapping {
m := NewMapping()
m.SetMemoryLimit(1)
return m
}(),
runBefore: func(_ *testing.B, table MappingSlice, _ Location) {
m := table.AppendEmpty()
m.SetMemoryLimit(1)
},
},
{
name: "with a duplicate mapping",
mapping: NewMapping(),
runBefore: func(_ *testing.B, table MappingSlice, obj Location) {
require.NoError(b, SetMapping(table, obj, NewMapping()))
},
},
{
name: "with a hundred mappings to loop through",
mapping: func() Mapping {
m := NewMapping()
m.SetMemoryLimit(1)
return m
}(),
runBefore: func(_ *testing.B, table MappingSlice, _ Location) {
for i := range 100 {
m := table.AppendEmpty()
m.SetMemoryLimit(uint64(i)) //nolint:gosec // overflow checked
}
},
},
} {
b.Run(bb.name, func(b *testing.B) {
table := NewMappingSlice()
obj := NewLocation()
if bb.runBefore != nil {
bb.runBefore(b, table, obj)
}
b.ResetTimer()
b.ReportAllocs()
for range b.N {
_ = SetMapping(table, obj, bb.mapping)
}
})
}
}