Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make android samples monotonic #337

Merged
merged 14 commits into from
Oct 18, 2023
Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Unreleased

**Features**


**Bug Fixes**
- Turn non-monotonic samples wall-clock time into monotonic. ([#337](https://github.com/getsentry/vroom/pull/337))

**Internal**


## 23.10.0

**Features**
Expand Down
50 changes: 50 additions & 0 deletions internal/profile/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,56 @@ func (p Android) TimestampGetter() func(EventTime) uint64 {
return buildTimestamp
}

// maxTimeNs: the highest time (in nanoseconds) in the sequence so far
// latestNs: the latest time value in ns (at time t-1) before it was updated
// currentNs: current value in ns (at time t) before it's updated.
func getAdjustedTime(maxTimeNs, latestNs, currentNs uint64) uint64 {
if currentNs < maxTimeNs && currentNs < latestNs {
return maxTimeNs + 1e9
}
return maxTimeNs + (currentNs - latestNs)
}

// Wall-clock time is supposed to be monotonic
// in a few rare cases we've noticed this was not the case.
// Due to some overflow happening client-side in the embedded
// profiler, the sequence might be decreasing at certain points.
//
// This is just a workaround to mitigate this issue, should it
// happen.
func (p *Android) FixSamplesTime() {
if p.Clock == GlobalClock || p.Clock == CPUClock {
return
}
threadMaxTimeNs := make(map[uint64]uint64)
threadLatestSampleTimeNs := make(map[uint64]uint64)
regressionIndex := -1

for i, event := range p.Events {
current := (event.Time.Monotonic.Wall.Secs * 1e9) + event.Time.Monotonic.Wall.Nanos
if current < threadLatestSampleTimeNs[event.ThreadID] {
regressionIndex = i
break
}
threadLatestSampleTimeNs[event.ThreadID] = current
threadMaxTimeNs[event.ThreadID] = max(threadMaxTimeNs[event.ThreadID], current)
}

if regressionIndex > 0 {
for i := regressionIndex; i < len(p.Events); i++ {
event := p.Events[i]
current := (event.Time.Monotonic.Wall.Secs * 1e9) + event.Time.Monotonic.Wall.Nanos

newTime := getAdjustedTime(threadMaxTimeNs[event.ThreadID], threadLatestSampleTimeNs[event.ThreadID], current)
threadMaxTimeNs[event.ThreadID] = max(threadMaxTimeNs[event.ThreadID], newTime)

threadLatestSampleTimeNs[event.ThreadID] = current
p.Events[i].Time.Monotonic.Wall.Secs = (newTime / 1e9)
p.Events[i].Time.Monotonic.Wall.Nanos = (newTime % 1e9)
}
}
}

// CallTrees generates call trees for a given profile.
func (p Android) CallTrees() map[uint64][]*nodetree.Node {
var activeThreadID uint64
Expand Down
315 changes: 315 additions & 0 deletions internal/profile/android_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,153 @@ var (
},
},
}

nonMonotonicTrace = Android{
Clock: "Dual",
Events: []AndroidEvent{
{
Action: "Enter",
ThreadID: 1,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 1,
Nanos: 1000,
},
},
},
},
{
Action: "Enter",
ThreadID: 1,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 2,
Nanos: 1000,
},
},
},
},
{
Action: "Enter",
ThreadID: 1,
MethodID: 3,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 7,
Nanos: 2000,
},
},
},
},
{
Action: "Exit",
ThreadID: 1,
MethodID: 3,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 6,
Nanos: 3000,
},
},
},
},
{
Action: "Exit",
ThreadID: 1,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 6,
Nanos: 3000,
},
},
},
},
{
Action: "Exit",
ThreadID: 1,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 9,
Nanos: 3000,
},
},
},
},
{
Action: "Enter",
ThreadID: 2,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 1,
Nanos: 3000,
},
},
},
},
{
Action: "Enter",
ThreadID: 2,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 2,
Nanos: 3000,
},
},
},
},
{
Action: "Exit",
ThreadID: 2,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 2,
Nanos: 3000,
},
},
},
},
{
Action: "Exit",
ThreadID: 2,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 3,
Nanos: 3000,
},
},
},
},
},
StartTime: 398635355383000,
Threads: []AndroidThread{
{
ID: 1,
Name: "main",
},
{
ID: 2,
Name: "background",
},
},
}
)

func TestSpeedscope(t *testing.T) {
Expand Down Expand Up @@ -428,3 +575,171 @@ func TestCallTrees(t *testing.T) {
})
}
}

func TestFixSamplesTime(t *testing.T) {
tests := []struct {
name string
trace Android
want Android
}{
{
name: "Make sample secs monotonic",
trace: nonMonotonicTrace,
want: Android{
Clock: "Dual",
Events: []AndroidEvent{
{
Action: "Enter",
ThreadID: 1,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 1,
Nanos: 1000,
},
},
},
},
{
Action: "Enter",
ThreadID: 1,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 2,
Nanos: 1000,
},
},
},
},
{
Action: "Enter",
ThreadID: 1,
MethodID: 3,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 7,
Nanos: 2000,
},
},
},
},
{
Action: "Exit",
ThreadID: 1,
MethodID: 3,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 8,
Nanos: 2000,
},
},
},
},
{
Action: "Exit",
ThreadID: 1,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 8,
Nanos: 2000,
},
},
},
},
{
Action: "Exit",
ThreadID: 1,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 11,
Nanos: 2000,
},
},
},
},
{
Action: "Enter",
ThreadID: 2,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 1,
Nanos: 3000,
},
},
},
},
{
Action: "Enter",
ThreadID: 2,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 2,
Nanos: 3000,
},
},
},
},
{
Action: "Exit",
ThreadID: 2,
MethodID: 2,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 2,
Nanos: 3000,
},
},
},
},
{
Action: "Exit",
ThreadID: 2,
MethodID: 1,
Time: EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: 3,
Nanos: 3000,
},
},
},
},
},
StartTime: 398635355383000,
Threads: []AndroidThread{
{
ID: 1,
Name: "main",
},
{
ID: 2,
Name: "background",
},
},
},
},
} // end tests

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.trace.FixSamplesTime()
if diff := testutil.Diff(test.trace, test.want); diff != "" {
t.Fatalf("Result mismatch: got - want +\n%s", diff)
}
})
}
}
Loading
Loading