diff --git a/internal/chunk/android.go b/internal/chunk/android.go index 4774af49..b0a03a98 100644 --- a/internal/chunk/android.go +++ b/internal/chunk/android.go @@ -132,5 +132,9 @@ func (c AndroidChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error return frame.Frame{}, frame.ErrFrameNotFound } +func (c AndroidChunk) GetFrameForRegression(target uint32) (frame.Frame, error) { + return c.Profile.GetFrameForRegression(target) +} + func (c *AndroidChunk) Normalize() { } diff --git a/internal/chunk/chunk.go b/internal/chunk/chunk.go index f2e60d28..fc9f2ec5 100644 --- a/internal/chunk/chunk.go +++ b/internal/chunk/chunk.go @@ -23,6 +23,7 @@ type ( GetRetentionDays() int GetOptions() options.Options GetFrameWithFingerprint(uint32) (frame.Frame, error) + GetFrameForRegression(uint32) (frame.Frame, error) CallTrees(activeThreadID *string) (map[string][]*nodetree.Node, error) DurationMS() uint64 @@ -137,6 +138,10 @@ func (c Chunk) GetFrameWithFingerprint(f uint32) (frame.Frame, error) { return c.chunk.GetFrameWithFingerprint(f) } +func (c Chunk) GetFrameForRegression(target uint32) (frame.Frame, error) { + return c.chunk.GetFrameForRegression(target) +} + func (c Chunk) CallTrees(activeThreadID *string) (map[string][]*nodetree.Node, error) { return c.chunk.CallTrees(activeThreadID) } diff --git a/internal/chunk/sample.go b/internal/chunk/sample.go index 2b124d20..f7ca54ca 100644 --- a/internal/chunk/sample.go +++ b/internal/chunk/sample.go @@ -253,6 +253,15 @@ func (c SampleChunk) GetOptions() options.Options { return c.Options } +func (c SampleChunk) GetFrameForRegression(target uint32) (frame.Frame, error) { + for _, f := range c.Profile.Frames { + if f.VroomrsFingerprint() == target { + return f, nil + } + } + return frame.Frame{}, frame.ErrFrameNotFound +} + func (c SampleChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { for _, f := range c.Profile.Frames { if f.Fingerprint() == target { diff --git a/internal/frame/frame.go b/internal/frame/frame.go index 72fc1b70..c1e8d331 100644 --- a/internal/frame/frame.go +++ b/internal/frame/frame.go @@ -240,6 +240,18 @@ func (f Frame) Fingerprint() uint32 { return uint32(h.Sum64()) } +// VroomrsFingerprint returns the frame fingerprint using the same FNV-1a +// algorithm as the vroomrs library. See [Frame::fingerprint] in vroomrs. +// +// [Frame::fingerprint]: https://github.com/getsentry/vroomrs/blob/54065818376d83d40ce2d95be81086543f7e8319/src/frame/mod.rs#L413-L427 +func (f Frame) VroomrsFingerprint() uint32 { + h := fnv.New64a() + h.Write([]byte(f.ModuleOrPackage())) + h.Write([]byte{':'}) + h.Write([]byte(f.Function)) + return uint32(h.Sum64()) +} + func defaultFormatter(f Frame) string { return f.Function } diff --git a/internal/frame/regression_fingerprint_test.go b/internal/frame/regression_fingerprint_test.go new file mode 100644 index 00000000..71145b6b --- /dev/null +++ b/internal/frame/regression_fingerprint_test.go @@ -0,0 +1,54 @@ +package frame + +import "testing" + +func TestRegressionFingerprint(t *testing.T) { + tests := []struct { + name string + frame Frame + legacy uint32 + vroomrs uint32 + }{ + { + name: "module", + frame: Frame{Module: "demo", Function: "work"}, + legacy: 25692501, + vroomrs: 152761553, + }, + { + name: "module takes precedence", + frame: Frame{Module: "demo", Package: "/tmp/other.so", Function: "work"}, + legacy: 25692501, + vroomrs: 152761553, + }, + { + name: "unix package", + frame: Frame{Package: "/tmp/demo.so", Function: "work"}, + legacy: 25692501, + vroomrs: 152761553, + }, + { + name: "windows package", + frame: Frame{Package: `C:\app\demo.dll`, Function: "work"}, + legacy: 25692501, + vroomrs: 152761553, + }, + { + name: "no module or package", + frame: Frame{File: "demo.py", Function: "work", InstructionAddr: "0x1234"}, + legacy: 2431592238, + vroomrs: 1176630088, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.frame.Fingerprint(); got != tt.legacy { + t.Fatalf("Fingerprint() = %d, want %d", got, tt.legacy) + } + if got := tt.frame.VroomrsFingerprint(); got != tt.vroomrs { + t.Fatalf("VroomrsFingerprint() = %d, want %d", got, tt.vroomrs) + } + }) + } +} diff --git a/internal/occurrence/regressed_frame.go b/internal/occurrence/regressed_frame.go index 9df0d0fa..ea36c4e4 100644 --- a/internal/occurrence/regressed_frame.go +++ b/internal/occurrence/regressed_frame.go @@ -98,13 +98,13 @@ func getPlatformAndFrame( if result, ok := res.(profile.ReadJobResult); ok { platform = result.Profile.Platform() - frame, err = result.Profile.GetFrameWithFingerprint(target) + frame, err = result.Profile.GetFrameForRegression(target) if err != nil { return platform, frame, err } } else if result, ok := res.(chunk.ReadJobResult); ok { platform = result.Chunk.GetPlatform() - frame, err = result.Chunk.GetFrameWithFingerprint(target) + frame, err = result.Chunk.GetFrameForRegression(target) if err != nil { return platform, frame, err } diff --git a/internal/occurrence/regressed_frame_test.go b/internal/occurrence/regressed_frame_test.go new file mode 100644 index 00000000..f4c8592d --- /dev/null +++ b/internal/occurrence/regressed_frame_test.go @@ -0,0 +1,172 @@ +package occurrence + +import ( + "context" + "errors" + "testing" + + "github.com/getsentry/vroom/internal/chunk" + "github.com/getsentry/vroom/internal/frame" + "github.com/getsentry/vroom/internal/platform" + "github.com/getsentry/vroom/internal/profile" + "github.com/getsentry/vroom/internal/sample" + "github.com/getsentry/vroom/internal/storageutil" + "github.com/getsentry/vroom/internal/testutil" +) + +type regressionFrameLookup interface { + GetFrameWithFingerprint(uint32) (frame.Frame, error) + GetFrameForRegression(uint32) (frame.Frame, error) +} + +type regressionFrameSource struct { + name string + lookup regressionFrameLookup + result storageutil.ReadJobResult + platform platform.Platform + frames []frame.Frame +} + +func regressionFrameSources(names ...string) []regressionFrameSource { + frames := make([]frame.Frame, len(names)) + methods := make([]profile.AndroidMethod, len(names)) + androidFrames := make([]frame.Frame, len(names)) + for i, name := range names { + frames[i] = frame.Frame{Function: name} + methods[i] = profile.AndroidMethod{ID: uint64(i + 1), Name: name} + androidFrames[i] = methods[i].Frame() + } + + sampled := profile.New(&sample.Profile{RawProfile: sample.RawProfile{ + Platform: platform.Node, + Trace: sample.Trace{Frames: frames}, + }}) + android := profile.New(&profile.LegacyProfile{ + RawProfile: profile.RawProfile{Platform: platform.Android}, + Trace: &profile.Android{Methods: methods}, + }) + sampledChunk := chunk.New(&chunk.SampleChunk{ + Platform: platform.Node, + Profile: chunk.SampleData{Frames: frames}, + }) + androidChunk := chunk.New(&chunk.AndroidChunk{ + Platform: platform.Android, + Profile: profile.Android{Methods: methods}, + }) + + return []regressionFrameSource{ + { + name: "sample profile", + lookup: &sampled, + result: profile.ReadJobResult{Profile: &sampled}, + platform: platform.Node, + frames: frames, + }, + { + name: "android profile", + lookup: &android, + result: profile.ReadJobResult{Profile: &android}, + platform: platform.Android, + frames: androidFrames, + }, + { + name: "sample chunk", + lookup: sampledChunk, + result: chunk.ReadJobResult{Chunk: &sampledChunk}, + platform: platform.Node, + frames: frames, + }, + { + name: "android chunk", + lookup: androidChunk, + result: chunk.ReadJobResult{Chunk: &androidChunk}, + platform: platform.Android, + frames: androidFrames, + }, + } +} + +func TestGetFrameForRegression(t *testing.T) { + tests := []struct { + name string + fingerprint uint32 + wantErr error + }{ + {name: "legacy fingerprint", fingerprint: 2431592238, wantErr: frame.ErrFrameNotFound}, + {name: "vroomrs fingerprint", fingerprint: 1176630088}, + {name: "unknown fingerprint", fingerprint: 0, wantErr: frame.ErrFrameNotFound}, + } + + for _, source := range regressionFrameSources("work") { + t.Run(source.name, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := source.lookup.GetFrameForRegression(tt.fingerprint) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("GetFrameForRegression() error = %v, want %v", err, tt.wantErr) + } + want := frame.Frame{} + if tt.wantErr == nil { + want = source.frames[0] + } + if diff := testutil.Diff(want, got); diff != "" { + t.Fatalf("GetFrameForRegression() mismatch (-want +got):\n%s", diff) + } + + gotPlatform, gotFrame, err := getPlatformAndFrame(context.Background(), source.result, tt.fingerprint) + if !errors.Is(err, tt.wantErr) { + t.Fatalf("getPlatformAndFrame() error = %v, want %v", err, tt.wantErr) + } + if gotPlatform != source.platform { + t.Fatalf("getPlatformAndFrame() platform = %s, want %s", gotPlatform, source.platform) + } + if diff := testutil.Diff(want, gotFrame); diff != "" { + t.Fatalf("getPlatformAndFrame() mismatch (-want +got):\n%s", diff) + } + }) + } + + got, err := source.lookup.GetFrameWithFingerprint(2431592238) + if err != nil { + t.Fatalf("legacy lookup failed: %v", err) + } + if diff := testutil.Diff(source.frames[0], got); diff != "" { + t.Fatalf("legacy lookup mismatch (-want +got):\n%s", diff) + } + got, err = source.lookup.GetFrameWithFingerprint(1176630088) + if !errors.Is(err, frame.ErrFrameNotFound) { + t.Fatalf("legacy lookup accepted vroomrs fingerprint: %v", err) + } + if diff := testutil.Diff(frame.Frame{}, got); diff != "" { + t.Fatalf("legacy lookup returned a frame (-want +got):\n%s", diff) + } + }) + } +} + +func TestGetFrameForRegressionEmpty(t *testing.T) { + for _, source := range regressionFrameSources() { + t.Run(source.name, func(t *testing.T) { + got, err := source.lookup.GetFrameForRegression(1176630088) + if !errors.Is(err, frame.ErrFrameNotFound) { + t.Fatalf("GetFrameForRegression() error = %v, want %v", err, frame.ErrFrameNotFound) + } + if diff := testutil.Diff(frame.Frame{}, got); diff != "" { + t.Fatalf("GetFrameForRegression() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestGetPlatformAndFrameReadError(t *testing.T) { + wantErr := errors.New("read failed") + for _, result := range []storageutil.ReadJobResult{ + profile.ReadJobResult{Err: wantErr}, + chunk.ReadJobResult{Err: wantErr}, + } { + _, _, err := getPlatformAndFrame(context.Background(), result, 1176630088) + if !errors.Is(err, wantErr) { + t.Fatalf("getPlatformAndFrame() error = %v, want %v", err, wantErr) + } + } +} diff --git a/internal/profile/android.go b/internal/profile/android.go index 49f8b5cb..23ec8a3b 100644 --- a/internal/profile/android.go +++ b/internal/profile/android.go @@ -669,6 +669,16 @@ func (p Android) ActiveThreadID() uint64 { return 0 } +func (p Android) GetFrameForRegression(target uint32) (frame.Frame, error) { + for _, m := range p.Methods { + f := m.Frame() + if f.VroomrsFingerprint() == target { + return f, nil + } + } + return frame.Frame{}, frame.ErrFrameNotFound +} + func (p Android) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { for _, m := range p.Methods { f := m.Frame() diff --git a/internal/profile/legacy.go b/internal/profile/legacy.go index 443cb4aa..1f488826 100644 --- a/internal/profile/legacy.go +++ b/internal/profile/legacy.go @@ -367,6 +367,10 @@ func (p LegacyProfile) GetFrameWithFingerprint(target uint32) (frame.Frame, erro return p.Trace.GetFrameWithFingerprint(target) } +func (p LegacyProfile) GetFrameForRegression(target uint32) (frame.Frame, error) { + return p.Trace.GetFrameForRegression(target) +} + // This is to be used for ReactNative JS profile only since it works based on the // assumption that we'll only have 1 thread in the JS profile, as is the case // for ReactNative. diff --git a/internal/profile/profile.go b/internal/profile/profile.go index 38b3f3d7..496c4989 100644 --- a/internal/profile/profile.go +++ b/internal/profile/profile.go @@ -44,6 +44,7 @@ type ( SetProfileID(ID string) GetOptions() options.Options GetFrameWithFingerprint(uint32) (frame.Frame, error) + GetFrameForRegression(uint32) (frame.Frame, error) } Profile struct { @@ -193,3 +194,7 @@ func (p *Profile) GetOptions() options.Options { func (p *Profile) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { return p.profile.GetFrameWithFingerprint(target) } + +func (p *Profile) GetFrameForRegression(target uint32) (frame.Frame, error) { + return p.profile.GetFrameForRegression(target) +} diff --git a/internal/profile/trace.go b/internal/profile/trace.go index a7c388b3..e00696fb 100644 --- a/internal/profile/trace.go +++ b/internal/profile/trace.go @@ -12,5 +12,6 @@ type ( CallTrees() map[uint64][]*nodetree.Node Speedscope() (speedscope.Output, error) GetFrameWithFingerprint(uint32) (frame.Frame, error) + GetFrameForRegression(uint32) (frame.Frame, error) } ) diff --git a/internal/sample/sample.go b/internal/sample/sample.go index ba773278..638a26ea 100644 --- a/internal/sample/sample.go +++ b/internal/sample/sample.go @@ -455,6 +455,15 @@ func (p *Profile) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { return frame.Frame{}, frame.ErrFrameNotFound } +func (p *Profile) GetFrameForRegression(target uint32) (frame.Frame, error) { + for _, f := range p.Trace.Frames { + if f.VroomrsFingerprint() == target { + return f, nil + } + } + return frame.Frame{}, frame.ErrFrameNotFound +} + func (t Trace) SamplesByThreadD() ([]uint64, map[uint64][]*Sample) { samples := make(map[uint64][]*Sample) var threadIDs []uint64