Compare commits

...

2 Commits

Author SHA1 Message Date
Nikhil Soni
e1246b64b0 chore: remove unnecessary comments 2026-07-23 16:49:32 +05:30
Nikhil Soni
06b9f21e75 fix(tracedetail): use span-derived bounds for flamegraph EndTimestampMillis
The trace_summary MV stores max(timestamp) as 'end', which is the maximum
span *start* time. For the final span of a trace, this misses the span's
duration entirely, making EndTimestampMillis shorter than the actual trace end.

FlamegraphTrace already computed the correct end via updateTimeRange
(end = max(span_start + duration)) but only used it internally for bucket
sampling. Add TimeRange() to expose these span-derived bounds and use them
in both getFullFlamegraph and getWindowedFlamegraph instead of summary.End.

The waterfall was unaffected — NewWaterfallTraceFromSpans already computes
endTime = max(span.TimeUnix + span.DurationNano) from actual spans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GxTqz7CqqmoFEPHFqhJUCa
2026-07-23 14:30:40 +05:30
3 changed files with 60 additions and 2 deletions

View File

@@ -182,7 +182,8 @@ func (m *module) getFullFlamegraph(ctx context.Context, traceID string, summary
return nil, spantypes.ErrTraceNotFound
}
flamegraphTrace := spantypes.NewFlamegraphTraceFromStorable(fullSpans, selectFields)
return spantypes.NewGettableFlamegraphTrace(flamegraphTrace.GetAllLevels(), summary.Start, summary.End, false), nil
start, end := flamegraphTrace.TimeRange()
return spantypes.NewGettableFlamegraphTrace(flamegraphTrace.GetAllLevels(), start, end, false), nil
}
// getWindowedFlamegraph returns a window of a max levels and max sampled spans per level around the selected span.
@@ -198,6 +199,8 @@ func (m *module) getWindowedFlamegraph(ctx context.Context, traceID, selectedSpa
flamegraphTrace := spantypes.NewFlamegraphTraceFromMinimal(minimalSpans)
minimalSpans = nil //nolint:ineffassign,wastedassign // release backing array before further db calls
start, end := flamegraphTrace.TimeRange()
cfg := m.config.Flamegraph
selectedSpans := flamegraphTrace.GetSelectedLevels(selectedSpanID, cfg.MaxSelectedLevels, cfg.MaxSpansPerLevel, cfg.SamplingTopLatencySpansCount, cfg.SamplingBucketCount)
if len(selectedSpans) == 0 {
@@ -210,5 +213,5 @@ func (m *module) getWindowedFlamegraph(ctx context.Context, traceID, selectedSpa
}
enrichedSpans := flamegraphTrace.EnrichSelectedSpans(selectedSpans, fullSpans, selectFields)
return spantypes.NewGettableFlamegraphTrace(enrichedSpans, summary.Start, summary.End, true), nil
return spantypes.NewGettableFlamegraphTrace(enrichedSpans, start, end, true), nil
}

View File

@@ -2,6 +2,7 @@ package spantypes
import (
"sort"
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -121,6 +122,11 @@ func (t *FlamegraphTrace) updateTimeRange(timestamp, durationNano uint64) {
}
}
// TimeRange returns the actual trace start and end computed from span timestamps.
func (t *FlamegraphTrace) TimeRange() (start, end time.Time) {
return time.Unix(0, int64(t.startTime)), time.Unix(0, int64(t.endTime))
}
func (t *FlamegraphTrace) buildSpanTree() {
for _, node := range t.nodeByID {
if node.ParentSpanID != "" {

View File

@@ -126,6 +126,55 @@ func TestGetAllLevels(t *testing.T) {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TimeRange
// ─────────────────────────────────────────────────────────────────────────────
func TestTimeRange(t *testing.T) {
tests := []struct {
name string
spans []MinimalSpan
wantStart uint64 // nanoseconds since epoch
wantEnd uint64 // nanoseconds since epoch (max of span_start + span_duration)
}{
{
// Single span: end must include duration, not just start.
name: "single_span_end_includes_duration",
spans: []MinimalSpan{mkMinimal("a", "", 1_000_000_000, 500_000_000)},
wantStart: 1_000_000_000,
wantEnd: 1_500_000_000,
},
{
// Two spans: end is the max of (start+duration) across all spans, not max(start).
name: "end_from_longest_span_not_latest_start",
spans: []MinimalSpan{
mkMinimal("a", "", 1_000_000_000, 1_000_000_000), // ends at 2_000_000_000
mkMinimal("b", "", 1_800_000_000, 100_000_000), // ends at 1_900_000_000; starts later but ends sooner
},
wantStart: 1_000_000_000,
wantEnd: 2_000_000_000, // must NOT be 1_900_000_000 (max start + its duration)
},
{
// Missing parent: synthetic span must not influence TimeRange beyond its children.
name: "missing_parent_does_not_skew_range",
spans: []MinimalSpan{
mkMinimal("child", "ghost", 1_000_000_000, 200_000_000),
},
wantStart: 1_000_000_000,
wantEnd: 1_200_000_000,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
trace := NewFlamegraphTraceFromMinimal(tc.spans)
start, end := trace.TimeRange()
assert.Equal(t, tc.wantStart, uint64(start.UnixNano()), "start mismatch")
assert.Equal(t, tc.wantEnd, uint64(end.UnixNano()), "end mismatch")
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedLevels
// ─────────────────────────────────────────────────────────────────────────────