Skip to content

Commit 3509421

Browse files
authored
fix: stack trace selector ignores conflicting functions (#3414)
1 parent 0169ee5 commit 3509421

File tree

4 files changed

+107
-55
lines changed

4 files changed

+107
-55
lines changed

pkg/phlaredb/symdb/resolver_pprof.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func buildPprof(
3939
// limit on the number of the nodes in the profile, or
4040
// if stack traces should be filtered by the call site.
4141
case maxNodes > 0 || len(selection.callSite) > 0:
42-
b = &pprofTree{maxNodes: maxNodes, callSite: selection.callSite}
42+
b = &pprofTree{maxNodes: maxNodes, selection: selection}
4343
}
4444
b.init(symbols, samples)
4545
if err := symbols.Stacktraces.ResolveStacktraceLocations(ctx, b, samples.StacktraceIDs); err != nil {

pkg/phlaredb/symdb/resolver_pprof_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,79 @@ func Test_Pprof_subtree(t *testing.T) {
141141
require.Equal(t, expected, actual)
142142
}
143143

144+
func Test_Pprof_subtree_multiple_versions(t *testing.T) {
145+
profile := &googlev1.Profile{
146+
StringTable: []string{"", "a", "b", "c", "d"},
147+
Function: []*googlev1.Function{
148+
{Id: 1, Name: 1}, // a
149+
{Id: 2, Name: 2}, // b
150+
{Id: 3, Name: 3}, // c
151+
{Id: 4, Name: 4, StartLine: 1}, // d
152+
{Id: 5, Name: 4, StartLine: 2}, // d(2)
153+
},
154+
Mapping: []*googlev1.Mapping{{Id: 1}},
155+
Location: []*googlev1.Location{
156+
{Id: 1, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 1, Line: 1}}}, // a
157+
{Id: 2, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 1}}}, // b:1
158+
{Id: 3, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 2}}}, // b:2
159+
{Id: 4, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 3, Line: 1}}}, // c
160+
{Id: 5, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 4, Line: 1}}}, // d
161+
{Id: 6, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 5, Line: 1}}}, // d(2)
162+
},
163+
Sample: []*googlev1.Sample{
164+
{LocationId: []uint64{5, 4, 2, 1}, Value: []int64{1}}, // a, b:1, c, d
165+
{LocationId: []uint64{6, 4, 3, 1}, Value: []int64{1}}, // a, b:2, c, d(2)
166+
{LocationId: []uint64{3, 1}, Value: []int64{1}}, // a, b:2
167+
{LocationId: []uint64{4, 1}, Value: []int64{1}}, // a, c
168+
{LocationId: []uint64{5}, Value: []int64{1}}, // d
169+
{LocationId: []uint64{6}, Value: []int64{1}}, // d (2)
170+
},
171+
}
172+
173+
db := NewSymDB(DefaultConfig().WithDirectory(t.TempDir()))
174+
w := db.WriteProfileSymbols(0, profile)
175+
r := NewResolver(context.Background(), db,
176+
WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
177+
CallSite: []*typesv1.Location{{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"}},
178+
}))
179+
180+
r.AddSamples(0, w[0].Samples)
181+
actual, err := r.Pprof()
182+
require.NoError(t, err)
183+
// Sample order is not deterministic.
184+
sort.Slice(actual.Sample, func(i, j int) bool {
185+
return slices.Compare(actual.Sample[i].LocationId, actual.Sample[j].LocationId) >= 0
186+
})
187+
188+
expected := &googlev1.Profile{
189+
PeriodType: &googlev1.ValueType{},
190+
SampleType: []*googlev1.ValueType{{}},
191+
StringTable: []string{"", "a", "b", "c", "d"},
192+
Function: []*googlev1.Function{
193+
{Id: 1, Name: 1}, // a
194+
{Id: 2, Name: 2}, // b
195+
{Id: 3, Name: 3}, // c
196+
{Id: 4, Name: 4, StartLine: 1}, // d
197+
{Id: 5, Name: 4, StartLine: 2}, // d(2)
198+
},
199+
Mapping: []*googlev1.Mapping{{Id: 1}},
200+
Location: []*googlev1.Location{
201+
{Id: 1, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 1, Line: 1}}}, // a
202+
{Id: 2, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 1}}}, // b:1
203+
{Id: 3, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 2}}}, // b:2
204+
{Id: 4, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 3, Line: 1}}}, // c
205+
{Id: 5, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 4, Line: 1}}}, // d
206+
{Id: 6, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 5, Line: 1}}}, // d(2)
207+
},
208+
Sample: []*googlev1.Sample{
209+
{LocationId: []uint64{6, 4, 3, 1}, Value: []int64{1}}, // a, b:2, c, d(2)
210+
{LocationId: []uint64{5, 4, 2, 1}, Value: []int64{1}}, // a, b:1, c, d
211+
},
212+
}
213+
214+
require.Equal(t, expected, actual)
215+
}
216+
144217
func Test_Resolver_pprof_options(t *testing.T) {
145218
s := newMemSuite(t, [][]string{{"testdata/profile.pb.gz"}})
146219
samples := s.indexed[0][0].Samples

pkg/phlaredb/symdb/resolver_pprof_tree.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ type pprofTree struct {
3131
functionsBuf []int32
3232
locationsBuf []uint64
3333

34-
callSite []uint32
35-
fnNames func(locations []int32) ([]int32, bool)
34+
selection *SelectedStackTraces
35+
fnNames func(locations []int32) ([]int32, bool)
3636

3737
// After truncation many samples will have the same stack trace.
3838
// The map is used to deduplicate them. The key is sample.LocationId
@@ -57,7 +57,7 @@ func (r *pprofTree) init(symbols *Symbols, samples schemav1.Samples) {
5757
r.functionTree = model.NewStacktraceTree(samples.Len() * 2)
5858
r.stacktraces = make([]truncatedStacktraceSample, 0, samples.Len())
5959
r.sampleMap = make(map[string]*googlev1.Sample, samples.Len())
60-
if len(r.callSite) > 0 {
60+
if r.selection != nil && len(r.selection.callSite) > 0 {
6161
r.fnNames = r.locFunctionsFiltered
6262
} else {
6363
r.fnNames = r.locFunctions
@@ -92,15 +92,15 @@ func (r *pprofTree) locFunctions(locations []int32) ([]int32, bool) {
9292
func (r *pprofTree) locFunctionsFiltered(locations []int32) ([]int32, bool) {
9393
r.functionsBuf = r.functionsBuf[:0]
9494
var pos int
95-
pathLen := len(r.callSite)
95+
pathLen := int(r.selection.depth)
9696
// Even if len(locations) < pathLen, we still
9797
// need to inspect locations line by line.
9898
for i := len(locations) - 1; i >= 0; i-- {
9999
lines := r.symbols.Locations[locations[i]].Line
100100
for j := len(lines) - 1; j >= 0; j-- {
101101
f := lines[j].FunctionId
102102
if pos < pathLen {
103-
if r.callSite[pos] != f {
103+
if r.selection.callSite[pos] != r.selection.funcNames[f] {
104104
return nil, false
105105
}
106106
pos++

pkg/phlaredb/symdb/stacktrace_selection.go

+28-49
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ type SelectedStackTraces struct {
4444
symbols *Symbols
4545
// Go PGO filter.
4646
gopgo *typesv1.GoPGO
47-
// call_site filter
47+
// Call site filter
4848
relations map[uint32]stackTraceLocationRelation
4949
callSiteSelector []*typesv1.Location
50-
callSite []uint32 // stack trace of the call site
51-
location uint32 // stack trace leaf
50+
callSite []string // call site strings in the original order.
51+
location string // stack trace leaf function.
5252
depth uint32
5353
buf []uint64
54+
// Function ID => name. The lookup table is used to
55+
// avoid unnecessary indirect accesses through the
56+
// strings[functions[id].Name] path. Instead, the
57+
// name can be resolved directly funcNames[id].
58+
funcNames []string
5459
}
5560

5661
func SelectStackTraces(symbols *Symbols, selector *typesv1.StackTraceSelector) *SelectedStackTraces {
@@ -59,10 +64,14 @@ func SelectStackTraces(symbols *Symbols, selector *typesv1.StackTraceSelector) *
5964
callSiteSelector: selector.GetCallSite(),
6065
gopgo: selector.GetGoPgo(),
6166
}
62-
x.callSite = findCallSite(symbols, x.callSiteSelector)
67+
x.callSite = callSiteFunctions(x.callSiteSelector)
6368
if x.depth = uint32(len(x.callSite)); x.depth > 0 {
6469
x.location = x.callSite[x.depth-1]
6570
}
71+
x.funcNames = make([]string, len(symbols.Functions))
72+
for i, f := range symbols.Functions {
73+
x.funcNames[i] = symbols.Strings[f.Name]
74+
}
6675
return x
6776
}
6877

@@ -144,64 +153,34 @@ func (x *SelectedStackTraces) appendStackTrace(locations []uint64) stackTraceLoc
144153
lines := x.symbols.Locations[locations[i]].Line
145154
for j := len(lines) - 1; j >= 0; j-- {
146155
f := lines[j].FunctionId
147-
n += eq(x.location, f)
148-
if pos < x.depth && pos == l {
149-
pos += eq(x.callSite[pos], f)
156+
if x.location == x.funcNames[f] {
157+
n++
158+
}
159+
if pos < x.depth && pos == l && x.callSite[pos] == x.funcNames[f] {
160+
pos++
150161
}
151162
l++
152163
}
153164
}
154165
if n == 0 {
155166
return 0
156167
}
168+
var isLeaf uint32
157169
leaf := x.symbols.Locations[locations[0]].Line[0]
158-
isLeaf := eq(x.location, leaf.FunctionId)
159-
inSubtree := ge(pos, x.depth)
160-
return stackTraceLocationRelation(inSubtree | isLeaf<<1 | (1-isLeaf)<<2)
161-
}
162-
163-
func eq(a, b uint32) uint32 {
164-
if a == b {
165-
return 1
170+
if x.location == x.funcNames[leaf.FunctionId] {
171+
isLeaf = 1
166172
}
167-
return 0
168-
}
169-
170-
func ge(a, b uint32) uint32 {
171-
if a >= b {
172-
return 1
173+
var inSubtree uint32
174+
if pos >= x.depth {
175+
inSubtree = 1
173176
}
174-
return 0
177+
return stackTraceLocationRelation(inSubtree | isLeaf<<1 | (1-isLeaf)<<2)
175178
}
176179

177-
// findCallSite returns the stack trace of the call site
178-
// where each element in the stack trace is represented by
179-
// the function ID. Call site is the last element.
180-
// TODO(kolesnikovae): Location should also include the line number.
181-
func findCallSite(symbols *Symbols, locations []*typesv1.Location) []uint32 {
182-
if len(locations) == 0 {
183-
return nil
184-
}
185-
m := make(map[string]uint32, len(locations))
186-
for _, loc := range locations {
187-
m[loc.Name] = 0
188-
}
189-
c := len(m) // Only count unique names.
190-
for f := 0; f < len(symbols.Functions) && c > 0; f++ {
191-
s := symbols.Strings[symbols.Functions[f].Name]
192-
if _, ok := m[s]; ok {
193-
// We assume that no functions have the same name.
194-
// Otherwise, the last one takes precedence.
195-
m[s] = uint32(f) // f is FunctionId
196-
c--
197-
}
198-
}
199-
if c > 0 {
200-
return nil
201-
}
202-
callSite := make([]uint32, len(locations))
180+
func callSiteFunctions(locations []*typesv1.Location) []string {
181+
callSite := make([]string, len(locations))
203182
for i, loc := range locations {
204-
callSite[i] = m[loc.Name]
183+
callSite[i] = loc.Name
205184
}
206185
return callSite
207186
}

0 commit comments

Comments
 (0)