Skip to content

Commit ffd2615

Browse files
authored
fix(godeltaprof): incorrect function names for generics functions (#106)
* fix(godeltaprof): port fix for golang/go#64528 * fix(godeltaprof): port fix for golang/go#64641 * add go 1.22 for testing
1 parent 1b1f973 commit ffd2615

File tree

3 files changed

+161
-5
lines changed

3 files changed

+161
-5
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
go: ['1.16', '1.17', '1.18', '1.19', '1.20', '1.21', 'tip']
15+
go: ['1.16', '1.17', '1.18', '1.19', '1.20', '1.21', '1.22', 'tip']
1616
steps:
1717
- name: Checkout
1818
uses: actions/checkout@v3

godeltaprof/compat/generics_go21_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ package compat
55

66
import (
77
"bytes"
8+
"fmt"
9+
"github.com/google/pprof/profile"
10+
"io"
811
"runtime"
912
"runtime/pprof"
13+
"slices"
14+
"strings"
1015
"sync"
1116
"testing"
1217
"time"
@@ -194,3 +199,153 @@ func TestMutex(t *testing.T) {
194199
expectStackFrames(t, buffer, expectedRealShape, 19)
195200
})
196201
}
202+
203+
func profileToStrings(p *profile.Profile) []string {
204+
var res []string
205+
for _, s := range p.Sample {
206+
res = append(res, sampleToString(s))
207+
}
208+
return res
209+
}
210+
211+
func sampleToString(s *profile.Sample) string {
212+
var funcs []string
213+
for i := len(s.Location) - 1; i >= 0; i-- {
214+
loc := s.Location[i]
215+
funcs = locationToStrings(loc, funcs)
216+
}
217+
return fmt.Sprintf("%s %v", strings.Join(funcs, ";"), s.Value)
218+
}
219+
220+
func locationToStrings(loc *profile.Location, funcs []string) []string {
221+
for j := range loc.Line {
222+
line := loc.Line[len(loc.Line)-1-j]
223+
funcs = append(funcs, line.Function.Name)
224+
}
225+
return funcs
226+
}
227+
228+
// This is a regression test for https://go.dev/issue/64528 .
229+
func TestGenericsHashKeyInPprofBuilder(t *testing.T) {
230+
previousRate := runtime.MemProfileRate
231+
runtime.MemProfileRate = 1
232+
defer func() {
233+
runtime.MemProfileRate = previousRate
234+
}()
235+
for _, sz := range []int{128, 256} {
236+
genericAllocFunc[uint32](sz / 4)
237+
}
238+
for _, sz := range []int{32, 64} {
239+
genericAllocFunc[uint64](sz / 8)
240+
}
241+
242+
runtime.GC()
243+
buf := bytes.NewBuffer(nil)
244+
if err := WriteHeapProfile(buf); err != nil {
245+
t.Fatalf("writing profile: %v", err)
246+
}
247+
p, err := profile.Parse(buf)
248+
if err != nil {
249+
t.Fatalf("profile.Parse: %v", err)
250+
}
251+
252+
actual := profileToStrings(p)
253+
expected := []string{
254+
"testing.tRunner;github.com/grafana/pyroscope-go/godeltaprof/compat.TestGenericsHashKeyInPprofBuilder;github.com/grafana/pyroscope-go/godeltaprof/compat.genericAllocFunc[go.shape.uint32] [1 128 0 0]",
255+
"testing.tRunner;github.com/grafana/pyroscope-go/godeltaprof/compat.TestGenericsHashKeyInPprofBuilder;github.com/grafana/pyroscope-go/godeltaprof/compat.genericAllocFunc[go.shape.uint32] [1 256 0 0]",
256+
"testing.tRunner;github.com/grafana/pyroscope-go/godeltaprof/compat.TestGenericsHashKeyInPprofBuilder;github.com/grafana/pyroscope-go/godeltaprof/compat.genericAllocFunc[go.shape.uint64] [1 32 0 0]",
257+
"testing.tRunner;github.com/grafana/pyroscope-go/godeltaprof/compat.TestGenericsHashKeyInPprofBuilder;github.com/grafana/pyroscope-go/godeltaprof/compat.genericAllocFunc[go.shape.uint64] [1 64 0 0]",
258+
}
259+
260+
for _, l := range expected {
261+
if !slices.Contains(actual, l) {
262+
t.Errorf("profile = %v\nwant = %v", strings.Join(actual, "\n"), l)
263+
}
264+
}
265+
}
266+
267+
type opAlloc struct {
268+
buf [128]byte
269+
}
270+
271+
type opCall struct {
272+
}
273+
274+
var sink []byte
275+
276+
func storeAlloc() {
277+
sink = make([]byte, 16)
278+
}
279+
280+
func nonRecursiveGenericAllocFunction[CurrentOp any, OtherOp any](alloc bool) {
281+
if alloc {
282+
storeAlloc()
283+
} else {
284+
nonRecursiveGenericAllocFunction[OtherOp, CurrentOp](true)
285+
}
286+
}
287+
288+
func TestGenericsInlineLocations(t *testing.T) {
289+
if OptimizationOff() {
290+
t.Skip("skipping test with optimizations disabled")
291+
}
292+
293+
previousRate := runtime.MemProfileRate
294+
runtime.MemProfileRate = 1
295+
defer func() {
296+
runtime.MemProfileRate = previousRate
297+
sink = nil
298+
}()
299+
300+
nonRecursiveGenericAllocFunction[opAlloc, opCall](true)
301+
nonRecursiveGenericAllocFunction[opCall, opAlloc](false)
302+
303+
runtime.GC()
304+
305+
buf := bytes.NewBuffer(nil)
306+
if err := WriteHeapProfile(buf); err != nil {
307+
t.Fatalf("writing profile: %v", err)
308+
}
309+
p, err := profile.Parse(buf)
310+
if err != nil {
311+
t.Fatalf("profile.Parse: %v", err)
312+
}
313+
314+
const expectedSample = "testing.tRunner;github.com/grafana/pyroscope-go/godeltaprof/compat.TestGenericsInlineLocations;github.com/grafana/pyroscope-go/godeltaprof/compat.nonRecursiveGenericAllocFunction[go.shape.struct {},go.shape.struct { github.com/grafana/pyroscope-go/godeltaprof/compat.buf [128]uint8 }];github.com/grafana/pyroscope-go/godeltaprof/compat.nonRecursiveGenericAllocFunction[go.shape.struct { github.com/grafana/pyroscope-go/godeltaprof/compat.buf [128]uint8 },go.shape.struct {}];github.com/grafana/pyroscope-go/godeltaprof/compat.storeAlloc [1 16 1 16]"
315+
const expectedLocation = "github.com/grafana/pyroscope-go/godeltaprof/compat.nonRecursiveGenericAllocFunction[go.shape.struct {},go.shape.struct { github.com/grafana/pyroscope-go/godeltaprof/compat.buf [128]uint8 }];github.com/grafana/pyroscope-go/godeltaprof/compat.nonRecursiveGenericAllocFunction[go.shape.struct { github.com/grafana/pyroscope-go/godeltaprof/compat.buf [128]uint8 },go.shape.struct {}];github.com/grafana/pyroscope-go/godeltaprof/compat.storeAlloc"
316+
const expectedLocationNewInliner = "github.com/grafana/pyroscope-go/godeltaprof/compat.TestGenericsInlineLocations;" + expectedLocation
317+
var s *profile.Sample
318+
for _, sample := range p.Sample {
319+
if sampleToString(sample) == expectedSample {
320+
s = sample
321+
break
322+
}
323+
}
324+
if s == nil {
325+
t.Fatalf("expected \n%s\ngot\n%s", expectedSample, strings.Join(profileToStrings(p), "\n"))
326+
}
327+
loc := s.Location[0]
328+
actual := strings.Join(locationToStrings(loc, nil), ";")
329+
if expectedLocation != actual && expectedLocationNewInliner != actual {
330+
t.Errorf("expected a location with at least 3 functions\n%s\ngot\n%s\n", expectedLocation, actual)
331+
}
332+
}
333+
334+
func OptimizationOff() bool {
335+
optimizationMarker := func() uintptr {
336+
pc, _, _, _ := runtime.Caller(0)
337+
return pc
338+
}
339+
pc := optimizationMarker()
340+
f := runtime.FuncForPC(runtime.FuncForPC(pc).Entry())
341+
return f.Name() == "github.com/grafana/pyroscope-go/godeltaprof/compat.OptimizationOff.func1"
342+
}
343+
344+
func WriteHeapProfile(w io.Writer) error {
345+
runtime.GC()
346+
dh := godeltaprof.NewHeapProfilerWithOptions(godeltaprof.ProfileOptions{
347+
GenericsFrames: true,
348+
LazyMappings: true,
349+
})
350+
return dh.Profile(w)
351+
}

godeltaprof/internal/pprof/proto.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ func (d *pcDeck) tryAdd(pc uintptr, frames []runtime.Frame, symbolizeResult symb
474474
if last.Entry != newFrame.Entry { // newFrame is for a different function.
475475
return false
476476
}
477-
if last.Function == newFrame.Function { // maybe recursion.
477+
if runtime_FrameSymbolName(&last) == runtime_FrameSymbolName(&newFrame) { // maybe recursion.
478478
return false
479479
}
480480
}
@@ -524,13 +524,14 @@ func (b *profileBuilder) emitLocation() uint64 {
524524
b.pb.uint64Opt(tagLocation_Address, uint64(firstFrame.PC))
525525
for _, frame := range b.deck.frames {
526526
// Write out each line in frame expansion.
527-
funcID := uint64(b.funcs[frame.Function])
527+
funcName := runtime_FrameSymbolName(&frame)
528+
funcID := uint64(b.funcs[funcName])
528529
if funcID == 0 {
529530
funcID = uint64(len(b.funcs)) + 1
530-
b.funcs[frame.Function] = int(funcID)
531+
b.funcs[funcName] = int(funcID)
531532
var name string
532533
if b.opt.GenericsFrames {
533-
name = runtime_FrameSymbolName(&frame)
534+
name = funcName
534535
} else {
535536
name = frame.Function
536537
}

0 commit comments

Comments
 (0)