Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions internal/chunk/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chunk

import (
"encoding/json"
"log/slog"
"strconv"
"time"

Expand Down Expand Up @@ -122,12 +123,35 @@ func (c AndroidChunk) GetOptions() options.Options {
}

func (c AndroidChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) {
// Try exact match first
for _, m := range c.Profile.Methods {
f := m.Frame()
if f.Fingerprint() == target {
return f, nil
}
}

// Build frames array for fallback matching
frames := make([]frame.Frame, 0, len(c.Profile.Methods))
for _, m := range c.Profile.Methods {
frames = append(frames, m.Frame())
}

// Try fallback with fingerprint variations
matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(frames, target)
if err == nil && usedFallback {
slog.Warn(
"Frame matched using fallback fingerprint computation",
"target_fingerprint", target,
"matched_frame_fingerprint", matchedFrame.Fingerprint(),
"matched_function", matchedFrame.Function,
"matched_module", matchedFrame.ModuleOrPackage(),
"chunk_id", c.ID,
"profiler_id", c.ProfilerID,
)
return matchedFrame, nil
}

return frame.Frame{}, frame.ErrFrameNotFound
}

Expand Down
18 changes: 18 additions & 0 deletions internal/chunk/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"hash/fnv"
"log/slog"
"math"
"sort"

Expand Down Expand Up @@ -254,10 +255,27 @@ func (c SampleChunk) GetOptions() options.Options {
}

func (c SampleChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) {
// Try exact match first
for _, f := range c.Profile.Frames {
if f.Fingerprint() == target {
return f, nil
}
}

// Try fallback with fingerprint variations
matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(c.Profile.Frames, target)
if err == nil && usedFallback {
slog.Warn(
"Frame matched using fallback fingerprint computation",
"target_fingerprint", target,
"matched_frame_fingerprint", matchedFrame.Fingerprint(),
"matched_function", matchedFrame.Function,
"matched_module", matchedFrame.ModuleOrPackage(),
"chunk_id", c.ID,
"profiler_id", c.ProfilerID,
)
return matchedFrame, nil
}

return frame.Frame{}, frame.ErrFrameNotFound
}
81 changes: 80 additions & 1 deletion internal/frame/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ var (
"hermes": {},
}

ErrFrameNotFound = errors.New("Unable to find matching frame")
ErrFrameNotFound = errors.New("Unable to find matching frame")
ErrFrameNotFoundWithFallback = errors.New("Unable to find matching frame even with fallback")
)

type (
Expand Down Expand Up @@ -240,6 +241,84 @@ func (f Frame) Fingerprint() uint32 {
return uint32(h.Sum64())
}

// computeFingerprintVariations computes alternative fingerprint values for a frame
// by trying different normalizations of module/package and function names.
// This helps match frames when the fingerprint computation differs slightly
// between client SDK and vroom (e.g., due to encoding, normalization, or edge cases).
func computeFingerprintVariations(f Frame) []uint32 {
variations := make([]uint32, 0, 8)
h := fnv.New64()

// Original fingerprint (already tried, but include for completeness)
variations = append(variations, f.Fingerprint())

// Try with raw Package instead of trimPackage(Package)
if f.Package != "" && f.Module == "" {
h.Reset()
h.Write([]byte(f.Package))
h.Write([]byte{':'})
h.Write([]byte(f.Function))
variations = append(variations, uint32(h.Sum64()))
}

// Try with File instead of Module/Package
if f.File != "" && (f.Module != "" || f.Package != "") {
h.Reset()
h.Write([]byte(f.File))
h.Write([]byte{':'})
h.Write([]byte(f.Function))
variations = append(variations, uint32(h.Sum64()))
}

// Try with Module alone (even if Package is set)
if f.Module != "" {
h.Reset()
h.Write([]byte(f.Module))
h.Write([]byte{':'})
h.Write([]byte(f.Function))
variations = append(variations, uint32(h.Sum64()))
}

// Try with empty module/package (just function name)
if f.Function != "" {
h.Reset()
h.Write([]byte{':'})
h.Write([]byte(f.Function))
variations = append(variations, uint32(h.Sum64()))

h.Reset()
h.Write([]byte(f.Function))
variations = append(variations, uint32(h.Sum64()))
}

return variations
}

// FindFrameByFingerprintWithFallback searches for a frame matching the target fingerprint.
// It first tries exact matches, then tries alternative fingerprint calculations
// to handle cases where the client SDK and vroom compute fingerprints differently.
// Returns the matching frame, whether fallback was used, and an error if no match found.
func FindFrameByFingerprintWithFallback(frames []Frame, targetFingerprint uint32) (Frame, bool, error) {
// First pass: try exact fingerprint match
for _, f := range frames {
if f.Fingerprint() == targetFingerprint {
return f, false, nil
}
}

// Second pass: try fingerprint variations (fallback)
for _, f := range frames {
variations := computeFingerprintVariations(f)
for _, variant := range variations {
if variant == targetFingerprint {
return f, true, nil
}
}
}

return Frame{}, false, ErrFrameNotFoundWithFallback
}

func defaultFormatter(f Frame) string {
return f.Function
}
Expand Down
126 changes: 126 additions & 0 deletions internal/frame/frame_fallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package frame

import (
"hash/fnv"
"testing"
)

func TestFindFrameByFingerprintWithFallback(t *testing.T) {
tests := []struct {
name string
frames []Frame
targetFingerprint uint32
expectMatch bool
expectFallback bool
expectedFunction string
}{
{
name: "exact match",
frames: []Frame{
{Function: "testFunc", Module: "testModule"},
},
targetFingerprint: Frame{Function: "testFunc", Module: "testModule"}.Fingerprint(),
expectMatch: true,
expectFallback: false,
expectedFunction: "testFunc",
},
{
name: "fallback match with raw package",
frames: []Frame{
{Function: "testFunc", Package: "/path/to/libtest.so"},
},
targetFingerprint: Frame{Function: "testFunc", Package: "/path/to/libtest.so"}.Fingerprint(),
expectMatch: true,
expectFallback: false,
expectedFunction: "testFunc",
},
{
name: "no match",
frames: []Frame{
{Function: "testFunc", Module: "testModule"},
},
targetFingerprint: Frame{Function: "differentFunc", Module: "differentModule"}.Fingerprint(),
expectMatch: false,
expectFallback: false,
},
{
name: "fallback match with file instead of module",
frames: []Frame{
{Function: "testFunc", File: "testFile.py", Module: "testModule"},
},
targetFingerprint: computeFingerprintFromFileAndFunction("testFile.py", "testFunc"),
expectMatch: true,
expectFallback: true,
expectedFunction: "testFunc",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matchedFrame, usedFallback, err := FindFrameByFingerprintWithFallback(tt.frames, tt.targetFingerprint)

if tt.expectMatch {
if err != nil {
t.Errorf("Expected match but got error: %v", err)
}
if matchedFrame.Function != tt.expectedFunction {
t.Errorf("Expected function %s, got %s", tt.expectedFunction, matchedFrame.Function)
}
if usedFallback != tt.expectFallback {
t.Errorf("Expected fallback=%v, got %v", tt.expectFallback, usedFallback)
}
} else {
if err == nil {
t.Errorf("Expected no match but found frame: %+v", matchedFrame)
}
}
})
}
}

func TestComputeFingerprintVariations(t *testing.T) {
tests := []struct {
name string
frame Frame
minVariations int
}{
{
name: "frame with module and function",
frame: Frame{Function: "testFunc", Module: "testModule"},
minVariations: 2,
},
{
name: "frame with package and function",
frame: Frame{Function: "testFunc", Package: "/path/to/lib.so"},
minVariations: 3,
},
{
name: "frame with file, module and function",
frame: Frame{Function: "testFunc", Module: "testModule", File: "test.py"},
minVariations: 3,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
variations := computeFingerprintVariations(tt.frame)
if len(variations) < tt.minVariations {
t.Errorf("Expected at least %d variations, got %d", tt.minVariations, len(variations))
}

// Verify first variation matches the standard fingerprint
if variations[0] != tt.frame.Fingerprint() {
t.Errorf("First variation should match standard fingerprint")
}
})
}
}

// Helper function to compute fingerprint from file and function.
func computeFingerprintFromFileAndFunction(file, function string) uint32 {
h := fnv.New64()
h.Write([]byte(file))
h.Write([]byte{':'})
h.Write([]byte(function))
return uint32(h.Sum64())
}
22 changes: 22 additions & 0 deletions internal/profile/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"hash/fnv"
"log/slog"
"math"
"path"
"strings"
Expand Down Expand Up @@ -670,12 +671,33 @@ func (p Android) ActiveThreadID() uint64 {
}

func (p Android) GetFrameWithFingerprint(target uint32) (frame.Frame, error) {
// Try exact match first
for _, m := range p.Methods {
f := m.Frame()
if f.Fingerprint() == target {
return f, nil
}
}

// Build frames array for fallback matching
frames := make([]frame.Frame, 0, len(p.Methods))
for _, m := range p.Methods {
frames = append(frames, m.Frame())
}

// Try fallback with fingerprint variations
matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(frames, target)
if err == nil && usedFallback {
slog.Warn(
"Frame matched using fallback fingerprint computation",
"target_fingerprint", target,
"matched_frame_fingerprint", matchedFrame.Fingerprint(),
"matched_function", matchedFrame.Function,
"matched_module", matchedFrame.ModuleOrPackage(),
)
return matchedFrame, nil
}

// TODO: handle react native
return frame.Frame{}, frame.ErrFrameNotFound
}
17 changes: 17 additions & 0 deletions internal/sample/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"hash/fnv"
"log/slog"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -447,11 +448,27 @@ func (p *Profile) GetOptions() options.Options {
}

func (p *Profile) GetFrameWithFingerprint(target uint32) (frame.Frame, error) {
// Try exact match first
for _, f := range p.Trace.Frames {
if f.Fingerprint() == target {
return f, nil
}
}

// Try fallback with fingerprint variations
matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(p.Trace.Frames, target)
if err == nil && usedFallback {
slog.Warn(
"Frame matched using fallback fingerprint computation",
"target_fingerprint", target,
"matched_frame_fingerprint", matchedFrame.Fingerprint(),
"matched_function", matchedFrame.Function,
"matched_module", matchedFrame.ModuleOrPackage(),
"profile_id", p.EventID,
)
return matchedFrame, nil
}

return frame.Frame{}, frame.ErrFrameNotFound
}

Expand Down
Loading