diff --git a/.chloggen/locations-read-write-table.yaml b/.chloggen/locations-read-write-table.yaml new file mode 100644 index 00000000000..63e5cef10f8 --- /dev/null +++ b/.chloggen/locations-read-write-table.yaml @@ -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 methods `FromLocationIndices` and `PutLocation` to read and modify the content of locations. + +# One or more tracking issues or pull requests related to the change +issues: [13150] + +# (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: [] diff --git a/pdata/pprofile/json_test.go b/pdata/pprofile/json_test.go index b6279176198..5757f5340b6 100644 --- a/pdata/pprofile/json_test.go +++ b/pdata/pprofile/json_test.go @@ -4,8 +4,6 @@ package pprofile import ( - "fmt" - "os" "testing" jsoniter "github.com/json-iterator/go" @@ -133,7 +131,6 @@ func TestJSONUnmarshal(t *testing.T) { func TestJSONMarshal(t *testing.T) { encoder := &JSONMarshaler{} jsonBuf, err := encoder.MarshalProfiles(profilesOTLP) - fmt.Fprintf(os.Stdout, "=================\n%#v\n----------------", string(jsonBuf)) require.NoError(t, err) assert.JSONEq(t, profilesJSON, string(jsonBuf)) } diff --git a/pdata/pprofile/line.go b/pdata/pprofile/line.go new file mode 100644 index 00000000000..ae97b8f839e --- /dev/null +++ b/pdata/pprofile/line.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +// Equal checks equality with another LineSlice +func (l LineSlice) Equal(val LineSlice) bool { + if l.Len() != val.Len() { + return false + } + + for i := range l.Len() { + if !l.At(i).Equal(val.At(i)) { + return false + } + } + + return true +} + +// Equal checks equality with another Line +func (l Line) Equal(val Line) bool { + return l.Column() == val.Column() && + l.FunctionIndex() == val.FunctionIndex() && + l.Line() == val.Line() +} diff --git a/pdata/pprofile/line_test.go b/pdata/pprofile/line_test.go new file mode 100644 index 00000000000..e40068487bd --- /dev/null +++ b/pdata/pprofile/line_test.go @@ -0,0 +1,129 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLineSliceEqual(t *testing.T) { + for _, tt := range []struct { + name string + orig LineSlice + dest LineSlice + want bool + }{ + { + name: "with empty slices", + orig: NewLineSlice(), + dest: NewLineSlice(), + want: true, + }, + { + name: "with non-empty equal slices", + orig: func() LineSlice { + ls := NewLineSlice() + ls.AppendEmpty().SetLine(1) + return ls + }(), + dest: func() LineSlice { + ls := NewLineSlice() + ls.AppendEmpty().SetLine(1) + return ls + }(), + want: true, + }, + { + name: "with different lengths", + orig: func() LineSlice { + ls := NewLineSlice() + ls.AppendEmpty() + return ls + }(), + dest: NewLineSlice(), + want: false, + }, + { + name: "with non-equal slices", + orig: func() LineSlice { + ls := NewLineSlice() + ls.AppendEmpty().SetLine(2) + return ls + }(), + dest: func() LineSlice { + ls := NewLineSlice() + ls.AppendEmpty().SetLine(1) + return ls + }(), + 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 TestLineEqual(t *testing.T) { + for _, tt := range []struct { + name string + orig Line + dest Line + want bool + }{ + { + name: "with empty lines", + orig: NewLine(), + dest: NewLine(), + want: true, + }, + { + name: "with non-empty lines", + orig: buildLine(1, 2, 3), + dest: buildLine(1, 2, 3), + want: true, + }, + { + name: "with non-equal column", + orig: buildLine(1, 2, 3), + dest: buildLine(2, 2, 3), + want: false, + }, + { + name: "with non-equal function index", + orig: buildLine(1, 2, 3), + dest: buildLine(1, 3, 3), + want: false, + }, + { + name: "with non-equal line", + orig: buildLine(1, 2, 3), + dest: buildLine(1, 2, 4), + 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 buildLine(col int64, funcIdx int32, line int64) Line { + l := NewLine() + l.SetColumn(col) + l.SetFunctionIndex(funcIdx) + l.SetLine(line) + + return l +} diff --git a/pdata/pprofile/location.go b/pdata/pprofile/location.go new file mode 100644 index 00000000000..4edd7d388dd --- /dev/null +++ b/pdata/pprofile/location.go @@ -0,0 +1,13 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +// Equal checks equality with another Location +func (l Location) Equal(val Location) bool { + return l.MappingIndex() == val.MappingIndex() && + l.Address() == val.Address() && + l.AttributeIndices().Equal(val.AttributeIndices()) && + l.IsFolded() == val.IsFolded() && + l.Line().Equal(val.Line()) +} diff --git a/pdata/pprofile/location_test.go b/pdata/pprofile/location_test.go new file mode 100644 index 00000000000..970edf6dbce --- /dev/null +++ b/pdata/pprofile/location_test.go @@ -0,0 +1,80 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLocationEqual(t *testing.T) { + for _, tt := range []struct { + name string + orig Location + dest Location + want bool + }{ + { + name: "empty locations", + orig: NewLocation(), + dest: NewLocation(), + want: true, + }, + { + name: "non-empty locations", + orig: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + dest: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + want: true, + }, + { + name: "with non-equal mapping index", + orig: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + dest: buildLocation(2, 2, []int32{3}, true, buildLine(1, 2, 3)), + want: false, + }, + { + name: "with non-equal address", + orig: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + dest: buildLocation(1, 3, []int32{3}, true, buildLine(1, 2, 3)), + want: false, + }, + { + name: "with non-equal attribute indices", + orig: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + dest: buildLocation(1, 2, []int32{5}, true, buildLine(1, 2, 3)), + want: false, + }, + { + name: "with non-equal is folded", + orig: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + dest: buildLocation(1, 2, []int32{3}, false, buildLine(1, 2, 3)), + want: false, + }, + { + name: "with non-equal lines", + orig: buildLocation(1, 2, []int32{3}, true, buildLine(4, 5, 6)), + dest: buildLocation(1, 2, []int32{3}, true, buildLine(1, 2, 3)), + 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 buildLocation(mapIdx int32, addr uint64, attrIdxs []int32, isFolded bool, line Line) Location { + l := NewLocation() + l.SetMappingIndex(mapIdx) + l.SetAddress(addr) + l.AttributeIndices().FromRaw(attrIdxs) + l.SetIsFolded(isFolded) + line.MoveTo(l.Line().AppendEmpty()) + return l +} diff --git a/pdata/pprofile/locations.go b/pdata/pprofile/locations.go new file mode 100644 index 00000000000..bf50ba59a2e --- /dev/null +++ b/pdata/pprofile/locations.go @@ -0,0 +1,65 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package pprofile // import "go.opentelemetry.io/collector/pdata/pprofile" + +import ( + "errors" + "fmt" + "math" +) + +// FromLocationIndices builds a slice containing all the locations of a Profile. +// Updates made to the returned map will not be applied back to the Profile. +func FromLocationIndices(table LocationSlice, record Profile) LocationSlice { + m := NewLocationSlice() + m.EnsureCapacity(record.LocationIndices().Len()) + + for _, idx := range record.LocationIndices().All() { + l := table.At(int(idx)) + l.CopyTo(m.AppendEmpty()) + } + + return m +} + +var errTooManyLocationTableEntries = errors.New("too many entries in LocationTable") + +// PutLocation updates a LocationTable and a Profile's LocationIndices to +// add or update a location. +func PutLocation(table LocationSlice, record Profile, loc Location) error { + for i, locIdx := range record.LocationIndices().All() { + idx := int(locIdx) + if idx < 0 || idx >= table.Len() { + return fmt.Errorf("index value %d out of range in LocationIndices[%d]", idx, i) + } + locAt := table.At(idx) + if locAt.Equal(loc) { + // Location already exists, nothing to do. + return nil + } + } + + if record.LocationIndices().Len() >= math.MaxInt32 { + return errors.New("too many entries in LocationIndices") + } + + for j, a := range table.All() { + if a.Equal(loc) { + if j > math.MaxInt32 { + return errTooManyLocationTableEntries + } + // Add the index of the existing location to the indices. + record.LocationIndices().Append(int32(j)) //nolint:gosec // overflow checked + return nil + } + } + + if table.Len() >= math.MaxInt32 { + return errTooManyLocationTableEntries + } + + loc.CopyTo(table.AppendEmpty()) + record.LocationIndices().Append(int32(table.Len() - 1)) //nolint:gosec // overflow checked + return nil +} diff --git a/pdata/pprofile/locations_test.go b/pdata/pprofile/locations_test.go new file mode 100644 index 00000000000..db8fff42143 --- /dev/null +++ b/pdata/pprofile/locations_test.go @@ -0,0 +1,175 @@ +// 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 TestFromLocationIndices(t *testing.T) { + table := NewLocationSlice() + table.AppendEmpty().SetAddress(1) + table.AppendEmpty().SetAddress(2) + + prof := NewProfile() + locs := FromLocationIndices(table, prof) + assert.Equal(t, locs, NewLocationSlice()) + + // Add a location + prof.LocationIndices().Append(0) + locs = FromLocationIndices(table, prof) + + tLoc := NewLocationSlice() + tLoc.AppendEmpty().SetAddress(1) + assert.Equal(t, tLoc, locs) + + // Add another location + prof.LocationIndices().Append(1) + + locs = FromLocationIndices(table, prof) + assert.Equal(t, table, locs) +} + +func TestPutLocation(t *testing.T) { + table := NewLocationSlice() + l := NewLocation() + l.SetAddress(1) + l2 := NewLocation() + l2.SetAddress(2) + l3 := NewLocation() + l3.SetAddress(3) + l4 := NewLocation() + l4.SetAddress(4) + prof := NewProfile() + + // Put a first location + require.NoError(t, PutLocation(table, prof, l)) + assert.Equal(t, 1, table.Len()) + assert.Equal(t, []int32{0}, prof.LocationIndices().AsRaw()) + + // Put the same location + // This should be a no-op. + require.NoError(t, PutLocation(table, prof, l)) + assert.Equal(t, 1, table.Len()) + assert.Equal(t, []int32{0}, prof.LocationIndices().AsRaw()) + + // Special case: removing and adding again should not change the table as + // this can lead to multiple identical locations in the table. + prof.LocationIndices().FromRaw([]int32{}) + require.NoError(t, PutLocation(table, prof, l)) + assert.Equal(t, 1, table.Len()) + assert.Equal(t, []int32{0}, prof.LocationIndices().AsRaw()) + + // Put a new location + // This adds an index and adds to the table. + require.NoError(t, PutLocation(table, prof, l2)) + assert.Equal(t, 2, table.Len()) + assert.Equal(t, []int32{0, 1}, prof.LocationIndices().AsRaw()) + + // Add a negative index to the prof. + prof.LocationIndices().Append(-1) + tableLen := table.Len() + indicesLen := prof.LocationIndices().Len() + // Try putting a new location, make sure it fails, and that table/indices didn't change. + require.Error(t, PutLocation(table, prof, l3)) + require.Equal(t, tableLen, table.Len()) + require.Equal(t, indicesLen, prof.LocationIndices().Len()) + + // Set the last index to the table length, which is out of range. + prof.LocationIndices().SetAt(indicesLen-1, int32(tableLen)) //nolint:gosec + // Try putting a new location, make sure it fails, and that table/indices didn't change. + require.Error(t, PutLocation(table, prof, l4)) + require.Equal(t, tableLen, table.Len()) + require.Equal(t, indicesLen, prof.LocationIndices().Len()) +} + +func BenchmarkFromLocationIndices(b *testing.B) { + table := NewLocationSlice() + + for i := range 10 { + table.AppendEmpty().SetAddress(uint64(i)) //nolint:gosec // overflow checked + } + + obj := NewProfile() + obj.LocationIndices().Append(1, 3, 7) + + b.ResetTimer() + b.ReportAllocs() + + for range b.N { + _ = FromLocationIndices(table, obj) + } +} + +func BenchmarkPutLocation(b *testing.B) { + for _, bb := range []struct { + name string + location Location + + runBefore func(*testing.B, LocationSlice, Profile) + }{ + { + name: "with a new location", + location: NewLocation(), + }, + { + name: "with an existing location", + location: func() Location { + l := NewLocation() + l.SetAddress(1) + return l + }(), + + runBefore: func(_ *testing.B, table LocationSlice, _ Profile) { + l := table.AppendEmpty() + l.SetAddress(1) + }, + }, + { + name: "with a duplicate location", + location: NewLocation(), + + runBefore: func(_ *testing.B, table LocationSlice, obj Profile) { + require.NoError(b, PutLocation(table, obj, NewLocation())) + }, + }, + { + name: "with a hundred locations to loop through", + location: func() Location { + l := NewLocation() + l.SetMappingIndex(1) + return l + }(), + + runBefore: func(_ *testing.B, table LocationSlice, _ Profile) { + for i := range 100 { + l := table.AppendEmpty() + l.SetAddress(uint64(i)) //nolint:gosec // overflow checked + } + + l := table.AppendEmpty() + l.SetMappingIndex(1) + }, + }, + } { + b.Run(bb.name, func(b *testing.B) { + table := NewLocationSlice() + obj := NewProfile() + + if bb.runBefore != nil { + bb.runBefore(b, table, obj) + } + + b.ResetTimer() + b.ReportAllocs() + + for range b.N { + _ = PutLocation(table, obj, bb.location) + } + }) + } +}