package tracedetail import ( "sort" "github.com/SigNoz/signoz/pkg/query-service/model" ) var ( SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50 SPAN_LIMIT_PER_LEVEL int = 100 TIMESTAMP_SAMPLING_BUCKET_COUNT int = 50 ) func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool { for _, v := range slice { if v.SpanID == item.SpanID { return true } } return false } func BfsTraversalForTrace(span *model.FlamegraphSpan, level int64) map[int64][]*model.FlamegraphSpan { bfs := map[int64][]*model.FlamegraphSpan{} bfs[level] = []*model.FlamegraphSpan{span} for _, child := range span.Children { childBfsMap := BfsTraversalForTrace(child, level+1) for _level, nodes := range childBfsMap { bfs[_level] = append(bfs[_level], nodes...) } } span.Level = level span.Children = make([]*model.FlamegraphSpan, 0) return bfs } func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId string) int { var selectedSpanLevel int = 0 for index, _spans := range spans { for _, span := range _spans { if span.SpanID == selectedSpanId { selectedSpanLevel = index break } } } return selectedSpanLevel } func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan { var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{} selectedSpans := [][]*model.FlamegraphSpan{} // sort the trace roots to add missing spans at the right order sort.Slice(traceRoots, func(i, j int) bool { if traceRoots[i].TimeUnixNano == traceRoots[j].TimeUnixNano { return traceRoots[i].Name < traceRoots[j].Name } return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano }) for _, rootSpanID := range traceRoots { if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists { bfsMapForTrace := BfsTraversalForTrace(rootNode, 0) traceIdLevelledFlamegraph[rootSpanID.SpanID] = bfsMapForTrace } } for _, trace := range traceRoots { keys := make([]int64, 0, len(traceIdLevelledFlamegraph[trace.SpanID])) for key := range traceIdLevelledFlamegraph[trace.SpanID] { keys = append(keys, key) } sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) for _, level := range keys { if ok, exists := traceIdLevelledFlamegraph[trace.SpanID][level]; exists { selectedSpans = append(selectedSpans, ok) } } } return selectedSpans } func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selectedSpanID string, isSelectedSpanIDPresent bool, startTime uint64, endTime uint64) []*model.FlamegraphSpan { var sampledSpans []*model.FlamegraphSpan // sort the spans by latency for latency filtering sort.Slice(spans, func(i, j int) bool { return spans[i].DurationNano > spans[j].DurationNano }) // pick the top 5 latency spans for idx := range 5 { sampledSpans = append(sampledSpans, spans[idx]) } // always add the selectedSpan if isSelectedSpanIDPresent { idx := -1 for _idx, span := range spans { if span.SpanID == selectedSpanID { idx = _idx } } if idx != -1 { sampledSpans = append(sampledSpans, spans[idx]) } } bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT) if bucketSize == 0 { bucketSize = 1 } bucketedSpans := make([][]*model.FlamegraphSpan, 50) for _, span := range spans { if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime { bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize) if bucketIndex >= 0 && bucketIndex < 50 { bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span) } } } for i := range bucketedSpans { if len(bucketedSpans[i]) > 2 { // Keep only the first 2 spans bucketedSpans[i] = bucketedSpans[i][:2] } } // Flatten the bucketed spans into a single slice for _, bucket := range bucketedSpans { sampledSpans = append(sampledSpans, bucket...) } return sampledSpans } func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan, startTime uint64, endTime uint64) [][]*model.FlamegraphSpan { var selectedSpansForRequest = make([][]*model.FlamegraphSpan, 0) var selectedIndex = 0 if selectedSpanID != "" { selectedIndex = FindIndexForSelectedSpan(selectedSpans, selectedSpanID) } lowerLimit := selectedIndex - int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.4) upperLimit := selectedIndex + int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.6) if lowerLimit < 0 { upperLimit = upperLimit - lowerLimit lowerLimit = 0 } if upperLimit > len(selectedSpans) { lowerLimit = lowerLimit - (upperLimit - len(selectedSpans)) upperLimit = len(selectedSpans) } if lowerLimit < 0 { lowerLimit = 0 } for i := lowerLimit; i < upperLimit; i++ { if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL { _spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime) selectedSpansForRequest = append(selectedSpansForRequest, _spans) } else { selectedSpansForRequest = append(selectedSpansForRequest, selectedSpans[i]) } } return selectedSpansForRequest }