Repository: open-telemetry/opentelemetry-ebpf-profiler (2025-08-26T05:49:58Z)

Issue:
Removal of libpf.Trace.Hash broke reporter
I believe https://github.com/open-telemetry/opentelemetry-ebpf-profiler/pull/673 broke reporter.
With the removal of `Hash` from `TraceAndMetaKey` all trace events for a given process thread (ie. with identical metadata: Pid,Tid,Comm,ProcessName,ExecutablePath,...) will all be incorrectly [aggregated](https://github.com/DataDog/opentelemetry-ebpf-profiler/blob/nsavoire/disable_tracehandler_cache/reporter/base_reporter.go#L95-L100) together with the stacktrace of the first trace event for this thread.


Discussion:
Seems so. Thanks for the report and sorry for the inconvience. Let's start by reverting the change.



Patch:
diff --git a/interpreter/apmint/apmint.go b/interpreter/apmint/apmint.go
index 5e9967ffa..ddfe142a8 100644
--- a/interpreter/apmint/apmint.go
+++ b/interpreter/apmint/apmint.go
@@ -8,11 +8,10 @@
 package apmint // import "go.opentelemetry.io/ebpf-profiler/interpreter/apmint"
 
 import (
+	"encoding/hex"
 	"errors"
 	"fmt"
-	"hash/fnv"
 	"regexp"
-	"strconv"
 	"unsafe"
 
 	log "github.com/sirupsen/logrus"
@@ -145,46 +144,31 @@ type Instance struct {
 
 var _ interpreter.Instance = &Instance{}
 
-// hashTrace calculates the hash of a trace and returns it.
-// Be aware that changes to this calculation will break the ability to
-// look backwards for the same TraceHash in our backend.
-func hashTrace(trace *libpf.Trace) (hash [16]byte) {
-	var buf [24]byte
-	h := fnv.New128a()
-	for _, uniqueFrame := range trace.Frames {
-		frame := uniqueFrame.Value()
-		_, _ = h.Write(frame.FileID.Bytes())
-		// Using FormatUint() or putting AppendUint() into a function leads
-		// to escaping to heap (allocation).
-		_, _ = h.Write(strconv.AppendUint(buf[:0], uint64(frame.AddressOrLineno), 10))
-	}
-	h.Sum(hash[:0])
-	return
-}
-
 // Detach implements the interpreter.Instance interface.
 func (i *Instance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
 	return ebpf.DeleteProcData(libpf.APMInt, pid)
 }
 
 // NotifyAPMAgent sends out collected traces to the connected APM agent.
-func (i *Instance) NotifyAPMAgent(pid libpf.PID, rawTrace *host.Trace, umTrace *libpf.Trace) {
+func (i *Instance) NotifyAPMAgent(
+	pid libpf.PID, rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) {
 	if rawTrace.APMTransactionID == libpf.InvalidAPMSpanID || i.socket == nil {
 		return
 	}
 
+	log.Debugf("Reporting %dx trace hash %s -> TX %s for PID %d",
+		count, umTraceHash.StringNoQuotes(),
+		hex.EncodeToString(rawTrace.APMTransactionID[:]), pid)
+
 	msg := traceCorrMsg{
 		MessageType:      1,
 		MinorVersion:     1,
 		APMTraceID:       rawTrace.APMTraceID,
 		APMTransactionID: rawTrace.APMTransactionID,
-		StackTraceID:     hashTrace(umTrace),
-		Count:            1,
+		StackTraceID:     umTraceHash,
+		Count:            count,
 	}
 
-	log.Debugf("Reporting a trace hash %x -> TX %x for PID %d",
-		msg.StackTraceID[:], rawTrace.APMTransactionID[:], pid)
-
 	if err := i.socket.SendMessage(msg.Serialize()); err != nil {
 		log.Debugf("Failed to send trace mappings to APM agent: %v", err)
 	}
diff --git a/interpreter/apmint/socket.go b/interpreter/apmint/socket.go
index 8f8c4eb29..6172c4370 100644
--- a/interpreter/apmint/socket.go
+++ b/interpreter/apmint/socket.go
@@ -103,7 +103,7 @@ type traceCorrMsg struct {
 	MinorVersion     uint16
 	APMTraceID       libpf.APMTraceID
 	APMTransactionID libpf.APMTransactionID
-	StackTraceID     [16]byte
+	StackTraceID     libpf.TraceHash
 	Count            uint16
 }
 
@@ -113,7 +113,7 @@ func (m *traceCorrMsg) Serialize() []byte {
 	_ = binary.Write(&buf, binary.LittleEndian, m.MinorVersion)
 	_, _ = buf.Write(m.APMTraceID[:])
 	_, _ = buf.Write(m.APMTransactionID[:])
-	_, _ = buf.Write(m.StackTraceID[:])
+	_, _ = buf.Write(m.StackTraceID.Bytes())
 	_ = binary.Write(&buf, binary.LittleEndian, m.Count)
 	return buf.Bytes()
 }
diff --git a/libpf/trace.go b/libpf/trace.go
index fda4b6221..bbae7c8d5 100644
--- a/libpf/trace.go
+++ b/libpf/trace.go
@@ -41,6 +41,7 @@ func (frames *Frames) Append(frame *Frame) {
 // Trace represents a stack trace.
 type Trace struct {
 	Frames       Frames
+	Hash         TraceHash
 	CustomLabels map[string]string
 }
 
diff --git a/libpf/tracehash.go b/libpf/tracehash.go
new file mode 100644
index 000000000..be2bee3b5
--- /dev/null
+++ b/libpf/tracehash.go
@@ -0,0 +1,60 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package libpf // import "go.opentelemetry.io/ebpf-profiler/libpf"
+
+import (
+	"encoding"
+	"encoding/base64"
+
+	"go.opentelemetry.io/ebpf-profiler/libpf/basehash"
+)
+
+// TraceHash represents the unique hash of a trace
+type TraceHash struct {
+	basehash.Hash128
+}
+
+func NewTraceHash(hi, lo uint64) TraceHash {
+	return TraceHash{basehash.New128(hi, lo)}
+}
+
+// TraceHashFromBytes parses a byte slice of a trace hash into the internal data representation.
+func TraceHashFromBytes(b []byte) (TraceHash, error) {
+	h, err := basehash.New128FromBytes(b)
+	if err != nil {
+		return TraceHash{}, err
+	}
+	return TraceHash{h}, nil
+}
+
+func (h TraceHash) Equal(other TraceHash) bool {
+	return h.Hash128.Equal(other.Hash128)
+}
+
+func (h TraceHash) Less(other TraceHash) bool {
+	return h.Hash128.Less(other.Hash128)
+}
+
+// EncodeTo encodes the hash into the base64 encoded representation
+// and stores it in the provided destination byte array.
+// The length of the destination must be at least EncodedLen().
+func (h TraceHash) EncodeTo(dst []byte) {
+	base64.RawURLEncoding.Encode(dst, h.Bytes())
+}
+
+// EncodedLen returns the length of the hash's base64 representation.
+func (TraceHash) EncodedLen() int {
+	// TraceHash is 16 bytes long, the base64 representation is one base64 byte per 6 bits.
+	return ((16)*8)/6 + 1
+}
+
+// Hash32 returns a 32 bits hash of the input.
+// It's main purpose is to be used for LRU caching.
+func (h TraceHash) Hash32() uint32 {
+	return uint32(h.Lo())
+}
+
+// Compile-time interface checks
+var _ encoding.TextUnmarshaler = (*TraceHash)(nil)
+var _ encoding.TextMarshaler = (*TraceHash)(nil)
diff --git a/processmanager/manager.go b/processmanager/manager.go
index dd46dec15..2cc103db4 100644
--- a/processmanager/manager.go
+++ b/processmanager/manager.go
@@ -26,6 +26,7 @@ import (
 	"go.opentelemetry.io/ebpf-profiler/reporter"
 	"go.opentelemetry.io/ebpf-profiler/times"
 	"go.opentelemetry.io/ebpf-profiler/tracer/types"
+	"go.opentelemetry.io/ebpf-profiler/traceutil"
 	"go.opentelemetry.io/ebpf-profiler/util"
 )
 
@@ -295,10 +296,12 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace
 			}
 		}
 	}
+	newTrace.Hash = traceutil.HashTrace(newTrace)
 	return newTrace
 }
 
-func (pm *ProcessManager) MaybeNotifyAPMAgent(rawTrace *host.Trace, umTrace *libpf.Trace) string {
+func (pm *ProcessManager) MaybeNotifyAPMAgent(
+	rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) string {
 	pm.mu.RLock()
 	pidInterp, ok := pm.interpreters[rawTrace.PID]
 	pm.mu.RUnlock()
@@ -309,7 +312,7 @@ func (pm *ProcessManager) MaybeNotifyAPMAgent(rawTrace *host.Trace, umTrace *lib
 	var serviceName string
 	for _, mapping := range pidInterp {
 		if apm, ok := mapping.(*apmint.Instance); ok {
-			apm.NotifyAPMAgent(rawTrace.PID, rawTrace, umTrace)
+			apm.NotifyAPMAgent(rawTrace.PID, rawTrace, umTraceHash, count)
 
 			if serviceName != "" {
 				log.Warnf("Overwriting APM service name from '%s' to '%s' for PID %d",
diff --git a/reporter/base_reporter.go b/reporter/base_reporter.go
index 9ba6c52d0..8a49f26d2 100644
--- a/reporter/base_reporter.go
+++ b/reporter/base_reporter.go
@@ -69,6 +69,7 @@ func (b *baseReporter) ReportTraceEvent(trace *libpf.Trace, meta *samples.TraceE
 
 	containerID := meta.ContainerID
 	key := samples.TraceAndMetaKey{
+		Hash:           trace.Hash,
 		Comm:           meta.Comm,
 		ProcessName:    meta.ProcessName,
 		ExecutablePath: meta.ExecutablePath,
diff --git a/reporter/samples/samples.go b/reporter/samples/samples.go
index 838783cd4..0f24e8597 100644
--- a/reporter/samples/samples.go
+++ b/reporter/samples/samples.go
@@ -34,6 +34,9 @@ type TraceEvents struct {
 // contain all trace fields that aren't already part of the trace hash to ensure
 // that we don't accidentally merge traces with different fields.
 type TraceAndMetaKey struct {
+	// Hash is not sent forward, but it is used as the primary key
+	// to not aggregate difference traces.
+	Hash libpf.TraceHash
 	// comm and apmServiceName are provided by the eBPF programs
 	Comm           string
 	ApmServiceName string
diff --git a/tracehandler/tracehandler.go b/tracehandler/tracehandler.go
index 97230b584..e89ae1f54 100644
--- a/tracehandler/tracehandler.go
+++ b/tracehandler/tracehandler.go
@@ -41,7 +41,7 @@ type TraceProcessor interface {
 	// MaybeNotifyAPMAgent notifies a potentially existing connected APM agent
 	// that a stack trace was collected in their process. If an APM agent is
 	// listening, the service name is returned.
-	MaybeNotifyAPMAgent(rawTrace *host.Trace, umTrace *libpf.Trace) string
+	MaybeNotifyAPMAgent(rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) string
 
 	// ConvertTrace converts a trace from eBPF into the form we want to send to
 	// the collection agent. Depending on the frame type it will attempt to symbolize
@@ -135,7 +135,7 @@ func (m *traceHandler) HandleTrace(bpfTrace *host.Trace) {
 		traceCacheLifetime); exists {
 		m.traceCacheHit++
 		// Fast path
-		meta.APMServiceName = m.traceProcessor.MaybeNotifyAPMAgent(bpfTrace, &trace)
+		meta.APMServiceName = m.traceProcessor.MaybeNotifyAPMAgent(bpfTrace, trace.Hash, 1)
 		if err := m.reporter.ReportTraceEvent(&trace, meta); err != nil {
 			log.Errorf("Failed to report trace event: %v", err)
 		}
@@ -147,7 +147,7 @@ func (m *traceHandler) HandleTrace(bpfTrace *host.Trace) {
 	umTrace := m.traceProcessor.ConvertTrace(bpfTrace)
 	m.traceCache.Add(bpfTrace.Hash, *umTrace)
 
-	meta.APMServiceName = m.traceProcessor.MaybeNotifyAPMAgent(bpfTrace, umTrace)
+	meta.APMServiceName = m.traceProcessor.MaybeNotifyAPMAgent(bpfTrace, umTrace.Hash, 1)
 	if err := m.reporter.ReportTraceEvent(umTrace, meta); err != nil {
 		log.Errorf("Failed to report trace event: %v", err)
 	}
diff --git a/traceutil/traceutil.go b/traceutil/traceutil.go
new file mode 100644
index 000000000..fead3a987
--- /dev/null
+++ b/traceutil/traceutil.go
@@ -0,0 +1,29 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package traceutil // import "go.opentelemetry.io/ebpf-profiler/traceutil"
+
+import (
+	"hash/fnv"
+	"strconv"
+
+	"go.opentelemetry.io/ebpf-profiler/libpf"
+)
+
+// HashTrace calculates the hash of a trace and returns it.
+// Be aware that changes to this calculation will break the ability to
+// look backwards for the same TraceHash in our backend.
+func HashTrace(trace *libpf.Trace) libpf.TraceHash {
+	var buf [24]byte
+	h := fnv.New128a()
+	for _, uniqueFrame := range trace.Frames {
+		frame := uniqueFrame.Value()
+		_, _ = h.Write(frame.FileID.Bytes())
+		// Using FormatUint() or putting AppendUint() into a function leads
+		// to escaping to heap (allocation).
+		_, _ = h.Write(strconv.AppendUint(buf[:0], uint64(frame.AddressOrLineno), 10))
+	}
+	// make instead of nil avoids a heap allocation
+	traceHash, _ := libpf.TraceHashFromBytes(h.Sum(make([]byte, 0, 16)))
+	return traceHash
+}


Test patch:
diff --git a/libpf/tracehash_test.go b/libpf/tracehash_test.go
new file mode 100644
index 000000000..ed26df687
--- /dev/null
+++ b/libpf/tracehash_test.go
@@ -0,0 +1,83 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package libpf
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestTraceHashSprintf(t *testing.T) {
+	origHash := NewTraceHash(0x0001C03F8D6B8520, 0xEDEAEEA9460BEEBB)
+
+	marshaled := fmt.Sprintf("%d", origHash)
+	//nolint:goconst
+	expected := "{492854164817184 17143777342331285179}"
+	assert.Equal(t, expected, marshaled)
+
+	marshaled = fmt.Sprintf("%s", origHash)
+	expected = "{%!s(uint64=492854164817184) %!s(uint64=17143777342331285179)}"
+	assert.Equal(t, expected, marshaled)
+
+	marshaled = fmt.Sprintf("%v", origHash)
+	//nolint:goconst
+	expected = "{492854164817184 17143777342331285179}"
+	assert.Equal(t, expected, marshaled)
+
+	marshaled = fmt.Sprintf("%#v", origHash)
+	expected = "0x1c03f8d6b8520edeaeea9460beebb"
+	assert.Equal(t, expected, marshaled)
+
+	// Values were chosen to test non-zero-padded output
+	traceHash := NewTraceHash(42, 100)
+
+	marshaled = fmt.Sprintf("%x", traceHash)
+	expected = "2a0000000000000064"
+	assert.Equal(t, expected, marshaled)
+
+	marshaled = fmt.Sprintf("%X", traceHash)
+	expected = "2A0000000000000064"
+	assert.Equal(t, expected, marshaled)
+
+	marshaled = fmt.Sprintf("%#x", traceHash)
+	expected = "0x2a0000000000000064"
+	assert.Equal(t, expected, marshaled)
+
+	marshaled = fmt.Sprintf("%#X", traceHash)
+	expected = "0x2A0000000000000064"
+	assert.Equal(t, expected, marshaled)
+}
+
+func TestTraceHashMarshal(t *testing.T) {
+	origHash := NewTraceHash(0x600DF00D, 0xF00D600D)
+
+	// Test (Un)MarshalJSON
+	data, err := origHash.MarshalJSON()
+	require.NoError(t, err)
+
+	marshaled := string(data)
+	expected := "\"00000000600df00d00000000f00d600d\""
+	assert.Equal(t, expected, marshaled)
+
+	var jsonHash TraceHash
+	err = jsonHash.UnmarshalJSON(data)
+	require.NoError(t, err)
+	assert.Equal(t, origHash, jsonHash)
+
+	// Test (Un)MarshalText
+	data, err = origHash.MarshalText()
+	require.NoError(t, err)
+
+	marshaled = string(data)
+	expected = "00000000600df00d00000000f00d600d"
+	assert.Equal(t, expected, marshaled)
+
+	var textHash TraceHash
+	err = textHash.UnmarshalText(data)
+	require.NoError(t, err)
+	assert.Equal(t, origHash, textHash)
+}
diff --git a/processmanager/manager_test.go b/processmanager/manager_test.go
index 3f7c967fa..6ada3571b 100644
--- a/processmanager/manager_test.go
+++ b/processmanager/manager_test.go
@@ -29,6 +29,7 @@ import (
 	"go.opentelemetry.io/ebpf-profiler/reporter"
 	"go.opentelemetry.io/ebpf-profiler/support"
 	tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types"
+	"go.opentelemetry.io/ebpf-profiler/traceutil"
 	"go.opentelemetry.io/ebpf-profiler/util"
 
 	"github.com/stretchr/testify/assert"
@@ -262,6 +263,87 @@ func (s *symbolReporterMockup) ExecutableMetadata(_ *reporter.ExecutableMetadata
 
 var _ reporter.SymbolReporter = (*symbolReporterMockup)(nil)
 
+func TestInterpreterConvertTrace(t *testing.T) {
+	partialNativeFrameFileID := uint64(0xabcdbeef)
+	nativeFrameLineno := libpf.AddressOrLineno(0x1234)
+
+	pythonAndNativeTrace := &host.Trace{
+		Frames: []host.Frame{{
+			// This represents a native frame
+			File:   host.FileID(partialNativeFrameFileID),
+			Lineno: nativeFrameLineno,
+			Type:   libpf.NativeFrame,
+		}, {
+			File:   host.FileID(42),
+			Lineno: libpf.AddressOrLineno(0x13e1bb8e), // same as runForeverTrace
+			Type:   libpf.PythonFrame,
+		}},
+	}
+
+	tests := map[string]struct {
+		trace  *host.Trace
+		expect *libpf.Trace
+	}{
+		"Convert Trace": {
+			trace: pythonAndNativeTrace,
+			expect: getExpectedTrace(pythonAndNativeTrace,
+				[]libpf.AddressOrLineno{0, 1}),
+		},
+	}
+
+	for name, testcase := range tests {
+		t.Run(name, func(t *testing.T) {
+			mapper := NewMapFileIDMapper()
+			for i, f := range testcase.trace.Frames {
+				mapper.Set(f.File, testcase.expect.Frames[i].Value().FileID)
+			}
+
+			// For this test do not include interpreters.
+			noIinterpreters, _ := tracertypes.Parse("")
+
+			ctx, cancel := context.WithCancel(t.Context())
+			defer cancel()
+
+			// To test ConvertTrace we do not require all parts of processmanager.
+			manager, err := New(ctx,
+				noIinterpreters,
+				1*time.Second,
+				nil,
+				nil,
+				&symbolReporterMockup{},
+				nil,
+				true,
+				libpf.Set[string]{})
+			require.NoError(t, err)
+
+			newTrace := manager.ConvertTrace(testcase.trace)
+
+			testcase.expect.Hash = traceutil.HashTrace(testcase.expect)
+			if testcase.expect.Hash == newTrace.Hash {
+				assert.Equal(t, testcase.expect, newTrace)
+			}
+		})
+	}
+}
+
+// getExpectedTrace returns a new libpf trace that is based on the provided host trace, but
+// with the linenos replaced by the provided values. This function is for generating an expected
+// trace for tests below.
+func getExpectedTrace(origTrace *host.Trace, linenos []libpf.AddressOrLineno) *libpf.Trace {
+	newTrace := &libpf.Trace{
+		Hash: libpf.NewTraceHash(uint64(origTrace.Hash), uint64(origTrace.Hash)),
+	}
+
+	for i, frame := range origTrace.Frames {
+		lineno := frame.Lineno
+		if linenos != nil {
+			lineno = linenos[i]
+		}
+		newTrace.AppendFrame(frame.Type, libpf.NewFileID(uint64(frame.File), 0), lineno)
+	}
+	return newTrace
+}
+
 func TestNewMapping(t *testing.T) {
 	tests := map[string]struct {
 		// newMapping holds the arguments that are passed to NewMapping() in the test.
diff --git a/reporter/samples/attrmgr_test.go b/reporter/samples/attrmgr_test.go
index c02706e1b..5b76ebb59 100644
--- a/reporter/samples/attrmgr_test.go
+++ b/reporter/samples/attrmgr_test.go
@@ -11,6 +11,7 @@ import (
 
 	"go.opentelemetry.io/collector/pdata/pcommon"
 	"go.opentelemetry.io/collector/pdata/pprofile"
+	"go.opentelemetry.io/ebpf-profiler/libpf"
 	semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
 )
 
@@ -28,6 +29,7 @@ func TestAttrTableManager(t *testing.T) {
 		"empty": {
 			k: []TraceAndMetaKey{
 				{
+					Hash:           libpf.TraceHash{},
 					Comm:           "",
 					ApmServiceName: "",
 					ContainerID:    "",
@@ -42,12 +44,14 @@ func TestAttrTableManager(t *testing.T) {
 		"duplicate": {
 			k: []TraceAndMetaKey{
 				{
+					Hash:           libpf.TraceHash{},
 					Comm:           "comm1",
 					ApmServiceName: "apmServiceName1",
 					ContainerID:    "containerID1",
 					Pid:            1234,
 				},
 				{
+					Hash:           libpf.TraceHash{},
 					Comm:           "comm1",
 					ApmServiceName: "apmServiceName1",
 					ContainerID:    "containerID1",
@@ -65,12 +69,14 @@ func TestAttrTableManager(t *testing.T) {
 		"different": {
 			k: []TraceAndMetaKey{
 				{
+					Hash:           libpf.TraceHash{},
 					Comm:           "comm1",
 					ApmServiceName: "apmServiceName1",
 					ContainerID:    "containerID1",
 					Pid:            1234,
 				},
 				{
+					Hash:           libpf.TraceHash{},
 					Comm:           "comm2",
 					ApmServiceName: "apmServiceName2",
 					ContainerID:    "containerID2",
diff --git a/tracehandler/tracehandler_test.go b/tracehandler/tracehandler_test.go
index 1a88d3d27..752a81c03 100644
--- a/tracehandler/tracehandler_test.go
+++ b/tracehandler/tracehandler_test.go
@@ -35,14 +35,14 @@ type fakeTraceProcessor struct{}
 var _ tracehandler.TraceProcessor = (*fakeTraceProcessor)(nil)
 
 func (f *fakeTraceProcessor) ConvertTrace(trace *host.Trace) *libpf.Trace {
-	converted := &libpf.Trace{}
-	converted.Frames.Append(&libpf.Frame{AddressOrLineno: libpf.AddressOrLineno(trace.Hash)})
-	return converted
+	var newTrace libpf.Trace
+	newTrace.Hash = libpf.NewTraceHash(uint64(trace.Hash), uint64(trace.Hash))
+	return &newTrace
 }
 
 func (f *fakeTraceProcessor) ProcessedUntil(times.KTime) {}
 
-func (f *fakeTraceProcessor) MaybeNotifyAPMAgent(*host.Trace, *libpf.Trace) string {
+func (f *fakeTraceProcessor) MaybeNotifyAPMAgent(*host.Trace, libpf.TraceHash, uint16) string {
 	return ""
 }
 
@@ -54,16 +54,15 @@ type arguments struct {
 
 type mockReporter struct {
 	t       *testing.T
-	reports map[host.TraceHash]uint16
+	reports map[libpf.TraceHash]uint16
 }
 
 func (m *mockReporter) ReportTraceEvent(trace *libpf.Trace, _ *samples.TraceEventMeta) error {
-	hash := host.TraceHash(trace.Frames[0].Value().AddressOrLineno)
-	if _, exists := m.reports[hash]; exists {
-		m.reports[hash]++
+	if _, exists := m.reports[trace.Hash]; exists {
+		m.reports[trace.Hash]++
 		return nil
 	}
-	m.reports[hash] = 1
+	m.reports[trace.Hash] = 1
 
 	return nil
 }
@@ -72,7 +71,7 @@ func TestTraceHandler(t *testing.T) {
 	tests := map[string]struct {
 		input          []arguments
 		expireTimeout  time.Duration
-		expectedEvents map[host.TraceHash]uint16
+		expectedEvents map[libpf.TraceHash]uint16
 	}{
 		// no input simulates a case where no data is provided as input
 		// to the functions of traceHandler.
@@ -82,8 +81,8 @@ func TestTraceHandler(t *testing.T) {
 		"single trace": {input: []arguments{
 			{trace: &host.Trace{Hash: host.TraceHash(0x1234)}},
 		},
-			expectedEvents: map[host.TraceHash]uint16{
-				0x1234: 1,
+			expectedEvents: map[libpf.TraceHash]uint16{
+				libpf.NewTraceHash(0x1234, 0x1234): 1,
 			},
 		},
 
@@ -92,8 +91,8 @@ func TestTraceHandler(t *testing.T) {
 			{trace: &host.Trace{Hash: host.TraceHash(4)}},
 			{trace: &host.Trace{Hash: host.TraceHash(4)}},
 		},
-			expectedEvents: map[host.TraceHash]uint16{
-				4: 2,
+			expectedEvents: map[libpf.TraceHash]uint16{
+				libpf.NewTraceHash(4, 4): 2,
 			},
 		},
 	}
@@ -102,7 +101,7 @@ func TestTraceHandler(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			r := &mockReporter{
 				t:       t,
-				reports: make(map[host.TraceHash]uint16),
+				reports: make(map[libpf.TraceHash]uint16),
 			}
 
 			traceChan := make(chan *host.Trace)
diff --git a/interpreter/apmint/hash_test.go b/traceutil/traceutil_test.go
similarity index 68%
rename from interpreter/apmint/hash_test.go
rename to traceutil/traceutil_test.go
index fd3e2a258..c1bc5d08c 100644
--- a/interpreter/apmint/hash_test.go
+++ b/traceutil/traceutil_test.go
@@ -1,7 +1,7 @@
 // Copyright The OpenTelemetry Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package apmint // import "go.opentelemetry.io/ebpf-profiler/interpreter/apmint"
+package traceutil
 
 import (
 	"testing"
@@ -30,23 +30,19 @@ func newPythonTrace() *libpf.Trace {
 func TestHashTrace(t *testing.T) {
 	tests := map[string]struct {
 		trace  *libpf.Trace
-		result [16]byte
+		result libpf.TraceHash
 	}{
 		"empty trace": {
-			trace: &libpf.Trace{},
-			result: [16]uint8{0x6c, 0x62, 0x27, 0x2e, 0x7, 0xbb, 0x1, 0x42,
-				0x62, 0xb8, 0x21, 0x75, 0x62, 0x95, 0xc5, 0x8d},
-		},
+			trace:  &libpf.Trace{},
+			result: libpf.NewTraceHash(0x6c62272e07bb0142, 0x62b821756295c58d)},
 		"python trace": {
-			trace: newPythonTrace(),
-			result: [16]byte{0x21, 0xc6, 0xfe, 0x4c, 0x62, 0x86, 0x88, 0x56,
-				0xcf, 0x51, 0x5, 0x96, 0xea, 0xb6, 0x8d, 0xc8},
-		},
+			trace:  newPythonTrace(),
+			result: libpf.NewTraceHash(0x21c6fe4c62868856, 0xcf510596eab68dc8)},
 	}
 
 	for name, testcase := range tests {
 		t.Run(name, func(t *testing.T) {
-			assert.Equal(t, testcase.result, hashTrace(testcase.trace))
+			assert.Equal(t, testcase.result, HashTrace(testcase.trace))
 		})
 	}
 }